#!/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()