July 2, 2026

radare2 and rizin for CTF: A Beginner's Workflow

Radare2 and rizin for CTF: the command-line RE workflow, the letter grammar, aaa/afl/pdf, the decompiler, patching in write mode, and a worked crackme.

What is the fastest way to triage a CTF binary on the command line?

Open it in radare2 (or its fork, rizin), run three commands, and you are reading annotated disassembly in under ten seconds. The whole opening move is this:

$ r2 ./crackme # open the binary (rizin: rz ./crackme)
[0x00001140]> aaa # analyze everything: functions, xrefs, strings, flags
[0x00001140]> afl # list the functions it found
[0x00001140]> s main # seek to main
[0x000011a9]> pdf # print disassembly of this function

That is the entire skeleton: aaa to analyze, afl to see what you have, s to move, pdf to read. If you already use Ghidra and GDB, you can think of radare2 as both of them living in one prompt with no window manager in between. You stay on the keyboard, the analysis is scriptable, and you can patch the file in the same session you used to read it.

Note: Every command in this guide works identically in radare2 and rizin unless noted. The binary names differ (r2 versus rz), and a few advanced commands diverged after the fork, but the core grammar below is shared. When it matters, this post calls out the difference.

The rest of this guide explains the grammar behind those commands so the prompt stops feeling like a wall of cryptic letters and starts feeling like a sentence you are writing. If you want the graphical sibling of this workflow, read the Ghidra guide; if you want the dynamic side, the GDB CTF guide covers single-stepping a live process.

Why reach for radare2 when Ghidra exists?

Ghidra is excellent and you should keep using it. radare2 is not a replacement so much as a different gear. Three things make it the better tool for a specific job:

  • Triage speed. radare2 opens and analyzes a small CTF binary in a second or two. There is no project to create, no import dialog, no language picker. For the first ten minutes of a challenge, when you are just trying to learn what the binary does, that latency difference matters.
  • Scriptability. Every action is a command, and every command can be chained, piped, or run from a script. You can drive the whole engine from r2pipe in Python, which makes it trivial to extract a key schedule or brute a check in a loop. The x86 assembly guide pairs well here once you start reading the disassembly it produces.
  • One session, read and write. You can disassemble a comparison, decide to flip a branch, and patch the bytes without ever leaving the prompt or opening a hex editor. That round trip is where radare2 shines for crackmes.
Ghidra is where you understand a function. radare2 is where you triage a binary, script against it, and patch it. Use the gear that fits the minute you are in.

What do all those single letters actually mean?

radare2 commands look like line noise until you learn that they are built from a small alphabet, and each letter names a category. Once you know the first letter, you can guess the rest. The roots that carry you through almost every CTF session:

LetterCategoryExamples you will use
aAnalyzeaaa analyze all, afl list functions, af analyze this function, axt xrefs to here
sSeeks main go to main, s 0x1234 go to an address, s- seek back
pPrintpdf disassemble function, pd 20 disassemble 20 ops, px hexdump, psz print a string
fFlagsf list named locations, fs strings switch flag space
iInfoi file info, iz strings in data, ii imports, is symbols
VVisualV visual mode, VV visual graph (the call/branch graph)
wWritewa write assembly, wx write hex, wao write-assembly-op (e.g. negate a jump)

The grammar composes. Append ? to any prefix to get its sub-help: a? lists every analysis command, p? lists every print command. Append j to most commands for JSON output you can pipe into jq or r2pipe. Append q for quiet, one-column output. So aflqj is "analyze functions, list them, quietly, as JSON." You are building sentences, not memorizing a phrasebook.

Key insight: The single most useful habit in radare2 is typing ? after a prefix instead of reaching for a browser. ax? tells you every cross-reference command in one screen. The tool teaches itself if you let it.

What is the standard opening sequence on a fresh binary?

Muscle-memory this and you will start every challenge the same way. The four moves are analyze, survey, seek, read.

$ r2 ./crackme
[0x00001140]> aaa # full analysis (functions, calls, strings, flags)
[0x00001140]> afl # list all functions, with addresses and sizes
0x000011a9 1 120 main
0x00001169 1 34 sym.check_password
0x00001140 1 46 entry0
[0x00001140]> s main # seek to main by name (a flag aaa created)
[0x000011a9]> pdf # print the disassembly of main

