Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 

Repository files navigation

PDFuzzer

PDFuzzer is an LLM-assisted fuzzer for JavaScript engines embedded in PDF readers. It turns documented and undocumented API specifications into per-parameter context-free grammars (CFGs), optionally infers relationships between APIs, generates JavaScript-bearing PDF files, and executes them under a Windows monitor.

This repository contains the implementation, but not all generated data. In particular, the grammar directory passed to fuzzing/run.py -p must be created before the fuzzer is run. The Data Preparation section identifies the producer or external prerequisite for each input used by the fuzzer.

Run fuzzing campaigns only against software you are authorized to test. A disposable, isolated Windows VM is strongly recommended because the monitor opens untrusted PDFs, interacts with application windows, and terminates reader processes.

Contents

Workflow

The latest PDFuzzer pipeline has two parallel branches after specification extraction: grammar generation and relationship inference.

Official manuals ──> API manual parser ───────────────┐
                                                      ├─> unified API specs
Execution traces ──> differential analysis ──> LLM ──┘          │
                                                                 ├─> parameter-level CFGs ─────────┐
                                                                 │                                  │
                                                                 └─> candidate/symbolic relations ─┤
                                                                                                    v
                                                                  instantiate, sequence, and Z3-solve API calls
                                                                                                    │
                                                                                                    v
                                                                                         generate PDFs and monitor

The differential-analysis step that produces raw undocumented API signatures is external to this repository. PDFuzzer provides the subsequent semantic recovery, grammar generation, relationship inference, PDF generation, and monitoring stages.

Requirements

  • Python 3.8 or later.
  • Windows for fuzzing/run.py and fuzzing/monitor.py. The current monitor imports Windows-only packages even when --dry is used.
  • At least one supported target reader:
    • Adobe Acrobat Reader DC
    • Foxit PDF Reader
    • PDF-XChange Editor
  • An OpenAI or Anthropic API key for LLM-backed preprocessing. Candidate relationship inference specifically requires an OpenAI API key and uses gpt-4o.
  • Network access while downloading the Adobe API manuals and invoking an LLM.

Installation

Clone the repository, create a virtual environment, and install its dependencies:

git clone https://github.com/ucsb-seclab/PDFuzzer.git
Set-Location PDFuzzer
python -m venv .venv
.\.venv\Scripts\Activate.ps1
python -m pip install -r requirements.txt

Set the key for the model that will be used:

# PowerShell
$env:OPENAI_API_KEY = "your-openai-api-key"
$env:ANTHROPIC_API_KEY = "your-anthropic-api-key"
# Bash
export OPENAI_API_KEY="your-openai-api-key"
export ANTHROPIC_API_KEY="your-anthropic-api-key"

The examples below use gpt-4o, which is accepted by the current model handler. Only the key for the selected provider is needed, except that RAG_relation_infer_4o.py always uses OpenAI.

Data Preparation

Which script produces each dataset?

