169 lines
5.4 KiB
Python
169 lines
5.4 KiB
Python
#!/usr/bin/env python3
|
|
|
|
# -*- coding: utf-8 -*-
|
|
|
|
# Source: https://github.com/csftech/Xiaomi-OpenWrt-firmware-toolkit
|
|
# Author: @csftech (Sheng-Fu Chang)
|
|
# MIT Licensed
|
|
|
|
import argparse
|
|
import binascii
|
|
import ctypes
|
|
import hashlib
|
|
import logging
|
|
import os
|
|
import time
|
|
|
|
|
|
class ImageHeader(ctypes.Structure):
|
|
_fields_ = [
|
|
("magic", ctypes.c_uint),
|
|
("signature_offset", ctypes.c_uint),
|
|
("crc32_checksum", ctypes.c_uint),
|
|
("file_type", ctypes.c_ushort),
|
|
("model", ctypes.c_ushort),
|
|
("segment_offsets", ctypes.c_uint * 8),
|
|
]
|
|
|
|
|
|
class SegmentHeader(ctypes.Structure):
|
|
_fields_ = [
|
|
("magic", ctypes.c_uint),
|
|
("flash_address", ctypes.c_uint),
|
|
("length", ctypes.c_uint),
|
|
("partition", ctypes.c_uint),
|
|
("segment_name", ctypes.c_char * 32),
|
|
]
|
|
|
|
|
|
class Firmware:
|
|
def __init__(self, path):
|
|
self.path = path
|
|
self.image_header = ImageHeader()
|
|
self.fd = open(self.path, "rb")
|
|
|
|
def verify(self, ignore_hash=False):
|
|
logging.info("[Jobs] Verifying firmware image...")
|
|
assert self.fd.readinto(self.image_header) == ctypes.sizeof(self.image_header)
|
|
|
|
# magic
|
|
logging.info(f"firmware magic: {hex(self.image_header.magic)}")
|
|
assert self.image_header.magic in [0x31524448, 0x32524448]
|
|
|
|
if self.image_header.magic == 0x32524448:
|
|
logging.info("NOTE: firmware seems to be encrypted?")
|
|
|
|
# signature
|
|
self.fd.seek(self.image_header.signature_offset)
|
|
signature_length = int.from_bytes(self.fd.read(16), "little")
|
|
signature = self.fd.read()
|
|
logging.info(
|
|
f"firmware signature_offset: {hex(self.image_header.signature_offset)}"
|
|
)
|
|
logging.info(f"firmware signature_length: {signature_length}")
|
|
logging.info(f"firmware signature: {signature.hex()}")
|
|
assert len(signature) == signature_length
|
|
|
|
# crc32
|
|
self.fd.seek(12)
|
|
logging.info(
|
|
f"firmware crc32_checksum: {hex(self.image_header.crc32_checksum)}"
|
|
)
|
|
computed_checksum = ~binascii.crc32(self.fd.read()) & 0xFFFFFFFF
|
|
logging.info(f"computed crc32_checksum: {hex(computed_checksum)}")
|
|
assert self.image_header.crc32_checksum == computed_checksum
|
|
|
|
# md5
|
|
self.fd.seek(0)
|
|
m = hashlib.md5()
|
|
m.update(self.fd.read())
|
|
hash = m.hexdigest()
|
|
logging.info(f"computed md5 hash: {hash}")
|
|
filename_hash = os.path.basename(self.path).split("_")[-2]
|
|
try:
|
|
assert hash[(len(filename_hash) * -1) :] == filename_hash
|
|
except AssertionError:
|
|
if ignore_hash:
|
|
logging.warning(
|
|
f'Warning: hash "{filename_hash}" does not match expected!'
|
|
)
|
|
pass
|
|
else:
|
|
raise
|
|
|
|
self.fd.seek(0)
|
|
return True
|
|
|
|
def extract(self, dest: str = None):
|
|
logging.info("[Jobs] Extracting firmware...")
|
|
current_time = time.strftime("%Y%m%d_%H%M%S", time.localtime())
|
|
self.dest_dir = os.path.join(
|
|
dest, f'{os.path.basename(self.path).replace(".bin", "")}_{current_time}'
|
|
)
|
|
if dest is not None:
|
|
self.dest_dir = dest
|
|
os.mkdir(self.dest_dir)
|
|
logging.info(f"create destination directory: {self.dest_dir}")
|
|
|
|
for address in self.image_header.segment_offsets:
|
|
if address:
|
|
self.fd.seek(address)
|
|
segment_header = SegmentHeader()
|
|
assert self.fd.readinto(segment_header) == ctypes.sizeof(segment_header)
|
|
|
|
# extract segment
|
|
with open(
|
|
os.path.join(self.dest_dir, segment_header.segment_name.decode()),
|
|
"wb",
|
|
) as s:
|
|
s.write(self.fd.read(segment_header.length))
|
|
logging.info(
|
|
f'extracting segment: {segment_header.segment_name.decode("ascii")}'
|
|
)
|
|
|
|
|
|
def run(path, extract=False, dest=None, ignore_hash=False):
|
|
logging.info(f"[MSG] Input file: {path}")
|
|
firmware = Firmware(path)
|
|
if firmware.verify(ignore_hash):
|
|
logging.info(
|
|
"[Jobs] Verification success: it's a genuine firmware from Xiaomi."
|
|
)
|
|
|
|
if extract:
|
|
firmware.extract(dest)
|
|
logging.info("[Jobs] Extraction complete.")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
logging.basicConfig(format="%(asctime)s - %(message)s", level=logging.INFO)
|
|
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument(
|
|
"-d",
|
|
"--dest",
|
|
help="Destination directory to store extracted files, default=current working directory",
|
|
action="store",
|
|
)
|
|
parser.add_argument(
|
|
"-i",
|
|
"--ignore-hash",
|
|
help="Do not fail if md5 hash does not match",
|
|
action="store_true",
|
|
)
|
|
parser.add_argument("-e", "--extract", help="Input your firmware", action="store")
|
|
parser.add_argument("-s", "--show", help="Show firmware info", action="store")
|
|
args = parser.parse_args()
|
|
|
|
if args.extract:
|
|
firmware_path = os.path.abspath(args.extract)
|
|
dest_path = os.getcwd()
|
|
if args.dest:
|
|
dest_path = args.dest
|
|
run(firmware_path, extract=True, dest=dest_path, ignore_hash=args.ignore_hash)
|
|
elif args.show:
|
|
firmware_path = os.path.abspath(args.show)
|
|
run(firmware_path, ignore_hash=args.ignore_hash)
|
|
else:
|
|
parser.print_help()
|