Created
September 2, 2025 23:39
-
-
Save MrARM/e41da1c69a35cf81870a564613f52cc3 to your computer and use it in GitHub Desktop.
Flipper Zero bin to dfu packer
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 | |
| """ | |
| DFU Binary Repacker - Firmware Construction Tool | |
| Build valid DFU containers from raw firmware for Flipper Zero | |
| """ | |
| import struct | |
| import sys | |
| import argparse | |
| import time | |
| import zlib | |
| from typing import List, Tuple | |
| # DFU file structure constants | |
| DFU_SUFFIX_LENGTH = 16 | |
| DFU_PREFIX_LENGTH = 11 | |
| DFU_TARGET_PREFIX_LENGTH = 274 | |
| # ANSI color codes for terminal styling | |
| class Colors: | |
| PURPLE = "\033[95m" | |
| CYAN = "\033[96m" | |
| DARKCYAN = "\033[36m" | |
| BLUE = "\033[94m" | |
| GREEN = "\033[92m" | |
| YELLOW = "\033[93m" | |
| RED = "\033[91m" | |
| BOLD = "\033[1m" | |
| UNDERLINE = "\033[4m" | |
| END = "\033[0m" | |
| def print_banner(): | |
| """Prints the script's banner.""" | |
| banner = f"""{Colors.CYAN} | |
| Be nice. Don't gatekeep. | |
| """ | |
| print(banner) | |
| def loading_animation(message, duration=0.5): | |
| """Display a cool loading animation.""" | |
| chars = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"] | |
| end_time = time.time() + duration | |
| i = 0 | |
| while time.time() < end_time: | |
| print( | |
| f"\r{Colors.GREEN}[{chars[i % len(chars)]}] {message}{Colors.END}", | |
| end="", | |
| flush=True, | |
| ) | |
| time.sleep(0.1) | |
| i += 1 | |
| print(f"\r{Colors.GREEN}[✓] {message}{Colors.END}") | |
| class DFUPacker: | |
| def __init__(self, vid: int, pid: int, version: int): | |
| self.vid = vid | |
| self.pid = pid | |
| self.version = version | |
| self.bcdDFU = 0x011A # DFU version 1.1a | |
| def _build_elements(self, binaries: List[Tuple[int, bytes]]) -> bytes: | |
| """Build the binary data blocks for all elements.""" | |
| elements_blob = bytearray() | |
| for addr, data in binaries: | |
| # Element Prefix: Address (4 bytes), Size (4 bytes) | |
| elements_blob.extend(struct.pack("<II", addr, len(data))) | |
| # Element Data | |
| elements_blob.extend(data) | |
| return bytes(elements_blob) | |
| def _build_target(self, elements_blob: bytes, num_elements: int) -> bytes: | |
| """Build a DfuSe target descriptor.""" | |
| target_blob = bytearray() | |
| # Target Prefix | |
| target_blob.extend(b"Target") # szSignature (6 bytes) | |
| target_blob.extend(b"\x00") # bAlternateSetting (1 byte) | |
| target_blob.extend(struct.pack("<I", 1)) # bTargetNamed (4 bytes, bool) | |
| # Target Name (255 bytes) | |
| # --- CHANGE: Updated target name for Flipper Zero --- | |
| target_name = b"Flipper Zero F7" | |
| target_blob.extend(target_name) | |
| target_blob.extend(b"\x00" * (255 - len(target_name))) | |
| # Target Size and Elements Count | |
| target_size = len(elements_blob) | |
| target_blob.extend(struct.pack("<II", target_size, num_elements)) | |
| # Append the actual element data | |
| target_blob.extend(elements_blob) | |
| return bytes(target_blob) | |
| def _build_dfuse_prefix(self, image_data: bytes, num_targets: int) -> bytes: | |
| """Build the main DfuSe prefix header.""" | |
| prefix = bytearray() | |
| prefix.extend(b"DfuSe") # szSignature (5 bytes) | |
| prefix.extend(b"\x01") # bVersion (1 byte) | |
| # Total image size (prefix + all targets) and number of targets | |
| image_size = len(image_data) + DFU_PREFIX_LENGTH | |
| prefix.extend(struct.pack("<IB", image_size, num_targets)) | |
| return bytes(prefix) | |
| def _build_suffix(self) -> bytes: | |
| """Build the DFU suffix (without CRC).""" | |
| suffix = bytearray() | |
| suffix.extend(struct.pack("<H", self.version)) # bcdDevice | |
| suffix.extend(struct.pack("<H", self.pid)) # idProduct | |
| suffix.extend(struct.pack("<H", self.vid)) # idVendor | |
| suffix.extend(struct.pack("<H", self.bcdDFU)) # bcdDFU | |
| suffix.extend(b"UFD") # ucDfuSignature | |
| suffix.extend(struct.pack("B", DFU_SUFFIX_LENGTH)) # bLength | |
| return bytes(suffix) | |
| def pack(self, binaries: List[Tuple[int, bytes]]) -> bytes: | |
| """ | |
| Construct the full DFU file from binary data. | |
| For Flipper, we usually have one target with one element. | |
| """ | |
| print(f"\n{Colors.YELLOW}[*] BUILDING DFU IMAGE...{Colors.END}") | |
| # 1. Build the elements block | |
| loading_animation("Constructing firmware elements", 1.0) | |
| elements_blob = self._build_elements(binaries) | |
| # 2. Build the target block (containing elements) | |
| loading_animation("Constructing DFU target descriptor", 1.0) | |
| # Assuming one target for all elements, which is standard for Flipper | |
| target_blob = self._build_target(elements_blob, len(binaries)) | |
| # 3. Build the DfuSe prefix | |
| loading_animation("Generating DfuSe file header", 1.0) | |
| dfuse_prefix = self._build_dfuse_prefix(target_blob, 1) | |
| # 4. Build the DFU suffix (without CRC) | |
| dfu_suffix_no_crc = self._build_suffix() | |
| # 5. Assemble the file for CRC calculation | |
| data_for_crc = dfuse_prefix + target_blob + dfu_suffix_no_crc | |
| # 6. Calculate and append CRC32 | |
| loading_animation("Calculating CRC32 checksum", 1.0) | |
| # --- FIX: Apply final XOR to match DfuSe specification --- | |
| crc = zlib.crc32(data_for_crc) ^ 0xFFFFFFFF | |
| final_dfu = data_for_crc + struct.pack("<I", crc) | |
| return final_dfu | |
| def parse_binary_arg(value: str) -> Tuple[int, str]: | |
| """Parse 'address:filepath' argument.""" | |
| try: | |
| addr_str, path = value.split(":", 1) | |
| return int(addr_str, 16), path | |
| except ValueError: | |
| raise argparse.ArgumentTypeError( | |
| f"Invalid format: '{value}'. Expected '0xAddress:filepath'" | |
| ) | |
| def main(): | |
| print_banner() | |
| parser = argparse.ArgumentParser( | |
| description="Repack raw binary files into a Flipper Zero DFU file." | |
| ) | |
| parser.add_argument( | |
| "binaries", | |
| nargs="+", | |
| type=parse_binary_arg, | |
| help="Binary file(s) to pack in '0xADDRESS:FILEPATH' format. Example: '0x08000000:firmware.bin'", | |
| ) | |
| parser.add_argument( | |
| "-o", "--output", required=True, help="Output DFU file path" | |
| ) | |
| parser.add_argument( | |
| "--vid", type=lambda x: int(x, 16), default=0x0483, help="USB Vendor ID (hex, default: 0483)" | |
| ) | |
| parser.add_argument( | |
| "--pid", type=lambda x: int(x, 16), default=0xDF11, help="USB Product ID (hex, default: DF11)" | |
| ) | |
| parser.add_argument( | |
| "--ver", type=lambda x: int(x, 16), default=0xFFFF, help="Device Version/bcdDevice (hex, default: 0200)" | |
| ) | |
| args = parser.parse_args() | |
| print(f"{Colors.YELLOW}[*] INITIALIZING DFU REPACKER...{Colors.END}") | |
| loading_animation("Loading DFU packer modules", 1.0) | |
| try: | |
| # Read all binary files into memory | |
| binary_data = [] | |
| for addr, path in args.binaries: | |
| print(f"{Colors.YELLOW}[*] READING SOURCE FILE: {Colors.BOLD}{path}{Colors.END}") | |
| with open(path, "rb") as f: | |
| data = f.read() | |
| binary_data.append((addr, data)) | |
| print(f"{Colors.GREEN}[+] Loaded {len(data):,} bytes from address 0x{addr:08X}{Colors.END}") | |
| # Create and run the packer | |
| packer = DFUPacker(vid=args.vid, pid=args.pid, version=args.ver) | |
| dfu_file_data = packer.pack(binary_data) | |
| # Write the final DFU file | |
| print(f"\n{Colors.YELLOW}[*] WRITING FINAL DFU FILE...{Colors.END}") | |
| with open(args.output, "wb") as f: | |
| f.write(dfu_file_data) | |
| loading_animation("Finalizing DFU container", 1.0) | |
| print(f"{Colors.GREEN}[+] DFU FILE CREATED SUCCESSFULLY!{Colors.END}") | |
| print(f"{Colors.CYAN} → Filename: {Colors.BOLD}{args.output}{Colors.END}") | |
| print(f"{Colors.CYAN} → Size: {Colors.BOLD}{len(dfu_file_data):,} bytes{Colors.END}") | |
| print(f"{Colors.CYAN} → Targets: {Colors.BOLD}1{Colors.END}") | |
| print(f"{Colors.CYAN} → Binaries Packed: {Colors.BOLD}{len(args.binaries)}{Colors.END}") | |
| print(f"\n{Colors.PURPLE}[✓] FIRMWARE REPACKING COMPLETE - READY TO FLASH!{Colors.END}") | |
| except IOError as e: | |
| print( | |
| f"{Colors.RED}[!] ERROR: Failed to read file - {e}{Colors.END}", | |
| file=sys.stderr, | |
| ) | |
| return 1 | |
| except Exception as e: | |
| print(f"{Colors.RED}[!] CRITICAL ERROR: {e}{Colors.END}", file=sys.stderr) | |
| print( | |
| f"{Colors.RED}[!] DFU packing failed. Check file paths and addresses.{Colors.END}" | |
| ) | |
| return 1 | |
| return 0 | |
| if __name__ == "__main__": | |
| sys.exit(main()) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment