Skip to main content

April 11, 2026

How to Use Ghidra for Reverse Engineering CTF Challenges

Ghidra for CTF reverse engineering: import binaries, read decompiled C, find flags in strings, and trace program logic with the Symbol Tree and cross-refs.

A dense band of tiny marks passing through a lens plate and emerging as four large clean blocks.

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.
Companion guides: Ghidra shows you the code, but you still have to read it. Pair this with the x86 assembly for CTF guide for the Listing pane, the GDB debugging guide for stepping through the same binary at runtime, and the radare2 and rizin guide for a terminal-first alternative.

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

Ghidra CodeBrowser window layoutThe CodeBrowser window divided into four panes. Down the left edge, the Symbol Tree sits above the Data Type Manager. The Listing pane, showing disassembly, occupies the centre. The Decompiler pane, showing C-like pseudocode, occupies the right and is the widest. A menu bar runs across the top.FileEditAnalysisWindowHelpSymbol TreeJump straight to mainor any named functionProgram Tree: same cornerData Type ManagerStructs and enumsGhidra knows aboutListingDisassembly: addresses,raw bytes, instructionsPatch Instruction lives hereDecompilerC-like pseudocode, and thepane you actually readRename a variable with Lclick an instruction here, the cursor lands there
The four panes you actually use. The Listing and the Decompiler are two views of the same code, kept in sync: clicking an instruction on the left moves the cursor to the matching line on the right, which is the single most useful habit to build early.
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.

It helps to know what you are looking at. Ghidra was built by the US National Security Agency and open-sourced in March 2019 under the Apache 2.0 licence. Its decompiler does not translate assembly to C directly: it lifts each instruction into an intermediate representation called P-Code, then analyses that. Because the analysis happens on P-Code rather than on x86 specifically, the same decompiler works across the dozens of processor families Ghidra ships definitions for, which is why an ARM, MIPS, or PowerPC challenge binary opens just as readably as an x86 one.

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.

When the decompiler output is unreadable

If the pseudocode is a wall of runtime calls, giant switch tables, or functions with names like runtime.mallocgc, the binary probably was not written in C, and Ghidra is faithfully decompiling a language runtime rather than the program logic. Reversing a different language is a different job, not a harder version of this one:

Checking file output and running the binary through the file magic identifier before importing takes ten seconds and saves you from an hour of reading the wrong thing.

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.

Ghidra finds more than strings does, and the reason is structural. The Unix strings(1) command scans for runs of at least 4 printable characters with no knowledge of the file, while Ghidra parses the ELF section table and knows which regions are .rodata, which are code, and which addresses reference each. That is why the Defined Strings window can tell you where a string is used, which is almost always the question you actually have.

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.

Two things help before you go back to the decompiler. Run the binary through the strings extractor in the browser to get the raw strings view alongside Ghidra's parsed one, since the two disagree in useful ways on packed and obfuscated binaries. Then, if the artifacts look like an encoding rather than a cipher, the CTF encodings guide covers what base64, hex, and XOR-encoded blobs look like in the wild, and reading hex dumps covers how to spot structure in the bytes themselves.

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.

On x86 this is usually a one-byte edit. Per the Intel manuals, the short-form conditional jumps are two bytes: an opcode plus a signed 8-bit displacement. je is 0x74 and jne is 0x75, so inverting a check means changing a single byte from 74 to 75. Replacing the opcode with 0xEB (short jmp) makes the branch unconditional, and 0x90 (nop) removes it entirely. Those three values cover nearly every patch a CTF binary needs.

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.

Patching is a large enough topic to have its own guide. For the full treatment, including patching from the command line without a GUI, keeping a byte-for-byte patch log, and the cases where a patched binary refuses to run at all, read binary patching for crackmes.

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.

When scripting Ghidra is the wrong tool: Scripting is for extracting and transforming what is already in the binary. If instead you need to solve for an input that satisfies a check you just read in the decompiler, stop writing Jython and hand the constraints to a solver. Z3 takes the comparison logic directly, and angr explores the binary symbolically without you transcribing anything. Both routinely finish challenges in minutes that take hours to invert by hand.

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.

Challenges to practice this workflow

Doing them in that order is deliberate. Secure Password Database is a clean read of a single hashing function. Gatekeeper asks you to satisfy a numeric check rather than bypass it, so you have to actually understand the comparison. Hidden Cipher 1 is UPX-packed, which means Ghidra shows you nothing useful until you unpack it, and the flag is XOR-encrypted rather than stored, so the decompiler only hands you the key and you finish the job outside it. If you want the full ladder around this guide, the reverse engineering roadmap sequences every RE post on this site from first crackme to symbolic execution.

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 + FFind references to the current address (cross-references)
Alt + LeftNavigate backward (like browser Back)
Alt + RightNavigate forward
DDisassemble at cursor (force-disassemble undefined bytes)
FCreate function at cursor
Ctrl + ZUndo last change (patch, rename, etc.)
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.

Sources and further reading

The decompiler is an approximation. When its output is ambiguous, these are what you check it against.

Run it in the browser

Tools on this site that do the work described above. No install, nothing uploaded: they run entirely in your browser.

Try it on these picoCTF challenges

Walkthroughs that put this technique to work, grouped by event.

Keep reading

Guides that build on the same ideas, plus the roadmap this topic sits under.