From f03144da3c3b5cb92d1e8e2507694ae3ee83500b Mon Sep 17 00:00:00 2001 From: mike mcginty Date: Sun, 16 Jul 2017 01:48:01 -0400 Subject: [PATCH] Modified md380_fw to include KG5RKI's MD2017 work --- md380_fw.py | 162 +++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 128 insertions(+), 34 deletions(-) diff --git a/md380_fw.py b/md380_fw.py index d215613f..06ec2bcc 100755 --- a/md380_fw.py +++ b/md380_fw.py @@ -8,8 +8,114 @@ 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 = b'' + l = max(len(a), len(b)) + for i in range(l): + out += chr(ord(a[i % len(a)]) ^ ord(b[i % len(b)])) + return out + +class MD2017FW(TYTFW): + #Many thanks to KG5RKI + #https://github.com/travisgoodspeed/md380tools/issues/789 + key = ('00aa89891f4beccf424514540065eb66417d4c88495a210df2f5c8e638edbcb9' + 'fb357133010a7f9e3b2903b6493e42b83f9f90bdaa3a7146cecdfd183255894a' + '5fc8839ce4069e0a9d0d2fa1356dd792eafd638590cbf02fd9595327d306b8f5' + 'b2ca886cd026913bf25b61becddbf21ac9fd8d8804f4e8f19a0292bc24e990e4' + '7ea3494dce529f58c17a5fb5186edb786408d56f0d9d9fb39930817e2be65b3c' + '4aba8be5e472118993d2f12d1e0fd9d5438704e2b4ae5e9e5f4ce916f2e65f28' + '9f7919dc1d6f2ff8efcbe1cee8a73659efe0e28800106dda73bc922b81cfe6ce' + '0447badb7e2f41ca5dcdf6417d1c382bef7d370af9a9148e5dea4466de8b3656' + '018c358a119b8f2a6540f72ee6582874cbc4cb0fa7629be3a63cc76f130097e9' + '1eb15390dd9b613e908cad3c29414e5b0c1f664013244900d51ce2ed281853af' + 'e41cdc96ea18fe2e6519e01450c1f10939f5cf4544d5680d72f15f8823b9b1cf' + 'da36984341f8af236d50575e62bf5aa5daadcfc5425e3e34062304e90ecdf871' + '89674e40e925bc452e97dcc16822d25877b12e6916a8149b181a9ab8f03b71bf' + '7718c834ea856dbb325735e569d49f4a9968b4d8c79a316a303de89cd2eb64de' + '2eafccc84d0209ae01f92b736dbc09a2c73a28ba5d1bdfcad6f6b83ebbc518f9' + '369623a41983da4521e386137dc25a898a8f54b9e11564e393add046b3b1d736' + '1533956f56ef26a91c7f0e6c9fced82669cffe7b5a6f09dceec8f95bc397e7bd' + '55f0e9d10c3036017a348b27ddc8cda2ec62efa8d01116dd70b0fb25f15f91b7' + '7d34e974442d5276c169c4eb3f987f249bb1efe94be3d3109fcd9e4e47f11d4c' + '16665bfd06cec2307b888261cc2737d5ff22c6e6d4cc879b0687aa7bcd35d3a3' + 'a7f0081758fbcd562ff88d318c5b3cdc9f1e3b4672b77ca62a47e6568a14fbe5' + 'b839b868449cbc106621ad02871dd862030e17b12e89f85a95731b878674dc39' + 'd2a93298d199d788a76baa7cc656518fb45822d10f2b44dece7511b6c93fbfc8' + '7ca8405007da66e37a3e4b4850ecf08a3966244b1d85a85b5db3908a5c5becba' + '3e9ea838ef48b14c6702590e2dc9fd7c1a9ee5ca607f6bf9cb9760ab46b2ab36' + 'a0f333f790c900e9f71f9d7566d3c08ce06a2cf4e102d7df9e8748c28f2a4464' + '2b0fa936f3469ae2b1fddc2602f480e31231c371a7f4323661ed127740adfe6d' + '665bd29c1ea8c8601e04e1c9091387a8385a70eaba3fc525993084715f222379' + 'd93d76d21bd5d28bc49d730584171b04db4ffc0723c9d8d5d0b86759f770f9af' + '0d1e5c7ff2b7008a2d2e59827aea851f82772f6fe97cb36e8ded82d60d81c938' + '89674d4ca9359986e1215ce9f3730d20b53ad0cb143e9d1759379f91ab3cda3c' + 'd57e11e04a36e7a666dc44e2f79afa30fc00a9c2adf9e0f8bbfe8431d88976e2').decode('hex') + + def __init__(self, base_address=0x800c000): + self.magic = b'OutSecurityBin' + self.jst = b'MD-9600\x00\x00' + self.foo = '\x30\x02\x00\x30\x00\x40\x00\x47' + self.bar = ('\x02\x19\x0C\x03\x04\x05\x06\x07' + '\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f' + '\x10\x11\x12\x13\x14\x15\x16\x17' + '\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f' + '\x20') + self.foob = ('\x02\x00\x00\x00\x00\x00\x06\x00') + self.start = base_address + self.app = None + self.footer = 'OutputBinDataEnd' + self.header_fmt = '<16s9s7s16s33s43s8sLLL112s' + self.footer_fmt = '<240s16s' + self.rsrcSize = 0x5D400 + + def unwrap(self, img): + header = struct.Struct(self.header_fmt) + header = header.unpack(img[:256]) -class MD380FW(object): + self.start = header[6] + app_len = header[7] + self.app = self.crypt(img[256:256 + app_len]) + + 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' * 7, self.foo, + self.bar, b'\xff' * 43, self.foob, self.rsrcSize, self.start, len(app)-self.rsrcSize, + b'\xff' * 112) + bin += self.crypt(self.app) + bin += footer.pack(b'\xff' * 240, self.footer) + return bin + +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 = ( @@ -93,10 +199,6 @@ def __init__(self, base_address=0x800c000): self.header_fmt = '<16s7s9s16s33s47sLL120s' self.footer_fmt = '<240s16s' - def pad(self, align=512, byte=b'\xff'): - pad_length = (align - len(self.app) % align) % align - self.app += byte * pad_length - def wrap(self): bin = b'' header = struct.Struct(self.header_fmt) @@ -111,39 +213,24 @@ def wrap(self): bin += footer.pack(b'\xff' * 240, self.footer) return bin - 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]) +def radioFW(name): + radios = { + "MD2017":MD2017FW, + "MD380":MD380FW + } + for k in radios.iterkeys(): + if name.upper() in k: + return radios[k] + raise KeyError - 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 = b'' - l = max(len(a), len(b)) - for i in range(l): - out += chr(ord(a[i % len(a)]) ^ ord(b[i % len(b)])) - return out def main(): def hex_int(x): return int(x, 0) - parser = argparse.ArgumentParser(description='Wrap and unwrap MD-380 firmware') + parser = argparse.ArgumentParser(description='Wrap and unwrap MD-380 and MD2017 firmware') + parser.add_argument('--radio', '-r', dest='radioname', default=None, help='Radio model (MD380 or MD2017, default MD380 if not provided and can\'t be guessed from input filename)') #default gets set below parser.add_argument('--wrap', '-w', dest='wrap', action='store_true', default=False, help='wrap app into firmware image') @@ -168,10 +255,17 @@ def hex_int(x): with open(args.input[0], 'rb') as f: input = f.read() + if args.radioname is None: #if not explicitly set, try and guess from filename + radioname = "MD2017" if "TYT2017" in args.input[0] else "MD380" + print("Guessing %s for radio model"%(radioname)) + else: + radioname = args.radioname + if args.wrap: if args.offset > 0: print('INFO: skipping 0x%x bytes in input file' % args.offset) - md = MD380FW(args.addr) + + md = radioFW(radioname)(args.addr) md.app = input[args.offset:] if len(md.app) == 0: sys.stderr.write('ERROR: seeking beyond end of input file\n') @@ -181,7 +275,7 @@ def hex_int(x): print('INFO: length 0x{0:x}'.format(len(md.app))) elif args.unwrap: - md = MD380FW() + md = radioFW(radioname)(args.addr) try: md.unwrap(input) except AssertionError: @@ -192,7 +286,7 @@ def hex_int(x): 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: 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])