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.
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 21sudo 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 locationunzip 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.
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
main or any named function instantly.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:
ifstatements 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 likecheck_flagare 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 renamingif (local_18 == param_1) {puts("Correct!");}// After renaming local_18 -> user_input, param_1 -> expected_flagif (user_input == expected_flag) {puts("Correct!");}
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): comparesnbytes. Often used when the flag is binary data rather than a printable string.strncmp(user_input, secret, n): compares only the firstncharacters. 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
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 functionfrom ghidra.program.model.symbol import RefTypefunc = 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.
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.
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.
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.
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.
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.
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
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 cursorCtrl + FSearch within the Listing or DecompilerCtrl + Shift + EOpen Defined Strings windowCtrl + Shift + FSearch all memory for a string or byte sequenceCtrl + LGo to next instructionAlt + LeftNavigate backward (like browser Back)Alt + RightNavigate forwardFDisassemble at cursor (force-disassemble undefined bytes)PCreate function at cursorCtrl + ZUndo last change (patch, rename, etc.)XFind all cross-references to the current addressSpaceToggle between Listing and Decompiler focusTool comparison
| Tool | Cost | Decompiler | Best for | Difficulty |
|---|---|---|---|---|
| Ghidra | Free | Yes | All CTF RE, beginners, multi-architecture | Medium |
| IDA Free / Pro | Free (limited) / $1,000+ | Pro only | Industry standard, better UI for some | Medium |
| Binary Ninja | Free (cloud) / $299/yr | Yes | Scripting, clean UI, intermediate users | Medium |
| strings | Free | No | First-pass flag search, plaintext flags | Easy |
| ltrace / strace | Free | No | Dynamic tracing, seeing strcmp arguments live | Easy |
RE challenge checklist
- Run
file ./binaryto confirm architecture (32-bit vs. 64-bit, stripped vs. not). - Run
strings ./binary | grep -i picobefore opening any decompiler. - Try
ltrace ./binarywith a dummy input and read thestrcmparguments in the output. - In Ghidra, always run auto-analysis before trying to read anything.
- Start from
mainin 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.