Interactive symbolic execution for Ghidra. Right-click, symbolize, solve — the plugin finds the password, the license key, the flag.
Not brute force, not fuzzing — equation solving. A 16-byte input has 2128 possibilities. The constraints reduce it to a system Z3 solves in seconds.
BVS("argv1", 32)if (input[0] == 'P') → fork: one path adds constraint byte0 == 0x50, the other byte0 != 0x50byte0==0x50 ∧ byte1==0x34 ∧ byte2^0x42==0x10 ∧ byte3+0x20==0x87P4RgThree layers of technology stack to turn a binary into an equation and solve it.
The check_password function compares each byte of the input. Here's what angr + Z3 see:
| Approach | 4-byte password | 16-byte license key | 32-byte AES key |
|---|---|---|---|
| Brute Force | 2³² = 4.3 billion ~minutes |
2¹²⁸ = 3.4 × 10³⁸ heat death of universe |
2²⁵⁶ impossible |
| SAT/SMT | 32 boolean vars < 1ms |
128 boolean vars < 1 second |
256 boolean vars seconds (if constraints exist) |
The trick: SAT/SMT doesn't try values — it derives them from constraints. Each learned clause permanently eliminates an exponential family of wrong guesses.
Ponce4Ghidra ships two engines. angr explores all paths; Triton follows one concrete path while tracking symbolic state — no path explosion, ever.
Triton translates every CPU instruction into a math expression. No intermediate representation — it works directly on x86/ARM machine code.
| Scenario | angr | Triton |
|---|---|---|
| Small function, find the password | Best — explores all paths, guaranteed to find it | Works — but needs a concrete input to start from |
| Large binary, DRM key derivation | Path explosion | Best — follows one trace, no explosion |
| Obfuscated code (VM/whitebox) | Constraint explosion | Best — doesn't need to "understand" the code |
| Have a Frida trace | Can't replay a trace | Best — replay trace with symbolic tracking |
| No concrete input available | Best — starts from scratch | Needs a starting input |
| Want all possible solutions | Best — finds every satisfying path | One path at a time (negate + re-run for alternatives) |
Both engines solving the same binaries on the same machine, via symbolize_function_argument:
| Binary | Input size | angr | Triton | Winner |
|---|---|---|---|---|
test_crackme4 byte-compares, no libc |
4 bytes | 0.18s ✓ | 0.065s ✓ | Triton 2.8× |
test_license19-byte key with strlen, XOR, checksum |
19 bytes | 3.65s ✓ | timeout ✗ | angr |
Why the difference?
Triton wins on pure computation — no path forking overhead, direct instruction-to-AST translation. For check_password (4 simple byte compares), it's 3× faster.
angr wins on functions that call libc (strlen, printf) — angr auto-hooks them with SimProcedures, while Triton tries to execute the real instructions (and the Mach-O libc stubs aren't loaded). For complex real-world functions, angr's SimOS layer is the deciding factor.
Rule of thumb: Use angr by default. Switch to Triton when angr path-explodes on a large function, or when you have a Frida trace to replay.
The DRM Reverse Engineering Workflow
Frida captures runtime data (keys, I/O samples) → Ponce4Ghidra analyzes the algorithm (angr for small funcs, Triton for large) → unidbg reproduces it as a standalone service.
Find up to 5 alternative solutions per variable. A password may have multiple valid inputs.
See every constraint the solver collected. Understand why a path is reachable — not just that it is.
Smart path merging at loop boundaries. Large binaries don't path-explode.
Concrete blocks execute via Unicorn (fast), symbolic only at branches touching symbolic data.
Save/Restore sessions across Ghidra restarts. Your Find/Avoid targets and symbolization commands survive.
Status bar updates every second during exploration: active paths, found count, step number, elapsed time.
| Method | Use when | Speed |
|---|---|---|
| Function Argument | You know which function checks the input. Analyzing a .so library. Binary is large. | Fast |
| argv[N] | Program reads input from command line. Binary is small/simple. Need whole-program context. | Medium |
| Register | The unknown is a scalar (int/long), not a buffer. | Fast |
| Memory | The unknown is at a known fixed memory address (global variable, struct field). | Fast |
Rule of thumb: start with Function Argument. Use argv only when you need the entire program's execution context.
| Format | Architectures | Status |
|---|---|---|
| Mach-O | x86_64 | Fully tested |
| ELF | x86_64, ARM64, ARM32, MIPS | Tested (static binaries) |
| Android .so | ARM64, ARM32 | Supported (deferred state) |