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.

Hard 75m 3 tasks

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.

✦ Answer the questions to complete this task

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.

✦ Answer the questions to complete this task

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.

✦ Answer the questions to complete this task

Why does ltrace help with crackmes that do string comparison?

💪 Exercises & Challenges

📝 MCQ Medium +20 XP

Reverse Engineering MCQ

Reverse Engineering MCQ

Start →
⚙️ Practical Medium +30 XP

Crack the Crackme

Crack the Crackme

Start →
🚩 Challenge Hard +50 XP

Find the Secret Code

Find the Secret Code

Start →