Finding a bug in a stripped binary
3 min read

Updated

Finding a bug in a stripped binary

A known color-profile bug exposed two problems in our analysis: function ranking and incomplete ground truth. Fixing them let the engine generate a differential reproducer.

The vulnerable function was ranked 4,839th in a queue of 5,494 functions. Our analysis budget ended long before it got there.

The binary had been stripped of its debug information and most useful function names. A ranking system that relied on those names had lost its strongest signals.

This case study used a previously reported bug in a color-management library. The question was whether the engine could investigate the binary without source or a supplied reproducer. It wasn’t a test of novel discovery.

Start with the information that remains

Stripping removes information useful to a researcher, but it doesn’t erase everything required to run a program. Imports, instructions, data, and references between them still offer starting points.

Our first experiment used a stripped, sanitizer-instrumented build. We ranked functions by their references to copy operations and sanitizer-related entry points, rather than relying on descriptive names.

The target function moved to rank 180, within the decompilation budget. That solved a scheduling problem: useful code reached the model instead of remaining outside its view.

The sanitizer build and the ordinary optimized build provided different signals. The final confirmation used a stripped optimized build; we shouldn’t describe the earlier ranking result as though it had been obtained under identical conditions.

We also had the wrong function

The library contained two functions named WriteCLUT. We initially investigated the wrong one.

Its callers appeared to be missing, which suggested a problem recovering indirect dispatch. Re-reading the patch showed that the reported crash belonged to the other function, reached through a direct call chain.

That changed the diagnosis. Part of the apparent call-graph problem was a decompilation-budget problem, and part was our own incorrect mapping of the report to the code.

A test can have a precise crash address and still be measuring the wrong thing if its source-to-binary mapping is wrong.

Recover the path to the input

Connected tiles representing recovered relationships between functions

Two changes followed. We used execution coverage to help choose functions for decompilation, and we improved recovery of call relationships, including indirect edges where they mattered.

Coverage answered a practical question: which code did an ordinary seed already reach? That helped prioritize the parser and its callers. Recovering additional relationships gave the analysis more context for tracing input fields toward suspicious operations.

The next run produced a roughly 3.7 KB ICC color profile. It demonstrated an out-of-bounds read at the intended site in the vulnerable build and ran cleanly in the patched build.

That was the acceptance condition. An explanation of the defect alone would not have passed.

What this result supports

On this target, the engine generated a working input without being given the known reproducer. The result shows why function selection, ground-truth checking, and reachability information belong in the evaluation.

It doesn’t establish that the model had never encountered the public bug. Withholding an input during a run doesn’t reveal what was in training. Nor does one successful target establish a general result for firmware or closed-source drivers.

Binary-only software can be fuzzed and analyzed with suitable tooling. The constraint is the information and execution access available to a particular workflow, not the absence of source alone.

Our four-task follow-up reached a different result: relevant code was located, but no input satisfied the differential confirmation rule. Together, the experiments show why locating a function and reproducing its defect need separate measurements.