A few notes on what just happened. aaa is the workhorse: it runs the analysis passes that find function boundaries, resolve calls, propagate types, and turn every named symbol into a flag you can s to by name. That is why s main works without you ever looking up an address. If analysis seems shallow on a stripped binary, escalate to aaaa (experimental, more aggressive) or run aac to analyze function calls more thoroughly.

The prompt itself is information: the hex in brackets, like [0x000011a9], is your current seek address. Every print command operates from there. Move with s, read with p, and the prompt tells you where you stand.

Tip: Start radare2 with r2 -A ./crackme to run aaa automatically on load, or put e bin.relocs.apply = true and other defaults in your ~/.radare2rc. For a binary you only want to read and never run, that one flag saves the first command of every session.

How do I read the disassembly and follow the control flow?

pdf gives you a linear disassembly of the current function, with radare2's annotations: resolved call targets, string contents inline next to the address that loads them, and ASCII art arrows on the left showing where each jump lands. For a short function that is often all you need.

[0x000011a9]> pdf
;-- main:
0x000011a9 push rbp
0x000011aa mov rbp, rsp
0x000011ad lea rdi, str.Enter_password: ; 0x2004
0x000011b4 call sym.imp.printf
0x000011b9 call sym.check_password
0x000011be test eax, eax
0x000011c0 je 0x11d0 ; jump if check returned 0
0x000011c2 lea rdi, str.Correct ; the branch we want
...

When the control flow gets tangled, switch to the visual graph. VV draws each basic block as a node with arrows for the branches, which is the closest thing radare2 has to Ghidra's function graph. Inside it, hjkl pan, + and - zoom, Tab cycles between blocks, and q backs out. Plain V is the non-graph visual mode: same disassembly, but you can scroll it like a pager and press p to rotate through print formats.

Note: In visual mode, lowercase q steps you back out one level toward the prompt; you do not lose your analysis. Think of V and VV as views layered on top of the same session, not separate programs.

Does radare2 have a decompiler like Ghidra?

Yes, with a catch. Radare2 ships pdc out of the box, a lightweight pseudo decompiler. It reformats disassembly into a C-like pseudocode by combining emulation and pseudo-syntax, and it works for every architecture radare2 supports. It does not recover real control flow (no proper if / for reconstruction) and it does not clean up dead code, so the output is verbose. For a quick gist, though, it is instant and always available.

[0x00001169]> pdc # built-in pseudo-decompiler, every arch, no plugin

For real decompilation, install the Ghidra decompiler as a native plugin. In radare2 that plugin is r2ghidra and its command is pdg. It embeds Ghidra's C++ decompiler engine, so you get Ghidra-quality output without launching the full Ghidra GUI:

[0x00001169]> pdg # Ghidra decompiler output (needs r2ghidra plugin)
[0x00001169]> pdga # side-by-side disassembly and decompilation
[0x00001169]> pdgo # decompilation annotated with addresses

rizin ships the same capability through its own integration, rz-ghidra, and uses the same pdg command family. One difference worth knowing: rizin removed the built-in pdc pseudo decompiler as part of trimming less-tested features, so on rizin you rely on pdg (via rz-ghidra) for decompilation. Install plugins through the package manager: r2pm -ci r2ghidra for radare2, or rz-pm install rz-ghidra for rizin.

Warning: The Ghidra plugin's output is good but not identical to the full Ghidra GUI, because the plugin feeds it radare2's analysis rather than Ghidra's own. When a function decompiles into nonsense, the fix is usually upstream: improve the analysis (rerun af, set the calling convention, fix a wrong variable type) and decompile again. Garbage in, garbage out.

The authoritative reference for the decompiler options is the radare2 book, which documents every pd variant and how the plugins hook in.

How do I find the interesting strings and what touches them?

Most crackmes hinge on a string: a hardcoded password, a format string for the success message, or the comparison buffer. radare2 has two string commands and the difference between them solves a common frustration.

[0x00001140]> iz # strings in the data sections only (the usual case)
[0x00001140]> izz # strings in the WHOLE binary, every section
vaddr=0x00002004 string=Enter password:
vaddr=0x00002013 string=Correct!
vaddr=0x0000201c string=sup3r_s3cr3t_k3y

