Skip to main content

Week 05 · Block II Simplification

Optimization versus obfuscation

The compiler flag is part of the experiment. Change it and a transformation can halve, double, or vanish — while the obfuscator's own output file stays exactly the same.

Block II · SimplificationStudio 24 March 2027Shape: Controlled comparison

Is this result about the obfuscator, or about the optimisation flag that came after it?

Week 3 ended with a transformation that mostly stopped existing when the optimiser was turned on. That was not an accident of that tool. Optimisation level is not a nuisance variable in this field — it is part of the treatment, and this week is about designing comparisons that survive it.

How the matrix controls for it

Two different problems, two different answers.

For the source-level lane the problem is easy to state and easy to get wrong: if you generate obfuscated C four times, once per optimisation level, you have four specimens and no control. So the matrix generates the C once and compiles that exact file at every level. Here is the check, in the two retained metadata records:

the O0 build

/Od /Ob0
  "optimization": "O0",
  "optimization_flag": "/Od /Ob0",
  "optimization_note": "",
  "dataset_role": "optimization_survival",
  "selection": "Tigress --Functions=demo_substitution",
  "canonical_source": "E:\\Workspace\\seeing_through_obfuscation\\src\\showcase.c",
  "canonical_sha256": "39FEF0BB2A9742870F3D6F7CC864F7CFFA129136F6D2A59C4FB37F25906B99D2",
lines 7–13 of 34open metadata.json(2 kB)

the maximum-optimisation build

/Ox, recorded as O3
  "optimization": "O3",
  "optimization_flag": "/Ox",
  "optimization_note": "MSVC has no /O3; /Ox is the maximum-optimization analogue.",
  "dataset_role": "optimization_survival",
  "selection": "Tigress --Functions=demo_substitution",
  "canonical_source": "E:\\Workspace\\seeing_through_obfuscation\\src\\showcase.c",
  "canonical_sha256": "39FEF0BB2A9742870F3D6F7CC864F7CFFA129136F6D2A59C4FB37F25906B99D2",
lines 7–13 of 34open metadata.json(2 kB)

input_source_sha256 is the same value in both records. One generated file, two compilations — so any difference between the two binaries is the compiler’s, and nothing else’s. Note also "optimization_note": "MSVC has no /O3; /Ox is the maximum-optimization analogue." The directory says O3; the flag was /Ox, and quoting it as /O3 would be a small lie that compounds.

For the IR-level lanes the problem is the opposite. If you run the obfuscation pass and then hand the result to a normal compilation, the middle-end runs twice and you cannot tell what the pass did from what the second run did. So the matrix emits post-pass IR once, then generates object and assembly from that exact IR with -Xclang -disable-llvm-passes. One middle-end run, per build, on purpose.

The same generated C, twice

target instructions at /Od — against a matched clean 249
426target instructions at /Od — against a matched clean 249optimization_survival.md
target instructions at /Ox — against a matched clean 106
226target instructions at /Ox — against a matched clean 106optimization_survival.md
the surviving ratio at maximum optimisation
≈2.1×the surviving ratio at maximum optimisationselected_metrics.md
the clean /Ox comparator — not the older, larger O2 figure
106the clean /Ox comparator — not the older, larger O2 figureobfuscator_comparison_summary.md

The absolute count nearly halved. The ratio against the correct comparator barely moved. Both statements are true, and only the second one is about the transformation.

That last number carries a warning worth repeating. An earlier measurement of the clean /Ox target overran the function boundary — it measured to the next export rather than to the return — and produced a larger clean figure and therefore a smaller, flattering ratio. The corrected comparator is 106 instructions. If you are ever tempted to compare against a number you did not scope yourself, that is the shape of the mistake.

What disappears

Now the result that ought to make you nervous about every -O0 finding in the literature.

Table 04

Opaque flow: what survives optimization?

BCF = bogus control flow. The observation is about the measured native target.

TreatmentAt O0O1 through O3 / OxInterpretation
OLLVM BCFClones and opaque branchesRESIDUESmall optimized opaque-loop structureOptimization removes much, not everything.
Hikari BCFInjected native gates already foldedREMOVEDMeasured target matches cleanIR injection does not imply a native predicate.
Polaris BCFStrong cloned/opaque topologyREMOVEDMeasured target matches cleanA strong O0 example can be weak at O1+.
Tigress AddOpaqueInitialization-dependent opaque flowPERSISTS/Ox: 6 conditional jumps vs 1 cleanRuntime state matters to proof and optimization.

