Last active
February 10, 2026 16:20
-
-
Save nemesifier/b5ed320f6d4ef0ef6782148b5d300c81 to your computer and use it in GitHub Desktop.
Minimal implementation of openwrt's fwtool -i in Python
This file contains hidden or 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 | |
| """ | |
| Extract OpenWrt firmware metadata from firmware images. | |
| Reimplementation of fwtool -i functionality in Python. | |
| """ | |
| import argparse | |
| import json | |
| import struct | |
| import sys | |
| # Magic number "FWx0" in big-endian | |
| FWIMAGE_MAGIC = 0x46577830 | |
| FWIMAGE_INFO = 1 | |
| FWIMAGE_SIGNATURE = 0 | |
| # Struct format for fwimage_trailer (big-endian) | |
| # magic: uint32, crc32: uint32, type: uint8, pad: 3 bytes, size: uint32 | |
| TRAILER_FORMAT = ">II B 3s I" | |
| TRAILER_SIZE = struct.calcsize(TRAILER_FORMAT) | |
| # Struct format for fwimage_header (big-endian) | |
| # version: uint32, flags: uint32 | |
| HEADER_FORMAT = ">II" | |
| HEADER_SIZE = struct.calcsize(HEADER_FORMAT) | |
| # CRC32 polynomial used by fwtool | |
| CRC32_POLYNOMIAL = 0xedb88320 | |
| def crc32_filltable(): | |
| """Generate CRC32 lookup table.""" | |
| table = [] | |
| for i in range(256): | |
| c = i | |
| for _ in range(8): | |
| c = ((c >> 1) ^ CRC32_POLYNOMIAL) if (c & 1) else (c >> 1) | |
| table.append(c) | |
| return table | |
| # Global CRC32 table | |
| CRC32_TABLE = crc32_filltable() | |
| def crc32_block(val, data): | |
| """ | |
| Calculate CRC32 over a block of data. | |
| Matches the crc32_block function in the C code. | |
| """ | |
| for byte in data: | |
| val = CRC32_TABLE[(val & 0xff) ^ byte] ^ (val >> 8) | |
| return val | |
| def extract_metadata(firmware_path): | |
| """ | |
| Extract metadata from an OpenWrt firmware image. | |
| Args: | |
| firmware_path: Path to the firmware image file | |
| Returns: | |
| dict: Parsed JSON metadata, or None if not found | |
| """ | |
| with open(firmware_path, 'rb') as f: | |
| # Read the entire file | |
| data = f.read() | |
| file_size = len(data) | |
| # Search for trailers from the end of the file | |
| # We need to scan backwards looking for the magic number | |
| offset = file_size - TRAILER_SIZE | |
| while offset >= 0: | |
| # Extract potential trailer | |
| trailer_data = data[offset:offset + TRAILER_SIZE] | |
| if len(trailer_data) < TRAILER_SIZE: | |
| break | |
| # Unpack the trailer | |
| magic, crc32_val, type_val, pad, size = struct.unpack(TRAILER_FORMAT, trailer_data) | |
| if magic != FWIMAGE_MAGIC: | |
| offset -= 1 | |
| continu | |
| # Calculate data boundaries | |
| # Size field includes trailer + data before it | |
| data_start = offset - (size - TRAILER_SIZE) | |
| data_end = offset | |
| if data_start < 0: | |
| # Invalid size, keep searching | |
| offset -= 1 | |
| continue | |
| # Calculate CRC over data from start of file to start of trailer | |
| # This matches how fwtool calculates it - over all preceding data | |
| crc_data = data[:data_end] | |
| calculated_crc = crc32_block(0xffffffff, crc_data) | |
| if calculated_crc != crc32_val: | |
| # CRC mismatch, but could be another valid trailer earlier | |
| offset = data_start | |
| continue | |
| # Check the type | |
| if type_val == FWIMAGE_INFO: | |
| # This is metadata | |
| # Skip the header (8 bytes: version + flags) | |
| metadata_start = data_start + HEADER_SIZE | |
| metadata_data = data[metadata_start:data_end] | |
| # Parse as JSON | |
| try: | |
| metadata = json.loads(metadata_data.decode('utf-8')) | |
| return metadata | |
| except (json.JSONDecodeError, UnicodeDecodeError): | |
| # Invalid JSON, keep searching | |
| offset = data_start | |
| continue | |
| # Move to the next potential trailer (before this one's data) | |
| offset = data_start | |
| return None | |
| def main(): | |
| parser = argparse.ArgumentParser( | |
| description='Extract OpenWrt firmware metadata' | |
| ) | |
| parser.add_argument('-i', '--input', required=True, | |
| help='Input firmware image file') | |
| parser.add_argument('-o', '--output', | |
| help='Output file for metadata (default: stdout)') | |
| args = parser.parse_args() | |
| metadata = extract_metadata(args.input) | |
| if metadata is None: | |
| print("Error: Metadata not found in firmware image", file=sys.stderr) | |
| sys.exit(1) | |
| # Format as JSON | |
| output = json.dumps(metadata, indent=4) | |
| if args.output: | |
| with open(args.output, 'w') as f: | |
| f.write(output) | |
| else: | |
| print(output) | |
| if __name__ == '__main__': | |
| main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment