Week 02 · Block I Representation
What is obfuscation?
Five tools, one program, and a definition strong enough to be useful — which means a definition that tells you what evidence would show whether anything happened.
An obfuscator ran and exited zero. What has it actually done, and what would show that?
Week 1 gave you four ways to describe one function. This week is about the people who change one of them on purpose.
A working definition, and then five hours of trouble with it.
Obfuscation is a transformation applied to a program that is intended to preserve its observable behaviour while making some specific analysis of it more expensive.
Everything interesting is in the two vague words. Which analysis, and preserve according to whom. This week is about turning both into questions with answers you can check.
Which analysis, and at which representation#
An obfuscator does not make a program “harder”. It makes one particular way of looking at the program harder, and it does that at one particular point in the pipeline. Week 1 gave you the four points. Here is what each choice commits the tool to:
Where does obfuscation happen?
The transformation stage determines which optimizations come afterwards.
| Layer | Examples | What changes? | What follows? |
|---|---|---|---|
| C source | Tigress | Expressions and control flow in generated C | The native compiler can reshape the result. |
| LLVM IR | OLLVM / Hikari / Polaris | IR operations, blocks and edges | LLVM optimization and instruction selection. |
| X86 backend | Polaris backend-obfu | Machine instructions after IR | Native changes may be invisible in IR metrics. |
| Linked binary | BinProtect | Instructions in an existing PE | No later compiler optimization in these tests. |
Compare the final machine code, not just the representation where obfuscation was added.
Polaris has separate IR and backend paths. BinProtect outputs were semantically invalid in the retained tests.Open the original lecture figure (SVG)
The consequence is the reason week 5 exists. A transformation applied to C source has to get past the entire native compiler. One applied to LLVM IR has to get past instruction selection. One applied to a linked binary has nothing after it — no optimiser will undo it, and no optimiser will fix it either.
The five lanes this course uses#
Four compilers and one post-link rewriter were tested. Three words first, because the evidence below is full of them.
Their exact identities are recorded too, because “OLLVM” is a family of implementations spanning a decade and results do not transfer between them for free:
compiler_inventory.json — the toolchains behind every number on this site
identity, not marketing "polaris": {
"path": "E:\\Workspace\\seeing_through_obfuscation\\builds\\polaris\\bin\\clang.exe",
"version": "clang version 16.0.6\nTarget: x86_64-pc-windows-msvc\nThread model: posix\nInstalledDir: E:\\Workspace\\seeing_through_obfuscation\\builds\\polaris\\bin",
"sha256": "88DC08535F71EFD84B3BFACE00080CF5BCA56DC47ADF6DD50FC4293568E50D10",
"identity": "Polaris LLVM16 / Clang 16.0.6"
},Five obfuscators, five useful lessons
A compact comparison of the tested implementations, not a universal ranking.
| Obfuscator | Best teaching contrast | Original PE status | Main qualification |
|---|---|---|---|
| OLLVM-16 | Classic SUB; working BCF and FLA rewrites | PASSSelected originals | Global pass selection in the retained matrix. |
| Hikari | Partial SUB coverage; FLA transfer | PASSSelected originals | SUB/BCF target effects disappear by O1. |
| Polaris | Strong MBA; BCF transfer; wrong FLA rewrite | PASSSelected originals | Original PASS does not validate a later rewrite. |
| Tigress | Source-level effects survive native optimization | PASSMatched environment | Preserve exact generated C and initialization. |
| BinProtect | Post-link arithmetic and state dispatch | INVALID0 / 8 valid outputs | Crash/hang: static observations only. |
An obfuscator's original output and a deobfuscator's rewritten output need separate validation.
PASS means the retained checks passed. It is not a guarantee for arbitrary programs or inputs.Open the original lecture figure (SVG)
Sixty-eight planned build conditions were produced from the frozen source. All sixty-eight linked as Windows x64 executables, ran, printed the six expected values and exited zero. That matters for a reason that is easy to skip past: it means every specimen you will read this semester is a program that worked. When something goes wrong later in the course, the obfuscator is not the suspect.
The lane that does not run#
This is the first of several places where the course’s habit of separating outcomes earns its keep. “The tool exited zero and its transformations are plainly visible in the output” and “the tool produced a working protected program” are different claims, and here the first is true and the second is false eight times out of eight.
It is also a warning about reading a tool’s own description of itself. Upstream capability descriptions do not override a local runtime failure, and a local runtime failure on one program does not prove the tool never works. Both halves of that are needed.
Targeting: what the tool was told to transform#
You would like to say “the obfuscator transformed this function”. For two of the four compilers tested, that turned out not to be available.
Annotation-only pilots were run first: mark one function with
__attribute__((annotate("sub"))), build, and check that only it changed. On
the tested OLLVM-16 and Hikari builds, the annotated target compiled and was
not transformed. In OLLVM-16’s case its annotation reader was written
against an older way of spelling pointers in LLVM IR, and did not recognise
what this front end emits. The retained
matrix therefore uses global switches — -mllvm -sub, -enable-subobf and
their siblings — which transform everything eligible in the module.
Polaris’s selective targeting did work, with suffix annotations paired to its pass pipelines.
The measurement consequence is precise and small. Target-function measurements stay isolated, because they are taken from the named function’s own bytes. But other functions in the same binary may also have been transformed, so nothing here supports a claim about function-isolated treatment, and nothing here proves annotation support is impossible in some other build of these compilers.
- planned build conditions that linked, ran and printed SHOWCASE PASS
- 68 / 68planned build conditions that linked, ran and printed SHOWCASE PASSshowcase_matrix_v1.md
- BinProtect protected outputs valid at runtime
- 0 / 8BinProtect protected outputs valid at runtimebinprotect_validation.md
- compilers with recorded hash and revision behind every claim
- 4compilers with recorded hash and revision behind every claimcompiler_inventory.json
- compilers where annotation-only targeting silently did nothing
- 2 of 4compilers where annotation-only targeting silently did nothingshowcase_matrix_v1.md
What “more complicated” looks like#

