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 with q
  • 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: pd disassembles, pdf a whole function, px hexdumps, ps prints a string
  • Flags (r2’s kind): named bookmarks for functions, strings, and sections like sym.main or str.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 / cmdDoesFlag / cmdDoes
-AOpen and run aaa on loadpdfDisassemble the current function
-dOpen under the debuggerpd NDisassemble N instructions from seek
-wOpen writable (for patching)iz / izzStrings in data / everywhere
-q -c CMDRun CMD and quit (scripting)iiList imports (system, gets, …)
aaaAnalyze all: functions, refs, stringsaxtWho cross-references this address
aflList analyzed functionssSeek: move the cursor (s main)
px NHexdump N bytes at the seek~Grep r2’s output (pdf ~call)
VVVisual graph (control-flow) modedb / dc / dsBreakpoint / continue / step
wa / wxAssemble / write hex at seekdrRead or dump registers
? suffixList 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#

  • afl names: sym.main and other sym.* are real symbols and your starting points; fcn.0040xxxx means code with no symbol, usually a stripped binary.
  • rabin2 -I protections: canary=true, nx=true, pic=true shape any pwn plan, so read them before forming one. stripped=true means lean on strings, imports, and xrefs to orient.
  • pdf auto-comments: r2’s inline string previews and resolved call targets are strong clues, not gospel; confirm by following the xref with axt.
  • Empty or tiny afl: you forgot aaa, 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#

ProblemCauseFix
command not foundNot installed / stale PATHReinstall; open a fresh shell
Cannot open fileWrong path or no read permCheck the path; chmod +r ./chall
Function list emptyAnalysis not runRun aaa, or launch r2 -A
Only fcn.* namesBinary is strippedOrient via strings, imports, and xrefs
pdf shows garbageWrong arch detectedSet e asm.arch=... and e asm.bits=...
Debugger will not startNot in -d / ptrace blockedLaunch r2 -d; check ptrace_scope in the VM
wa/wx do nothingOpened read-onlyReopen with -w, or oo+ to enable writing
Commands differ from a guider2 vs rizin, or version driftCheck r2 -v; append ? for current sub-commands

Gotchas#

  • aaa before 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.
  • s moves your seek, @ does not. pdf @ main reads main and leaves your cursor where it was; s main; pdf actually relocates you. Mixing them up reads or edits the wrong spot in write mode.
  • Patch the copy, never your only sample. -w writes changes straight to disk with no undo prompt, so keep chall.orig and diff against it with radiff2.
  • 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 ~call filters inside the session; there is no external pipe from the [0x...]> prompt, so shell | grep does not apply there.
  • A dynamic run executes the sample. Under -d an 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.

Find us elsewhere

Merch, stickers, and moreSupport the work at the Solvere Labs shop