Skip to main content
Obfuscation

Opaque Predicates: How Obfuscators Hide Dead Code and Real Logic

By Mithun··7 min read
Quick Answer

An opaque predicate is a condition whose outcome the obfuscator already knows — it's *always* true or *always* false — but which a reverse-engineer can't easily determine from the code. Example: x*x >= 0 is true for every integer (tested), yet reads like a real runtime check. Obfuscators use them two ways: an opaquely-false predicate guards junk/dead code that never runs, and an opaquely-true one wraps the *real* logic — so an analyst can't safely prune branches without solving each condition. It's bogus control flow: a deterrent that inflates and confuses, not encryption. Our Python Obfuscator injects opaque-predicate-style junk as part of its layered output.

When a reverse-engineer reads obfuscated code, one of their most powerful moves is pruning: delete the branches that can't run, and the real logic stands out. Opaque predicates exist to break that move. They're conditions the obfuscator has *proven* are constant — always true or always false — but that look like ordinary runtime checks, so the analyst can't tell which branches are dead without solving each one.

The technique (from Collberg's foundational obfuscation work) is built on conditions that are mathematically guaranteed. I verified each example below across tens of thousands of inputs on Python 3.14, so these are provably constant, not just usually.

What is an opaque predicate?

It's a predicate (a true/false condition) whose value is opaque to the reader but known to whoever inserted it. Two flavours do all the work:

  • Opaquely true — always evaluates to True (e.g. x*x >= 0). Used to wrap real code, so the branch always runs but can't be removed without proof.
  • Opaquely false — always evaluates to False (e.g. x*x < 0). Used to guard junk/dead code that never runs but litters the listing.
import random
# each of these is the SAME for every integer x —
# obvious to the obfuscator, not to a reader:
assert all((x*x >= 0) for x in range(-10000, 10000)) # always True
assert all((x*(x+1) % 2 == 0) for x in range(-10000, 10000)) # always True
assert all(not (x*x < 0) for x in range(-10000, 10000)) # always False
# x*(x+1) is a product of two consecutive integers, so it's always even.
provably-constant conditions — tested on 3.14

How they hide dead code and real logic

Here's the whole trick in one snippet. The obfuscator inserts branches that *look* conditional but aren't — burying junk behind an always-false guard and wrapping the real work behind an always-true one:

# opaquely FALSE — this junk never runs, but a reverser can't
# statically prove it's dead, so it stays and wastes their time:
if x * x < 0:
log_fake_telemetry(payload) # pure noise
# opaquely TRUE — the real check always runs, but deleting the
# 'if' isn't obviously safe, so the control flow looks branchy:
if x * (x + 1) % 2 == 0:
granted = verify_license(key)
opaque predicates in action

Sprinkle dozens of these through a function and the control-flow graph balloons with branches that never fork in practice. The reverser now has to evaluate every predicate to know which paths are real — exactly the work opaque predicates are designed to impose.

This is why opaque predicates pair so well with control-flow flattening and junk-code injection: flattening scrambles the order, junk adds fake states, and opaque predicates make the fake states indistinguishable from real ones.

Why they resist reversing

A human can eyeball x*x >= 0, but obfuscators use predicates that require actual reasoning to resolve. The classic is number-theoretic — Collberg's example that `7·y² − 1` is never a perfect square, so a check based on it is always true, but proving it takes number theory, not a glance:

from math import isqrt
# 7*y*y - 1 is never a perfect square, so this branch is ALWAYS taken:
if 7*y*y - 1 != isqrt(7*y*y - 1) ** 2:
run_real_code()
# tested: holds for every y in range(-200000, 200001)
a number-theoretic opaque predicate — verified for |y| ≤ 200000

Automated tools don't fare much better on the strong forms. Static analysis and compiler optimizers can't fold these away, and defeating them generally needs symbolic execution or an SMT solver to prove each predicate constant — expensive, and defeatable further by varying the predicate at every site so nothing pattern-matches.

As always: opaque predicates raise the *cost* of analysis — they're a deterrent, not encryption. A determined analyst with symbolic-execution tooling can still resolve them; the point is to make that far slower than the code is worth.

Kinds of opaque predicate

TypeBasisExample
Number-theoreticAlgebraic identityx*x >= 0, 7*y*y-1 never square
BitwiseBit-level invariants(x | 1) % 2 == 1 (always odd)
AliasingPointer/reference facts hard to analyzetwo refs that always (or never) alias
EnvironmentalA runtime fact always true in contexta value the program guarantees earlier

Number-theoretic and bitwise predicates are the easiest to generate and verify in Python (they're pure integer math). Aliasing- and environment-based predicates are harder for automated tools to crack but also harder to construct correctly.

Trade-offs and applying it

  • Overhead: every opaque predicate is real code that runs, so heavy use adds instructions and branches — apply it around *sensitive* logic, not everywhere.
  • Correctness: an opaque predicate must be genuinely constant for all inputs, or you change behavior — which is why obfuscators stick to proven identities (like the ones tested above).
  • One layer: opaque predicates confuse *control flow*; they don't hide names, strings, or constants. Real protection combines them with the other techniques.

Inserting these by hand is fiddly and risky. Our free Python Obfuscator injects opaque-predicate-style junk and bogus control flow automatically as part of its layered pass, alongside MBA integer encoding, renaming, string encryption, and control-flow flattening — see the full picture in Python obfuscation explained.

Obfuscate your Python — free

Layered AST obfuscation — bogus control flow, MBA encoding, renaming, string encryption and flattening — in one pass. In your browser, standalone output.

Open the Python Obfuscator

Free tools mentioned here

Related guides

Frequently asked questions

What is an opaque predicate?

An opaque predicate is a condition that is always true or always false — the obfuscator knows its value, but a reverse-engineer can't easily determine it from the code. It looks like a normal runtime check (e.g. x*x >= 0, which is true for every integer), and it's used to insert branches that never actually fork, confusing control-flow analysis.

How do opaque predicates obfuscate code?

Two ways. An opaquely-false predicate (like x*x < 0) guards junk or dead code that never runs but can't be statically pruned. An opaquely-true predicate (like x*(x+1) % 2 == 0) wraps the real logic so deleting the branch isn't obviously safe. Together they make a function's control-flow graph look far more branchy than it is.

Can opaque predicates be removed automatically?

Weak ones (obvious to a human) can, but strong forms resist it. Compiler optimizers and simple static analysis can't fold number-theoretic predicates like '7*y*y - 1 is never a perfect square', and defeating them generally requires symbolic execution or an SMT solver to prove each one constant — which is expensive and can be frustrated by varying the predicate at every site.

Is x*x >= 0 really always true in Python?

Yes. For any integer x, x*x is a square and therefore non-negative, and Python's integers are arbitrary precision so there's no overflow to break it. Tested across tens of thousands of values including negatives on Python 3.14, x*x >= 0 holds every time — which is exactly what makes it a usable opaquely-true predicate.

Are opaque predicates enough to protect my code?

No — they're one layer. Opaque predicates and bogus control flow raise the cost of understanding a program's logic, but they don't hide identifiers, strings, or constants, and they're a deterrent rather than encryption. Combine them with renaming, string encryption, MBA integer encoding, and control-flow flattening for meaningful protection.

Keep reading