mirror of
https://github.com/openwrt/openwrt.git
synced 2026-10-13 02:28:43 +04:00
Mercusys devices whose stock web UI expects an fwup container reject the tplink-image-2022 container: the loader picks its verification algorithm from the fw-type string at offset 0x14, which that container does not write. Build the container the loader accepts, with the fw-type string, the support list and the MD5 placeholder digest in place. This is a separate script rather than a flag on tplink-mkimage-2022.py because only the first 0x14 bytes match, the BE length and the salted MD5 over file[4:]. That script writes 0xff filler up to 0x1014, a table there (>2I rootfs_size/num_items, then >I32s2I entries), data at a fixed 0x1814 base, the rootfs first and a packed >4B1I2I soft version. The fwup container has no table: 0x2c records of name[32] + >III base, next_off, size, each followed by its own data and chained through next_off until 0, the rootfs appended after the last record, and the soft version as ASCII soft_ver: in an ordinary record. Signed-off-by: Johan Alvarado <contact@c127.dev> Link: https://github.com/openwrt/openwrt/pull/24919 Signed-off-by: Robert Marko <robimarko@gmail.com>
92 lines
3.2 KiB
Python
Executable File
92 lines
3.2 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
# SPDX-License-Identifier: GPL-2.0-only
|
|
#
|
|
# Wrap an OpenWrt UBI image in a Mercusys "fwup" container that the stock
|
|
# web UI (nvrammanager) accepts via firmware upgrade.
|
|
#
|
|
# The stock loader selects its verification algorithm from the `fw-type`
|
|
# string in the header: only "Cloud" maps to RSA-2048; any other value falls
|
|
# back to an MD5-only integrity check (salt-seeded, keyless). We use that
|
|
# fallback, so no vendor signing key is required. Structure and MD5 range were
|
|
# reverse-engineered from /usr/bin/nvrammanager.
|
|
#
|
|
# Header (0x1014 bytes):
|
|
# [0x000] BE u32 file_len [0x004..0x13] MD5 [0x014] "fw-type:<name>"
|
|
# Payload (@0x1014): interleaved 44-byte records {name[32], base BE, next_off
|
|
# BE, size BE}, data inline after each record; rootfs UBI appended after the
|
|
# last record.
|
|
# MD5: over file[4:] with [0x04:0x14] set to the salt, stored back into
|
|
# [0x04:0x14].
|
|
|
|
import argparse
|
|
import hashlib
|
|
import struct
|
|
import sys
|
|
|
|
MD5_SALT = bytes.fromhex("7a2b15ed9b98596de504ab44ac2a9f4e")
|
|
HDR_LEN, REC_LEN = 0x1014, 0x2c
|
|
MIN_LEN, MAX_LEN = 0x1814, 0x3701814
|
|
|
|
|
|
def unescape(text):
|
|
"""Turn the literal \\r\\n / \\n escapes used in image Makefiles into
|
|
real line breaks, dropping the line-continuation padding around them."""
|
|
return text.replace("\\r\\n ", "\\r\\n").replace("\\r\\n", "\r\n") \
|
|
.replace("\\n ", "\\n").replace("\\n", "\n")
|
|
|
|
|
|
def record(name, base, next_off, size):
|
|
return name.ljust(0x20, b"\0") + struct.pack(">III", base, next_off, size)
|
|
|
|
|
|
def build(rootfs, fw_type, sections):
|
|
payload = b""
|
|
for i, (name, data) in enumerate(sections):
|
|
base = len(payload) + REC_LEN
|
|
next_off = 0 if i == len(sections) - 1 else base + len(data)
|
|
payload += record(name, base, next_off, len(data)) + data
|
|
payload += rootfs
|
|
|
|
img = bytearray(HDR_LEN) + payload
|
|
img[0x14:0x14 + 8 + len(fw_type)] = b"fw-type:" + fw_type
|
|
struct.pack_into(">I", img, 0, len(img))
|
|
img[0x04:0x14] = MD5_SALT
|
|
img[0x04:0x14] = hashlib.md5(bytes(img[4:])).digest()
|
|
return bytes(img)
|
|
|
|
|
|
def main():
|
|
ap = argparse.ArgumentParser(
|
|
description="Create a Mercusys fwup image from an OpenWrt UBI image")
|
|
ap.add_argument("rootfs", help="UBI image holding kernel and rootfs")
|
|
ap.add_argument("-o", "--out", required=True, help="output image")
|
|
ap.add_argument("--fw-type", default="MR80X",
|
|
help='fw-type string; anything but "Cloud" skips RSA')
|
|
ap.add_argument("--support", required=True,
|
|
help="contents of the support-list section")
|
|
ap.add_argument("--soft-ver", default="2.0.0 Build OpenWrt",
|
|
help="version reported in the soft-version section")
|
|
a = ap.parse_args()
|
|
|
|
if len(a.fw_type) > 0x20:
|
|
sys.exit("fwup: fw-type too long")
|
|
|
|
sections = [
|
|
(b"support-list", unescape(a.support).encode()),
|
|
(b"soft-version", ("soft_ver:%s\n\n" % a.soft_ver).encode()),
|
|
]
|
|
|
|
with open(a.rootfs, "rb") as f:
|
|
rootfs = f.read()
|
|
|
|
img = build(rootfs, a.fw_type.encode(), sections)
|
|
if not MIN_LEN <= len(img) <= MAX_LEN:
|
|
sys.exit("fwup: image length %d out of accepted range" % len(img))
|
|
|
|
with open(a.out, "wb") as f:
|
|
f.write(img)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|