cradare2

CI Docs Crystal Version License: MIT

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


Features


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:


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.