Last active
June 18, 2024 21:26
-
-
Save Wh1terat/70ea3dc777ca5c793d06e771e3fec741 to your computer and use it in GitHub Desktop.
Lumi FW Unpacker
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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