Skip to content

Latest commit

 

History

38 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

CrystalRE

Overview

CrystalRE is an IDA Pro plugin designed to enhance reverse engineering of binaries compiled from the Crystal programming language. Crystal uses a sophisticated symbol mangling scheme and custom runtime structures that IDA doesn't handle well on its own. This plugin automatically demangles symbols, applies type information, and identifies runtime structures to make Crystal binaries easier to analyze.

What It Does

Symbol Demangling

Crystal encodes rich type information in symbol names, but in a way that IDA doesn't like. This plugin automatically extracts and displays this information in a readable format.

Using the standalone parser in symbols.py, we can see how these are parsed (a few examples):

RVA: 0xaaae0
  Type: FUNCTION
  Original: *Array(Pointer(Void))@Array(T)::new:Array(Pointer(Void))
  Parsed data: {'self_type': 'Array(Pointer(Void))', 'name': 'new', 'return_type': 'Array(Pointer(Void))', 'metaclass': 'Array(T)', 'class_method?': True}

RVA: 0x12a310
  Type: FUNCTION
  Original: *Pointer(Tuple(UInt64, UInt64, String))@Object::==<Pointer(Tuple(UInt64, UInt64, String)).class>:Bool
  Parsed data: {'self_type': 'Pointer(Tuple(UInt64, UInt64, String))', 'name': '==', 'args': ['Pointer(Tuple(UInt64, UInt64, String)).class'], 'return_type': 'Bool', 'metaclass': 'Object', 'class_method?': True}

RVA: 0xef4b0
  Type: FUNCTION
  Original: *Array(UInt64)@Array(T)::new<Int32>:Array(UInt64)
  Parsed data: {'self_type': 'Array(UInt64)', 'name': 'new', 'args': ['Int32'], 'return_type': 'Array(UInt64)', 'metaclass': 'Array(T)', 'class_method?': True}

Automatic Function Signatures

Using the parsed symbol information, the plugin automatically sets correct function signatures including:

  • Accurate argument counts
  • Type information where available
  • Special return semantics (like NoReturn for functions that don't return)

String Detection

Crystal's String type has a specific and predictable runtime layout. The plugin scans the binary to identify and label String objects, making them easy to reference and understand.

Set __crystal Calling Convention Action

__crystal is a custom calling convention the plugin registers. x86_64 compiled binaries use the normal __fastcall convention, but structs that are passed by value typically have the struct's contents "inlined" in the function parameters. Take this function for example:

def do_thing(a : Int32, b : Slice(UInt32)) : Nil
end

Slice(T) structs are defined like this

struct Slice(T) {
  Int32 size; // offset 0
  Bool read_only; // offset 4
  T* pointer; // offset 8
};

Since this struct is <= 32 bytes in size it'll get passed by value. Notice how size and read_only fit in 8 bytes, so one might think that they can be passed together in a single register (IDA assumes this too!), but that is not the case. Instead, the compiler essentially turns the function into this

# NOTE: the symbol still shows Slice(Int32), this is just to showcase how each member gets split up
def do_thing(a : Int32, b.size : Int32, b.read_only : Bool, b.pointer : Pointer(Int32))
end

So if you set the Slice(Int32) type as the second arg in IDA, it assumes that size and read_only get passed in the same register which breaks decompilation outputs. One way to circumvent this is by using __usercall and manually specifying the registers used in the slice param, but that's a lot of manual work and the function prototype ends up looking super ugly.

The solution? Creating a custom calling convention. When you set the __crystal calling convention on a function, it'll emulate how __fastcall works, but if it ever encounters a user defined type (udt), it'll extract all the members and let them have their own registers/stack space.

To use it, right click on the function definition and select "Set __crystal calling convention" or click the function definition and press Shift + C

Showcase Setting Custom Calling Convention

Set Crystal Types

Crystal has type names that conflict heavily with the default type parser IDA provides, so to set crystal types you can right click a variable and select Set Crystal type... and input a type. You can also click on a variable and use Shift + Y to do the same thing.

Note that the type doesn't have to exist in the database for it to be created. If you input something that doesn't exist yet and it's a valid crystal type, it'll automatically create the type then set it.

Showcase Setting Crystal Type

Showcase

Function List View

Before After
Function list before plugin Function list after plugin - demangled names

Decompiled Output

Before After
String references before plugin String references after plugin - inline comments
String contents appear as inline comments in decompiled code and function names are much nicer to read

Global String Objects

Before After
Global strings before plugin Global strings after plugin - typed and labeled
String objects are automatically detected, typed, and labeled

Installation

Prerequisites

  • IDA Pro 9.0 or later with Hex-Rays Decompiler
  • Python 3 support enabled in IDA

Quick Installation (Recommended)

  1. Install IDA's hcli

  2. Install plugin

hcli plugin install CrystalRE

Manual Installation Steps

  1. Install dependencies:

    # Make sure to use the same python installation that IDA uses
    python3 -m pip install pyelftools regex
  2. Copy the plugin files to your IDA plugins directory:

    # On Linux/macOS
    cp crystalre.py ~/.idapro/plugins/
    cp -r crystalre ~/.idapro/plugins/
    
    # On Windows
    copy crystalre.py %APPDATA%\Hex-Rays\IDA Pro\plugins\
    xcopy /E crystalre %APPDATA%\Hex-Rays\IDA Pro\plugins\crystalre\
  3. Restart IDA Pro

Usage

The plugin operates automatically on first load. It:

  1. Validates the binary is a Crystal executable
  2. Parses and applies function symbols
  3. Scans for and labels String objects
  4. Installs decompiler enhancements

The plugin stores its state in the IDA database, so it won't re-run on subsequent loads. To reinitialize, delete the IDB file and reopen the binary.

Known Issues

  • If a binary has dwarf symbols and you load it with default options, every unhandled function will be of type int __cdecl () and it ruins decompilations. This is an issue with crystal itself and not the plugin. The only way to fix this is to uncheck Apply calling conventions and Function prototypes are definitive when the DWARF info pop-up appears.
  • If a Proc contains a closure, the crystal codegen will inject the closure as the first arg. There's no way of knowing if this closure parameter has been injected from the symbol name alone, so we just assume all procs don't have closures. If you're analyzing a proc and the decomp look messed up, it's probably because the closure is the real first arg.
  • If a function has a struct return and that struct is greater than 32 bytes in size, a return pointer gets inserted as the first parameter into the function. This parameter doesn't appear in the function symbol, so it's very hard to detect if it's inserted or not. To avoid false positives, it is never checked for, so make sure to be weary for those.
  • If a function is large enough and uses enough complex types, you'll likely encounter issues with var reuse. This makes decomps look very ugly and unfortunately I don't know any good ways to split vars to prevent this ):

Limitations

  • Works best if symbols are included (there's no strip option when building, so most binaries include symbols)
  • Optimized for x86-64 Linux binaries (32-bit binaries likely wont work well)

Credits

Author: Nico Posada

About

Make reversing crystal binaries less annoying.

Resources

Stars

9 stars

Watchers

0 watching

Forks

Releases

Contributors

Languages