April 11, 2026

How to Use Ghidra for Reverse Engineering CTF Challenges

A practical guide to using Ghidra for CTF reverse engineering challenges -- importing binaries, reading decompiled C, finding flags in strings, and tracing logic with the Symbol Tree.

Introduction

Ghidra is a free, open-source reverse engineering framework released by the NSA. It decompiles compiled binaries back into readable C-like pseudocode, which lets you understand what a program does without running it. For CTF reverse engineering challenges, this is invaluable: most RE flags are either stored as strings inside the binary, computed through a series of transformations you need to invert, or gated behind a comparison you need to satisfy.

Ghidra handles ELF binaries (Linux), PE binaries (Windows .exe), Mach-O binaries (macOS), and raw shellcode. It supports x86, x86-64, ARM, MIPS, and many other architectures. Unlike IDA Pro (expensive) or Binary Ninja (subscription), Ghidra is completely free and runs on Linux, macOS, and Windows.

This guide is written for CTF beginners who have a binary to analyze but have never used a decompiler. It links from reverse engineering writeups on this site so you can come back here when you encounter a new tool or technique.

New to the Linux command line? Ghidra analysis often starts with running the binary, checking file type, and using tools like strings and ltrace before opening a decompiler. If you are not yet comfortable with the terminal, read the Linux CLI for CTF Challenges guide first.

Installing Ghidra

Ghidra requires a JDK (Java Development Kit) version 21 or newer. Install the JDK first, then download Ghidra from the official site.

Linux (Debian / Ubuntu / Kali)

# Install JDK 21
sudo apt update && sudo apt install openjdk-21-jdk
# Verify Java version (should print 21.x.x or higher)
java -version
# Download the latest Ghidra release from:
# https://ghidra-sre.org/
# (look for ghidra_11.x.x_PUBLIC_YYYYMMDD.zip)
# Unzip to a permanent location
unzip ghidra_11.x.x_PUBLIC_*.zip -d ~/tools/
# Launch Ghidra
~/tools/ghidra_11.x.x_PUBLIC/ghidraRun

macOS / Windows

On macOS, install JDK 21 via Homebrew (brew install openjdk@21) then download and unzip Ghidra. On Windows, download JDK 21 from adoptium.net, then run ghidraRun.bat from the extracted folder. The UI is identical across all platforms.

First launch: creating a project

When Ghidra opens, it shows the Project Manager. Every binary you analyze lives inside a project. Create one with File > New Project, choose a directory (a folder named after the CTF event works well), and name the project. You only need to do this once per CTF.

Tip: Kali Linux includes Ghidra in its package repos (sudo apt install ghidra), but this version is often one or two releases behind. For the latest features, install directly from ghidra-sre.org.

Importing a binary

With your project open, import the challenge binary via File > Import File (or drag and drop the file onto the project window). Ghidra will open an Import dialog showing the detected file format and architecture. For most picoCTF challenges (64-bit Linux ELF binaries), it guesses correctly. Accept the defaults and click OK.

Opening CodeBrowser and running auto-analysis

After import, double-click the binary in the project window to open it in CodeBrowser, which is the main analysis window. Ghidra will ask if you want to run auto-analysis. Click Yes and accept all defaults. This takes 10 to 60 seconds depending on binary size. Auto-analysis identifies functions, resolves cross-references, and produces the initial decompiler output.

CodeBrowser panels at a glance

Listing (center-left): The disassembly view. Shows raw assembly instructions with addresses, bytes, and Ghidra's annotations. This is what you scroll through when reading low-level code.
Decompiler (right): C-like pseudocode generated from the assembly. This is your primary reading pane for understanding logic. Click any function in the Listing and the Decompiler updates to show that function.
Symbol Tree (top-left): A tree of all functions, imports, exports, labels, and data objects. Use it to jump to main or any named function instantly.
Program Tree (top-left tab): Shows the binary's sections (.text, .data, .rodata, etc.). Useful for navigating to specific segments when you know where data lives.
Note: If Ghidra guessed the architecture wrong (e.g., 32-bit vs. 64-bit), delete the imported file from the project and re-import, manually selecting the correct Language in the Import dialog. You can check the real architecture with file ./binary in the terminal.

Reading the decompiler output

The Decompiler panel shows C-like pseudocode that approximates what a human programmer might have written. It is not perfect, but it is close enough to understand control flow, variable relationships, and comparisons, which is all you need for most CTF challenges.

What to ignore