Use iz first; it is faster and almost always finds what you want. Reach for izz when a string is hiding outside the standard data sections, for example packed into .rodata oddly or embedded in an unusual segment. izz scans everything, so it is the catch-all when iz comes up empty.

Finding the string is half the move. The other half is finding the code that uses it, which is what cross-references (xrefs) are for. Seek to the string's address, then ask what refers to it:

[0x00001140]> s 0x0000201c # seek to the secret string
[0x0000201c]> axt # 'analyze xrefs TO' this address
main 0x11b9 [DATA] lea rsi, str.sup3r_s3cr3t_k3y
[0x0000201c]> s 0x11b9 # jump to the instruction that used it
[0x000011b9]> pdf # read the function around it

axt (xrefs to) is the command you will type most after pdf. Its mirror is axf (xrefs from), which lists what the current instruction points at. Together they let you walk the call graph by hand: find the interesting data, ask who touches it, jump there, repeat.

Tip: Chain it in one line. axt @@ str.* runs axt at every flag whose name starts with str., printing every xref to every string at once. The @@ iterator is radare2's for-each, and it works with any command.

How do I patch a binary to bypass a check?

Sometimes the fastest path to the flag is not to defeat the check but to delete it. If a crackme prints the flag only when check_password returns nonzero, you can flip the conditional jump so the success branch always runs. radare2 patches in the same session you used to read the binary; you just have to open it writable.

$ r2 -w ./crackme # -w opens the file in write mode
[0x00001140]> aaa
[0x00001140]> s 0x11c0 # the conditional jump we want to neutralize
[0x000011c0]> pd 1
0x000011c0 je 0x11d0 ; jump-if-equal to the FAILURE branch

There are three ways to write the change, in increasing order of convenience:

  • wx writes raw hex bytes. If you know the opcode you want, say two nop bytes 0x90 0x90, you type wx 9090 at the seek and the bytes land there.
  • wa writes assembly. You give it the instruction in text and radare2 assembles it for you: wa nop or wa jmp 0x11c2 rewrites the instruction at the current address. No manual opcode lookup.
  • wao is the write-assembly-op helper, and for branch flipping it is the cleanest of all. wao nop turns the current instruction into the right number of nops, and wao recj (reverse conditional jump) flips a je into a jne and vice versa without you touching a single byte by hand.
[0x000011c0]> wao recj # flip je -> jne so success always runs
[0x000011c0]> pd 1 # confirm the change took
0x000011c0 jne 0x11d0
[0x000011c0]> q # changes are already written to the file on disk
Warning: Write mode edits the file on disk in place. Always work on a copy: cp crackme crackme.patched first, then r2 -w ./crackme.patched. If you would rather preview edits without touching the real file, open with r2 -w on a cache layer using e io.cache = true and the writes stay in memory until you commit them with wci.

What does a full crackme flow look like start to finish?

Here is the complete loop on a typical "enter the password" crackme, from a cold open to either reading the key or patching past it. This is the workflow you will repeat on most reversing challenges.

# 1) Open and analyze
$ r2 -A ./crackme
# 2) Survey: what functions and strings exist?
[0x00001140]> afl
[0x00001140]> iz
vaddr=0x0000201c string=sup3r_s3cr3t_k3y <- suspicious
# 3) Find the code that uses the suspicious string
[0x00001140]> s 0x0000201c
[0x0000201c]> axt
sym.check_password 0x1180 [DATA] lea rsi, str.sup3r_s3cr3t_k3y
# 4) Read the check
[0x0000201c]> s sym.check_password
[0x00001169]> pdf
... call sym.imp.strcmp ; compares input against the key
... test eax, eax
... setne al ; returns 0 only when they MATCH

At step 4 the binary tells you everything. It loads sup3r_s3cr3t_k3y and runs strcmp against your input, so the password is simply that string. You are done; type it in and read the flag. But suppose the comparison were obfuscated, or the key were computed at runtime rather than stored. Then you flip the branch instead:

# 5a) The easy win: the key was right there in the strings
$ ./crackme
Enter password: sup3r_s3cr3t_k3y
Correct! picoCTF{...}
# 5b) The fallback: patch the comparison so any input passes
$ cp crackme crackme.patched
$ r2 -w -A ./crackme.patched
[..]> s main
[..]> pdf # find the 'je' that guards the success print
[..]> s 0x000011c0
[..]> wao recj # flip the branch
[..]> q
$ ./crackme.patched # now any password reaches the success path
Key insight: Notice the decision point. If the check compares against a stored value, recover it and enter it; the flag is often derived from the real input. If the check computes something you cannot easily invert, patch past it. Reading versus patching is the core judgment call in every crackme, and radare2 lets you do both from the same prompt without switching tools.

What is the difference between radare2, rizin, and Cutter?

A bit of history clears up the confusion. radare2 is the original command-line reverse engineering framework. In 2020 a group of contributors forked it into rizin, aiming for a more stable command set, cleaner internal APIs, and a documented plugin system. The two share most of their DNA, so the grammar in this guide works in both. The practical differences:

  • Command stability. rizin froze and documented its command set and removed some less-tested features (like the built-in pdc pseudo decompiler). radare2 moves faster and keeps more experimental commands.
  • Plugin ecosystems. Plugins are not cross-compatible. radare2 uses r2pm and the r2ghidra decompiler; rizin uses rz-pm and rz-ghidra. Pick a lane and install accordingly.
  • The GUI. Cutter is the graphical front end. It began on radare2 and now ships on top of rizin, giving you a window with a graph view, a decompiler panel, and a hex editor while still exposing the command line underneath. If the all-keyboard prompt feels like too much at first, Cutter is the on-ramp: you click around, watch which commands it runs, and graduate to the CLI.

For CTF, either engine is fine. Many players run rizin with Cutter for the graph view and drop to the command line for scripting and patching. The official project homes are the radare2 site, the rizin site, and Cutter; all three are free and open source.

Note: You do not have to choose forever. The skills transfer almost perfectly. Learn the command grammar once and you can sit down at radare2, rizin, or Cutter's embedded console and be productive in minutes.

Which picoCTF challenges are good practice for this workflow?

The reversing track is the natural home for radare2. A few challenges on this site map cleanly onto the read-or-patch loop above:

  • picoCTF 2024 Classic Crackme 0x100 is the canonical "find the check, read or patch it" exercise. Open it, aaa, iz, then axt your way to the comparison.
  • picoCTF 2022 keygenme asks you to understand a key-generation routine rather than just read a stored string, so it is where the pdg decompiler earns its keep over raw pdf.
  • picoCTF 2023 reverse is a gentle on-ramp: the answer is close to the surface, perfect for practicing the opening sequence without obfuscation getting in the way.
  • picoCTF 2021 crackme-py is a Python target rather than a native binary, a useful reminder that radare2 is for machine code; for bytecode you reach for a different tool, and knowing where the line sits is part of the skill.

Work them in that order and you will have run every command in the quick reference below against a real target at least once.

Quick reference

radare2 / rizin command cheat sheet

# Open and analyze
r2 ./bin open (rizin: rz ./bin)
r2 -A ./bin open and run aaa automatically
r2 -w ./bin open in write mode (patching)
aaa analyze all (functions, xrefs, strings, flags)
aaaa more aggressive, experimental analysis
# Navigate and survey
afl list functions
s main seek to a function by name
s 0x1234 seek to an address
f list flags (named locations)
# Read
pdf disassemble the current function
pd 20 disassemble 20 instructions
VV visual call/branch graph (q to exit)
pdc built-in pseudo decompiler (r2 only)
pdg Ghidra decompiler (needs r2ghidra / rz-ghidra)
# Strings and cross-references
iz strings in data sections
izz strings in the whole binary
axt xrefs TO the current address
axf xrefs FROM the current address
# Patch (requires -w)
wx 9090 write raw hex bytes
wa nop assemble and write an instruction
wao recj flip a conditional jump (je <-> jne)
# Help, always
a? p? ax? w? sub-help for any command prefix

Learn the seven roots (a s p f i V w), append ? whenever you are stuck, and a CTF binary goes from opaque blob to readable program in the time it takes Ghidra to finish importing.