Reverse Engineering (5/5): obfuscation, angr and protecting your own software
TL;DR
No client-side protection is unbreakable; it only raises the cost. angr quickly solves input checks; for your own product, move trust to the server.
No client-side protection is unbreakable; it only raises the cost of analysis. Understanding obfuscation from both sides makes you a better analyst and helps you decide how much protection your own product really needs. The final part of the Reverse Engineering for Developers series covers anti-reversing techniques, symbolic execution with angr, binary diffing, and a practical protection strategy.
Protection techniques and how analysts handle them
| Technique | What it does | Analyst's response |
|---|---|---|
| Symbol stripping | Removes function names | Library signatures, string xrefs, type recovery (part 2) |
| Packing (UPX, Themida, VMProtect) | Compresses/encrypts code, unpacks at runtime | upx -d for UPX; otherwise run to the original entry point (OEP) and dump memory |
| String encryption | Hides strings until used | Hook the decrypt routine with Frida or emulate it (Unicorn, Ghidra's emulator) |
| Control-flow flattening | Turns a function into one big switch dispatcher loop | Trace real execution, deobfuscation scripts, symbolic execution |
| Opaque predicates | Inserts always-true/always-false branches | Prove the branch constant with an SMT solver and remove it |
| Code virtualisation | Translates code into custom bytecode run by an embedded VM | Hardest case: map the VM handlers, write a disassembler for the bytecode |
| Anti-debugging | IsDebuggerPresent, ptrace, timing checks | Debugger-hiding plugins, patch the check, or hook it with Frida |
Symbolic execution with angr
Instead of guessing input, describe it as symbols and let a solver find values that reach a target address. angr is a Python framework that does this on real binaries:
import angr, claripy
proj = angr.Project('./crackme', auto_load_libs=False)
pw = claripy.BVS('pw', 8 * 16) # 16 symbolic bytes
state = proj.factory.entry_state(args=['./crackme', pw])
simgr = proj.factory.simulation_manager(state)
simgr.explore(find=0x401234, avoid=0x401250) # prints "Correct" / "Wrong"
if simgr.found:
print(simgr.found[0].solver.eval(pw, cast_to=bytes))
The find/avoid addresses are examples — take them from Ghidra for your binary. angr shines on input checks and short functions but hits path explosion on large programs. There, modelling just the check and solving it with Z3 is often faster.
Binary diffing
When a vendor ships a security patch, diffing the old and new binaries with BinDiff or Diaphora shows exactly which functions changed. That is how researchers analyse freshly patched vulnerabilities, and how you can verify what an update really changed.
Protecting your own software — a practical stance
- Assume the client is fully readable. Never ship secrets (write-capable API keys, signing keys, admin endpoints) in an app.
- Move trust to the server: licensing, pricing, feature entitlements and anti-cheat must be validated server-side.
- Use platform integrity services (Play Integrity API, Apple App Attest) so the server can judge whether a request comes from a genuine app.
- Obfuscate proportionally: R8/ProGuard on Android; a commercial obfuscator only when IP truly needs it, and measure the performance and debugging cost.
- Keep debug symbols private (upload them to your crash reporter) and strip release builds.
Risks and ethics
- The skills that analyse malware can also bypass licensing. Work within authorised scope, disclose vulnerabilities responsibly to the vendor, and never distribute cracked software.
- Heavy obfuscation makes crash reports hard to read and can draw suspicion from app stores or antivirus products.
How to apply it
- Dev: grab a control-flow-flattened crackme from crackmes.one, try reading it in Ghidra, then solve it with the angr script above; compare the time each approach takes.
- Tech Lead: threat-model before choosing an obfuscator: what does an attacker gain by reading the code? The answer usually leads to server-side checks, not another layer of encryption.
- PM: size the protection budget to the real damage (fraud, lost revenue); don't buy "uncrackable" protection — it doesn't exist.
Sources & references
- angr — angr.io
- angr documentation — angr
- google/bindiff — GitHub
- joxeankoret/diaphora — GitHub
- Flare-On Challenge — flare-on.com
- OWASP MASTG — OWASP