The decompiler output contains a lot of noise. You can safely ignore most of it at first:

  • Excessive casts like (int)(char)(uint)variable: these are artifacts of the decompiler reconstructing types it is uncertain about.
  • Variable names like local_18, param_1: these are stack offsets and register names assigned automatically. Rename them once you understand their purpose.
  • Pointer arithmetic that looks complex: focus on the high-level conditional structure first, then zoom into the math if you need to.

What to focus on

  • Conditionals: if statements that compare user input to a stored value or a computed result are almost always the flag check.
  • String references: any string literal in the decompiler is a lead. Strings like "Correct!", "Wrong password", or a partial flag like "pico" point you toward the relevant code path.
  • Function calls: calls to strcmp, memcmp, strncmp, or a custom function named something like check_flag are the most common flag-comparison sites.

Renaming variables

As you understand what a variable does, rename it: right-click the variable name in the Decompiler panel and choose Rename Variable (or press L). The new name appears everywhere that variable is used. This makes the pseudocode dramatically easier to read within a few renames.

// Before renaming
if (local_18 == param_1) {
puts("Correct!");
}
// After renaming local_18 -> user_input, param_1 -> expected_flag
if (user_input == expected_flag) {
puts("Correct!");
}
Goal: You do not need to understand every instruction. You need to answer one question: what value does the program expect from the user? Everything else is noise.

Finding flags in strings

Many CTF binaries embed the flag as a plaintext string or compare user input against a stored string constant. Ghidra's Defined Strings window lets you search all string data in the binary without reading any assembly.

Opening the Defined Strings window

Go to Window > Defined Strings. This lists every string Ghidra found in the binary's data sections. The table shows the address, string length, and value. You can sort by value or search within it.

Use the filter box at the top of the Defined Strings window and type pico, flag, or CTF to narrow the list. If the flag is stored in plaintext, it appears here immediately.

Jumping to a string and finding its references

Double-clicking any string in the Defined Strings list jumps to its address in the Listing pane. From there, right-click the address label and choose References > Find References to Address. This shows every place in the code that reads or uses that string. Double-clicking a reference jumps into the function that uses it, which is often the comparison or print routine you care about.

When the flag is not a literal string

Some challenges obfuscate the flag: it may be XOR-encoded, base64-encoded, or constructed character by character at runtime. In those cases, the Defined Strings window will not show the complete flag, but it may show partial strings or encoding artifacts that hint at the encoding scheme. The full flag reconstruction then happens in the decompiler view.

Challenges where strings analysis is central

Navigating functions with Symbol Tree

The Symbol Tree (top-left panel, Functions folder) lists every function Ghidra identified. For stripped binaries, these will be namedFUN_00401234 (address-based). For unstripped binaries, you get the original names: main, check_flag, validate_input, etc.

Starting from main

Always start with main. Expand the Functions folder in the Symbol Tree, findmain, and double-click it. The Listing and Decompiler both jump to that function. Read the decompiler output top-to-bottom, following any function calls that look related to input or flag validation.

Finding the key comparison function

Scan the decompiler output for calls to standard library functions:

  • strcmp(user_input, secret): compares two null-terminated strings. The second argument is often a hardcoded string or a pointer to data you can read.
  • memcmp(buf1, buf2, n): compares n bytes. Often used when the flag is binary data rather than a printable string.
  • strncmp(user_input, secret, n): compares only the first n characters. Common in challenges that check the flag prefix separately.

When you find a comparison, click the second argument (the expected value) in the Decompiler. If it is a pointer to a string, Ghidra highlights the string in the Listing. If it is a local variable, trace where it was assigned earlier in the function.

Tracing data flow backwards

If the expected value is computed (not a literal), you need to trace backwards from the comparison. Right-click the variable in the Decompiler and choose Find References to see all assignments. Follow the chain of assignments until you reach a constant, a user-controlled value, or a formula you can invert. This is the core skill of reverse engineering.

Patching binaries (skip checks)

Sometimes understanding the flag logic is harder than simply patching the binary to always take the success path. If a program prints the flag only when a comparison succeeds, you can flip the conditional jump so it always jumps to the success block, regardless of your input.

Patching a jump instruction

In the Listing pane, find the conditional jump instruction that guards the flag print. Common ones are JNZ (jump if not zero), JNE (jump if not equal), or JZ (jump if zero). Right-click the instruction and choose Patch Instruction. Change JNZ to JMP (unconditional jump) to always fall through to the success path, or change it to NOP (no operation) to remove the jump entirely.

Exporting the patched binary

After patching, export the modified binary via File > Export Program. Choose format Original File to export only the bytes (not a Ghidra project file). Mark the exported file as executable and run it:

