radare2
radare2 turns an unknown binary into something you can read: it disassembles code, maps out functions, lists strings and imports, traces execution under a debugger, and lets you script the whole thing. Reach for it when a CTF hands you a compiled challenge and the only way forward is understanding what the program actually does, or when you need to confirm what a stripped executable references before you trust it.
Mental model: r2 [flags] ./file, then terse commands at the [0x...]> prompt. Run aaa first, and everything acts at the current seek.
New to radare2? Core concepts
- Interactive shell: launch
r2 ./chall, type commands at the[0x...]>prompt, quit withq - Analysis first:
aaa(analyze all) builds the function list, cross-refs, and strings; raw r2 just shows bytes - Seek (
s): your cursor in the binary; nearly every command acts at the current address (s main,s 0x401136) - Command grammar: terse letters that nest -
print,analyze,seek,info,debug,write; append?for sub-commands (p?) - Print modes:
pddisassembles,pdfa whole function,pxhexdumps,psprints a string - Flags (r2’s kind): named bookmarks for functions, strings, and sections like
sym.mainorstr.password; not the CTF kind @modifier: run a command at a temporary address without moving your seek (pdf @ main)~grep: filter r2’s own output from inside the session (pdf ~call)
When to reach for radare2 (and when not)
Reach for it to understand a CTF reversing or pwn binary, find a win condition, confirm protections (canary, NX, PIC), trace input validation in a crackme, statically triage a malware sample’s strings and imports before a controlled run, or diff a patched and unpatched build.
Reach for something else when you want readable pseudo-C fast on a large program (Ghidra, IDA, Binary Ninja), pure file carving or signature extraction (binwalk), quick metadata only (file, strings, rabin2 -I), or heavy dynamic pwn and exploit development (gdb with GEF or pwndbg). A packed binary needs unpacking first, or static analysis just shows the stub.
Install, update, verify#
sudo apt install -y radare2 # Debian / Ubuntu / Kali
brew install radare2 # macOS
git clone https://github.com/radareorg/radare2 # from source (newest build)
cd radare2 && sys/install.sh
sudo apt install --only-upgrade -y radare2 # update (distro)
cd radare2 && git pull && sys/install.sh # update (source)
r2 -v # version and build commit
command -v r2 # confirm it is on PATH
rabin2 -I ./chall # self-test: print any binary's header + protections
Distro packages lag because r2 moves fast, so the source sys/install.sh gives the newest analysis heuristics. rabin2, radiff2, and rax2 are separate helper binaries that ship alongside r2. Plugins are managed with r2pm update then r2pm -ci r2ghidra.
Flags you’ll actually use#
radare2 mixes command-line launch flags with terse in-shell commands, and these are the ones you reach for. Skim these first; the cheat sheet below is these flags combined.
| Flag / cmd | Does | Flag / cmd | Does |
|---|---|---|---|
-A | Open and run aaa on load | pdf | Disassemble the current function |
-d | Open under the debugger | pd N | Disassemble N instructions from seek |
-w | Open writable (for patching) | iz / izz | Strings in data / everywhere |
-q -c CMD | Run CMD and quit (scripting) | ii | List imports (system, gets, …) |
aaa | Analyze all: functions, refs, strings | axt | Who cross-references this address |
afl | List analyzed functions | s | Seek: move the cursor (s main) |
px N | Hexdump N bytes at the seek | ~ | Grep r2’s output (pdf ~call) |
VV | Visual graph (control-flow) mode | db / dc / ds | Breakpoint / continue / step |
wa / wx | Assemble / write hex at seek | dr | Read or dump registers |
? suffix | List a command’s sub-commands | @ | Run at a temp address, keep seek |
Cheat sheet#
Each block groups one task; the launch and navigate blocks build up a flag at a time, so you can stop at the line that does the job.
# Static triage: no session, touches only the file
rabin2 -I ./chall # header: arch, bits, nx, canary, pic, stripped
rabin2 -z ./chall # strings in data sections, with addresses
rabin2 -zz ./chall # all strings, including non-section ones
rabin2 -i ./chall # imports (system, gets, strcmp, ...)
rabin2 -E ./chall # exports
rabin2 -s ./chall # symbol table
# Launch the shell: stop at the mode you need
r2 ./chall # 1. open read-only
r2 -A ./chall # 2. + run full analysis (aaa) on load
r2 -w ./chall # 3. + write mode, for patching
r2 -d ./chall # 4. debugger instead, for a live run
# Analyze and orient (inside the shell)
aaa # analyze all: functions, refs, strings
afl # list functions: address, size, name
afl ~main # filter that list to lines with "main"
ii # imports; iz strings; is symbols
# Navigate and read: build up from a jump to a whole function
s sym.main # seek to a symbol
s 0x00401136 # seek to a raw address
pd 20 # disassemble 20 instructions from here
pdf # disassemble the whole current function
pdf @ main # disassemble main without moving the seek
pdf @ main ~call # + show only its call instructions
VV # read it as a control-flow graph; q to leave
# Strings and cross-references: string -> the code that uses it
iz # strings in data sections
izz~password # search every string for "password"
axt @ str.password # who references that string
axt @ sym.imp.system # every caller of an imported function
# Patch (write mode only; always on a copy)
s 0x0040118a # seek to the instruction to change
wa nop # assemble a nop at the seek
wao nop # nop out the whole current instruction
wx 9090 # write raw hex bytes at the seek
wa jmp 0x401200 # assemble any instruction at the seek
oo+ # re-open the current file as writable
# Debug (dynamic; isolated VM for untrusted samples)
db main # set a breakpoint at main
dc # continue until it hits
ds # step one instruction
dso # step over a call
dr # dump all registers
dr rdi # read one register
dm # list memory maps
ood # reopen the current file in debug mode
# Headless and scripting
r2 -q -c 'iz' ./chall # run one command and quit
r2 -q -c 's main; pdf' ./chall # capture main's disassembly
r2 -qc 'aaa; afl' ./chall # analyze then list, non-interactively
radiff2 -C ./chall.orig ./chall # diff two builds, function by function
rax2 0x539 # base conversion: 0x539 -> 1337
Command breakdowns#
Each block below explains one situation, the exact command, and what to expect back.
Dump a binary’s header and protections#
rabin2 -I ./chall
Prints arch, bits, nx, canary, pic, stripped, and more. Read it before anything else: it sets expectations and, for pwn, tells you which mitigations are in play. No session, changes nothing.
List every string with its address#
rabin2 -z ./chall # strings in data sections
rabin2 -zz ./chall # everything, including non-section bytes
Prompts, format strings, file paths, and hints, each with an offset. The single best first move in reversing. Note the interesting offsets; you cross-reference them next.
Find the dangerous imports#
rabin2 -i ./chall
system, execve, gets, strcpy, sprintf are immediate leads: command execution and classic memory-corruption sinks. An empty or tiny import list often means the binary is static or stripped.
Open with analysis done and list the functions#
r2 -A ./chall
[0x00401050]> afl
-A runs aaa on load so you skip typing it. afl prints one line per function with address, size, and name: your map of the code. sym.* entries are named starting points; fcn.* names mean r2 found code but no symbol.
Disassemble main and filter to the calls#
[0x00401050]> pdf @ main ~call
pdf disassembles a whole function, @ main targets it without moving your seek, and ~call greps r2’s own output down to the call instructions. A fast way to see what a function reaches out to.
Trace a string back to the code that uses it#
[0x00401050]> izz~password # find the string and its address
[0x00401050]> axt @ str.password # who references it
axt (analyze xrefs to) lists every instruction that loads that string. Seek to a result to read the check around it. A string existing means nothing until something references it.
Read branchy logic as a graph#
[0x00401050]> s main
[0x00401136]> VV
Visual graph mode draws the basic blocks and the branches between them. Move with hjkl or arrows, p cycles layouts, q leaves. Far easier than scrolling pd for anything with nested conditionals.
Patch a conditional jump (lab crackme)#
r2 -w ./chall
[0x00401050]> s 0x0040118a # the je you want to neutralize
[0x0040118a]> wao nop # nop out the whole instruction
-w opens writable; wao nop replaces the current instruction with padding of the right length. Work on a copy and keep chall.orig pristine. wx 9090 writes raw bytes if you prefer.
Debug: break at main, step, read a register#
r2 -d ./chall
[0x7f...]> db main; dc # breakpoint, then continue to it
[0x00401136]> ds # step one instruction
[0x00401139]> dr rdi # read the rdi register
The prompt starts in the loader (0x7f...) and lands at main after dc. dr with no argument dumps every register. Values are real only for that one run with that one input.
Feed a run from a file on stdin#
r2 -d ./chall
[0x7f...]> dor stdin=./input.txt # redirect the child's stdin (rarun2 option)
[0x7f...]> dc # run and watch behavior at breakpoints
Drives a program that reads from stdin without retyping each run. Keep any network-touching sample isolated so it cannot phone home.
Capture a function’s disassembly to a file#
r2 -q -c 's main; pdf' ./chall > notes/main.asm
-q quits after the -c commands run, and ; chains them. Good for saving evidence into notes without holding a live session open.
Diff two builds to spot a patched check#
radiff2 -C ./chall.orig ./chall
-C matches functions between the two files and reports which changed. The fastest way to find what a vendor patch or a challenge’s “fixed” build actually touched.
Convert values without leaving the terminal#
rax2 0x539 # hex -> decimal: 1337
rax2 1337 # decimal -> hex: 0x539
rax2 -s 41424344 # hex bytes -> ASCII: ABCD
The pocket calculator for reversing: bases, endianness, and ASCII, all from one helper that ships with r2.
Script repeatable analysis with r2pipe#
import r2pipe # pip install r2pipe
r2 = r2pipe.open("./chall")
r2.cmd("aaa")
for f in r2.cmdj("aflj"): # 'j' suffix returns JSON; cmdj parses it
print(hex(f["offset"]), f["name"], f["size"])
r2.quit()
cmdj runs a command and parses its JSON output into Python objects. Turns a manual triage you repeat every challenge into a script you run once.
Reading the output#
aflnames:sym.mainand othersym.*are real symbols and your starting points;fcn.0040xxxxmeans code with no symbol, usually a stripped binary.rabin2 -Iprotections:canary=true,nx=true,pic=trueshape any pwn plan, so read them before forming one.stripped=truemeans lean on strings, imports, and xrefs to orient.pdfauto-comments: r2’s inline string previews and resolved call targets are strong clues, not gospel; confirm by following the xref withaxt.- Empty or tiny
afl: you forgotaaa, or the binary is stripped or packed. Run analysis; if it is still empty, unpack first. - Debugger values: true only for that run with that input. A different input takes a different branch, so do not over-generalize from one trace.
Troubleshooting#
| Problem | Cause | Fix |
|---|---|---|
command not found | Not installed / stale PATH | Reinstall; open a fresh shell |
Cannot open file | Wrong path or no read perm | Check the path; chmod +r ./chall |
| Function list empty | Analysis not run | Run aaa, or launch r2 -A |
Only fcn.* names | Binary is stripped | Orient via strings, imports, and xrefs |
pdf shows garbage | Wrong arch detected | Set e asm.arch=... and e asm.bits=... |
| Debugger will not start | Not in -d / ptrace blocked | Launch r2 -d; check ptrace_scope in the VM |
wa/wx do nothing | Opened read-only | Reopen with -w, or oo+ to enable writing |
| Commands differ from a guide | r2 vs rizin, or version drift | Check r2 -v; append ? for current sub-commands |
Gotchas#
aaabefore you navigate, always. Raw r2 shows bytes with no function list, so skipping analysis is the single most common reason a session feels empty and cryptic.smoves your seek,@does not.pdf @ mainreads main and leaves your cursor where it was;s main; pdfactually relocates you. Mixing them up reads or edits the wrong spot in write mode.- Patch the copy, never your only sample.
-wwrites changes straight to disk with no undo prompt, so keepchall.origand diff against it withradiff2. - r2 and rizin have drifted. rizin renamed commands and helper binaries, so a one-liner copied from a rizin guide may silently do nothing or error in r2. Confirm which fork a guide targets.
~is r2’s grep, not the shell’s.pdf ~callfilters inside the session; there is no external pipe from the[0x...]>prompt, so shell| grepdoes not apply there.- A dynamic run executes the sample. Under
-dan untrusted binary really runs and can touch the filesystem, spawn processes, or beacon out. Do it only in an isolated, network-disabled VM.
Pairs with#
binwalk carves firmware and extracts embedded files before you disassemble the code inside them, and file, strings, and xxd answer the quick first-glance questions faster than opening a session. For readable pseudo-C on a large function, bounce to Ghidra or its r2ghidra plugin; for heavy dynamic pwn and exploit development, gdb with GEF or pwndbg is friendlier. Reach for the rizin fork and its Cutter GUI when you want r2’s engine with a steadier command set. radiff2, rabin2, and rax2 all ship with r2, so lean on them before adding another tool.