Skip to content

Sensiel/softsec

Repository files navigation

CS-412 Fuzzing Lab: libpng with AFL++

Reproducible AFL++ fuzzing environment for the CS-412 Software Security fuzzing lab. The target is libpng 1.6.50, the reference PNG decoder.

The Dockerfile compiles libpng three times and ships four harness binaries so every campaign and measurement required by the report runs from the same container:

Binary libpng build Purpose
png_fuzz afl-clang-fast + ASan Main white-box campaign, crash triage
png_fuzz_nosan afl-clang-fast, no sanitizer Q8 no-ASan exec-speed baseline
png_fuzz_persistent afl-clang-fast + ASan Q8 persistent-mode exec-speed measurement
png_fuzz_qemu vanilla gcc -O1 -g Q7 binary-only AFL++ QEMU campaign

The harness drives the standard libpng decoding pipeline (png_read_info → transforms → png_read_update_infopng_read_imagepng_read_end) and enables png_set_quantize, which is what makes the png_do_quantize path (CVE-2025-64505 on libpng < 1.6.51) reachable.

Repository Layout

.
|-- Dockerfile                 # Reproducible AFL++ + libpng build environment
|-- Makefile                   # build / fuzz* / plot* / sanity / shell / clean
|-- project.pdf                # Assignment handout
|-- report.pdf                 # Final report
|-- Report_tex/                # USENIX-style report sources and figures
|-- q8_data.txt                # Q8 edge counts and exec-speed numbers
|-- png.dict                   # PNG grammar tokens for AFL++ dictionary mutations
|-- patches/
|   `-- png_crc_finish-return0.patch  # CRC-bypass patch for libpng 1.6.x
|-- src/
|   |-- harness.c              # Fork-mode harness (also supports SYNTHETIC_BUG_Q5)
|   `-- harness_persistent.c   # Persistent-mode harness (__AFL_LOOP)
|-- seeds/
|   |-- gen_seeds.py           # Regenerates the 8 PNG seeds below
|   |-- palette_small.png      # color_type=3, 4-entry PLTE (reaches png_do_quantize)
|   |-- palette_trns.png       # palette + tRNS
|   |-- rgb.png / rgba.png
|   |-- gray.png / gray16.png  # gray16.png exercises png_set_strip_16
|   |-- interlaced.png         # Adam7
|   `-- text_gamma.png         # tEXt + gAMA ancillary chunks
|-- findings/                  # Instrumented + ASan campaign output (gitignored)
|-- findings-nosan/            # No-ASan + fork-mode campaign output (gitignored)
|-- findings-persistent/       # Persistent-mode campaign output (gitignored)
|-- findings-qemu/             # QEMU-mode campaign output (gitignored)
|-- findings-synthetic/        # Q5 synthetic-bug campaign output (gitignored)
|-- plot_output/               # afl-plot graphs, instrumented (gitignored)
`-- plot_output_qemu/          # afl-plot graphs, QEMU (gitignored)

Target Version and CRC Patch

The Dockerfile builds libpng-1.6.50.

AFL++'s bundled libpng_no_checksum/libpng-nocrc.patch only matches the 1.2.x source layout, so this repository ships a custom patch:

patches/png_crc_finish-return0.patch

It short-circuits png_crc_error() in pngrutil.c to always return 0 while still consuming the 4 CRC bytes from the stream, so the parser stays in sync. png_crc_error() is the single chokepoint reached by both png_crc_finish() and png_crc_finish_critical() in libpng 1.6.x, so the patch covers critical and ancillary chunks alike. Without it, mutated chunks would be rejected at the CRC boundary and coverage would stall.

Harness Design

src/harness.c (fork-mode) and src/harness_persistent.c (persistent mode via __AFL_LOOP) share the same fuzz_one(data, size) body:

  1. Reject inputs shorter than the 8-byte PNG signature.
  2. Match the signature with png_sig_cmp.
  3. Create png_structp / png_infop.
  4. Install a setjmp handler so libpng png_error/longjmp returns cleanly instead of being recorded as an AFL++ crash.
  5. Register a memory-backed read callback (mem_read_fn) so libpng parses straight from the fuzzer buffer with no file-system detour.
  6. Call png_read_info.
  7. Enforce width, height ≤ 4096 to keep per-row mallocs small and stability high.
  8. Enable transformations: png_set_quantize, png_set_expand, png_set_strip_16, png_set_gray_to_rgb.
  9. png_read_update_info, allocate row buffers, png_read_image, png_read_end, free, destroy.