chmod +x patched_binary
./patched_binary
# Enter any input when prompted -- the jump is gone
When to patch vs. when to read: Patching is fastest when the program prints the flag at runtime (it has the flag somewhere in memory). If the flag is constructed from your input (e.g., the program checks a password and you need to find that password), patching bypasses the check but does not reveal the expected input. In that case, read the logic and reconstruct the flag directly.

Ghidra scripting basics

Ghidra ships with a Script Manager that lets you run Python (Jython) or Java scripts against an open binary. For CTF work, scripting is useful when you need to automate repetitive tasks: extracting every string from a specific function, XOR-decoding an embedded blob, or iterating through a large lookup table.

Opening the Script Manager

Go to Window > Script Manager. Ghidra ships with dozens of example scripts. You can create your own by clicking the New Script button (top-left of the Script Manager window) and choosing Python.

Example: print all string references from a function

The following Jython script iterates over every instruction in the current function and prints the address and value of any string it references. Paste it into a new script file and click Run:

# Ghidra Jython script
# Prints all string cross-references from the current function
from ghidra.program.model.symbol import RefType
func = getFunctionContaining(currentAddress)
if func is None:
print('No function at cursor')
else:
body = func.getBody()
listing = currentProgram.getListing()
for instr in listing.getInstructions(body, True):
for ref in getReferencesFrom(instr.getAddress()):
if ref.getReferenceType() == RefType.DATA:
data = listing.getDataAt(ref.getToAddress())
if data and data.hasStringValue():
print(ref.getToAddress(), '->', data.getValue())

The output appears in the Ghidra console at the bottom of the window. For XOR decoding, replace the inner loop body with your decoding logic and print the decoded bytes as a string.

Recommended RE workflow for CTFs

Follow this decision tree at the start of every reverse engineering challenge. Start fast and cheap, then escalate to heavier tools only when needed.

1

Quick string grep (10 seconds)

Run strings ./binary | grep -i pico in the terminal. If the flag is stored in plaintext, this finds it instantly. No Ghidra needed.

2

Dynamic tracing with ltrace and strace (1 minute)

Run ltrace ./binary to see every library call the program makes, including strcmp with its arguments. Run strace ./binary to see system calls. Often you see the expected password passed directly to strcmp and can read it from the trace output.

3

Open in Ghidra: Defined Strings (2-5 minutes)

Import the binary, run auto-analysis, and open Window > Defined Strings. Search forpico, flag, and CTF. If the flag appears, you are done.

4

Find main and trace to the key comparison

Navigate to main via the Symbol Tree. Read the Decompiler output. Follow function calls until you reach a strcmp or equivalent. Read the expected value argument.

5

Rename variables and reconstruct the flag

Rename variables as you understand them. If the flag is computed (XOR, Caesar, etc.), replicate the computation in Python with the known constants and print the result.

6

Patch the binary as a last resort

If reading the logic is too complex but the binary prints the flag when the check passes, patch the conditional jump and run the modified binary.

Quick reference

Keyboard shortcuts for Ghidra (CodeBrowser) and a comparison table for choosing between common RE tools.

Ghidra keyboard shortcuts

GGo to address (type any address or symbol name)
LRename label or variable at cursor
Ctrl + FSearch within the Listing or Decompiler
Ctrl + Shift + EOpen Defined Strings window
Ctrl + Shift + FSearch all memory for a string or byte sequence
Ctrl + LGo to next instruction
Alt + LeftNavigate backward (like browser Back)
Alt + RightNavigate forward
FDisassemble at cursor (force-disassemble undefined bytes)
PCreate function at cursor
Ctrl + ZUndo last change (patch, rename, etc.)
XFind all cross-references to the current address
SpaceToggle between Listing and Decompiler focus

Tool comparison

ToolCostDecompilerBest forDifficulty
GhidraFreeYesAll CTF RE, beginners, multi-architectureMedium
IDA Free / ProFree (limited) / $1,000+Pro onlyIndustry standard, better UI for someMedium
Binary NinjaFree (cloud) / $299/yrYesScripting, clean UI, intermediate usersMedium
stringsFreeNoFirst-pass flag search, plaintext flagsEasy
ltrace / straceFreeNoDynamic tracing, seeing strcmp arguments liveEasy

RE challenge checklist

  • Run file ./binary to confirm architecture (32-bit vs. 64-bit, stripped vs. not).
  • Run strings ./binary | grep -i pico before opening any decompiler.
  • Try ltrace ./binary with a dummy input and read the strcmp arguments in the output.
  • In Ghidra, always run auto-analysis before trying to read anything.
  • Start from main in the Symbol Tree, not from a random address.
  • Rename variables as soon as you understand them. Unreadable names slow you down.
  • When you find a comparison, click through to the expected-value argument before assuming you need to do complex math.