# Case study: triaging CVE-2018-25032 across an optimization mismatch

CVE-2018-25032 is the zlib memory-corruption bug fixed between 1.2.11 and 1.2.12.
The fix reworked how deflate stores match/literal symbols: the old overlapping
`l_buf`/`d_buf` inside `pending_buf` became a separate `sym_buf` with `sym_next`
and `sym_end` (see the `deflate.h` diff). That change ripples through the deflate
and trees functions that read and write that buffer.

This is a good test for behavioral fingerprinting because the interesting builds
usually are not compiled the same way. Here the corpora are built at `-O0` and
the unknown target is built at `-O2`, so a CFG diff (bindiff, diaphora) spends
its time on optimization noise. fnprint compares behavior, not structure, so the
opt-level gap matters much less.

## Setup

- vuln corpus: zlib 1.2.11, gcc `-O0`
- patched corpus: zlib 1.2.12, gcc `-O0`
- target (the "unknown" build, carries the vulnerable code): zlib 1.2.11, gcc `-O2`

```
fnprint index vuln.so    -o vuln.db
fnprint index patched.so -o patched.db
fnprint triage target.so --vuln vuln.db --patched patched.db
```

## Result (actual output)

```
52 functions triaged: 5 look vulnerable, 0 patched, 47 inconclusive

review queue (vuln-leaning, strongest first):
   addr         vuln%  patched%  margin  matches
   0x00002d40  100.0     69.5   +30.5  crc32_big vs adler32_z
   0x0000d7e0  100.0     40.6   +59.4  _tr_tally vs _tr_tally
   0x00004270   79.7     66.4   +13.3  deflate_stored vs deflate_stored
   0x0000ce50   78.9     66.4   +12.5  _tr_stored_block vs _tr_stored_block
   0x0000c860   68.8     30.5   +38.3  compress_block vs compress_block
```

## Reading it

Ground truth from the 1.2.11 -> 1.2.12 source diff: the change lives in
`deflate.c` (152 changed lines) and `trees.c` (111 changed lines), all around the
`sym_buf` rework. The functions that actually touch that buffer are `_tr_tally`
(writes symbols), `compress_block` (reads them back), `deflate_stored`, and
`_tr_stored_block`.

Four of the five things fnprint put in front of you, `_tr_tally`,
`deflate_stored`, `_tr_stored_block`, and `compress_block`, are exactly those
CVE-changed functions, and it found them even though the target was `-O2` and the
corpora were `-O0`. The other 47 functions that did not change came back
inconclusive, so they stay out of your review queue instead of cluttering it, and
nothing was misfiled as patched.

The fifth entry, `crc32_big` matched to `adler32_z`, is a false lead: two
checksum routines whose behavior looks alike. That is the documented weak spot,
pure-compute functions with thin, collision-prone behavior. Worth knowing it is
there, and it is one line to skip versus four real leads it surfaced.

So it does not replace the RE. It ranks what to open first when the builds do not
line up, which is most real 1-day work.

## Reproduce

Needs gcc and the fnprint binary. Builds both zlib versions and the cross-opt
target, then runs the pipeline above:

```
for v in 1.2.11 1.2.12; do
  curl -sSL "https://github.com/madler/zlib/archive/refs/tags/v$v.tar.gz" | tar xz
done
srcfiles() { ls "$1"/*.c | grep -vE 'example|minigzip|infcover|test|gz(close|lib|read|write)'; }
gcc -shared -fPIC -O0 -w -Izlib-1.2.11 $(srcfiles zlib-1.2.11) -o vuln.so
gcc -shared -fPIC -O0 -w -Izlib-1.2.12 $(srcfiles zlib-1.2.12) -o patched.so
gcc -shared -fPIC -O2 -w -Izlib-1.2.11 $(srcfiles zlib-1.2.11) -o target.so
fnprint index vuln.so -o vuln.db
fnprint index patched.so -o patched.db
fnprint triage target.so --vuln vuln.db --patched patched.db
```

Numbers vary a little with compiler version, but the CVE-changed deflate and trees
functions land at the top of the vuln-leaning queue.
