Triage at Scale for 0-Click MMS Vulnerability in Samsung s10 (CVE-2020-8899)
In our last post we black-box fuzzed Samsung's libhwui.so (Skia/Android frontend) and re-created the CVE-2020-8899 class of issues.
AFL++ turned up ~674 unique crashes. This follow-up is the so what?: we'll turn those opaque SIGSEGVs into actionable bug buckets using an AddressSanitizer-enabled harness and a Python triage script that batch-runs every crash, captures full ASAN reports, and scores exploitability.
Sep 3
Automating Crash Triage using AI: Parsing ASAN Logs at Scale
In our previous articles, we walked through how we extracted the filesystem from Samsung S10+ firmware, pulled out the libhwui.so (Skia) library, and fuzzed it in a black-box setup using AFL++ in QEMU mode. This allowed us to recreate the crash behind CVE-2020-8899, a 0-click MMS vulnerability, and collect 674 unique crash samples during fuzzing.

Moving Beyond Crashes: Triage & Analysis:
- Finding hundreds of crashes is only the first step. The real value comes from understanding, classifying, and prioritizing them. In this follow-up blog post, we'll show how we move from raw crashes to actionable insights by:
- Building a fuzzing harness with AddressSanitizer (ASAN) to capture precise memory errors inside libhwui.so.
- Running every crash through ASAN under QEMU to generate detailed backtraces.
- Creating a Python-based crash analysis pipeline that automatically parses ASAN logs, extracts bug type, stack trace, severity, and exploitability.
- Generating triage reports (Markdown, CSV, JSON) to bucket crashes, spot duplicates, and highlight the most critical findings.
By the end, you'll see how we transformed a mountain of 674 raw crashes into a structured dataset of distinct vulnerabilities, complete with severity and exploitability scores, helping us zero in on high-value bugs like heap buffer overflows in Qmage decoding paths.
This step is crucial for anyone fuzzing complex real-world targets: fuzzing finds the crashes, but triage reveals the vulnerabilities.
Rebuilding the harness with ASAN
We'll reuse the "shim + harness" approach (SkAndroidCodec frontdoor) and just compile the harness with ASAN:
Compile the shim library with debug enabled "-g ".
Compile the shim library with debug enabled "-g ".
Compiling the harness with AddressSanitizer enabled (-fsanitize=address)
Heads-up: since the target process runs on Android's Bionic libc (from the firmware rootfs), you need to use ASAN's Android runtime (e.g., libclang_rt.asan-aarch64-android.so). This library is included in the NDK toolchains and must be made available to the target through the same rootfs and LD_LIBRARY_PATH setup used during fuzzing.
Running ASAN under QEMU (Android userland)
- QEMU_LD_PREFIX=/path/to/your/android/rootfs (QEMU will use this as the target's.)
- QEMU_SET_ENV=... to inject env vars into the target, not the host.

A heap buffer over-read occurs inside QmageDecCommon_MakeColorTable_Rev8253_140615 (in libhwui.so) while it is doing a memcpy. The function tries to read 117,431 bytes from a heap buffer that is only 7,374 bytes long. ASAN catches the read past the end and aborts.
Error type & access direction
This is an OOB READ (not a write). The address is immediately after a small heap allocation (see below), so the source buffer is too small for the requested copy.
Faulting instruction
Call chain
So your harness asks your shim to decode -> that calls SkAndroidCodec::getAndroidPixels -> which funnels into SkCodec::getPixels -> which eventually triggers QmageDecCommon_MakeColorTable -> where the OOB read happens.
That's enough to bucket (same top frames same bug), tag root cause areas (QMG color table, frame decode, etc.), and decide whether to prioritize.
Since analyzing each crash manually would take a long time, let's use Cursor AI in agent mode to build a script and runt the script that automatically generates ASAN reports, parses them, and triages crashes based on the existing analysis.

Using Cursor AI (Agent Mode) to Accelerate Crash Triage
We didn't hand-write the entire triage pipeline line-by-line. Instead, we used Cursor AI in Agent Mode as a co-pilot to draft, refine, and harden the scripts that
(1) replay crashes under afl-qemu-trace
(2) capture Android ASAN logs reliably
(3) parse + bucket results at scale.
Code Generation Speed: Creating consistent subprocess wrappers, CSV/JSON/Markdown writers, and regex scaffolding can be tedious. Agent Mode made this process much faster.
(1) replay crashes under afl-qemu-trace
(2) capture Android ASAN logs reliably
(3) parse + bucket results at scale.
Code Generation Speed: Creating consistent subprocess wrappers, CSV/JSON/Markdown writers, and regex scaffolding can be tedious. Agent Mode made this process much faster.
Rapid Iteration: We could instruct the agent to perform tasks like "add a -q fallback to the script", "group by error, library, or function", or "reduce exploitability for READ operations" and it would update the code directly.
Consistency Across Hundreds of Files: Once the agent generated a clean parser, we applied it universally, ensuring no discrepancies between manual analysis and automated processes.
Even with an AI agent, we were strict about these constraints:
Even with an AI agent, we were strict about these constraints:
Target vs host env: Always set QEMU_LD_PREFIX + QEMU_SET_ENV (Android/bionic ASAN). Clear host LD_PRELOAD.
Robust capture: Wrap runs in script -q -c to avoid truncated ASAN logs; detect truncation and retry without wrapper.
Deterministic parsing: Prefer ASAN SUMMARY: for primary lib/func; otherwise pick the first non-ASAN frame.
Heuristics, not magic: Severity maps are explicit (e.g., UAF/DF -> CRITICAL). Exploitability is a documented bump/downgrade (e.g., WRITE > READ; QMG path bumps).
Prompts we used with Cursor (Agent Mode)
Open the project folder that includes the harness and crash file directory in Cursor AI, then copy these prompts into Agent Mode and target your repository.
Build the replay runner with Android ASAN env

This is the CSV report below, detailing the crash analysis of 674 crashes evaluated by AI.

Crash analysis using Cursor AI agent mode

From the ASAN log, we requested the AI agent to generate a table listing the categories and counts of the crashes, along with a graph.

| tableCategory | Count | Percentage |
| write | 16 | 1.86% |
| read-memcpy | 287 | 68.01% |
| read-4 | 102 | 24.17% |
| read-16 | 3 | 0.71% |
| sigabrt | 14 | 3.32% |
What the agent produced
- The agent generated the runner (with QEMU_LD_PREFIX, QEMU_SET_ENV, script -q wrapper, truncation detection) and the parser (regexes for SUMMARY, frames, access type/size, PC/fault).
- We validated on known inputs: first ran one crash manually, compared the terminal ASAN to the saved log, then ensured the parser extracted the same SUMMARY and call chain.
- We asked the agent to tweak heuristics: e.g., "If access is WRITE and the path goes through memcpy/memmove, lift exploitability one tier."
Generating Script Using AI-Agent
We ask Cursor AI to create the script , that automates
Drop this script in the harness directory. It:
Drop this script in the harness directory. It:
- iterates all out/default/crashes/*
- runs: afl-qemu-trace ./harness_shim_debug <crash>
- forces target env via QEMU_SET_ENV (and clears LD_PRELOAD)
- auto-retries via script -q -c if the log looks truncated
- parses ASAN, extracts error type, PC, fault address, frames
- tags Severity and Exploitability (heuristics)
- writes Markdown, CSV, and JSON reports
Limits of AI help (and where to be human)
AI is great at skeleton, but you still need to decide:
- which buckets represent unique root causes,
- where heuristics should rank exploitability,
- and whether an ASAN "read overflow" is likely exploitable in practice given surrounding code/allocs.
We used the agent to reach a clean baseline, then iterated manually on buckets, severity, and exploitability logic as our understanding of the QMG paths improved.
Conclusion
In this journey, we moved from extracting the Samsung Note 10+ firmware to building a fully functional fuzzing harness around libhwui.so. Starting with failed attempts at resolving brittle Skia symbols, we pivoted to designing a stable shim layer leveraging Skia's clean APIs (SkData, SkAndroidCodec). From there, we constructed a harness, integrated persistent mode with AFL++ QEMU, and injected fuzzing inputs directly into memory for speed and reliability.
This approach not only allowed us to reproduce the crash associated with CVE-2020-8899, but also established a reusable framework for fuzzing other closed-source Android libraries shipped in vendor firmware. By carefully layering shims, persistence hooks, and harness design, we've turned a black-box binary into a fuzzing-ready target.
To take this further, we enhanced our workflow by compiling the harness with AddressSanitizer (ASAN) and leveraging AI agent assisted the automation to handle crash analysis. Instead of manually inspecting each case, AI helped generate scripts to parse ASAN logs and build a triage pipeline. Every crash discovered by the fuzzer was replayed under ASAN, and the resulting logs were automatically analyzed by our Python tooling to classify vulnerabilities (e.g., heap overflows, use-after-free, invalid frees). This automated triage step allowed us to separate actionable, potentially exploitable crashes from duplicates or benign cases, making the overall analysis both scalable and effective
Our work here demonstrates how firmware extraction, thoughtful harness engineering, and structured crash triage can come together to rediscover and validate real-world vulnerabilities. And this is just the start future explorations may extend these techniques to other system components, from graphics to baseband, opening up broader research opportunities in Android device security.
Want to learn advanced fuzzing and exploitation? Check out our courses, “Android Userland Fuzzing and Exploitation” and “Android Kernel Fuzzing and Exploitation” or do some of our free labs.
Check out our training:
https://www.mobilehackinglab.com/courses
https://www.mobilehackinglab.com/courses
Copyright © 2024
Company
Registration:
89905814
VAT:
NL004770321B63
89905814
VAT:
NL004770321B63