Opaque Predicates: Branches Only the Author Can Trust

Everything so far removed information: names, strings, constants. This technique does something more aggressive, it adds false information. An opaque predicate is a branch condition whose outcome the obfuscator knows at build time (always true, or always false) but which looks like a genuine runtime decision to a reader. Wrap the real code in an always-true branch, hang convincing junk off an always-false one, and the file stops being trustworthy.

Imagine a maze with doors. Some doors are painted to look like promising passages but are actually bricked up behind, and one plain-looking door is the only real way through. The maze author knows exactly which doors are fake because they built it. A stranger does not, so they have to test every door, wasting time on the bricked ones. An opaque predicate is a painted door: the author knows it never opens, the reader has to prove it.

The transform

function pay(user) {
  return charge(user.card);
}
function pay(user) {
  const k = (user.id * user.id) % 4;
  if ((k * k - k) % 2 !== 0) {   // k*(k-1) is always even: never true
    wipeDatabase();              // dead branch, never runs
  }
  if (((k | 1) & 1) === 1) {     // always true
    return charge(user.card);    // the real work
  }
  return null;                   // dead
}

The always-false branch guards a call that looks alarming and relevant, and it will never execute. The always-true branch hides the real logic behind a test that seems data-dependent. Both predicates lean on small number-theory facts (the product of consecutive integers is even; x with its lowest bit forced on is odd) that hold for every input, but nothing in the file announces that.

What happens once the predicate is proven

The attacker's whole job here is to prove which branches are constant. The moment they succeed, whether by recognizing the number-theory identity or by running a solver, the disguise collapses and a pruning pass deletes the fake branches mechanically. The tool below shows that final step: given branches whose conditions are already known to be constant, a real dead-code pass removes the unreachable side and unwraps the always-true guard, leaving exactly the original logic. Drag the slider to watch the junk fall away.

Why it raises cost

Until now the attacker could at least trust that the code they read is the code that runs. Opaque predicates end that. Every branch now carries a question, does this ever execute, and answering it takes either mathematical proof or runtime observation. Dead branches bait time-wasting detours, and automated pruning tools cannot safely delete code they cannot prove dead. The reading problem quietly became a verification problem, which is a far more expensive kind of problem.

The honest limit

Opaque predicates come in families, and families have signatures. Once a reader recognizes that k*(k-1)%2 pattern, every instance of it in the file falls at once, so a tool that draws from a small pattern pool loses the whole layer to one insight. The stronger attack is symbolic execution: automated systems that treat inputs as symbols and mathematically decide many predicate families outright. Against a well-equipped adversary, opaque predicates are speed bumps whose value depends on variety and on being combined with the other layers.

The precise arms race is over how easily a solver can decide a predicate. A symbolic-execution engine treats each input as a free variable and asks an SMT solver whether the condition can ever be false (for an always-true predicate) or ever true (for an always-false one); if the solver proves it cannot, the branch is pruned automatically. Defenders answer by tying predicates to values the solver cannot cheaply model: state that depends on a long loop it would have to unroll, aliased memory it cannot reason about, or genuine runtime inputs. The general problem is undecidable, so no pruner wins every case, but that cuts both ways, and it is why this layer is deployed as one cost-raiser among several rather than the main event.

Design lesson hiding in this technique: a defense that is cheap to recognize is a defense you only get to use once. Variety across builds, not cleverness in one build, is what makes junk code keep costing attackers time.

Frequently asked questions

Does dead code slow down my program?

The dead branches never execute, so their runtime cost is near zero; the predicates themselves cost a few arithmetic operations. The real price is file size, which is why tools budget how much junk they inject rather than flooding the output.

Can symbolic execution defeat opaque predicates?

Many classic predicate families, yes: a solver can prove an always-true condition true for all inputs and prune the fake branches automatically. Defenders respond with predicates tied to values the solver cannot easily model, and by treating this layer as one cost-raiser among several rather than the main event.

Keep learning

  • Control Flow Flattening: Turning Structure into a State Machine
  • Constant Obfuscation: Making 100 Stop Looking Like 100
  • How Attackers Actually Deobfuscate Code
  • VM Obfuscation vs Identifier Renaming
  • JavaScript Obfuscator
  • Java & JAR Obfuscator

All lessons