cradare2
cradare2
A powerful, idiomatic Crystal framework and high-level DSL for controlling radare2 via the r2pipe protocol across Windows, Linux, and macOS.
Includes low-level command framing, strongly typed JSON::Serializable models, rich DSL modules for binary analysis, memory forensics, disassembly, custom type definitions, Crystal runtime inspection, source-to-assembly correlation, and native debugging (breakpoints, CPU registers, stack unwinding, memory maps, and demangled crash reporting).
In-Depth Guides
- Architecture & Internals Guide: Transports, client internals, DSL layers, and error model.
- Game Engine & GDExtension Integration Guide: Godot & Lapis forensics, ClassDB symbol injection, Variant
pfdecoding, and vtable analysis. - Radare2 Plugin Development Guide: Writing radare2 plugins in Crystal with custom command dispatchers and interactive pipe servers.
- Source Line Matching & Line Engine Guide: DWARF/PDB line resolution,
llvm-symbolizer, path remapping, and interleaved listings. - Native Debugging, Memory Forensics & Crash Diagnostics Guide: Registers, memory classifier, crash diagnosis, stack unwinding, and object inspection.
Features
- Cross-Platform Transports:
- Subprocess Spawn: Spawns
r2 -q0child process with reliable null-byte (\0) stream framing and asynchronous fiber timeout enforcement on Windows, Linux, and macOS. - HTTP REST: Connects to remote radare2 webservers (
http://orhttps://). - TCP Socket: Connects directly to remote radare2 TCP instances (
tcp://). - In-Session (
#!pipe): Connects to the parent radare2 session when run as a script inside radare2 viaR2PIPE_IN/R2PIPE_OUTorR2PIPE_PATH. - Deterministic Mocking: Zero-dependency
MockTransportfor deterministic unit testing.
- Subprocess Spawn: Spawns
- Fluent Configuration (
Cradare2::Options):- Fine-grained session setup via options object or builder block (
Cradare2.open(path) { |opts| opts.timeout(10.seconds) }).
- Fine-grained session setup via options object or builder block (
- Unified Address Representation (
Address):- Transparently accepts
UInt64,Int64,Int32, orString(hex0x..., decimal, registers, flags) across all APIs.
- Transparently accepts
- Multi-Module & Memory Forensics (
r2.debug.modules):- Parse loaded PE/ELF/Mach-O binaries, resolve module boundaries, and classify instruction pointers (
module_at,base_address_of).
- Parse loaded PE/ELF/Mach-O binaries, resolve module boundaries, and classify instruction pointers (
- Flags & Flag Spaces (
r2.flags):- Fluent flag creation, retrieval, batch setting, and symbol isolation via Flag Spaces (
r2.flags.space("godot")).
- Fluent flag creation, retrieval, batch setting, and symbol isolation via Flag Spaces (
- Safe Comments DSL (
r2.comments):- Automatic Base64
CCuencoding and decoding, eliminating shell quoting, whitespace, and injection bugs.
- Automatic Base64
- Custom Types & Structs (
r2.types):- Register C/engine struct formats (
define_struct) and decode memory into structured Crystal Hashes viapfj.
- Register C/engine struct formats (
- Expressive DSLs:
r2.analyze: Fluent binary analysis (all,calls,functions,references,autoname,preludes,emulate).r2.disasm: Disassembly to formatted text, typed instructions, and Ghidra decompilation (pdc/pdg).r2.memory: Direct reading & writing of integers (u8..u128,i8..i128,f32..f64), 128-bit QWORD Emotion Engine SIMD, strings, C-strings (read_cstring), typed arrays (read_pointer32_array,read_pointer_array,read_u32_array,read_f32_array), hexdump, diffing (hexdiff), and pattern searching (search_hex).r2.debug: Process control, breakpoints, memory classification, crash reporting, and architecture-agnostic registers with first-class x86_64, ARM, and MIPS R5900/I named registers.
- PlayStation 2 & Citrine Support (
require "cradare2/ps2/citrine"):- Privileged GS registers (
PMODE,CSR), drawing registers (PRIM,RGBAQ,XYZ2,XYZ3,TEST_1,FRAME_1,ZBUF_1), 128-bit GIFTag decoding, packet dissection, SPRAM canary verification (0xDEADBEEF), and EE/IOP disassembler presets.
- Privileged GS registers (
- Godot 4 & Lapis Support (
require "cradare2/godot/lapis"):- GDExtension library verification (
verify_gdextension), 24-byte Variant decoding (read_variant), object header parsing (read_object), vtable symbol resolution (inspect_vtable), and crash origin classification (classify_crash).
- GDExtension library verification (
- In-Memory Buffers (
Cradare2.open_bytes):- Disassemble, inspect symbols, and debug ephemeral byte slices with automatic temporary file lifecycle management.
- Lightweight GDB Remote Serial Protocol Client (
Cradare2::Gdb::Client):- Direct socket-level RSP query engine for rapid, zero-overhead memory and register queries against PCSX2, QEMU, or gdbserver stubs during CI audits.
- Toolchain Doctor Diagnostics (
Cradare2.doctor&r2-crystal doctor):- Audit radare2 binary, ABI version, disassembler architectures,
llvm-symbolizer, and Crystal compiler with colored status badges.
- Audit radare2 binary, ABI version, disassembler architectures,
- Extensible Plugin Framework & Interactive Pipe Server:
- Modular
Commandclasses with configurable prefix (CommandDispatcher.new(client, prefix: "godot")), automatic-jJSON support, andCradare2::Plugin::Server.run.
- Modular
- Source Line to Assembly Matching (
r2.crystal.lines):- Pluggable
LineProviderstrategy interface (R2CodelineProvider,LlvmSymbolizerProvider), path remapping, and $O(\log N)$ binary search instruction matching.
- Pluggable
Installation
Add cradare2 to your shard.yml:
dependencies:
cradare2:
github: sol-vin/cradare2
branch: master
Then run:
shards install
Make sure radare2 is installed on your system:
- Windows:
scoop install radare2orchoco install radare2 - macOS:
brew install radare2 - Linux:
apt install radare2or compile from source
Quick Start
require "cradare2"
# Open a binary with fluent options
Cradare2.open("my_app.exe") do |r2|
# Run analysis
r2.analyze.all
# Target metadata
info = r2.info
puts "Target: #{info.format} #{info.arch} (#{info.bits}-bit) on #{info.os}"
# Functions
r2.functions.first(5).each do |fn|
puts "Function: #{fn.name} @ 0x#{fn.offset.to_s(16)} (#{fn.size} bytes)"
end
# Sections
r2.sections.each do |sec|
puts "Section #{sec.name}: 0x#{sec.vaddr.to_s(16)} [#{sec.perm}]"
end
# Disassemble 5 instructions
puts r2.disasm.text(5)
end
Fluent Options Builder
# Configure timeouts, analysis flags, and write access via builder block
Cradare2.open("my_app.exe") do |opts|
opts.write_mode
opts.timeout(15.seconds)
opts.skip_analysis
end.as(Cradare2::Client)
Low-Level API (cmd, cmdj, safe_cmdj, batch)
Execute any radare2 command directly:
# Raw string output
puts r2.cmd("?e Hello from radare2")
# Dynamic JSON parsing (returns JSON::Any)
json = r2.cmdj("ij")
puts json["bin"]["arch"].as_s
# Type-safe deserialization directly into any JSON::Serializable model
functions = r2.cmdj("aflj", as: Array(Cradare2::Model::Function))
# Safe JSON query (returns nil on parse error or empty output)
info = r2.safe_cmdj("ij", as: Cradare2::Model::BinaryInfo)
# High-throughput batch command execution
r2.batch([
"s 0x140001000",
"wx 909090",
"CC \"Patched with NOPs\""
])
# Profile command latency
r2.on_command do |cmd, elapsed|
puts "Executed #{cmd} in #{elapsed.total_milliseconds}ms" if elapsed > 50.milliseconds
end
High-Level DSL Reference
Analysis (r2.analyze)
r2.analyze.all # "aaa"
r2.analyze.calls # "aac"
r2.analyze.functions # "aaf"
r2.analyze.references # "aar"
r2.analyze.autoname # "aan"
r2.analyze.preludes # "aap"
r2.analyze.emulate # "aae"
# Block form
r2.analyze do |a|
a.all
a.calls
end
Memory Manipulation (r2.memory)
# Reading integers of all widths
u8 = r2.memory.read_u8(0x401000)
u16 = r2.memory.read_u16(0x401000)
u32 = r2.memory.read_u32(0x401000)
u64 = r2.memory.read_u64(0x401000)
# Writing integers
r2.memory.write_u8(0x401000, 0x90_u8)
r2.memory.write_u16(0x401000, 0x1234_u16)
r2.memory.write_u32(0x401000, 0x1337BEEF_u32)
r2.memory.write_u64(0x401000, 0x1122334455667788_u64)
# Reading null-terminated C-strings (auto-trims escaped nulls)
class_name = r2.memory.read_cstring(0x402000, max_len: 128)
# Reading 64-bit pointer arrays (ideal for vtables and function tables)
vtables = r2.memory.read_pointer_array(0x403000, count: 8)
# Pattern Searching
hits = r2.memory.search_bytes(Bytes[0x90, 0x90])
string_hits = r2.memory.search_string("Query")
# Hexdump
puts r2.memory.hexdump(0x401000, size: 64)
Disassembly & Decompilation (r2.disasm)
# Text disassembly
puts r2.disasm.text(10, at: 0x401000)
# Typed instruction models
instructions = r2.disasm.instructions(10, at: 0x401000)
instructions.each do |ins|
puts "0x#{ins.offset.to_s(16)}: #{ins.opcode} (type: #{ins.type})"
end
# Decompilation (pdc or Ghidra pdg)
puts r2.disasm.decompile("main")
Flags & Flag Spaces (r2.flags)
# Switch to 'engine' flag space to isolate symbols
r2.flags.space("engine")
# Set individual flag
r2.flags.set("engine.main_loop", 0x140001000, size: 64)
# Batch set hundreds of symbols in a single pipe write
r2.flags.batch_set({
"engine.Node2D.position" => 0x140002000_u64,
"engine.Node2D.rotation" => 0x140002040_u64,
})
# Filter flags matching a pattern
r2.flags.matching(/Node2D/).each do |f|
puts "#{f.name} @ 0x#{f.offset.to_s(16)}"
end
# Return to root flag space
r2.flags.clear_space
Safe Comments DSL (r2.comments)
# Annotate code safely using Base64 encoding (immune to quotes, semicolons, and spaces)
r2.comments.set(0x140001000, "Player#_ready(delta: Float64); key=\"val\"")
# Read back comment (auto-decodes Base64 into plain text)
puts r2.comments.get(0x140001000)
# => "Player#_ready(delta: Float64); key=\"val\""
# Clear comment
r2.comments.clear(0x140001000)
Custom Types & Structs (r2.types)
# Define a 24-byte Godot Variant format ('d' = u32, 'w' = u16, 'q' = u64)
r2.types.define_struct("godot_variant", [
{"d", "type_tag"},
{"w", "flags"},
{"w", "padding"},
{"q", "val0"},
{"q", "val1"}
])
# Decode memory directly into a structured Crystal Hash via 'pfj'
variant = r2.types.print_format("godot_variant", 0x140500000)
puts "Type Tag: #{variant["type_tag"]}"
puts "Value 0: #{variant["val0"]}"
Multi-Module Forensics & Debugging (r2.debug)
Cradare2.open("godot.exe", debug: true) do |r2|
r2.debug do |d|
d.breakpoint("main")
d.continue
# Module Forensics
d.modules.each do |mod|
puts "#{mod.name}: 0x#{mod.base_address.to_s(16)} - 0x#{mod.end_address.to_s(16)}"
end
# Identify module owning the instruction pointer
if mod = d.module_at(d.registers.pc)
puts "Currently executing inside: #{mod.name}"
end
# Architecture-neutral registers
puts "PC: 0x#{d.registers.pc.to_s(16)}"
puts "SP: 0x#{d.registers.sp.to_s(16)}"
puts "BP: 0x#{d.registers.bp.to_s(16)}"
# Structured root-cause crash diagnosis
diagnosis = d.diagnose_crash
puts "Probable Cause: #{diagnosis.probable_cause}"
# Full formatted demangled crash report
puts d.crash_report
end
end
Crystal Binary & Runtime Inspection (r2.crystal)
Cradare2.open("my_crystal_app") do |r2|
# Binary detection
puts "Crystal Binary: #{r2.crystal.crystal_binary?}"
# Discover classes and methods
r2.crystal.classes.each do |cls|
methods = r2.crystal.methods_for_class(cls)
puts "#{cls}: #{methods.size} methods"
end
# Inspect runtime Crystal String in memory
str = r2.crystal.read_string(0x1000)
puts "String: #{str.value} (#{str.bytesize} bytes)"
# Inspect Crystal Array(T) header
arr = r2.crystal.read_array_header(0x2000)
puts "Array size: #{arr.size}, capacity: #{arr.capacity}"
# Inspect Crystal Slice(T) header
slice = r2.crystal.read_slice_header(0x3000)
puts "Slice size: #{slice.size}, read_only: #{slice.read_only?}"
# Inspect Crystal Fiber
fiber = r2.crystal.read_fiber(0x4000)
puts "Fiber stack: 0x#{fiber.stack_address.to_s(16)}"
end
Source Line to Assembly Matching (r2.crystal.lines)
Cradare2.open("my_app.exe") do |r2|
# Query source location for an instruction address
if loc = r2.crystal.lines.at(0x140001007)
puts "0x140001007 -> #{loc.file}:#{loc.line}"
puts "Source code: #{loc.source_code}"
end
# Find all machine instructions generated for a source line
instructions = r2.crystal.lines.for_line("src/main.cr", 42)
instructions.each do |ins|
puts " 0x#{ins.address.to_s(16)}: #{ins.opcode}"
end
# Compiler-grade interleaved source and assembly listing
puts r2.crystal.lines.interleaved("main")
# Synchronize line mappings into radare2's native `CL` table & `CC` comments
count = r2.crystal.lines.sync_to_r2(annotate_comments: true)
puts "Synchronized #{count} lines to radare2 session."
end
Radare2 Plugin & In-Session CLI (r2-crystal)
Run standalone against any binary or inside a live radare2 session via #!pipe r2-crystal:
# Standalone CLI
r2-crystal my_app.exe detect
r2-crystal my_app.exe demangle "pdb._2A.add.3C.Int32.3E..3A.Int32"
r2-crystal my_app.exe lines sync
r2-crystal my_app.exe src 0x140001007
r2-crystal my_app.exe asm src/main.cr:42
r2-crystal my_app.exe interleaved main
r2-crystal my_app.exe inspect string 0x1400de0c0
r2-crystal my_app.exe --server # Launch interactive radare2 pipe server
Inside radare2:
| Command | Description |
|---|---|
crystal help |
Display plugin help and command reference |
crystal detect |
Verify if target is a Crystal binary and report entrypoint |
crystal info |
Display Crystal runtime information and compiler metadata |
crystal demangle <sym> |
Demangle Crystal symbols, operators, and LLVM/MSVC PDB escapes |
crystal demangle-all |
Batch demangle all symbols and apply clean names to radare2 (afn/fr) |
crystal lines |
List all discovered source files and mapped line counts |
crystal lines sync |
Populate radare2's CL line table and annotate CC source comments |
crystal src <addr> |
Show original source code snippet and context for an address |
crystal asm <file:line> |
Show machine instructions generated for a source code line |
crystal interleaved <target> |
Generate interleaved source code and assembly listing |
crystal classes |
List all detected Crystal classes and modules |
crystal methods <class> |
List methods, addresses, and sizes for a given class |
crystal inspect string <addr> |
Inspect runtime Crystal String struct layout in memory |
crystal inspect array <addr> |
Inspect runtime Crystal Array(T) header in memory |
crystal inspect slice <addr> |
Inspect runtime Crystal Slice(T) header in memory |
crystal crash |
Generate full demangled crash report with stack unwinding |
Building Custom Radare2 Plugins
External shards and game engines can build specialized command suites with custom prefixes and pipe servers:
require "cradare2"
class GodotNodesCommand < Cradare2::Plugin::Command
def initialize
super(name: "nodes", summary: "List active Godot nodes", aliases: ["nd"])
end
def execute(client : Cradare2::Client, args : Array(String), json : Bool = false) : String
json ? %({"nodes":42}) : "Active Nodes: 42"
end
end
client = Cradare2.in_session
dispatcher = Cradare2::Plugin::CommandDispatcher.new(client, prefix: "godot")
dispatcher.register(GodotNodesCommand.new)
# Run interactive pipe server loop
Cradare2::Plugin::Server.run(dispatcher)
Testing
Run the test suite using Crystal's built-in spec runner:
crystal spec --verbose
All exhaustive specs run out-of-the-box using the internal MockTransport without requiring external network access or dependencies.
License
MIT License. See LICENSE for details.
cradare2
- 0
- 0
- 0
- 3
- 2
- 8 days ago
- September 29, 2026
MIT License
Wed, 14 Oct 2026 17:11:30 GMT