From 746d75bc8a4a951ee7025047b0706cce27dcda66 Mon Sep 17 00:00:00 2001 From: remittor Date: Tue, 1 Feb 2022 12:02:30 +0300 Subject: [PATCH] Add: build HDR1 images with custom kernel and rootfs --- xqimage.py | 231 ++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 218 insertions(+), 13 deletions(-) diff --git a/xqimage.py b/xqimage.py index 1ab56f2..8bfbe11 100644 --- a/xqimage.py +++ b/xqimage.py @@ -8,6 +8,9 @@ import platform import ctypes import binascii +sys.path.append(os.path.dirname(os.path.abspath(__file__))) +from envbuffer import * + class XQImgHdr(ctypes.Structure): _fields_ = [("magic", ctypes.c_uint), # HDR1 @@ -87,11 +90,23 @@ def get_modelid_by_name(name): return i return -1 +def DIE(msg): + print('ERROR:', msg) + sys.exit(1) + +def buf_align(buf, align, padfill = b'\x00'): + mod = len(buf) & (align - 1) + if mod > 0: + buf += padfill * (align - mod) + return buf + class XQImage(): model = None type = 0 version = None + align = 128*1024 + padfill = b'\xFF' files = [] # list of files def __init__(self, model, type = 0): @@ -100,6 +115,7 @@ class XQImage(): self.header = XQImgHdr() self.version = None self.files = [] + self.data = None def add_version(self, version, channel = 'release'): self.version = None @@ -111,24 +127,29 @@ class XQImage(): data += "\t" + "option CHANNEL '{}'\n".format(channel.lower()) data += "\t" + "option HARDWARE '{}'\n".format(self.model) self.version = data.encode('latin_1') - if len(self.version) & 3 != 0: - self.version += b'\x00' * (4 - len(self.version) & 3) - self.add_file(self.version, 'xiaoqiang_version') + self.version = buf_align(self.version, 16) + self.add_file(self.version, 'xiaoqiang_version', align = 16) - def add_file(self, data, name, mtd = None): + def add_file(self, data, name, mtd = None, align = 0, padfill = b'\xFF'): file = types.SimpleNamespace() file.data = data + if align is not None: + if align == 0: # use default + file.data = buf_align(data, self.align, self.padfill) + if align >= 2: + file.data = buf_align(data, align, padfill) file.header = XQImgFile() file.header.magic = int.from_bytes(b'\xBE\xBA', byteorder='little') file.header.rsvd0 = 0 file.header.addr = 0xFFFFFFFF - file.header.size = len(data) + file.header.size = len(file.data) file.header.mtd = 0xFFFF if mtd is None else mtd file.header.dummy = 0 file.header.name = name.encode('latin_1') self.files.append(file) def build_image(self, sign = None): + self.data = None buf = bytearray() self.header = XQImgHdr() self.header.magic = int.from_bytes(b'HDR1', byteorder='little') @@ -150,13 +171,14 @@ class XQImage(): buf[:ctypes.sizeof(self.header)] = bytes(self.header) self.header.crc32 = 0xFFFFFFFF - binascii.crc32(buf[12:]) # JAMCRC buf[:ctypes.sizeof(self.header)] = bytes(self.header) + self.data = buf return buf def save_image(self, filename, sign = None): self.outfilename = filename - buf = self.build_image(sign) + data = self.build_image(sign) with open(filename, 'wb') as file: - file.write(buf) + file.write(data) def build_sign(self): def i2b(value): @@ -164,21 +186,21 @@ class XQImage(): payload = None if self.model == "R3G": poffset = 0x1058 - payload = i2b(0x416078) + i2b(0) + i2b(0) + i2b(0x402810) + payload = i2b(0x416078) + i2b(0) + i2b(0) + i2b(0x402810) # 2.25.124, 2.28.44 if self.model == "R3600": # AX3600 poffset = 0x1070 - payload = i2b(0x415290) + i2b(0) + i2b(0x402634) + i2b(0) + payload = i2b(0x415290) + i2b(0) + i2b(0x402634) + i2b(0) # 1.0.17 ... 1.1.19 if self.model == "RA69": # AX6 poffset = 0x1070 - payload = i2b(0x4152A8) + i2b(0) + i2b(0x402634) + i2b(0) + payload = i2b(0x4152A8) + i2b(0) + i2b(0x402634) + i2b(0) # ... 1.1.10 if self.model == "RA70": # AX9000 poffset = 0x1078 - payload = i2b(0x4152D0) + i2b(0) + i2b(0x40265C) + i2b(0) + payload = i2b(0x4152D0) + i2b(0) + i2b(0x40265C) + i2b(0) # 1.0.82 ... 1.0.140 if self.model == "RA72": # AX6000 poffset = 0x1078 - payload = i2b(0x4152E0) + i2b(0) + i2b(0x402630) + i2b(0) + payload = i2b(0x4152E0) + i2b(0) + i2b(0x402630) + i2b(0) # 1.0.41 ... 1.0.55 if not payload: - raise OSError('HDR1 Payload is not defined for device "{}".'.format(self.model)) + DIE('HDR1 Payload is not defined for device "{}".'.format(self.model)) # add header of sign section (16 bytes) sign = i2b(poffset) + (b'\x00' * 12) # add fake sign @@ -190,4 +212,187 @@ class XQImage(): return sign +def create_xqimage(model, name, mtd, size, data, outfilename = None): + img = XQImage(model) + if data is None: + data = b'' + if len(data) > size: + data = data[:size] + filedata = data + if len(data) < size: + filedata += b'\xFF' * (size - len(data)) + #img.add_version("1.1.1") + img.add_file(filedata, name, mtd) + if outfilename: + img.save_image(outfilename) + else: + img.build_image() + return img + + +def build_xq_openwrt(fwdir, model, outfilename): + model = model.upper() + MAX_KERNEL_SIZE = 0x400000 # 4MiB + ERASE_SIZE=128*1024 + kernel = None + rootfs = None + fit = None + bl = None + fn_list = [f for f in os.listdir(fwdir) if os.path.isfile(os.path.join(fwdir, f))] + for i, fname in enumerate(fn_list): + fname = fwdir + fname + fsize = os.path.getsize(fname) + if fsize < 80*1024: + continue + with open(fname, "rb") as file: + fdata = file.read() + if fdata[:4] == b"\x27\x05\x19\x56": # uImage + print('Parse image file "{}" ...'.format(fname)) + pos = 0x0C + kernel_size = int.from_bytes(fdata[pos:pos+4], byteorder='big') + kernel_size += 0x40 + kernel_name = fdata[0x20:0x40] + if kernel_name.find(b'Breed') == 0 or kernel_name.find(b'NAND Flash') == 0: + if bl: + DIE('Second bootloader founded') + bl = types.SimpleNamespace() + bl.data = fdata + bl.type = 'breed' if kernel_name.find(b'Breed') == 0 else '' + if len(fdata) > 2*ERASE_SIZE or kernel_size > 2*ERASE_SIZE: + DIE('Bootloader size is too large! (size: {} KB)'.format(len(fdata) // 1024)) + continue + if kernel: + DIE('Second kernel founded') + if kernel_size < 0x100000: + DIE('Kernel size is too small! (size: {} KB)'.format(kernel_size // 1024)) + kernel = types.SimpleNamespace() + kernel.ostype = '' + kernel.data = fdata[:kernel_size] + if kernel_name[0:1] == b'\x03' or kernel_name[0:1] == b'\x04': # padavan kernel version + if kernel_name[2:3] == b'\x03': # padavan fw version + kernel.ostype = 'padavan' + kernel_size = int.from_bytes(kernel_name[0x1C:0x20], byteorder='big') + if kernel_size > MAX_KERNEL_SIZE: + DIE('Kernel size is too large! (size: {} KB)'.format(kernel_size // 1024)) + #kernel.data = fdata[:kernel_size] + x = fdata.find(b'hsqs', kernel_size) + if x < 0: + DIE('Rootfs not found in padavan firmware') + if fdata[x+28:x+32] != b'\x04\x00\x00\x00': + DIE('Rootfs not found in padavan firmware') + if rootfs: + DIE('Second rootfs founded') + kernel.data = fdata[:x] + if x > MAX_KERNEL_SIZE: + DIE('Padavan kernel size is too large! (size: {} KB)'.format(x // 1024)) + rootfs = types.SimpleNamespace() + rootfs.data = fdata[x:] + continue + if kernel_size > MAX_KERNEL_SIZE: + DIE('Kernel size is too large! (size: {} KB)'.format(kernel_size // 1024)) + if kernel_size + 0x100000 < len(fdata): + data = fdata[kernel_size:] + x = data.find(b'UBI#\x01\x00\x00\x00') + if x >= 0: + if rootfs: + DIE('Second rootfs founded') + rootfs = types.SimpleNamespace() + rootfs.data = data[x:] + if fdata[:8] == b'UBI#\x01\x00\x00\x00': + print('Parse image file "{}" ...'.format(fname)) + if rootfs: + DIE('Second rootfs founded') + rootfs = types.SimpleNamespace() + rootfs.data = fdata + if bl and not kernel: + pass + else: + if not kernel and not rootfs: + DIE('The firmware was not found in the "{}" folder!'.format(fwdir)) + if not rootfs: + DIE('Cannot found rootfs image') + x = rootfs.data.find(b'\x01\x00\x00\x06' + b'kernel' + b'\x00') + if x > 0x800 and x <= 0x4000: + if kernel: + DIE('Second kernel founded into FIT image') + fit = rootfs + rootfs = None + if not kernel and not fit: + DIE('Cannot found kernel image') + if kernel.ostype == 'padavan': + if not bl or (bl and bl.type != 'breed'): + DIE('Padavan firmware supported only with Breed bootloader') + if bl: + BREED_ENV_SIZE = 0x20000 + BREED_ENV_ADDR = 0x60000 + BREED_ENV_OFFSET = BREED_ENV_ADDR + if bl.type == 'breed': + if len(bl.data) > BREED_ENV_OFFSET: + data = bl.data[:BREED_ENV_OFFSET] + else: + data = buf_align(bl.data, BREED_ENV_OFFSET, b'\xFF') + bl.data = data + env_file = fwdir + 'breed_env.txt' + if os.path.exists(env_file): + with open(env_file, 'r', encoding = 'latin_1') as file: + env_data = file.read() + print('Parse ENV file: "{}"'.format(env_file)) + env = EnvBuffer(env_data, '\n', encoding = 'latin_1') + env_data = env.pack(BREED_ENV_SIZE) + bl.data += b'ENV\x00' + env_data[4:] + if model == 'R3P' and len(bl.data) > 2*ERASE_SIZE: + DIE('Router R3P have small bootloader partition') + img = XQImage(model) + mtd = None + if fit: + if model == 'R3600': + mtd = { 'rootfs': 12, 'rootfs_1': 13 } + if not mtd: + DIE('Device "{}" currently not supported.'.format(model)) + img.add_file(fit.data, 'firmware_squashfs.bin', mtd['rootfs']) + img.add_file(fit.data, 'firmware_squashfs.bin', mtd['rootfs_1']) + else: + mtd = { 'bootloader': 1, 'kernel0': 8, 'kernel1': 9, 'rootfs0': 10, 'rootfs1': 11, 'overlay': 12 } + if bl: + img.add_file(bl.data, 'bootloader.bin', mtd['bootloader']) + if kernel: + img.add_file(kernel.data, 'kernel0.bin', mtd['kernel0']) + img.add_file(kernel.data, 'kernel1.bin', mtd['kernel1']) + img.add_file(rootfs.data, 'rootfs.bin', mtd['rootfs0']) + img.save_image(outfilename) + + +# python xqimage.py R3600 crash.bin 10 0x80000 "\x12\x34\xAB\xCD" r3600_crash_1234.bin +# hexdump -v -s 6 -n 4 -e '2/1 "%02x "' /dev/mtd10 | echo "" +# python xqimage.py R3G crash.bin 5 0x40000 "\xA5\x5A\x00\x00" r3g_crash_A55A.bin +# python xqimage.py R3G crash.bin 5 0x40000 "" r3g_crash.bin +# python xqimage.py R3G miwifi_r3g_openwrt_21.02.bin + +if __name__ == "__main__": + fn = '' + + if len(sys.argv) > 6: + model = sys.argv[1] + name = sys.argv[2] + mtd = int(sys.argv[3]) + size = sys.argv[4] + size = int(size, 16) if size.lower().startswith('0x') else int(size, 10) + if size <= 0: + size = 128*1024 + data = None + if len(sys.argv[5]) > 0: + data = sys.argv[5] + data = data.encode('latin_1').decode('unicode-escape').encode('latin_1') + outfilename = sys.argv[6] + create_xqimage(model, name, mtd, size, data, outfilename) + fn = outfilename + + if len(sys.argv) == 3: + model = sys.argv[1] + fn = sys.argv[2] + build_xq_openwrt('firmware/', model, fn) + if fn: + print("#### File '{}' created ####".format(fn)) + else: + print("ERROR: Incorrect arguments!")