The same function before and after one arithmetic transformation, at a zoom where no instruction is readable. Both are a single basic block — the control flow is identical and all the growth is inside one straight line of code. Nothing here ranks one tool against another, and nothing here is a measurement: the retained instruction counts come from the disassembly, not from the height of a rectangle.
Keep this image in mind for two reasons. It is the honest reason obfuscation works — someone has to read that — and it is a trap, because block count and expression size are independent, and in week 6 you will meet a transformation that explodes the graph while barely touching the arithmetic.
Studio: audit the five lanes#
In pairs, build a five-row table of your own. One row per tool, and these columns: the representation it transforms, what it takes as input, what it emits, and what the retained evidence establishes about its output’s validity.
Then answer two questions in writing:
- Which of the five lanes cannot be used for a runnable exercise in this course, and what exactly is the evidence for that? Quote the number.
- Pick any row and write the sentence that would be an overclaim. Then write the version that is supported. This is the skill A1 marks.
- obfuscators.mdthe corrected obfuscator comparison, with its qualifications7 kB
- compiler_inventory.jsonexact compiler paths, versions and hashes2 kB
- showcase_matrix_v1.mdthe matrix: build method, targeting pilots, O0 comparison12 kB
- binprotect_validation.mdthe eight invalid protected outputs, in detail22 kB
- CLAIMS_AND_CAVEATS.mdthe safe formulation and the overclaim, side by side, for every result10 kB
Come to the studio with
The studio assumes you have done these. Each is written so you can tell whether you have, without asking.
- you have finished week 1's studio and can point at one operation in all three representations
- you can state what `-O0` means to a compiler, and what it does not mean
- you have skimmed the claim boundaries page and know why it exists