141 lines
5.6 KiB
Python
141 lines
5.6 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""
|
|
Verify that ImGui font headers (fa.h, RobotoRegular.h) contain genuine
|
|
stb_compress-compressed TrueType font data and no executable payload.
|
|
|
|
Method: port of stb_decompress from imgui_draw.cpp (stb.h, public domain).
|
|
"""
|
|
import re, struct, sys, zlib
|
|
|
|
def parse_font_header(path):
|
|
"""Extract compressed byte stream from an ImGui font header."""
|
|
with open(path, 'rb') as f:
|
|
raw = f.read()
|
|
# detect BOM / encoding
|
|
if raw[:2] in (b'\xff\xfe', b'\xfe\xff'):
|
|
text = raw.decode('utf-16')
|
|
else:
|
|
text = raw.decode('latin-1')
|
|
# grab all hex u32 literals inside the array
|
|
values = re.findall(r'0x([0-9a-fA-F]{8})', text)
|
|
if not values:
|
|
raise ValueError("no array data found in " + path)
|
|
data = bytearray()
|
|
for v in values:
|
|
data += struct.pack('<I', int(v, 16))
|
|
# size sanity: header says compressed_size = N; expect N bytes
|
|
m = re.search(r'compressed_size\s*=\s*(\d+)', text)
|
|
declared = int(m.group(1)) if m else None
|
|
return bytes(data), declared
|
|
|
|
def stb_decompress(data):
|
|
"""Port of imgui_draw.cpp stb_decompress."""
|
|
def in2(x): return (data[x] << 8) + data[x+1]
|
|
def in3(x): return (data[x] << 16) + (data[x+1] << 8) + data[x+2]
|
|
def in4(x): return (data[x] << 24) + (data[x+1] << 16) + (data[x+2] << 8) + data[x+3]
|
|
|
|
if in4(0) != 0x57BC0000:
|
|
return None, "bad magic"
|
|
if in4(4) != 0:
|
|
return None, "stream >4GB"
|
|
olen = in4(8)
|
|
out = bytearray()
|
|
i = 16
|
|
n = len(data)
|
|
while i < n:
|
|
old_i = i
|
|
c = data[i]
|
|
if c >= 0x20:
|
|
if c >= 0x80:
|
|
dist = data[i+1] + 1; ln = c - 0x80 + 1; i += 2
|
|
# match: copy ln bytes from out[-dist]
|
|
for _ in range(ln): out.append(out[-dist])
|
|
elif c >= 0x40:
|
|
dist = in2(i) - 0x4000 + 1; ln = data[i+2] + 1; i += 3
|
|
for _ in range(ln): out.append(out[-dist])
|
|
else:
|
|
ln = c - 0x20 + 1
|
|
for _ in range(ln): out.append(data[i+1]); i += 1
|
|
i += 1
|
|
else:
|
|
if c >= 0x18:
|
|
dist = in3(i) - 0x180000 + 1; ln = data[i+3] + 1; i += 4
|
|
for _ in range(ln): out.append(out[-dist])
|
|
elif c >= 0x10:
|
|
dist = in3(i) - 0x100000 + 1; ln = in2(i+3) + 1; i += 5
|
|
for _ in range(ln): out.append(out[-dist])
|
|
elif c >= 0x08:
|
|
ln = in2(i) - 0x0800 + 1
|
|
for _ in range(ln): out.append(data[i+2]); i += 1
|
|
i += 2
|
|
elif c == 0x07:
|
|
ln = in2(i+1) + 1
|
|
for _ in range(ln): out.append(data[i+3]); i += 1
|
|
i += 3
|
|
elif c == 0x06:
|
|
dist = in3(i+1) + 1; ln = data[i+4] + 1; i += 5
|
|
for _ in range(ln): out.append(out[-dist])
|
|
elif c == 0x04:
|
|
dist = in3(i+1) + 1; ln = in2(i+4) + 1; i += 6
|
|
for _ in range(ln): out.append(out[-dist])
|
|
else:
|
|
# 0x00..0x03 / 0x05 are not used as token starts; 0x05 0xfa is terminator
|
|
if c == 0x05 and i+1 < n and data[i+1] == 0xfa:
|
|
break
|
|
return None, "unexpected token 0x%02x at %d" % (c, i)
|
|
if i == old_i:
|
|
return None, "stalled at %d" % i
|
|
|
|
if len(out) != olen:
|
|
return None, "len mismatch: out=%d olen=%d" % (len(out), olen)
|
|
# verify adler32: stored right after the 05 FA terminator (big-endian)
|
|
if i + 2 + 4 > n:
|
|
return None, "no adler trailer"
|
|
expected_adler = in4(i + 2)
|
|
if zlib.adler32(bytes(out)) != expected_adler:
|
|
return None, "adler mismatch: got 0x%08x expected 0x%08x" % (zlib.adler32(bytes(out)), expected_adler)
|
|
return bytes(out), None
|
|
def check_ttf(buf, name):
|
|
print("=" * 70)
|
|
print("[%s] decompressed size: %d bytes" % (name, len(buf)))
|
|
print(" head16:", buf[:16].hex())
|
|
# TTF/OTF sfnt version
|
|
sfnt = buf[:4]
|
|
if sfnt in (b'\x00\x01\x00\x00', b'OTTO', b'true', b'typ1'):
|
|
print(" sfnt version: OK (%s)" % sfnt.hex())
|
|
else:
|
|
print(" sfnt version: UNEXPECTED (%s)" % sfnt.hex())
|
|
num_tables = struct.unpack('>H', buf[4:6])[0]
|
|
print(" numTables: %d" % num_tables)
|
|
tags = []
|
|
for i in range(min(num_tables, 64)):
|
|
off = 12 + i * 16
|
|
if off + 16 > len(buf): break
|
|
tag = buf[off:off+4].decode('latin-1', 'replace')
|
|
tags.append(tag)
|
|
print(" tables:", ' '.join(tags))
|
|
# executable payload markers
|
|
mz = buf.find(b'MZ')
|
|
pe = buf.find(b'PE\x00\x00')
|
|
print(" MZ header at: %s | PE header at: %s" % (mz if mz>=0 else 'none', pe if pe>=0 else 'none'))
|
|
# any http urls / suspicious ascii
|
|
for pat in (b'http', b'powershell', b'cmd.exe', b'CreateRemoteThread', b'WinExec'):
|
|
idx = buf.find(pat)
|
|
if idx >= 0:
|
|
print(" !! found '%s' at offset %d" % (pat.decode(), idx))
|
|
|
|
for path in [r"F:\desktop\CFramework\CFramework\Framework\ImGui\Fonts\fa.h",
|
|
r"F:\desktop\CFramework\CFramework\Framework\ImGui\Fonts\RobotoRegular.h"]:
|
|
data, declared = parse_font_header(path)
|
|
print("=" * 70)
|
|
print("file:", path)
|
|
print(" raw bytes extracted: %d | declared compressed_size: %s" % (len(data), declared))
|
|
print(" magic:", data[:4].hex(), "| size field:", int.from_bytes(data[8:12], 'big'), "| adler:", hex(int.from_bytes(data[12:16], 'big')))
|
|
out, err = stb_decompress(data)
|
|
if err:
|
|
print(" DECOMPRESS FAILED:", err)
|
|
sys.exit(1)
|
|
check_ttf(out, path.split('\\')[-1])
|
|
print("=" * 70)
|
|
print("VERIFICATION COMPLETE")
|