Skip to content

Instantly share code, notes, and snippets.

@Wh1terat
Last active June 18, 2024 21:26
Show Gist options
  • Save Wh1terat/70ea3dc777ca5c793d06e771e3fec741 to your computer and use it in GitHub Desktop.
Save Wh1terat/70ea3dc777ca5c793d06e771e3fec741 to your computer and use it in GitHub Desktop.
Lumi FW Unpacker
#!/usr/bin/env python3
import os
import sys
import ctypes
import tarfile
from io import BytesIO, RawIOBase
from textwrap import dedent
from base64 import b64decode
__title__ = "fw_unpack"
__description__ = "Lumi FW Unpacker"
__version__ = "0.1"
__author__ = "Wh1terat"
__license__ = "MIT"
keys = {
"linux.bin": "b1b767ae89e84817e3e7554acc4c0aef",
"default": ""
}
class LumiHdr(ctypes.Structure):
_pack_ = 1
_fields_ = [
("magic", ctypes.c_char * 5),
("header_sz", ctypes.c_ushort),
("version", ctypes.c_char * 16),
("size", ctypes.c_uint32),
("payload_crc", ctypes.c_uint32),
("model_id", ctypes.c_char * 36),
("payload_md5", ctypes.c_char * 33),
("header_crc", ctypes.c_uint32),
]
class LumiTarStream(RawIOBase):
def __init__(self, fh, tar_offset):
self.generator = self.stream(fh, tar_offset)
def stream(self, fh, tar_offset):
fh.seek(tar_offset)
key = None
while True:
chunk = bytearray(fh.read(4096))
if not chunk:
break
if key is None:
key = chunk[64:96]
for i in range(len(chunk)):
chunk[i] = (chunk[i] - (key[i & 0x1F])) & 0xFF
yield chunk
def readinto(self, b):
try:
output = next(self.generator)
b[: len(output)] = output
return len(output)
except StopIteration:
return 0
def eprint(*args, **kwargs):
print(*args, file=sys.stderr, **kwargs)
def print_header(hdr):
eprint(
dedent(
f"""\
Magic: {hdr.magic.decode('utf-8')}
Header Length: {hdr.header_sz}
Model: {hdr.model_id.decode('utf-8')}
Version: {hdr.version.decode('utf-8')}
Payload Size: {hdr.size}
Payload CRC: {hdr.payload_crc}
Payload MD5: {hdr.payload_md5.decode('utf-8')}
Header CRC: {hdr.header_crc}
"""
)
)
def expand_key(key):
buff = bytearray(range(256))
k = 0
j = 0
for i in range(256):
j = (key[k] + buff[i] + j) & 0xFF
buff[j], buff[i] = buff[i], buff[j]
k = k + 1 if k != 15 else 0
return buff
def decrypt(key, data):
ekey = expand_key(key)
k = 0
j = 0
for i in range(len(data)):
j = (i + 1) & 0xFF
k = (k + ekey[j]) & 0xFF
ekey[k], ekey[j] = ekey[j], ekey[k]
data[i] ^= ekey[(ekey[j] + ekey[k]) & 0xFF]
return data
def get_key(data):
offsets = [
1024, 1058, 1072, 1159,
1357, 1258, 1269, 1301,
1309, 1407, 1496, 1432,
1495, 1508, 1511, 1579,
]
return bytearray([data[offset] for offset in offsets]).hex()
def unpack(fh, filename):
key = bytes.fromhex(keys.get(filename, keys["default"]))
if key:
with open(filename, "w+b") as fw:
while True:
chunk = bytearray(fh.read(0x4000))
if not chunk:
break
out = decrypt(key, chunk)
fw.write(out)
if keys["default"] == '':
keys['default'] = get_key(out)
def usage():
name = os.path.basename(__file__)
print(f'{__description__} v{__version__}')
print(f'Usage: {name} < firmware.bin')
sys.exit(1)
def main():
if sys.stdin.isatty():
usage()
fh = sys.stdin.buffer
hdr = LumiHdr()
fh.readinto(hdr)
print_header(hdr)
sig_b64 = fh.read(hdr.header_sz-104)
sig_bin = b64decode(sig_b64)
lumitar = LumiTarStream(fh, hdr.header_sz)
with tarfile.open(fileobj=lumitar, mode="r|", bufsize=4096) as tar:
for entry in tar:
print(f"Unpacking {entry.name}...", end="", flush=True)
unpack(tar.extractfile(entry), entry.name)
print('[DONE]')
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment