Image decoder exploitation
Image decoders are the softest 0-click attack surface left in modern stacks. They parse adversary-controlled binary blobs in privileged-ish processes (browser renderer, messenger preview, OS thumbnailer, lock-screen wallpaper handler), they ship in every OS, and they are written in C/C++ with hand-rolled bitstream parsers. A single malformed WebP, AVIF, or PNG can land you code execution in iMessage’s ImageIO, in Chrome’s renderer, or in Windows Explorer’s preview pane — often before the user does anything. If you ship offensive tooling or you defend a fleet, the libwebp BLASTPASS chain (CVE-2023-4863 / CVE-2023-41064) is the reference case study for the decade.
Mental model
An image decoder is a state machine over a chunked container (RIFF for WebP, ISO-BMFF for AVIF/HEIC, IHDR/IDAT chain for PNG, SOI/SOF/SOS for JPEG). For each chunk the parser reads a declared length, allocates buffers sized from header fields, then walks a bit-packed inner stream (Huffman, VP8L prefix codes, arithmetic coding) to fill pixel buffers. Bugs cluster into five families:
- Heap overflow in code-table construction. The classic. libwebp CVE-2023-4863 lives in
BuildHuffmanTable()— VP8L lets the encoder declare prefix codes whose worst-case expansion is larger than the conservatively sized table. Crafted color-cache + meta-Huffman combinations write past the second-level table. - OOB read from malformed chunk length. libpng historically trusts the IHDR-derived row stride; AVIF tile grids (
tileWidth * tileHeight * bpp) can wrap. libjpeg-turbo hadJPEG_REACHED_SOSstate machine confusion (CVE-2020-13790) leading to OOB reads inget_8bit_row. - Integer overflow into stride/allocation math.
width * height * bytes_per_pixeloverflows a 32-bit size_t on 32-bit builds; allocation is small, the loop is big. Skia had several variants inSkBitmap::tryAllocPixels. - Type confusion in colorspace conversion. ICC profile parsing in Apple ImageIO (CVE-2021-30860 FORCEDENTRY) — a JBIG2-in-PDF-in-image trick, but the pattern of “wrong codec dispatch on attacker-controlled MIME” recurs in libavif and HEIF.
- Use-after-free across progressive decode. JPEG progressive scans, PNG APNG frame disposal, AVIF animated grids — frame N frees a buffer frame N+1 still references.
The BLASTPASS chain, end to end:
1
2
3
4
5
6
7
8
9
10
iMessage attachment (PassKit .pkpass)
-> BlastDoor sandboxed parse (entitlements stripped, seatbelt)
-> ImageIO dispatches WebP -> libwebp VP8L decode
-> BuildHuffmanTable() heap overflow corrupts adjacent libmalloc metadata
-> shape libmalloc tiny/small zone via repeated chunk allocs
-> arbitrary read/write inside BlastDoor
-> JBIG2-style integer VM (FORCEDENTRY-era trick) for stable primitive
-> escape BlastDoor via XPC message to imagent / identityservicesd
-> kernel LPE chain (separate bug, often DCP or AppleAVE2)
-> persistent implant (Pegasus/Predator class)
Citizen Lab caught the in-the-wild sample on a Catalan civil-society device; Apple shipped iOS 16.6.1 within a week, Google force-patched Chrome libwebp the next day, and every Electron app on earth had to re-roll.
Tradecraft
Build a fuzzing harness against the library, not the app. libwebp ships a libFuzzer target already:
1
2
3
4
5
6
7
8
9
10
git clone https://chromium.googlesource.com/webm/libwebp
cd libwebp
CC=clang CXX=clang++ CFLAGS="-fsanitize=address,fuzzer-no-link -g" \
./autogen.sh && ./configure --enable-libwebpdecoder
make -j
clang++ -fsanitize=address,fuzzer -g \
tests/fuzzer/advanced_api_fuzzer.cc \
src/.libs/libwebp.a src/.libs/libwebpdecoder.a -o webp_fuzz
mkdir corpus && cp tests/fuzzer/dictionaries/webp.dict .
./webp_fuzz corpus/ -dict=webp.dict -max_len=65536 -jobs=8
For VP8L specifically, the productive mutation is bit-level on the prefix-code header. Stock libFuzzer mutates bytes; you want a custom mutator that respects the VP8L bitstream:
1
2
3
4
5
6
extern "C" size_t LLVMFuzzerCustomMutator(uint8_t *Data, size_t Size,
size_t MaxSize, unsigned int Seed) {
// flip bits inside the meta-Huffman region (offset derived from RIFF parse)
// bias toward color_cache_bits and num_huff_groups fields
...
}
Honggfuzz with a seed corpus pulled from Open Images V7 plus the WebP test suite finds the boring crashes in hours:
1
honggfuzz -i corpus/ -o findings/ -n 16 -- ./webp_fuzz ___FILE___
Differential fuzzing across decoders catches the interesting class — same input, different output across libwebp, Skia’s SkWebpCodec, and Apple ImageIO means one of them is wrong about chunk length or color-cache state. Wrap each in a small process, render to PNG, hash, diverge.
For triage of a candidate crash, ASan + a minimal repro driver beats running the full browser:
1
2
3
clang++ -fsanitize=address -g repro.cc -lwebp -o repro
./repro crash-9f3b1c.webp
# look for heap-buffer-overflow WRITE of size 4 in BuildHuffmanTable
Real exploitation past the crash is heap-shape work. On macOS, study nano_zone / tiny allocator interleaving; on Windows, the segment heap LFH; on Linux glibc, tcache poisoning. See heap-exploitation-linux and heap-exploitation-windows for primitives. Browser specifics — V8 sandbox, PartitionAlloc, site isolation — live in browser-exploitation-primer. Pwn2Own entries from 2024-2025 leaned on image and font parsers repeatedly; see pwn2own-2024-2025-research-roundup.
Detection
Pre-exploit detection of an image bug is approximately impossible — the bytes look like a valid image until the decoder gets confused. You hunt post-exploit:
- macOS:
ReportCrashentries forMessagesBlastDoorService,com.apple.WebKit.WebContent,quicklookd,PreviewwithEXC_BAD_ACCESSnearWebPDecode,BuildHuffmanTable,VP8LDecodeImage. ESF eventsES_EVENT_TYPE_NOTIFY_EXITwith non-zero status from these binaries are the signal. - Windows: Defender for Endpoint surfaces
BrowserRenderProcessTerminatedandImageParsingException. Hunt in advanced hunting:
DeviceProcessEvents
| where InitiatingProcessFileName in ("msedge.exe","chrome.exe","explorer.exe")
| where ProcessCommandLine has_any ("--type=renderer","preview")
| join kind=inner (DeviceEvents
| where ActionType == "ProcessCrash"
| where AdditionalFields has_any ("libwebp","WebPDecode","libavif"))
on DeviceId
| project Timestamp, DeviceName, FileName, AdditionalFields
- Linux/Chrome:
chrome_crashpad_handlerminidumps withlibwebp.soframes;journalctl -u gdmfor repeatedgnome-shellthumbnailer crashes. - Network: BLASTPASS-style 0-click attachments arrive over iMessage (APNs), WhatsApp media CDN, Signal attachment service — TLS-encrypted, so you correlate on attachment-fetch timing + subsequent process crash, not payload.
- Heap-spray tells: GPU process or renderer RSS jumping 200-800 MB in seconds before crash;
MADV_HUGEPAGEadvice from a non-JIT process.
See macos-userland-mitigations for what BlastDoor, PAC, and Lockdown Mode actually buy you, and fuzzing-windows-drivers for the kernel-side analogue once you escape the renderer.
OPSEC pitfalls
- Testing exploits against your own iCloud or WhatsApp account burns the bug — Apple and Meta collect crashes from every device and Citizen Lab gets notified. Use offline rigs with telemetry disabled.
- Distributing a PoC
.webpover GitHub gets the repo nuked within hours by GitHub’s malware scanner once any AV vendor flags the hash. Ship the generator, not the artifact. - Forgetting that ImageIO dispatches by magic bytes, not extension. Renaming
.webpto.jpgstill triggers the WebP decoder on macOS — useful for delivery, also useful for accidentally popping your own QuickLook while triaging. - Differential fuzzing harnesses that link multiple decoders into one process — one decoder’s ASan poisoning hides another’s bug. Run each in a child process.
- Assuming the patch in upstream libwebp means the bug is dead. Every Electron app, every Android OEM fork, every embedded set-top box ships its own pinned copy. The long tail is years.
References
- https://blog.isosceles.com/the-webp-0day/
- https://citizenlab.ca/2023/09/blastpass-nso-group-iphone-zero-click-zero-day-exploit-captured-in-the-wild/
- https://nvd.nist.gov/vuln/detail/CVE-2023-4863
- https://googleprojectzero.blogspot.com/2021/12/a-deep-dive-into-nso-zero-click.html
- https://chromium.googlesource.com/webm/libwebp/+/refs/heads/main/src/utils/huffman_utils.c
- https://llvm.org/docs/LibFuzzer.html
See also: browser-exploitation-primer, pwn2own-2024-2025-research-roundup, fuzzing-windows-drivers, heap-exploitation-linux, heap-exploitation-windows, macos-userland-mitigations