From ab306eab660ea090e6f6741cebac6e7e7125ba95 Mon Sep 17 00:00:00 2001 From: Silvano Seva Date: Mon, 30 Mar 2020 16:58:03 +0200 Subject: [PATCH] Add loading scripts, update README.md, requirements.txt and .gitignore --- .gitignore | 13 ++ README.md | 50 +++++ requirements.txt | 1 + scripts/DFU.py | 293 +++++++++++++++++++++++++ scripts/dfu_suffix.py | 88 ++++++++ scripts/md380_dfu.py | 493 ++++++++++++++++++++++++++++++++++++++++++ scripts/md380_fw.py | 204 +++++++++++++++++ 7 files changed, 1142 insertions(+) create mode 100644 README.md create mode 100644 requirements.txt create mode 100644 scripts/DFU.py create mode 100644 scripts/dfu_suffix.py create mode 100755 scripts/md380_dfu.py create mode 100755 scripts/md380_fw.py diff --git a/.gitignore b/.gitignore index 259148fa..ea0b68fe 100644 --- a/.gitignore +++ b/.gitignore @@ -30,3 +30,16 @@ *.exe *.out *.app + +# Binaries +*.bin +*.elf +*.hex +*.map + +# Python byte-compiled +*/__pycache__ + +# Kdevelop files +*.kdev4 +.kdev4/ diff --git a/README.md b/README.md new file mode 100644 index 00000000..4abcd9f8 --- /dev/null +++ b/README.md @@ -0,0 +1,50 @@ +# OpenDMR +## Open source firmware for the TYT MD380 + +This firmware is *highly experimental* and is not in a usable state right now, +however contributions and testing are welcome and accepted. + +## Installation + +To build and install the firmware, first clone this repository: + +``` +git clone https://github.com/n1zzo/OpenDMR +``` + +To build the firmware you need to have a toolchain for the ARM ISA installed +on you system, you can install one using your package manager. +You can then proceed in building the firmware: + +``` +cd OpenDMR +make +``` + +If everithing compiled without errors you can connect your radio via USB, +put it in recovery mode (by powering it on with the PTT and the button +above it pressed), and flash the firmware: + +``` +make flash +``` + +Now you can power cycle your radio and enjoy the new breath of freedom! + +## License + +This software is released under the GNU GPL v3, the modified wrapping scripts +from Travis Goodspeed are licensed in exchange of two liters of India Pale Ale, +we still owe you the two liters, Travis! + +## Credits + +OpenDMR was created by: + +- Niccolò Izzo IU2KIN +- Silvano Seva IU2KWO + +All this was made possible by the huge reverse engineering effort of +Travis Goodspeed and all the contributors of [md380tools](https://github.com/travisgoodspeed/md380tools). +A huge thank goes to Roger Clark, and his [OpenGD77](https://github.com/rogerclarkmelbourne/OpenGD77) which inspired this project, +and which we aspire becoming a part of. diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 00000000..6513d5e3 --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +pyusb diff --git a/scripts/DFU.py b/scripts/DFU.py new file mode 100644 index 00000000..4528e91b --- /dev/null +++ b/scripts/DFU.py @@ -0,0 +1,293 @@ +# -*- coding: utf-8 -*- + +import struct +import sys +import time + + +class Enumeration(object): + def __init__(self, id, name): + self._id = id + self._name = name + setattr(self.__class__, name, self) + self.map[id] = self + + def __int__(self): + return self.id + + def __repr__(self): + return self.name + + @property + def id(self): + return self._id + + @property + def name(self): + return self._name + + @classmethod + def create_from_map(cls): + for id, name in list(cls.map.items()): + cls(id, name) + + +class Request(Enumeration): + map = { + 0: 'DETACH', + 1: 'DNLOAD', + 2: 'UPLOAD', + 3: 'GETSTATUS', + 4: 'CLRSTATUS', + 5: 'GETSTATE', + 6: 'ABORT', + } + + +Request.create_from_map() + + +class State(Enumeration): + map = { + 0: 'appIDLE', + 1: 'appDETACH', + 2: 'dfuIDLE', + 3: 'dfuDNLOAD_SYNC', + 4: 'dfuDNBUSY', + 5: 'dfuDNLOAD_IDLE', + 6: 'dfuMANIFEST_SYNC', + 7: 'dfuMANIFEST', + 8: 'dfuMANIFEST_WAIT_RESET', + 9: 'dfuUPLOAD_IDLE', + 10: 'dfuERROR', + } + + +State.create_from_map() + + +class Status(Enumeration): + map = { + 0x00: 'OK', + 0x01: 'errTARGET', + 0x02: 'errFILE', + 0x03: 'errWRITE', + 0x04: 'errERASE', + 0x05: 'errCHECK_ERASED', + 0x06: 'errPROG', + 0x07: 'errVERIFY', + 0x08: 'errADDRESS', + 0x09: 'errNOTDONE', + 0x0A: 'errFIRMWARE', + 0x0B: 'errVENDOR', + 0x0C: 'errUSBR', + 0x0D: 'errPOR', + 0x0E: 'errUNKNOWN', + 0x0F: 'errSTALLEDPKT', + } + + +Status.create_from_map() + + +class DFU(object): + verbose = False + + def __init__(self, device, alt): + device.set_interface_altsetting(interface=0, alternate_setting=alt) + self._device = device + + def detach(self): + """Detaches from the DFU target.""" + self._device.ctrl_transfer(0x21, Request.DETACH, 0, 0, None) + + def get_string(self, i=0): + """Gets a USB descriptor string, to distinguish firmware types.""" + import usb + + # Linux and Mac have different calling conventions for usb.util.get_string(), + # so we'll try each of them and hope for the best. + try: + # Mac calling convention. + return usb.util.get_string(self._device, 255, i, None) + except: + # Linux calling convention. + return usb.util.get_string(self._device, i, None) + + def bcd(self, b): + """Converts a byte from BCD to integer.""" + return int("%02x" % b) + + def get_time(self): + """Returns a datetime object for the radio's current time.""" + self.md380_custom(0x91, 0x01) # Programming Mode + self.md380_custom(0xA2, 0x08) # Access the clock memory. + time = self.upload(0, 7) # Read the time bytes as BCD. + # hexdump("Time is: "+time); + from datetime import datetime + dt = datetime(self.bcd(time[0]) * 100 + self.bcd(time[1]), + self.bcd(time[2]), + self.bcd(time[3]), + self.bcd(time[4]), + self.bcd(time[5]), + self.bcd(time[6])) + return dt + + def set_time(self): + from datetime import datetime + if len(sys.argv) == 3: + try: + time_to_set = datetime.strptime(sys.argv[2], '%m/%d/%Y %H:%M:%S') + except ValueError: + print("Usage: md380-dfu settime \"mm/dd/yyyy HH:MM:SS\" (with quotes)") + exit() + else: + time_to_set = datetime.now() + dt = datetime.strftime(time_to_set, '%Y%m%d%H%M%S').decode("hex") + self.md380_custom(0x91, 0x02) + self.download(0, b"\xb5" + dt) + self.wait_till_ready() + self.md380_reboot() + + def download(self, block_number, data): + self._device.ctrl_transfer(0x21, Request.DNLOAD, block_number, 0, data) + # time.sleep(0.1); + + def set_address(self, address): + a = address & 0xFF + b = (address >> 8) & 0xFF + c = (address >> 16) & 0xFF + d = (address >> 24) & 0xFF + self._device.ctrl_transfer(0x21, Request.DNLOAD, 0, 0, [0x21, a, b, c, d]) + self.get_status() # this changes state + status = self.get_status() # this gets the status + if status[2] == State.dfuDNLOAD_IDLE: + if self.verbose: + print("Set pointer to 0x%08x." % address) + self.enter_dfu_mode() + else: + if self.verbose: + print("Failed to set pointer.") + return False + return True + + def erase_block(self, address): + a = address & 0xFF + b = (address >> 8) & 0xFF + c = (address >> 16) & 0xFF + d = (address >> 24) & 0xFF + self._device.ctrl_transfer(0x21, Request.DNLOAD, 0, 0, [0x41, a, b, c, d]) + # time.sleep(0.5); + self.get_status() # this changes state + status = self.get_status() # this gets the status + if status[2] == State.dfuDNLOAD_IDLE: + if self.verbose: + print("Erased 0x%08x." % address) + self.enter_dfu_mode() + else: + if self.verbose: + print("Failed to erase block.") + return False + return True + + def md380_custom(self, a, b): + """Sends a secret MD380 command.""" + a &= 0xFF + b &= 0xFF + self._device.ctrl_transfer(0x21, Request.DNLOAD, 0, 0, [a, b]) + self.get_status() # this changes state + time.sleep(0.1) + status = self.get_status() # this gets the status + if status[2] == State.dfuDNLOAD_IDLE: + if self.verbose: + print("Sent custom %02x %02x." % (a, b)) + self.enter_dfu_mode() + else: + print("Failed to send custom %02x %02x." % (a, b)) + return False + return True + + def md380_reboot(self): + """Sends the MD380's secret reboot command.""" + a = 0x91 + b = 0x05 + self._device.ctrl_transfer(0x21, Request.DNLOAD, 0, 0, [a, b]) + try: + self.get_status() # this changes state + except: + pass + return True + + def upload(self, block_number, length, index=0): + if self.verbose: + print("Fetching block 0x%x." % block_number) + data = self._device.ctrl_transfer(0xA1, # request type + Request.UPLOAD, # request + block_number, # wValue + index, # index + length) # length + return data + + def get_command(self): + data = self._device.ctrl_transfer(0xA1, # request type + Request.UPLOAD, # request + 0, # wValue + 0, # index + 32) # length + self.get_status() + return data + + def get_status(self): + status_packed = self._device.ctrl_transfer(0xA1, Request.GETSTATUS, 0, 0, 6) + status = struct.unpack('> 1) ^ 0xedb88320 + else: + t >>= 1 + crc_table.append(t) + + +def crc32(data): + crc = 0xffffffff + for byte in data: + crc = (crc >> 8) ^ crc_table[(crc ^ ord(byte)) & 0xff] + return crc + + +def check_suffix(firmware): + """Check the dfu suffix""" + print('Checking firmware signature') + + data = firmware[:-4] + length = ord(firmware[-5]) + suffix = firmware[-length:] + + # Will always have these fields + dwCRC = unpack(' 0: + packet, data = data[:block_size], data[block_size:] + if len(packet) < block_size: + packet += b'\xFF' * (block_size - len(packet)) + dfu.download(block_number, packet) + status, timeout, state, discarded = dfu.get_status() + sys.stdout.write('.') + sys.stdout.flush() + block_number += 1 + finally: + print() + + +def download_codeplug(dfu, data): + """Downloads a codeplug to the MD380.""" + block_size = 1024 + + dfu.md380_custom(0x91, 0x01) # Programming Mode + dfu.md380_custom(0x91, 0x01) # Programming Mode + # dfu.md380_custom(0xa2,0x01); #Returns "DR780...", seems to crash client. + # hexdump(dfu.get_command()); #Gets a string. + dfu.md380_custom(0xa2, 0x02) + hexdump(dfu.get_command()) # Gets a string. + time.sleep(2) + dfu.md380_custom(0xa2, 0x02) + dfu.md380_custom(0xa2, 0x03) + dfu.md380_custom(0xa2, 0x04) + dfu.md380_custom(0xa2, 0x07) + + dfu.erase_block(0x00000000) + dfu.erase_block(0x00010000) + dfu.erase_block(0x00020000) + dfu.erase_block(0x00030000) + + dfu.set_address(0x00000000) # Zero address, used by configuration tool. + + # sys.exit(); + + status, timeout, state, discarded = dfu.get_status() + # print(status, timeout, state, discarded) + + block_number = 2 + + try: + while len(data) > 0: + packet, data = data[:block_size], data[block_size:] + if len(packet) < block_size: + packet += b'\xFF' * (block_size - len(packet)) + dfu.download(block_number, packet) + state = 11 + while state != State.dfuDNLOAD_IDLE: + status, timeout, state, discarded = dfu.get_status() + # print(status, timeout, state, discarded) + sys.stdout.write('.') + sys.stdout.flush() + block_number += 1 + finally: + print() + + +def hexdump(string): + """God awful hex dump function for testing.""" + buf = "" + i = 0 + for c in string: + buf += "%02x" % c + i += 1 + if i & 3 == 0: + buf += " " + if i & 0xf == 0: + buf += " " + if i & 0x1f == 0: + buf += "\n" + + print(buf) + + +def upload_bootloader(dfu, filename): + """Dumps the bootloader, but only on Mac.""" + # dfu.set_address(0x00000000); # Address is ignored, so it doesn't really matter. + + # Bootloader stretches from 0x08000000 to 0x0800C000, but our + # address and block number are ignored, so we set the block size + # ot 0xC000 to yank the entire thing in one go. The application + # comes later, I think. + block_size = 0xC000 # 0xC000; + + f = None + if filename is not None: + f = open(filename, 'wb') + + print("Dumping bootloader. This only works in radio mode, not programming mode.") + try: + data = dfu.upload(2, block_size) + status, timeout, state, discarded = dfu.get_status() + if len(data) == block_size: + print("Got it all!") + else: + print("Only got %i bytes. Older versions would give it all." % len(data)) + # raise Exception('Upload failed to read full block. Got %i bytes.' % len(data)) + if f is not None: + f.write(data) + else: + hexdump(data) + + finally: + print("Done.") + + +def upload_codeplug(dfu, filename): + """Uploads a codeplug from the radio to the host.""" + dfu.md380_custom(0x91, 0x01) # Programming Mode + # dfu.md380_custom(0xa2,0x01); #Returns "DR780...", seems to crash client. + # hexdump(dfu.get_command()); #Gets a string. + dfu.md380_custom(0xa2, 0x02) + dfu.md380_custom(0xa2, 0x02) + dfu.md380_custom(0xa2, 0x03) + dfu.md380_custom(0xa2, 0x04) + dfu.md380_custom(0xa2, 0x07) + + dfu.set_address(0x00000000) # Zero address, used by configuration tool. + + f = open(filename, 'wb') + block_size = 1024 + try: + # Codeplug region is 0 to 3ffffff, but only the first 256k are used. + for block_number in range(2, 0x102): + data = dfu.upload(block_number, block_size) + status, timeout, state, discarded = dfu.get_status() + # print("Status is: %x %x %x %x" % (status, timeout, state, discarded)) + sys.stdout.write('.') + sys.stdout.flush() + if len(data) == block_size: + f.write(data) + # hexdump(data); + else: + raise Exception('Upload failed to read full block. Got %i bytes.' % len(data)) + # dfu.md380_reboot() + finally: + print("Done.") + + +def download_firmware(dfu, data): + """ Download new firmware binary to the radio. """ + addresses = [ + 0x0800c000, + 0x08010000, + 0x08020000, + 0x08040000, + 0x08060000, + 0x08080000, + 0x080a0000, + 0x080c0000, + 0x080e0000] + sizes = [0x4000, # 0c + 0x10000, # 1 + 0x20000, # 2 + 0x20000, # 4 + 0x20000, # 6 + 0x20000, # 8 + 0x20000, # a + 0x20000, # c + 0x20000] # e + block_ends = [0x11, 0x41, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81] + try: + # Are we in the right mode? + mfg = dfu.get_string(1) + if mfg != u'AnyRoad Technology': + print("""ERROR: You forgot to enter the bootloader. +Please hold PTT and the button above it while rebooting. You +should see the LED blinking green and red, and then your +radio will be radio to accept this firmware update.""") + sys.exit(1) + + print("Beginning firmware upgrade.") + sys.stdout.flush() # let text appear immediately (for mingw) + status, timeout, state, discarded = dfu.get_status() + assert state == State.dfuIDLE + + dfu.md380_custom(0x91, 0x01) + dfu.md380_custom(0x91, 0x31) + + for address in addresses: + if dfu.verbose: + print("Erasing address@ 0x%x" % address) + sys.stdout.flush() + dfu.erase_block(address) + + block_size = 1024 + block_start = 2 + address_idx = 0 + + if data[0:14] == b"OutSecurityBin": # skip header if present + if dfu.verbose: + print("Skipping 0x100 byte header in data file") + header, data = data[:0x100], data[0x100:] + + print("Writing firmware:") + + assert len(addresses) == len(sizes) + numaddresses = len(addresses) + + while address_idx < numaddresses: # for each section + print("%0d%% complete" % (address_idx * 100 / numaddresses)) + sys.stdout.flush() # let text appear immediately (for mingw) + address = addresses[address_idx] + size = sizes[address_idx] + dfu.set_address(address) + + if address_idx != len(addresses) - 1: + assert address + size == addresses[address_idx + 1] + + datawritten = 0 + block_number = block_start + + while len(data) > 0 and size > datawritten: # for each block + assert block_number <= block_ends[address_idx] + packet, data = data[:block_size], data[block_size:] + + if len(packet) < block_size: + packet += b'\xFF' * (block_size - len(packet)) + + dfu.download(block_number, packet) + dfu.wait_till_ready() + + datawritten += len(packet) + block_number += 1 + # if dfu.verbose: sys.stdout.write('.'); sys.stdout.flush() + # if dfu.verbose: sys.stdout.write('_\n'); sys.stdout.flush() + address_idx += 1 + print("100% complete, now safe to disconnect and/or reboot radio") + return True + except Exception as e: + print(e) + return False + + +def upload(dfu, flash_address, length, path): + # block_size = 1 << 8 + block_size = 1 << 14 + + print("Address: 0x%08x" % flash_address) + print("Block Size: 0x%04x" % block_size) + + if flash_address & (block_size - 1) != 0: + raise Exception('Upload must start at block boundary') + + block_number = flash_address / block_size + assert block_number * block_size == flash_address + # block_number=0x8000; + print("Block Number: 0x%04x" % block_number) + + cmds = dfu.get_command() + print("%i supported commands." % len(cmds)) + for cmd in cmds: + print("Command %02x is supported by UPLOAD." % cmd) + + dfu.set_address(0x08001000) # RAM + block_number = 2 + + f = open(path, 'wb') + + try: + while length > 0: + data = dfu.upload(block_number, block_size) + status, timeout, state, discarded = dfu.get_status() + print("Status is: %x %x %x %x" % (status, timeout, state, discarded)) + sys.stdout.write('.') + sys.stdout.flush() + if len(data) == block_size: + f.write(data) + else: + raise Exception('Upload failed to read full block. Got %i bytes.' % len(data)) + block_number += 1 + length -= len(data) + finally: + f.close() + print() + + +def detach(dfu): + if dfu.get_state() == State.dfuIDLE: + dfu.detach() + print('Detached') + else: + print('In unexpected state: %s' % dfu.get_state()) + + +def init_dfu(alt=0): + dev = usb.core.find(idVendor=md380_vendor, + idProduct=md380_product) + + if dev is None: + raise RuntimeError('Device not found') + + dfu = DFU(dev, alt) + dev.default_timeout = 6000 + + try: + dfu.enter_dfu_mode() + except usb.core.USBError as e: + if len(e.args) > 0 and e.args[0] == 'Pipe error': + raise RuntimeError('Failed to enter DFU mode. Is bootloader running?') + else: + raise e + + return dfu + + +def usage(): + print(""" +Usage: md380-dfu + +Write a codeplug to the radio. Supported file types: RDT (from official Tytera editor), DFU (with suffix) and raw binaries + md380-dfu write + md380-dfu write + md380-dfu write + +Write firmware to the radio. + md380-dfu upgrade + +Read a codeplug and write it to a file. + md380-dfu read + +Dump the bootloader from Flash memory. + md380-dfu readboot + + +Print the time from the MD380. + md380-dfu time + +Set time and date on MD380 to system time or specified time. + md380-dfu settime + md380-dfu settime "mm/dd/yyyy HH:MM:SS" (with quotes) + +Detach the bootloader and execute the application firmware: + md380-dfu detach + +Close the bootloader session. + md380-dfu reboot + + +Upgrade to new firmware: + md380-dfu upgrade foo.bin +""") + + + +def main(): + try: + if len(sys.argv) == 3: + if sys.argv[1] == 'read': + dfu = init_dfu() + upload_codeplug(dfu, sys.argv[2]) + print('Read complete') + elif sys.argv[1] == 'readboot': + print("This only works from OS X. Use the one in md380-tool with patched firmware for other bootloaders.") + dfu = init_dfu() + upload_bootloader(dfu, sys.argv[2]) + + elif sys.argv[1] == "upgrade": + dfu = init_dfu() + with open(sys.argv[2], 'rb') as f: + data = f.read() + result = download_firmware(dfu, data) + + elif sys.argv[1] == 'write': + f = open(sys.argv[2], 'rb') + data = f.read() + f.close() + + firmware = None + + if sys.argv[2][-4:] == '.dfu': + suf_len, vendor, product = dfu_suffix.check_suffix(data) + dfu = init_dfu() + firmware = data[:-suf_len] + elif sys.argv[2][-4:] == '.rdt': + if len(data) == 262709 and data[0:5] == 'DfuSe': + dfu = init_dfu() + firmware = data[549:len(data) - 16] + else: + print('%s not a valid codeplug (wrong size, or wrong magic).' % sys.argv[2]) + else: + dfu = init_dfu() + firmware = data + + if firmware is not None: + download_codeplug(dfu, firmware) + print('Write complete') + + elif sys.argv[1] == 'sign': + filename = sys.argv[2] + + f = open(filename, 'rb') + firmware = f.read() + f.close() + + data = dfu_suffix.add_suffix(firmware, md380_vendor, md380_product) + + dfu_file = filename[:-4] + '.dfu' + f = open(dfu_file, 'wb') + f.write(data) + f.close() + print("Signed file written: %s" % dfu_file) + + elif sys.argv[1] == 'settime': + dfu = init_dfu() + dfu.set_time() + else: + usage() + + elif len(sys.argv) == 2: + if sys.argv[1] == 'detach': + dfu = init_dfu() + dfu.set_address(0x08000000) # Radio Application + detach(dfu) + elif sys.argv[1] == 'time': + dfu = init_dfu() + print(dfu.get_time()) + elif sys.argv[1] == 'settime': + dfu = init_dfu() + dfu.set_time() + elif sys.argv[1] == 'reboot': + dfu = init_dfu() + dfu.md380_custom(0x91, 0x01) # Programming Mode + dfu.md380_custom(0x91, 0x01) # Programming Mode + # dfu.md380_custom(0x91,0x01); #Programming Mode + # dfu.drawtext("Rebooting",160,50); + dfu.md380_reboot() + elif sys.argv[1] == 'abort': + dfu = init_dfu() + dfu.abort() + else: + usage() + else: + usage() + except RuntimeError as e: + print(e.args[0]) + exit(1) + except Exception as e: + print(e) + # print(dfu.get_status()) + exit(1) + + +if __name__ == '__main__': + main() diff --git a/scripts/md380_fw.py b/scripts/md380_fw.py new file mode 100755 index 00000000..53830859 --- /dev/null +++ b/scripts/md380_fw.py @@ -0,0 +1,204 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +# This script was originally part of md380tools codebase by Travis Goodspeed + +import argparse +import binascii +import struct +import sys + +class TYTFW(object): + def pad(self, align=512, byte=b'\xff'): + pad_length = (align - len(self.app) % align) % align + self.app += byte * pad_length + + def unwrap(self, img): + header = struct.Struct(self.header_fmt) + header = header.unpack(img[:256]) + + self.start = header[6] + app_len = header[7] + self.app = self.crypt(img[256:256 + app_len]) + + assert header[0].startswith(self.magic) + assert header[1].startswith(self.jst) + assert header[3].startswith(self.foo) + assert header[4] == self.bar + assert 0x8000000 <= header[6] < 0x8200000 + assert header[7] == len(img) - 512 + + def crypt(self, data): + return self.xor(data, self.key) + + @staticmethod + def xor(a, b): + # FIXME: optimized version + out = bytearray() + l = max(len(a), len(b)) + for i in range(l): + out += bytes([a[i % len(a)] ^ b[i % len(b)]]) + return out + +class MD380FW(TYTFW): + # The stream cipher of MD-380 OEM firmware updates boils down + # to a cyclic, static XOR key block, and here it is: + key = (b'\x2e\xdf\x40\xb5\xbd\xda\x91\x35\x21\x42\xe3\xe2\x6d\xa9\x0b\x90' + b'\x31\x30\x3a\xfa\x4f\x05\x74\x64\x0a\x29\x44\x7e\x60\x77\xad\x8c' + b'\x9a\xe2\x63\xc4\x21\xfe\x3c\xf7\x93\xc2\xe1\x74\x16\x8c\xc9\x2a' + b'\xed\x65\x68\x0c\x49\x86\xa3\xba\x61\x1c\x88\x5d\xc4\x49\x3c\xd2' + b'\xee\x6b\x34\x0c\x1a\xa0\xa8\xb3\x58\x8a\x45\x11\xdf\x4f\x23\x2f' + b'\xa4\xe4\xf6\x3b\x2c\x8c\x88\x2d\x9e\x9b\x67\xab\x1c\x80\xda\x29' + b'\x53\x02\x1a\x54\x51\xca\xbf\xb1\x97\x22\x79\x81\x70\xfc\x00\xe9' + b'\x81\x36\x4e\x4f\xa0\x1c\x0b\x07\xea\x2f\x49\x2f\x0f\x25\x71\xd7' + b'\xf1\x30\x7d\x66\x6e\x83\x68\x38\x79\x13\xe3\x8c\x70\x9a\x4a\x9e' + b'\xa9\xe2\xd6\x10\x4f\x40\x14\x8e\x6c\x5e\x96\xb2\x46\x3e\xe8\x25' + b'\xef\x7c\xc5\x08\x18\xd4\x8b\x92\x26\xe3\xed\xfa\x88\x32\xe8\x97' + b'\x47\x70\xf8\x46\xde\xff\x8b\x0c\x4d\xb3\xb6\xfc\x69\xd6\x27\x5b' + b'\x76\x6f\x5b\x03\xf7\xc3\x11\x05\xc5\x1d\xfe\x92\x5f\xcb\xc2\x1c' + b'\x81\x69\x1b\xb8\xf8\x62\x58\xc7\xb4\xb3\x11\xd5\x1f\xf2\x16\xc1' + b'\xad\x8f\xa5\x1e\xb4\x5b\xe0\xda\x7f\x46\x7d\x1d\x9e\x6d\xc0\x74' + b'\x7f\x54\xa6\x2f\x43\x6f\x64\x08\xca\xe8\x0f\x05\x10\x9c\x9d\x9f' + b'\xbd\x67\x0c\x23\xf7\xa1\xe1\x59\x7b\xe8\xd4\x64\xec\x20\xca\xe9' + b'\x6a\xb9\x03\x73\x67\x30\x95\x16\xb6\xd9\x19\x53\xe5\xdb\xa4\x3c' + b'\xcd\x7c\xf9\xd8\x67\x9f\xfc\xc9\xe2\x8a\x6a\x2c\xf2\xed\xc8\xc1' + b'\x6a\x20\x99\x4c\x0d\xad\xd4\x3b\xa1\x0e\x95\x88\x46\xb8\x13\xe1' + b'\x06\x58\xd2\x07\xad\x5c\x1a\x74\xdb\xb5\xa7\x40\x57\xdb\xa2\x45' + b'\xa6\x12\xd0\x82\xdd\xed\x0a\xbd\xb3\x10\xed\x6c\xda\x39\xd2\xd6' + b'\x90\x82\x00\x76\x71\xe0\x21\xa0\x8f\xf0\xf3\x67\xc4\xf3\x40\xbd' + b'\x47\x16\x10\xdc\x7e\xf8\x1d\xe5\x13\x66\x87\xc7\x4a\x69\xc9\x63' + b'\x92\x82\xec\xee\x5a\x34\xfb\x96\x25\xc3\xb6\x68\xe1\x3c\x8a\x71' + b'\x74\xb5\xc1\x23\x99\xd6\xf7\xfb\xea\x98\xcd\x61\x3d\x4d\xe1\xd0' + b'\x34\xe1\xfd\x36\x10\x5f\x8e\x9e\xc6\xb6\x58\x0c\x55\xbe\x69\xa8' + b'\x56\x76\x4b\x1f\xd5\x90\x7e\x47\x5f\x2f\x25\x02\x5c\xef\x00\x64' + b'\xa0\x26\x9a\x18\x3c\x69\xc4\xff\x9a\x52\x41\x1b\xc9\x81\xc3\xac' + b'\x15\xe1\x17\x98\xdb\x2c\x9c\x10\x9b\xb2\xf9\x71\x4f\x56\x0f\x68' + b'\xfb\xd9\x2d\x5a\x86\x5b\x83\x03\xc8\x1e\xda\x5d\xe4\x8e\x82\xc3' + b'\xd8\x7e\x8b\x56\x52\xb5\x38\xa0\xc6\xa9\xb0\x77\xbd\x8a\xf7\x24' + b'\x70\x82\x1d\xc5\x95\x3c\xb5\xf0\x79\xa3\x89\x99\x4f\xec\x8c\x36' + b'\xc7\xd6\x10\x20\xe3\x30\x39\x3d\x07\x9c\xb2\xdc\x4f\x94\x9e\xe0' + b'\x24\xaa\xd2\x21\x12\x14\x41\x0f\xd4\x67\xb7\x99\xb1\xa3\xcb\x4d' + b'\x0c\x70\x0f\xc0\x36\xa7\x89\x30\x86\x14\x67\x68\xac\x7b\xee\xe4' + b'\x42\xd8\xb4\x36\xa4\xeb\x0f\xa8\x02\xf4\xcd\x23\xb3\xbc\x25\x4f' + b'\xcc\xd4\xee\xfc\xf2\x21\x0f\xc1\x6c\x99\x37\xe2\x7c\x47\xce\x77' + b'\xf0\x95\x2b\xcb\xf4\xca\x07\x03\x2a\xd2\x31\x00\xfd\x3e\x84\x86' + b'\x32\x8b\x17\x9d\xbf\xa7\xb3\x37\xe1\xb1\x8a\x14\x69\x00\x25\xe3' + b'\x56\x68\x9f\xaa\xa9\xb8\x11\x67\x75\x87\x4d\xf8\x36\x31\xcf\x38' + b'\x63\x1c\xf0\x6b\x47\x40\x5d\xdc\x0c\xe6\xc8\xc4\x19\xaf\xdd\x6e' + b'\x9e\xd9\x78\x99\x6c\xbe\x15\x1e\x0b\x9d\x88\xd2\x06\x9d\xee\xae' + b'\x8a\x0f\xe3\x2d\x2f\xf4\xf5\xf6\x16\xbf\x59\xbb\x34\x5c\xdd\x61' + b'\xed\x70\x1e\x61\xe5\xe3\xfb\x6e\x13\x9c\x49\x58\x17\x8b\xc8\x30' + b'\xcd\xed\x56\xad\x22\xcb\x63\xce\x26\xc4\xa5\xc1\x63\x0d\x0d\x04' + b'\x6e\xb6\xf9\xca\xbb\x2f\xab\xa0\xb5\x0a\xfa\x50\x0e\x02\x47\x05' + b'\x54\x3d\xb3\xb1\xc6\xce\x8f\xac\x65\x7e\x15\x9e\x4e\xcc\x55\x9e' + b'\x46\x32\x71\x9b\x97\xaa\x0d\xfb\x1b\x71\x02\x83\x96\x0b\x52\x77' + b'\x48\x87\x61\x02\xc3\x04\x62\xd7\xfb\x74\x0f\x19\x9c\xa0\x9d\x79' + b'\xa0\x6d\xef\x9e\x20\x5d\x0a\xc9\x6a\x58\xc9\xb9\x55\xad\xd1\xcc' + b'\xd1\x54\xc8\x68\xc2\x76\xc2\x99\x0f\x2e\xfc\xfb\xf5\x92\xcd\xdb' + b'\xa2\xed\xd9\x99\xff\x4f\x88\x50\xcd\x48\xb7\xb9\xf3\xf0\xad\x4d' + b'\x16\x2a\x50\xaa\x6b\x2a\x98\x38\xc9\x35\x45\x0c\x03\xa8\xcd\x0d' + b'\x74\x3c\x99\x55\xdb\x88\x70\xda\x6a\xc8\x34\x4d\x19\xdc\xcc\x42' + b'\x40\x94\x61\x92\x65\x2a\xcd\xfd\x52\x10\x50\x14\x6b\xec\x85\x57' + b'\x3f\xe2\x95\x9a\x5d\x11\xab\xad\x69\x60\xa8\x3b\x6f\x7a\x17\xf3' + b'\x76\x17\x63\xe6\x59\x7e\x47\x30\xd2\x47\x87\xdb\xd8\x66\xde\x00' + b'\x2b\x65\x37\x2f\x2d\xf1\x20\x11\xf3\x98\x7b\x4c\x9c\xd1\x76\xa7' + b'\xe1\x3d\xbe\x6f\xee\x2c\xf0\x19\x70\x63\x51\x28\xf0\x1d\xbe\x52' + b'\x5f\x4f\xe6\xde\xf2\x30\xb6\x50\x30\xf9\x15\x48\x49\xe9\xd2\xa8' + b'\xa9\x8d\xda\xf5\xcd\x3e\xaf\x00\x55\xeb\x15\xc5\x5b\x19\x0f\x93' + b'\x04\x27\x09\x6d\x54\xd7\x57\xb1\x47\x0a\xde\xf7\x1d\xcb\x11\x3c' + b'\xf5\x8f\x20\x40\x9d\xbb\x6b\x2c\xa9\x67\x3d\x78\xc2\x62\xb7\x0c') + + def __init__(self, base_address=0x800c000): + self.magic = b'OutSecurityBin' + self.jst = b'JST51' + self.foo = b'\x30\x02\x00\x30\x00\x40\x00\x47' + self.bar = (b'\x01\x0d\x02\x03\x04\x05\x06\x07' + b'\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f' + b'\x10\x11\x12\x13\x14\x15\x16\x17' + b'\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f' + b'\x20') + self.start = base_address + self.app = None + self.footer = b'OutputBinDataEnd' + self.header_fmt = '<16s7s9s16s33s47sLL120s' + self.footer_fmt = '<240s16s' + + def wrap(self): + bin = b'' + header = struct.Struct(self.header_fmt) + footer = struct.Struct(self.footer_fmt) + self.pad() + app = self.crypt(self.app) + bin += header.pack( + self.magic, self.jst, b'\xff' * 9, self.foo, + self.bar, b'\xff' * 47, self.start, len(app), + b'\xff' * 120) + bin += self.crypt(self.app) + bin += footer.pack(b'\xff' * 240, self.footer) + return bin + + +def main(): + def hex_int(x): + return int(x, 0) + + parser = argparse.ArgumentParser(description='Wrap and unwrap MD-380 firmware') + parser.add_argument('--wrap', '-w', dest='wrap', action='store_true', + default=False, + help='wrap app into firmware image') + parser.add_argument('--unwrap', '-u', dest='unwrap', action='store_true', + default=False, + help='unwrap app from firmware image') + parser.add_argument('--addr', '-a', dest='addr', type=hex_int, + default=0x800c000, + help='base address in flash') + parser.add_argument('--offset', '-o', dest='offset', type=hex_int, + default=0, + help='offset to skip in app binary') + parser.add_argument('input', nargs=1, help='input file') + parser.add_argument('output', nargs=1, help='output file') + args = parser.parse_args() + + if not (args.wrap ^ args.unwrap): + sys.stderr.write('ERROR: --wrap or --unwrap?') + sys.exit(5) + + print('DEBUG: reading "%s"' % args.input[0]) + with open(args.input[0], 'rb') as f: + input = f.read() + + if args.wrap: + if args.offset > 0: + print('INFO: skipping 0x%x bytes in input file' % args.offset) + + md = MD380FW(args.addr) + md.app = input[args.offset:] + if len(md.app) == 0: + sys.stderr.write('ERROR: seeking beyond end of input file\n') + sys.exit(5) + output = md.wrap() + print('INFO: base address 0x{0:x}'.format(md.start)) + print('INFO: length 0x{0:x}'.format(len(md.app))) + + elif args.unwrap: + md = MD380FW(args.addr) + try: + md.unwrap(input) + except AssertionError: + sys.stderr.write('WARNING: Funky header:\n') + for i in range(0, 256, 16): + hl = binascii.hexlify(input[i:i + 16]) + hl = ' '.join(hl[i:i + 2] for i in range(0, 32, 2)) + sys.stderr.write(hl + '\n') + sys.stderr.write('Trying anyway.\n') + output = md.app + #print('INFO: base address 0x{0:x}'.format(md.start)) + print('INFO: length 0x{0:x}'.format(len(md.app))) + + print('DEBUG: writing "%s"' % args.output[0]) + with open(args.output[0], 'wb') as f: + f.write(output) + + +if __name__ == "__main__": + main()