Spine lecture 4 · Opens Block IV Limits
The smaller graph problem
Two results in this corpus look like successes and are not. Both were caught by running the program; neither would have been caught by looking at the output. That is the course's conclusion, and this is where it gets argued.
The argument#
The last block is one sentence with two pieces of evidence behind it: a cleaner-looking result is not a correct result, and nothing about its appearance will tell you which it is.
The first piece of evidence. A stock unflattener is pointed at a flattened
function. It succeeds — exit code 0, patch generated, executable written. The
graph drops from 56 blocks to 10 — both figures from Miasm recursive graphs,
which split at calls, so they are comparable with each other and with nothing
in week 8 — and that is the most dramatic simplification anywhere in this
corpus. The file is a valid PE with the same size, the same sections and the
same exports. It computes the wrong answer. Four
legitimate switch alternatives from the original C were grouped under a single
incoming state and emitted one after another, so all four now run in sequence.
The program prints F9CD8332 where it should print DBEFFCE7 and exits 1.
The second piece. A whole-function lifter is pointed at a valid, passing native
binary and produces LLVM IR. The IR verifies. It recompiles. It is smaller and
more readable than the machine code it came from. It miscomputes 3,917 of 4,118
inputs, at every one of four optimisation stages, because the raw lift folded
(~A & B) | (A & C) into a select on the strength of known-bits extrema that do
not actually prove A is all-zeros or all-ones. A = 1, B = 0, C = 2 is enough
to break it.
Neither failure is exotic. Both tools are reasonable tools. Both failures are invisible in the output and obvious the first time you execute a test vector.
What survived the corpus#
Not everything is negative, and the pattern in what transferred is the thing to take away.
What generalised across implementations: the semantic branch proof plus a separate layout-preserving patcher, which worked on OLLVM-16 and Polaris; selected local arithmetic recovery, where every accepted candidate had an independent equivalence proof; and stock unflattening from one LLVM-family implementation to a related one, where the dispatcher and state assumptions happened to hold.
What did not: a fixed seed as a reconstruction key; local expression recovery as
evidence about whole functions; an -O0 result as evidence about -O2; a
mathematical opaque identity under machine-width overflow; and an
initialised-state invariant as a claim about arbitrary entry state.
The methods that travelled carried an explicit model of state and a separate validation step. The methods that did not travel recognised a shape.
Where this ends#
Week 10 audits the incorrect rewrite and the discovery failure beside it. Week 11 does the same for lifting, with one passing case and one failing case from the same tool on the same day. Week 12 asks what you would choose, and why, and what you would have to check — which is the capstone.
The weeks this opens
- Week 10. Unflattening and hidden assumptions — The tool exited zero and the graph got smaller. How would you find out whether it worked?
- Week 11. Lifting machine code back to LLVM IR — The lifted IR verifies, recompiles and is smaller than the machine code. Is it right?
- Week 12. How generic can deobfuscation be? — Given an unfamiliar obfuscated function, what would you do first, and what would you have to check?