From 3738a758e3fc177fa68f76a56b8ad165f9ca96e7 Mon Sep 17 00:00:00 2001 From: BTChip Date: Sun, 22 Jan 2017 18:04:38 +0100 Subject: [PATCH] Compatibility with Blue firmware 2.0 (production release) --- ledgerblue/hexLoader.py | 49 ++++--- ledgerblue/hexParser.py | 292 ++++++++++++++++++++++++---------------- ledgerblue/loadApp.py | 62 ++++++--- ledgerblue/loadMCU.py | 2 +- 4 files changed, 252 insertions(+), 153 deletions(-) diff --git a/ledgerblue/hexLoader.py b/ledgerblue/hexLoader.py index 22ea0ff..e7ab4b9 100644 --- a/ledgerblue/hexLoader.py +++ b/ledgerblue/hexLoader.py @@ -22,6 +22,9 @@ import struct import hashlib import binascii +LOAD_SEGMENT_CHUNK_HEADER_LENGTH = 3 +MIN_PADDING_LENGTH = 1 + class HexLoader: def __init__(self, card, cla=0xF0, secure=False, key=None, relative=True): self.card = card @@ -89,7 +92,7 @@ class HexLoader: while (len(paddedData) % 16) != 0: paddedData += b'\x00' cipher = AES.new(self.key, AES.MODE_CBC, self.iv) - encryptedData = cipher.encrypt(paddedData) + encryptedData = cipher.encrypt(str(paddedData)) self.iv = encryptedData[len(encryptedData) - 16:] return encryptedData @@ -136,15 +139,27 @@ class HexLoader: if (signature != None): data += chr(len(signature)) + signature data = self.encryptAES(data) - self.exchange(self.cla, 0x00, 0x00, 0x00, data) + self.exchange(self.cla, 0x00, 0x00, 0x00, data) - def createApp(self, appflags, applength, appname, icon=None, path=None): - data = b'\x0B' + struct.pack('>I', applength) + struct.pack('>I', appflags) + struct.pack('>B', len(appname)) + appname - if (icon != None): - data += struct.pack('>B', len(icon))+ icon - if (path != None): + def createApp(self, appflags, applength, appname, icon=None, path=None, iconOffset=None, iconSize=None, appversion=None): + data = b'\x0B' + struct.pack('>I', applength) + struct.pack('>I', appflags) + struct.pack('>B', len(appname)) + appname + if iconOffset is None: + if not (icon is None): + data += struct.pack('>B', len(icon)) + icon + else: + data += b'\x00' + + if not (path is None): data += struct.pack('>B', len(path)) + path - + else: + data += b'\x00' + + if not iconOffset is None: + data += struct.pack('>I', iconOffset) + struct.pack('>H', iconSize) + + if not appversion is None: + data += struct.pack('>B', len(appversion)) + appversion + data = self.encryptAES(data) self.exchange(self.cla, 0x00, 0x00, 0x00, data) @@ -175,12 +190,12 @@ class HexLoader: result.append(item) return result - def load(self, erase_u8, max_length_per_apdu, hexAreas, bootaddr): + def load(self, erase_u8, max_length_per_apdu, hexFile): initialAddress = 0 - if (len(hexAreas) != 0) and self.relative: - initialAddress = hexAreas[0].getStart() + if self.relative: + initialAddress = hexFile.minAddr() sha256 = hashlib.new('sha256') - for area in hexAreas: + for area in hexFile.getAreas(): startAddress = area.getStart() - initialAddress data = area.getData() self.selectSegment(startAddress) @@ -192,8 +207,8 @@ class HexLoader: offset = 0 length = len(data) while (length > 0): - if length > max_length_per_apdu: - chunkLen = max_length_per_apdu + if length > max_length_per_apdu - LOAD_SEGMENT_CHUNK_HEADER_LENGTH - MIN_PADDING_LENGTH: + chunkLen = max_length_per_apdu - LOAD_SEGMENT_CHUNK_HEADER_LENGTH - MIN_PADDING_LENGTH else: chunkLen = length chunk = data[offset : offset + chunkLen] @@ -205,9 +220,9 @@ class HexLoader: self.crcSegment(0, len(data), crc) return sha256.hexdigest() - def run(self, hexAreas, bootaddr, signature=None): + def run(self, hexFile, bootaddr, signature=None): initialAddress = 0 - if (len(hexAreas) != 0) and self.relative: - initialAddress = hexAreas[0].getStart() + if self.relative: + initialAddress = hexFile.minAddr() self.boot(bootaddr - initialAddress, signature) diff --git a/ledgerblue/hexParser.py b/ledgerblue/hexParser.py index 1381886..3ac7dfe 100644 --- a/ledgerblue/hexParser.py +++ b/ledgerblue/hexParser.py @@ -18,139 +18,199 @@ """ class IntelHexArea: - def __init__(self, start, data): - self.start = start - self.data = data - self.bootAddr = 0 + def __init__(self, start, data): + self.start = start + self.data = data - def getStart(self): - return self.start + def getStart(self): + return self.start + + def getData(self): + return self.data + +def insertAreaSorted(areas, area): + i=0 + while i < len(areas): + if area.start < areas[i].start: + break + i+=1 + #areas = areas[0:i] + [area] + areas[i:i] = [area] + return areas - def getData(self): - return self.data class IntelHexParser: - def __init__(self, fileName): - self.areas = [] - lineNumber = 0 - startZone = None - startFirst = None - current = None - zoneData = b'' - file = open(fileName, "r") - for data in file: - lineNumber += 1 - data = data.rstrip('\r\n') - if len(data) == 0: - continue - if data[0] != ':': - raise Exception("Invalid data at line %d" % lineNumber) - data = bytearray.fromhex(data[1:]) #binascii.unhexlify(data[1:]) - count = data[0] - address = (data[1] << 8) + data[2] - recordType = data[3] - if recordType == 0x00: - if startZone == None: - raise Exception("Data record but no zone defined at line " + lineNumber) - if startFirst == None: - startFirst = address - current = startFirst - if address != current: - self.areas.append(IntelHexArea((startZone << 16) + startFirst, zoneData)) - zoneData = "" - startFirst = address - current = address - zoneData += data[4:4 + count] - current += count - if recordType == 0x01: - if len(zoneData) != 0: - self.areas.append(IntelHexArea((startZone << 16) + startFirst, zoneData)) - zoneData = "" - startZone = None - startFirst = None - current = None - if recordType == 0x02: - raise Exception("Unsupported record 02") - if recordType == 0x03: - raise Exception("Unsupported record 03") - if recordType == 0x04: - if len(zoneData) != 0: - self.areas.append(IntelHexArea((startZone << 16) + startFirst, zoneData)) - zoneData = "" - startZone = None - startFirst = None - current = None - startZone = (data[4] << 8) + data[5] - if recordType == 0x05: - self.bootAddr = ((data[4]&0xFF) << 24) + ((data[5]&0xFF) << 16) + ((data[6]&0xFF) << 8) + (data[7]&0xFF) - file.close() + # order by start address + def _addArea(self, area): + self.areas = insertAreaSorted(self.areas, area) - def getAreas(self): - return self.areas + def __init__(self, fileName): + self.bootAddr = 0 + self.areas = [] + lineNumber = 0 + startZone = None + startFirst = None + current = None + zoneData = b'' + file = open(fileName, "r") + for data in file: + lineNumber += 1 + data = data.rstrip('\r\n') + if len(data) == 0: + continue + if data[0] != ':': + raise Exception("Invalid data at line %d" % lineNumber) + data = bytearray.fromhex(data[1:]) + count = data[0] + address = (data[1] << 8) + data[2] + recordType = data[3] + if recordType == 0x00: + if startZone == None: + raise Exception("Data record but no zone defined at line " + lineNumber) + if startFirst == None: + startFirst = address + current = startFirst + if address != current: + self._addArea(IntelHexArea((startZone << 16) + startFirst, zoneData)) + zoneData = "" + startFirst = address + current = address + zoneData += data[4:4 + count] + current += count + if recordType == 0x01: + if len(zoneData) != 0: + self._addArea(IntelHexArea((startZone << 16) + startFirst, zoneData)) + zoneData = "" + startZone = None + startFirst = None + current = None + if recordType == 0x02: + raise Exception("Unsupported record 02") + if recordType == 0x03: + raise Exception("Unsupported record 03") + if recordType == 0x04: + if len(zoneData) != 0: + self._addArea(IntelHexArea((startZone << 16) + startFirst, zoneData)) + zoneData = "" + startZone = None + startFirst = None + current = None + startZone = (data[4] << 8) + data[5] + if recordType == 0x05: + self.bootAddr = ((data[4]&0xFF) << 24) + ((data[5]&0xFF) << 16) + ((data[6]&0xFF) << 8) + (data[7]&0xFF) + file.close() - def getBootAddr(self): - return self.bootAddr + def getAreas(self): + return self.areas + + def getBootAddr(self): + return self.bootAddr - def maxAddr(self): - addr = 0 - for a in self.areas: - if (a.start+len(a.data) > addr): - addr = a.start+len(a.data) - return addr + def maxAddr(self): + addr = 0 + for a in self.areas: + if (a.start+len(a.data) > addr): + addr = a.start+len(a.data) + return addr + + def minAddr(self): + addr = 0xFFFFFFFF + for a in self.areas: + if (a.start < addr): + addr = a.start + return addr import binascii class IntelHexPrinter: - def __init__(self, parser=None, eol="\r\n"): - self.areas = [] - self.eol = eol - self.bootAddr = 0 - # build bound to the parser - if (parser): - self.areas = parser.areas - self.bootAddr = parser.bootAddr - - def addArea(self, startaddress, data): - self.areas.append(IntelHexArea(startaddress, data)) + def addArea(self, startaddress, data): + #order by start address + #self.areas.append(IntelHexArea(startaddress, data)) + self.areas = insertAreaSorted(self.areas, IntelHexArea(startaddress, data)) - def setBootAddr(self, bootAddr): - self.bootAddr = int(bootAddr) + def __init__(self, parser=None, eol="\r\n"): + self.areas = [] + self.eol = eol + self.bootAddr = 0 + # build bound to the parser + if (parser): + for a in parser.areas: + self.addArea(a.start, a.data); + self.bootAddr = parser.bootAddr - def checksum(self, bin): - cks = 0 - for b in bin: - cks += b - cks = (-cks) & 0x0FF - return cks + def getAreas(self): + return self.areas - def _emit_binary(self, file, bin): - cks = self.checksum(bin) - file.write((":" + binascii.hexlify(bin) + hex(0x100+cks)[3:] + self.eol).upper()) + def getBootAddr(self): + return self.bootAddr - def writeTo(self, fileName, blocksize=32): - file = open(fileName, "w") - for area in self.areas: - off = 0 - # force the emission of selection record at start - oldoff = area.start + 0x10000 - while off < len(area.data): - # emit a offset selection record - if ((off & 0xFFFF0000) != (oldoff & 0xFFFF0000) ): - self._emit_binary(file, bytearray(("02000004" + hex(0x10000+(area.start>>16))[3:7]).decode('hex'))) + def maxAddr(self): + addr = 0 + for a in self.areas: + if (a.start+len(a.data) > addr): + addr = a.start+len(a.data) + return addr - # emit data record - if (off+blocksize > len(area.data)): - self._emit_binary(file, bytearray((hex(0x100+(len(area.data)-off))[3:] + hex(0x10000+off+(area.start&0xFFFF))[3:] + "00").decode('hex')) + area.data[off:len(area.data)]) - else: - self._emit_binary(file, bytearray((hex(0x100+blocksize)[3:] + hex(0x10000+off+(area.start&0xFFFF))[3:] + "00").decode('hex')) + area.data[off:off+blocksize]) + def minAddr(self): + addr = 0xFFFFFFFF + for a in self.areas: + if (a.start < addr): + addr = a.start + return addr - oldoff = off; - off += blocksize + def setBootAddr(self, bootAddr): + self.bootAddr = int(bootAddr) + + def checksum(self, bin): + cks = 0 + for b in bin: + cks += b + cks = (-cks) & 0x0FF + return cks + + def _emit_binary(self, file, bin): + cks = self.checksum(bin) + s = (":" + binascii.hexlify(bin) + hex(0x100+cks)[3:] + self.eol).upper() + if (file != None): + file.write(s) + else: + print(s) + + def writeTo(self, fileName, blocksize=32): + file = None + if(fileName != None): + file = open(fileName, "w") + for area in self.areas: + off = 0 + # force the emission of selection record at start + oldoff = area.start + 0x10000 + while off < len(area.data): + # emit a offset selection record + if ((off & 0xFFFF0000) != (oldoff & 0xFFFF0000) ): + self._emit_binary(file, bytearray(("02000004" + hex(0x10000+(area.start>>16))[3:7]).decode('hex'))) + + # emit data record + if (off+blocksize > len(area.data)): + self._emit_binary(file, bytearray((hex(0x100+(len(area.data)-off))[3:] + hex(0x10000+off+(area.start&0xFFFF))[3:] + "00").decode('hex')) + area.data[off:len(area.data)]) + else: + self._emit_binary(file, bytearray((hex(0x100+blocksize)[3:] + hex(0x10000+off+(area.start&0xFFFF))[3:] + "00").decode('hex')) + area.data[off:off+blocksize]) + + oldoff = off; + off += blocksize - bootAddrHex = hex(0x100000000+self.bootAddr)[3:] - file.write(":04000005"+bootAddrHex+hex(0x100+self.checksum( bytearray(("04000005"+bootAddrHex).decode('hex'))))[3:]+self.eol) + bootAddrHex = hex(0x100000000+self.bootAddr)[3:] + s = ":04000005"+bootAddrHex+hex(0x100+self.checksum( bytearray(("04000005"+bootAddrHex).decode('hex'))))[3:]+self.eol + if (file != None): + file.write(s) + else: + print(s) - file.write(":00000001FF"+self.eol) + s = ":00000001FF"+self.eol - file.close() - + if (file != None): + file.write(s) + else: + print(s) + + if (file != None): + file.close() \ No newline at end of file diff --git a/ledgerblue/loadApp.py b/ledgerblue/loadApp.py index ee5f402..8de5933 100644 --- a/ledgerblue/loadApp.py +++ b/ledgerblue/loadApp.py @@ -17,9 +17,11 @@ ******************************************************************************** """ +DEFAULT_ALIGNMENT = 1024 + from .ecWrapper import PrivateKey from .comm import getDongle -from .hexParser import IntelHexParser +from .hexParser import IntelHexParser, IntelHexPrinter from .hexLoader import HexLoader from .deployed import getDeployedSecretV1, getDeployedSecretV2 import argparse @@ -59,6 +61,9 @@ parser.add_argument("--rootPrivateKey", help="Set the root private key") parser.add_argument("--apdu", help="Display APDU log", action='store_true') parser.add_argument("--deployLegacy", help="Use legacy deployment API", action='store_true') parser.add_argument("--apilevel", help="Use given API level when interacting with the device", type=auto_int) +parser.add_argument("--delete", help="Delete app before installing it", action='store_true') +parser.add_argument("--params", help="Store icon and install parameters in a parameter section before the code", action='store_true') +parser.add_argument("--appVersion", help="Set the application version (text)") args = parser.parse_args() @@ -115,7 +120,34 @@ else: if len(args.path) > 1: print("Multiple path levels not supported using this API level, ignoring") else: - path = parse_bip32_path(args.path[0], args.apilevel) + path = parse_bip32_path(args.path[0], args.apilevel) + +icon = None +if not args.icon is None: + icon = bytearray.fromhex(args.icon) + +signature = None +if not args.signature is None: + signature = bytearray.fromhex(args.signature) + +#prepend app's data with the icon content (could also add other various install parameters) +printer = IntelHexPrinter(parser) +#todo build a TLV zone to keep install params +#todo dney nvm_write in that section ? +paramsSectionContent = [] +if icon: + paramsSectionContent = icon + +# prepend the param section (arbitrary) +if (args.params): + #take care of aligning the parameters sections to avoid possible invalid dereference of aligned words in the program nvram. + #also use the default MPU alignment + param_start = printer.minAddr()-len(paramsSectionContent)-(DEFAULT_ALIGNMENT-(len(paramsSectionContent)%DEFAULT_ALIGNMENT)) + printer.addArea(param_start, paramsSectionContent) + +# account for added regions (install parameters, icon ...) +appLength = printer.maxAddr() - printer.minAddr() + dongle = getDongle(args.apdu) @@ -125,23 +157,15 @@ else: secret = getDeployedSecretV2(dongle, bytearray.fromhex(args.rootPrivateKey), args.targetId) loader = HexLoader(dongle, 0xe0, True, secret) -if (not (args.appFlags & 2)): - loader.deleteApp(args.appName) +if (not (args.appFlags & 2)) and args.delete: + loader.deleteApp(args.appName) -appLength = 0 -for area in parser.getAreas(): - appLength += len(area.getData()) +#heuristic to guess how to pass the icon +if (args.params): + loader.createApp(args.appFlags, appLength, args.appName, None, path, 0, len(paramsSectionContent), args.appVersion) +else: + loader.createApp(args.appFlags, appLength, args.appName, icon, path, None, None, args.appVersion) -icon = None -if args.icon != None: - icon = bytes(bytearray.fromhex(args.icon)) - -signature = None -if args.signature != None: - signature = bytes(bytearray.fromhex(args.signature)) - - -loader.createApp(args.appFlags, appLength, args.appName, icon, path) -hash = loader.load(0x0, 0xE0, parser.getAreas(), args.bootAddr) +hash = loader.load(0x0, 0xF0, printer) print("Application hash : " + hash) -loader.run(parser.getAreas(), args.bootAddr, signature) +loader.run(printer, args.bootAddr, signature) diff --git a/ledgerblue/loadMCU.py b/ledgerblue/loadMCU.py index e5c8e02..a14d60a 100644 --- a/ledgerblue/loadMCU.py +++ b/ledgerblue/loadMCU.py @@ -47,5 +47,5 @@ dongle = getDongle(args.apdu) loader = HexLoader(dongle, 0xe0, False, None, False) loader.validateTargetId(args.targetId) -hash = loader.load(0xFF, 0xF0, parser.getAreas(), args.bootAddr) +hash = loader.load(0xFF, 0xF0, parser) loader.run(parser.getAreas(), args.bootAddr)