Reverse Engineering with Ghidra
Disassemble and decompile binaries with Ghidra to understand program logic, find vulnerabilities, bypass license checks, and solve CTF reverse engineering challenges.
Learning Objectives
- → Install and navigate Ghidra's code browser, disassembly, and decompiler
- → Identify program control flow: functions, loops, conditionals
- → Recognize common patterns: string comparison, authentication checks, license validation
- → Use cross-references and symbol renaming to build program understanding
- → Solve CTF reverse engineering challenges with Ghidra and GDB
Why Reverse Engineering?
Use cases for reverse engineering:
- Malware analysis: understand what malware does
- Vulnerability research: find bugs without source code
- CTF challenges: solve crackme/RE challenges
- Interoperability: understand proprietary protocols
- License bypass (research only — may be illegal for commercial software)
- Firmware analysis: embedded devices
Legal considerations:
- Authorized security research: legal
- CTF challenges: explicitly authorized
- Malware analysis: legal (DMCA exemption in US)
- Commercial software bypass: may violate DMCA/EULA
Ghidra Installation & Setup
# Install Ghidra (NSA open-source RE tool):
# 1. Install Java 17+: apt install openjdk-17-jdk
# 2. Download: https://github.com/NationalSecurityAgency/ghidra/releases
# 3. Extract: unzip ghidra_*.zip
# 4. Run: ./ghidraRun
# First time setup:
# 1. Create project: File -> New Project -> Name: "RE_Lab"
# 2. Import file: File -> Import File -> select binary
# 3. Auto-analysis: click Yes when prompted
# (runs: Function Detection, Data Analysis, Control Flow, Demangler)
# 4. Open Code Browser: double-click the imported file
# Key windows:
# - Listing (disassembly): assembly instructions
# - Decompiler: C-like pseudocode (right panel)
# - Symbol Tree: functions, labels, namespaces
# - References (xrefs): who calls/uses this
# - Program Tree: sections (.text, .data, .rodata)
Reading x86 Assembly Basics
; Key registers (32-bit):
; EAX, EBX, ECX, EDX — general purpose
; ESP — stack pointer (top of stack)
; EBP — base pointer (stack frame)
; EIP — instruction pointer
; Common instructions:
mov eax, 1 ; eax = 1
mov eax, [ebp-0x8] ; eax = value at memory address (ebp-0x8)
push eax ; push eax onto stack
pop eax ; pop from stack into eax
call function ; call a function (push EIP, jump)
ret ; return (pop EIP from stack)
jmp address ; unconditional jump
je address ; jump if equal (ZF=1 after cmp)
jne address ; jump if not equal (ZF=0)
jg address ; jump if greater
jl address ; jump if less
cmp eax, ebx ; compare (sets flags, doesn't store)
test eax, eax ; test eax with itself (check if zero)
lea eax, [ebp-0x10] ; load effective address (pointer math)
xor eax, eax ; eax = 0 (common register clear)
add eax, 4 ; eax += 4
sub esp, 0x20 ; allocate 32 bytes on stack
Finding Main Function
Binary Entry Points:
1. Ghidra Symbol Tree -> Functions -> main
2. If stripped: Look for entry point → calls __libc_start_main → first arg is main
3. Search for strings: Window -> Defined Strings
Find "Enter password:" → double-click → shows code that uses it
4. Search for xrefs: right-click on string → References -> Show References to
Recognizing Common Patterns
String Comparison (Password Check)
// Source code:
if (strcmp(input, "SecretPass") == 0) {
printf("Correct!
");
}
// In assembly / Ghidra decompiler:
// call strcmp
// test eax, eax ; strcmp returns 0 if equal
// jne LAB_wrong ; jump if NOT zero (wrong password)
// ...correct path...
// LAB_wrong:
// ...wrong path...
// Patching to bypass:
// Change 'jne' to 'jmp' (always jump to wrong, no — change to 'je' for wrong=success)
// Or: change 'test eax,eax' to 'xor eax,eax' (force eax=0 = equal)
// Or: change conditional jump 'jne' → 'nop nop' (no operation)
License/Serial Key Validation
// Pattern 1: Simple comparison
if (serial == calculate_valid_serial(username)) { /* success */ }
// In Ghidra: find calculate_valid_serial()
// Understand the algorithm, implement in Python
// Pattern 2: Checksum validation
int valid = 0;
for (int i = 0; i < strlen(key); i++) {
valid += key[i];
}
if (valid == 0x1337) { /* success */ }
// Reverse: find target sum 0x1337, construct valid key
Anti-Debugging Detection
// Common anti-debug tricks:
// 1. IsDebuggerPresent()
if (IsDebuggerPresent()) { ExitProcess(1); }
// 2. Timing checks (debugger slows execution)
DWORD t1 = GetTickCount();
// ... some operations ...
DWORD t2 = GetTickCount();
if (t2 - t1 > 100) { /* being debugged */ }
// 3. Parent process check
// 4. Self-modifying code
// 5. Exception handling tricks
// Bypass in Ghidra/GDB:
// Patch out the check (NOP the jmp)
// Set IsDebuggerPresent return value in GDB: set $eax=0
Ghidra Key Operations
Navigation:
G or double-click → go to address
Ctrl+F → search for string/instruction
Alt+Left → back navigation
Analysis:
Right-click function → Edit Function Name (rename for clarity)
Right-click variable → Retype Variable (fix type)
Right-click → References → Show References To address
Decompiler shortcuts:
L → rename variable/function
Ctrl+L → retype variable
; → add comment
Ctrl+; → add pre-comment
Patching:
Right-click → Patch Instruction → change instruction
Window -> Bytes → edit raw bytes
Export patched binary:
File -> Export Program -> Original File (with patches)
Practical: Solving a Crackme
Crackme: small program that asks for a password/serial
Goal: find the correct input without looking at source
Steps:
1. Static: strings → find any hardcoded password? (often not)
2. Ghidra: import, auto-analyze
3. Find main: search for "Enter password" string → xref → function
4. Trace logic in decompiler:
a. fgets (read input)
b. some_check_function(input)
c. if result: print "Correct" else "Wrong"
5. Analyze some_check_function:
- Simple strcmp: extract the string being compared
- Algorithm: implement in Python to find valid input
- Key derivation from username: write keygen
6. Patch approach: change jne to je (nop out the check)
Dynamic Analysis with GDB/pwndbg
# Debug while reversing for dynamic info:
gdb ./crackme
(gdb) break main
(gdb) run
(gdb) next / ni (next instruction)
(gdb) info registers # see all register values
(gdb) x/s 0x804a010 # read string at address
(gdb) set $eax = 0 # modify register (bypass checks)
(gdb) jump *0x08048542 # jump to address (skip checks)
# With pwndbg:
pwndbg> decompile # show decompiled current function
pwndbg> telescope $esp # dereference stack
pwndbg> retaddr # show return address
CTF RE Challenge Approach
Standard CTF RE approach:
1. file binary — identify format
2. strings binary — quick wins (hardcoded flags?)
3. ltrace ./binary — trace library calls
4. strace ./binary — trace system calls
5. Ghidra/IDA — static analysis
6. GDB/x64dbg — dynamic analysis
Common CTF RE patterns:
- Simple strcmp to a hardcoded string
- XOR decryption of flag at runtime
- Custom alphabet substitution
- Anti-debug checks (bypass with NOP)
- Obfuscated code that decrypts itself
pwntools for automation:
from pwn import *
p = process('./crackme')
p.sendline(b'password')
flag = p.recvall()
Import a simple binary into Ghidra (use a crackme from crackmes.one — free RE challenges): (1) create a new Ghidra project, (2) import the binary and run auto-analysis, (3) find the main function via Symbol Tree, (4) switch between Listing (disassembly) and Decompiler views, (5) find a string 'Enter password' or similar in Defined Strings, (6) follow the xref to the code that uses it, (7) rename key variables in the decompiler for clarity.
What is the advantage of Ghidra's decompiler over the raw disassembly?
What does a cross-reference (xref) in Ghidra tell you?
Find and bypass a password check in a crackme: (1) load in Ghidra and find the authentication logic, (2) identify the comparison: strcmp, custom check, or hash comparison, (3) extract the correct password if hardcoded OR understand the algorithm to derive it, (4) as an alternative: patch the binary — find the conditional jump (je/jne), change it to always succeed, (5) test the patched binary, (6) export patched binary from Ghidra.
What is the effect of replacing a 'jne' (jump if not equal) instruction with 'nop nop'?
Solve a beginner RE challenge from picoCTF or pwn.college: (1) run 'strings' first — is the flag visible? (2) use ltrace and strace to observe library/system calls, (3) load in Ghidra if strings didn't reveal flag, (4) trace the flag checking logic, (5) if XOR encrypted: extract the key and XOR with the ciphertext in Python, (6) submit the flag.
Why does ltrace help with crackmes that do string comparison?