png_set_quantize is what activates the png_do_quantize pipeline, which is where CVE-2025-64505 lives.

Important Guards

  • size < 8 and png_sig_cmp(...) != 0: skip obviously-invalid inputs before allocating any libpng state.
  • setjmp(png_jmpbuf(png)): turns normal libpng parse errors into clean harness returns instead of false-positive AFL++ crashes.
  • mem_read_fn bounds check: a truncated input becomes a libpng png_error/longjmp instead of a harness out-of-bounds read.
  • MAX_DIM = 4096: rejects mutated IHDRs that claim huge dimensions (e.g. 65535×65535), which would otherwise OOM-kill the fuzzer and drop stability.
  • Per-row malloc return checks: an allocation failure on row i frees rows 0…i-1 so persistent-mode iterations do not leak state.
  • MAX_INPUT_SIZE (fork-mode main): caps testcase size at 16 MiB.

Dictionary

png.dict contains the PNG grammar terminals AFL++ can inject during mutation: the 8-byte file signature, the critical chunk types (IHDR, PLTE, IDAT, IEND), and the ancillary chunk types (tRNS, gAMA, cHRM, sRGB, iCCP, tEXt, zTXt, iTXt, bKGD, pHYs, tIME, sBIT, hIST, sPLT).

Seeds

seeds/gen_seeds.py produces eight minimal PNGs, each exercising a distinct format axis (grayscale, RGB, RGBA, 4-entry palette, palette + tRNS, 16-bit, interlaced, ancillary chunks). To regenerate them:

python3 seeds/gen_seeds.py

The Docker build also copies AFL++'s bundled PNG testcases from /AFLplusplus/testcases/images/png/ into /work/seeds/.

Build

make build

The Docker build:

  1. Starts from aflplusplus/aflplusplus:latest.
  2. Downloads libpng-1.6.50.
  3. Applies patches/png_crc_finish-return0.patch.
  4. Builds libpng three times (AFL+ASan, AFL no-ASan, vanilla GCC).
  5. Compiles png_fuzz, png_fuzz_nosan, png_fuzz_persistent, and png_fuzz_qemu.

Sanity Check

make sanity

Runs ./png_fuzz on the first PNG seed available in the container. Should print exit: 0 with no ASan report.

Fuzzing Campaigns

All campaigns default to DURATION_SEC=1800 (30 minutes). Override with DURATION_SEC=<seconds> make ....

Target Binary used Output directory
make fuzz png_fuzz findings/
make fuzz-nosan png_fuzz_nosan findings-nosan/
make fuzz-persistent png_fuzz_persistent findings-persistent/
make fuzz-qemu png_fuzz_qemu (-Q) findings-qemu/
make fuzz-synthetic png_fuzz_synthetic (built on demand with -DSYNTHETIC_BUG_Q5) findings-synthetic/

fuzz-synthetic rebuilds the harness with the off-by-many synthetic write enabled (gated on data[15] != 'R') and is used in Q5 to prove the campaign pipeline catches injected memory bugs.

Plotting Results

make plot       # findings/default     -> plot_output/
make plot-qemu  # findings-qemu/default -> plot_output_qemu/

Each output directory contains index.html, edges.png, exec_speed.png, high_freq.png, and low_freq.png.

Interactive Container

make shell

Bind-mounts every findings* directory and drops into a bash shell — useful for afl-tmin, ASan symbolization, and inspecting raw AFL++ output.

Cleaning Generated Artifacts

make clean

Removes every findings*/ and plot_output*/ directory (via the container so root-owned files inside disappear too) and deletes the softsec-libpng-fuzz Docker image.

Crash Triage Workflow

If findings/default/crashes/ is populated:

make shell
./png_fuzz findings/default/crashes/<crash-file>
afl-tmin -i findings/default/crashes/<crash-file> -o minimized.png -- ./png_fuzz @@
ASAN_OPTIONS=symbolize=1:abort_on_error=1:detect_leaks=0 ./png_fuzz minimized.png

The instrumented and QEMU 30-minute campaigns documented in report.pdf produced no real crashes; the Q5 synthetic-bug variant (make fuzz-synthetic) is the demonstration that the setup detects memory bugs end-to-end.

Report

The written report is report.pdf (USENIX style, 4 pages + appendix). LaTeX sources and figures live in Report_tex/. Raw numbers for Q8 (edge counts and exec speed) are in q8_data.txt.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors