Skip to content

Instantly share code, notes, and snippets.

@nemesifier
Last active February 10, 2026 16:20
Show Gist options
  • Select an option

  • Save nemesifier/b5ed320f6d4ef0ef6782148b5d300c81 to your computer and use it in GitHub Desktop.

Select an option

Save nemesifier/b5ed320f6d4ef0ef6782148b5d300c81 to your computer and use it in GitHub Desktop.
Minimal implementation of openwrt's fwtool -i in Python
#!/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