Open a protected binary and the disassembly is a wall: arithmetic that does
nothing, branches that never branch, and a giant while/switch where the real
logic used to be. None of it is unbreakable — each obfuscation is a transform with
a known inverse. The work is recognising which one you're looking at and reaching
for the right solvent. VM-based obfuscation is the
heavyweight; these three are what you meet first.
- toolchain
- an LLVM-based obfuscator (MBA + opaque predicates + CFF)
- goal
- recover the handful of real operations under the noise
MBA — arithmetic that hides arithmetic#
Mixed boolean-arithmetic rewrites a simple operation as a sprawling mix of + - ^& | ~ that is algebraically identical but unreadable. The canonical seed
identity:
Apply it recursively and a + b becomes a half-screen of nonsense. You don't
simplify it by staring — you either pattern-match known identities, or treat the
expression as a black box and synthesize the simplest equivalent from its
input/output behaviour:
For linear MBA this is fast and exact; SMT-backed simplifiers (arybo) prove the
equivalence outright. Once each blob collapses to its one real op, the function
becomes legible again.
opaque predicates — branches that aren't#
An opaque predicate is a test the obfuscator knows the outcome of — e.g.
7*x*x - 1 != y*y is true for all integers — wired into an if so one side is
dead code (often bogus, to waste your time). Don't reason about it; ask a solver
whether the condition can ever take the other value:
unsat on the negation means the predicate is constant: prune the dead branch and
the bogus control flow it guarded evaporates.
control-flow flattening — the dispatcher#
Flattening shreds a function into basic blocks hung off a central switch (state)
dispatcher; the original order survives only as a runtime state variable. Static
reading order tells you nothing. Symbolically execute the dispatcher: start at
each block, let the engine compute the next state, and rebuild the edges into
the original CFG:
Triton/angr drive this; Hex-Rays plugins (d810-style) do it inline in the
decompiler. The output is the de-flattened function — the dispatcher and its state
machine gone, the real basic-block order restored.
what was under all of it
Three transforms, three solvents — and the "protected" check was nine lines:
The mindset that matters: obfuscation raises the cost of understanding, it doesn't change what the code does. MBA is identities, opaque predicates are solver queries, flattening is a state machine you can replay. Match each to its inverse and the wall comes down to the few lines it was always hiding.