At DEF CON CTF Quals 2025, a challenge called vault32 stumped hundreds of teams because it looked like a password checker — but the real logic was buried three function calls deep. Teams that opened it in Ghidra found the answer in under ten minutes. Teams that stayed in a debugger alone spent hours. This walkthrough shows you exactly how to use Ghidra to crack that kind of challenge.
Loading the Binary and Finding Your Footing
Ghidra is NSA’s open-source reverse engineering suite. It decompiles x86, ARM, MIPS, and more into readable C-like pseudocode. That pseudocode is your map.
Start by running file and checksec before you open anything in Ghidra. Knowing the architecture and protections shapes every decision you make next.
user@ctfbox:~/challenges$ file vault32
vault32: ELF 32-bit LSB executable, Intel 80386, statically linked, stripped
user@ctfbox:~/challenges$ checksec --file=vault32
[*] '/home/user/challenges/vault32'
Arch: i386-32-little
RELRO: No RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
Stripped means no symbol names — functions will show up as FUN_08048xyz instead of main or check_password. No PIE means addresses are static, which makes cross-referencing trivial. No stack canary is a bonus if you pivot to exploitation later.
Import the binary into Ghidra (File → Import File), let auto-analysis finish, then open the Symbol Tree. In a stripped binary, hunt for entry point first. Press G, type entry, and Ghidra drops you at the ELF entry stub. From there, look for the call into __libc_start_main — its first argument is your real main.
Reading the Decompiler Output Like an Attacker
Once you're in main, open the Decompiler panel (Window → Decompiler). Here is a realistic slice of what vault32 produced:
undefined4 FUN_08048610(void)
{
char local_2c [32];
int local_c;
printf("Enter the vault code: ");
fgets(local_2c, 0x20, stdin);
local_c = FUN_080484f0(local_2c);
if (local_c == 0x539) {
puts("Access granted.");
FUN_08048560(local_2c);
}
else {
puts("Wrong code.");
}
return 0;
}
Three things jump out immediately. First, fgets reads 32 bytes into local_2c — safe input, no overflow here. Second, FUN_080484f0 transforms the input and returns an integer. Third, that integer is compared against 0x539, which is decimal 1337. Classic CTF humor.
The flag is almost certainly inside FUN_08048560, called only on success. But you don't need to pass the real check to see it. Double-click FUN_080484f0 to inspect the transform logic. In vault32, it turned out to be a simple checksum — XOR every byte, accumulate. You could either reverse the math to craft a valid input, or patch the binary.
Patching is faster in a CTF. Right-click the if (local_c == 0x539) branch in the Listing view, find the corresponding JNZ instruction, and use Patch Instruction to change it to JMP (unconditional jump). Export the patched binary with File → Export Program → Original File, run it, and feed it any input.
user@ctfbox:~/challenges$ ./vault32_patched
Enter the vault code: aaaa
Access granted.
CTF{gh1dra_s33s_4ll_7h1ngs}
Flag in hand. Total time from import to solve: about eight minutes.
Hunting Strings When Logic Gets Hairy
Some challenges hide the flag behind crypto or obfuscation heavy enough that patching one branch won't help. When decompiler logic looks like a wall of XOR loops, switch tactics: search strings and cross-reference them.
In Ghidra, use Search → For Strings. Set minimum length to 4, ASCII only. In vault32's harder cousin — a challenge called deeplock — this surfaced something interesting:
Address Length String
-------- ------ ------
0804a020 18 Decryption key: %s
0804a033 12 flag_%s.txt
0804a040 26 /tmp/.session_key_0xdeadbeef
That /tmp/.session_key_0xdeadbeef string is a runtime artifact — the binary writes a derived key to that temp path during execution. Double-click the address, then right-click → References → Show References to Address. Ghidra shows you every function that touches that string. Navigate to the writer function, read what it derives and writes, then pull that key after running the binary under a normal user session. The flag decrypts immediately.
Strings searching is not a cheat. It is reconnaissance inside the binary — the same instinct that makes you run grep before you write a script.
What To Do Now
Download crackmes.one, grab a beginner-rated Linux ELF, and spend 30 minutes tracing from entry to the password check using only Ghidra's decompiler — no dynamic debugging allowed. Force yourself to read the pseudocode until you can predict what input will satisfy the branch condition, then verify by running the binary. That single constraint builds the static analysis muscle that separates fast CTF solvers from everyone else.