Choose the optimization level deliberately: it can erase the phenomenon being taught.

Scope: demo_bogus_control_flow in the retained optimization study. Jump counts alone do not prove opacity.Open the original lecture figure (SVG)

Two of the four opaque-control-flow lanes are gone by -O1. Not reduced — the measured target is structurally identical to the clean target. One of them was already mostly folded at -O0, because its injected guards were literal icmp eq i32 1, 1 and the backend does not need an optimiser’s permission to resolve that.

For arithmetic, the same pattern with different winners:

Table 03

Arithmetic: complexity can shrink or survive

Optimization acts on the obfuscated program before we inspect its machine code.

TreatmentAt O0At higher optimizationMain lesson
OLLVM SUB24 IR sites transformed; native XOR/SUB recognizableWEAKENEDO1+: all 24 native operations reappearLarge IR trees need not survive code generation.
Hikari SUBPartial coverage; 213 native instructionsREMOVEDO1+: measured target matches cleanCoverage and optimization survival are different.
Polaris SUBXOR 8/8; ADD 8/8; SUB 3/8 transformedWEAKENEDO1+: native operations recognizableClassic SUB is not the stronger MBA treatment.
Tigress EncodeArithmetic426 native instructionsPERSISTS/Ox: 226 vs 106 cleanExact same generated C; substantial complexity remains.

IR expansion, native instruction count and semantic difficulty are different measurements.

Target: demo_substitution. Tigress uses MSVC /Ox (recorded as O3); the 106-instruction clean is also /Ox.Open the original lecture figure (SVG)

And for flattening, the opposite result entirely:

Table 05

Flattening can survive without an indirect jump

FLA = control-flow flattening. Look for state-driven dispatch, not one opcode.

TreatmentAt O0At higher optimizationWhat to look for
OLLVM FLAState/switch dispatcherStrong flattening persistsRepeated state tests and returns to dispatch.
Hikari FLAState/switch dispatcherStrong flattening persistsDispatcher structure despite simpler blocks.
Polaris FLAStrong state dispatcherSmaller, but still flattenedSimplification is not full unflattening.
Tigress FlattenIndirect jump tableO1: direct networkO2 and /Ox: indirect tableSame generated C, different native dispatch.

A direct chain of comparisons can implement the same flattened state machine as a jump table.

Scope: demo_flattening. Survival of obfuscation does not establish compatibility with an unflattener.Open the original lecture figure (SVG)

Flattening is the durable family. Two implementations keep essentially their whole dispatcher from -O1 through -O3; a third loses about half its structure and still has a dispatcher; the source-level one changes how the dispatch is lowered — an indirect jump table at some levels, a direct comparison network at another — without losing the state machine at all.

That last point is the one people get wrong. A missing indirect jump is not evidence that flattening disappeared. It is evidence about lowering.

What this does to course design, and to yours

The study’s own recommendation is blunt: teach bogus control flow with explicit -O0-versus--O2 pairs, because two of the four implementations vanish at -O1 and presenting their -O0 structure as “an obfuscated binary” would be straightforwardly false.

Which is why weeks 6 to 8 work on -O0 specimens and say so in every sentence, and why week 10’s flattening work can use higher levels.

Studio: compare two builds fairly

  1. Open the two metadata records and confirm for yourself that input_source_sha256 matches. Write down what that fact rules out.
  2. Compare the two disassembly listings. Find one arithmetic site in each and say what the optimiser did to it.
  3. Compute both ratios — obfuscated against clean, at each level, using the matched control. Then write the one sentence that is supported by them.
  4. Now write the sentence that is not supported: the cross-tool comparison that this data does not license. Say precisely which control is missing.

A1 is due Friday at 17:00. Bring a draft of its “what this does not establish” section to the studio if you want it read.

Come to the studio with

The studio assumes you have done these. Each is written so you can tell whether you have, without asking.

Enlarged evidence

Open the file