Data Producer Consumer
pre_fuzz/document_parser/output/ pre_fuzz/document_parser/web_scraper.py json_parser_web.py
pre_fuzz/document_parser/json/ pre_fuzz/document_parser/json_parser_web.py Semantic recovery, grammar generation, and symbolic relationship inference
pre_fuzz/data/undoc_<target>/ External differential analysis of execution traces; no generator is included in this repository undoc_semantic_recovery.py
pre_fuzz/results/undoc_<target>_description/ pre_fuzz/undoc_semantic_recovery.py Grammar generation and symbolic relationship inference
fuzzing/data/object_grammar_param_all_<target>/ pre_fuzz/grammar_generator_param.py Required by fuzzing/run.py -p
pre_fuzz/relation_infer/documents/all_apis.txt Included API-name list RAG_relation_infer_4o.py
pre_fuzz/relation_infer/documents/Undoc_APIs_4o.json External aggregate; no exporter is included in this repository RAG_relation_infer_4o.py
fuzzing/config/all_relation.json pre_fuzz/relation_infer/RAG_relation_infer_4o.py Required by fuzzing/run.py --relation
fuzzing/config/all_symbolic.json pre_fuzz/relation_infer/Symbolic_relation_infer.py Required by fuzzing/run.py --symbolic
fuzzing/test/*.pdf fuzzing/run.py fuzzing/monitor.py or fuzzing/run.py --run

A basic run needs only a generated grammar tree. --relation additionally requires all_relation.json; --symbolic additionally requires all_symbolic.json. The full relationship-aware mode normally enables both flags.

Several scripts resolve prompts/, documents/, config/, and test/ relative to the current working directory. Run each command from the directory shown below. The commands use PowerShell and return to the repository root at the end of each block unless stated otherwise.

1. Parse documented APIs

From the repository root:

Set-Location pre_fuzz/document_parser
python web_scraper.py -m A
python web_scraper.py -m D
python json_parser_web.py
Set-Location ../..

web_scraper.py accepts only -m A (the main JavaScript API manual) or -m D (the Doc API manual). It writes extracted text to output/ and caches downloaded HTML in result/. json_parser_web.py has no command-line arguments; it converts its fixed ./output/ input into structured JSON under ./json/.

The resulting repository-relative paths are:

pre_fuzz/document_parser/output/
pre_fuzz/document_parser/json/

2. Recover undocumented API specifications (optional)

The -u input is a directory of raw signatures obtained through external differential analysis. It must use this layout:

pre_fuzz/data/undoc_adobe/
└── <Object>/
    ├── methods/
    │   └── <API>.json
    └── properties/
        └── <API>.json

Once those signatures are available, run the semantic recovery stage from pre_fuzz/:

Set-Location pre_fuzz
python undoc_semantic_recovery.py `
    -d document_parser/json `
    -u data/undoc_adobe `
    -o results/undoc_adobe_description `
    -m gpt-4o
Set-Location ..

Replace adobe with the target represented by the raw signatures. This stage does not itself collect execution traces or perform differential analysis.

3. Generate the grammar tree required by run.py

Generate grammars for the documented APIs from pre_fuzz/:

Set-Location pre_fuzz
python grammar_generator_param.py `
    -i document_parser/json `
    -o ../fuzzing/data/object_grammar_param_all_adobe `
    -m gpt-4o
Set-Location ..

If undocumented specifications were recovered in the previous step, add them to the same output tree:

Set-Location pre_fuzz
python grammar_generator_param.py `
    -i results/undoc_adobe_description `
    -o ../fuzzing/data/object_grammar_param_all_adobe `
    -m gpt-4o
Set-Location ..

The output contains one directory per object, API_INFO.json for each API, and grammar.json for each parameter. This generated directory is the value passed to run.py -p; it is not included in a clean checkout.

4. Generate relationship data (optional)

Create fuzzing/config/ before writing relationship outputs.

Candidate relationship inference uses the API list in pre_fuzz/relation_infer/documents/all_apis.txt and writes all_relation.json. The checked-in list contains one Object.API name per line; it is not regenerated or synchronized with newly produced specifications. Review and update it whenever the documented or undocumented spec set changes.

New-Item -ItemType Directory -Force fuzzing/config | Out-Null
Set-Location pre_fuzz/relation_infer
python RAG_relation_infer_4o.py `
    -i documents/all_apis.txt `
    -o ../../fuzzing/config/all_relation.json
Set-Location ../..

Important: RAG_relation_infer_4o.py also has a hard-coded dependency on pre_fuzz/relation_infer/documents/Undoc_APIs_4o.json. That aggregate undocumented-specification file is not present in a clean checkout, and this repository does not include a script that exports it. Supply the file before running this step; the per-file output of undoc_semantic_recovery.py is not a drop-in replacement for this aggregate file. Without it, candidate and full relationship-aware modes cannot be prepared from the clean checkout; basic grammar-based fuzzing remains available.

After candidate relationships have been generated, infer strong symbolic relationships. The Adobe example below requires the Step 2 output at pre_fuzz/results/undoc_adobe_description/:

Set-Location pre_fuzz/relation_infer
$env:PYTHONPATH = ".."
python Symbolic_relation_infer.py `
    -d ../document_parser/json `
    -u ../results/undoc_adobe_description `
    -r ../../fuzzing/config/all_relation.json `
    -o ../../fuzzing/config/all_symbolic.json `
    -m gpt-4o
Set-Location ../..

-d and -u must point to existing documented and undocumented specification trees. If Step 2 was skipped, create an empty tree from the repository root and pass ../results/undoc_empty to -u instead:

New-Item -ItemType Directory -Force pre_fuzz/results/undoc_empty | Out-Null

Setting PYTHONPATH to .. lets the script import pre_fuzz/agentlib while it is run from relation_infer/, where its prompt files are resolved.

Running PDFuzzer

All fuzzing commands must be run from fuzzing/ because the program resolves data/, config/, test/, save/, and runlog.txt against the current working directory:

Set-Location fuzzing

The current defaults in run.py are 30,000 PDF files (TNUM) and 2,048 API calls per file (SNUM). These values are source constants, not CLI options. Reduce them in run.py before a short smoke test.

Generate PDFs without opening a reader

python run.py `
    -p data/object_grammar_param_all_adobe `
    -t adobe `
    --dry

Generate and execute PDFs

python run.py `
    -p data/object_grammar_param_all_adobe `
    -t adobe

Use foxit or xchange as the target when the corresponding reader is installed and configured. If the grammar tree includes target-specific undocumented APIs, also generate and pass the matching object_grammar_param_all_foxit or object_grammar_param_all_xchange tree.

Enable candidate and symbolic relationships

This command requires both JSON files generated in the optional relationship stage:

python run.py `
    -p data/object_grammar_param_all_adobe `
    -t adobe `
    --relation `
    --symbolic

Execute PDFs already in fuzzing/test/

python run.py `
    -p data/object_grammar_param_all_adobe `
    -t adobe `
    --run

Although --run skips generation, -p remains required and the grammar tree is loaded during initialization.

Monitor one PDF

python monitor.py -t adobe -i 0.pdf

-i is a file name under fuzzing/test/, not a path such as test/0.pdf. The monitor adds the test/ prefix itself. Standalone monitor.py saves a non-finish case under save/<status>/, but it does not append to runlog.txt.

run.py options

Option Meaning
-p, --base_directory Generated grammar tree; required in every mode
-t, --target Reader monitor: adobe, foxit, or xchange
--dry Generate PDFs without executing them
--relation Load config/all_relation.json and prefer candidate-related APIs
--symbolic Load config/all_symbolic.json and solve strong constraints with Z3
--run Execute existing PDFs from test/ instead of generating new ones

Configuration

PDF reader executable paths

The monitor currently uses these default paths:

Adobe:   C:\Program Files (x86)\Adobe\Acrobat Reader DC\Reader\AcroRd32.exe
Foxit:   C:\Program Files (x86)\Foxit Software\Foxit PDF Reader\FoxitPDFReader.exe
XChange: C:\Program Files\Tracker Software\PDF Editor\PDFXEdit.exe

If a reader is installed elsewhere, update the corresponding APP_PATH in fuzzing/monitor.py.

API selection lists

fuzzing/config/blocklist.txt and fuzzing/config/limitlist.txt are optional. When absent, they are treated as empty lists. Add one fully qualified API name per line, using the Object.API form. These lists control API selection in the relationship-aware generation path.

Relationship files

The paths below are fixed relative to fuzzing/:

config/all_relation.json
config/all_symbolic.json

The corresponding file must exist before its CLI flag is enabled.

Outputs

When commands are run from fuzzing/, PDFuzzer writes:

fuzzing/
├── test/                 # All generated PDF test cases
├── runlog.txt            # Status lines for PDFs monitored through run.py
└── save/
    └── <status>/         # Copies of non-finish cases, e.g. crash/hang/error

--dry creates files under fuzzing/test/ but does not create execution results. The fuzzer does not automatically create the results/ or logs/ directories described by older versions of this README.

Project Layout

PDFuzzer/
├── pre_fuzz/
│   ├── agentlib/                    # OpenAI/Anthropic model adapters
│   ├── document_parser/             # Manual downloader and JSON parser
│   │   ├── output/                  # Generated manual text
│   │   └── json/                    # Generated documented API specs
│   ├── relation_infer/              # Candidate and symbolic inference
│   ├── data/                        # External raw undocumented signatures
│   ├── results/                     # Generated undocumented specs
│   ├── grammar_generator_param.py   # Produces the grammar tree used by -p
│   └── undoc_semantic_recovery.py   # Enriches raw undocumented signatures
├── fuzzing/
│   ├── param_grammar/               # Runtime CFG/API generators
│   ├── data/                        # Generated grammar trees
│   ├── config/                      # Optional generated/provided config
│   ├── test/                        # Runtime-generated PDFs
│   ├── save/                        # Runtime-saved non-finish cases
│   ├── mPDF.py                      # Embeds generated JavaScript in PDFs
│   ├── monitor.py                   # Reader execution and monitoring
│   └── run.py                       # Main entry point
├── requirements.txt
└── README.md

Directories marked generated, external, or runtime-created do not necessarily exist in a clean checkout.

Troubleshooting

A prompt or document file cannot be found

Use the working directories shown above:

  • Run web_scraper.py and json_parser_web.py from pre_fuzz/document_parser/.
  • Run undoc_semantic_recovery.py and grammar_generator_param.py from pre_fuzz/.
  • Run both relationship scripts from pre_fuzz/relation_infer/.
  • Run run.py and monitor.py from fuzzing/.

all_relation.json or all_symbolic.json cannot be found

Do not enable the corresponding relationship flag until its data preparation stage has completed. Basic fuzzing without these flags needs only the grammar tree supplied with -p.

The target reader cannot be opened

Verify the installation path and update APP_PATH in fuzzing/monitor.py if the reader is not installed at the default location.

An API key is reported missing

Set OPENAI_API_KEY or ANTHROPIC_API_KEY in the same shell that launches the preprocessing script. Candidate relationship inference always requires OPENAI_API_KEY.

About

From Documentation to Zero-day Vulnerabilities: LLM-Driven Fuzzing of JavaScript Engines in PDF Readers (CCS 2026)

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages