Reconstructing Rust Types

A Practical Guide for Reverse Engineers

Cindy Xiao

(she/her, they/them)
Senior Security Researcher, CrowdStrike

February 28, 2025

My Background

  • I currently do malware analysis, reverse engineering, and cyber threat intelligence.
  • I used to be a C/C++ developer, and I’m interested in approaching things from the developer’s perspective.
  • We were seeing more Rust malware in our analysis queue, and we needed practical skills to deal with them.

Some of my previous work on Rust reversing:

  • 2023 - Analysis of Rust code for GDI in the Windows Kernel - Lightning Talk @ RECon
  • 2023 - Rust Type Layout Helper - Binary Ninja Plugin
  • 2023 - Rust String Slicer - Binary Ninja Plugin
  • 2023 - Using panic metadata to recover source code information from Rust binaries - Blog Post
  • 2024 - Reversing Rust Binaries: One step beyond strings - Workshop @ NorthSec
  • 2024 - Reversing Rust Binaries: One step beyond strings - Workshop @ RECon

The State of Rust Reversing In 2025

Rust RE skills needed for:

  • Malware
    • Remote access tools (RATs), downloaders, loaders, ransomware, infostealers, adware, etc…
  • Windows kernel
  • Windows drivers
  • Android libraries
  • Maybe everything???

Rust is becoming increasingly popular as a general purpose systems programming language.

The State of Rust Reversing In 2025

We have:

  • Function signatures for the standard library (e.g. shipped with IDA)
  • The ability to generate function signatures for third-party libraries (e.g. rustbinsign)
  • Some basic information on the metadata that you get from dumping a binary’s strings
    • See 2024 - Reversing Rust Binaries: One step beyond strings - Workshop @ RECon

We don’t have:

  • Tools for reconstructing Rust types.
  • Good systematic explanations of static Rust reversing.

The State of Rust Reversing in 2025

📖 The rustc Book > Codegen Options > strip

Note that, at any level, removing debuginfo only necessarily impacts “friendly” introspection. -Cstrip cannot be relied on as a meaningful security or obfuscation measure, as disassemblers and decompilers can extract considerable information even in the absence of symbols.

(emphasis added)

haha…yes…..we can totally extract considerable information…….

What we’ll cover today

  • The basic building blocks of the Rust type system: The programmer’s perspective
  • The basic building blocks of the Rust type system: The compiler’s perspective
  • Constructing standard library types from the building blocks
  • Features of Rust binaries that give information about type layout

Our example: The RustyClaw malware

The RustyClaw malware, first publicly reported by Cisco Talos in October 2024, is a downloader used to deliver a backdoor: 🔗 UAT-5647 targets Ukrainian and Polish entities with RomCom malware variants

We will be looking at the sample with SHA-256 hash

b1fe8fbbb0b6de0f1dcd4146d674a71c511488a9eb4538689294bd782df040df

🔗 Sample download from MalwareBazaar

This is an x86_32 Windows binary.

Call to core::result::unwrap_failed

Specifically, we will be trying to annotate the code inside this one block as much as possible!

  • This block is located inside a function where the malware checks for the Windows version.

Call to core::result::unwrap_failed

Here’s a preview of the nicely annotated result.

Inside core::result::unwrap_failed

We’ll also be reversing a function called inside this block.

  • This actually ends up being the Rust standard library function core::result::unwrap_failed.

Inside core::result::unwrap_failed

The basic building blocks of the Rust type system: The programmer’s perspective

Understanding Rust types, from the source code side.

Learning a little bit of Rust

We can’t learn all of Rust today, but we will need to understand some Rust source code.

  • Rust is a very different language from C.
    • We need to take the source emitted by our decompiler and go one abstraction level up.
  • The Rust standard library is written in Rust, and so is the Rust compiler.
    • Sometimes, we will need to look at those when trying to figure out how something works.
    • Reducing “magic” as much as possible.
    • We should be able to find where in the Rust toolchain something comes from, so that we can be prepared if / when something changes.

It’s important to understand a little bit about the programming ecosystem, and to look at things from the programmer’s perspective, even if you yourself are not a programmer. Something that takes one line of code to write for the programmer may end up being a massive complicated thing for the reverser. If you can read a little bit of Rust code, you can take advantage of the open source nature of the compiler and standard library while trying to figure out how something works.

Reading Rust

We will need to read some Rust source code today, so a crash course for C programmers on some syntax:

  • A variable declaration, with a type annotation:
let counter: u64 = 0;
  • Functions look like this:
fn transform_value(input: u64) -> u64 {
    // Function body here
}

let value: u64 = transform_value(10);

Reading Rust

  • Generics use angle brackets:
let values: Vec<u8> = Vec::new();
  • Reference types have & :
fn sum_values(input: &Vec<u8>) -> i64 {
    // Function body here
}
  • To take a reference, also use &:
let values: Vec<u8> = vec![0, 1, 2];

let sum: i64 = sum_values(&values);

Basic types

  • i64 -> Signed 64-bit integer
  • u8 -> Unsigned 8-bit integer
  • f32 -> 32-bit floating point value
  • usize, isize -> The size of a pointer, on whatever platform you’re on (think size_t, ssize_t)
  • bool -> True, or False. Always a size of 1 byte.
  • () -> The empty “unit” type.
  • [u64; 128] -> An array of unsigned 64-bit integers, with 128 entries.

Reading Rust

Code examples in this talk are simplified for easier reading and clarity:

  • No lifetime annotations
  • No pub keyword
  • No mut keyword
  • Some namespaces are expanded for clarity

Things we won’t talk about

  • We will (mostly) not talk about:
    • The borrow checker
    • Lifetimes
    • Mutability
    • unsafe
  • If you want to learn Rust as a programmer, these are important.
  • However, they (mostly) don’t affect type layout.

Slices

Syntax: &[T], where T is some type.

  • A special type of reference - a sized view into some collection of data.
  • An &[u8] is a sized view into a collection of unsigned 8-bit integers (u8).
  • This reference contains information about not only the address of the first element, but also about the length of how many elements we want to slice.
  • This is essentially a pointer, but with additional length metadata.

Slices

Suppose we have an array of bytes:

let array: [u8; 5] = [0, 1, 2, 3, 4];

We can take a slice into that array:

let array_slice = &array[1..3];
println!("Slice contents from index 1 (inclusive) to 3 (exclusive): {:?}", array_slice);
Slice contents from index 1 (inclusive) to 3 (exclusive): [1, 2]

And query its length:

println!("Slice length: {}", array_slice.len())
Slice length: 2

Strings

(thank you Johannes for this beautiful analog meme from 38c3)

We will be focusing on just two string types:

  • The primitive type, &str
    • Also called a string slice
    • A reference / sized view to some static string data somewhere
  • The standard library type, std::string::String
    • A growable string, (usually) on the heap

The primitive &str type

You will see string slices (&str) for:

  • String literals (similar to const char*):
let PROGRAM_NAME: &str = "tunnel_tool";
  • Slices of the standard library type, std::string::String:
let full_name: String = String::from("Cindy Xiao");
let first_name: &str = &full_name[0..5];
println!("Slice of length {}, with data {}", first_name.len(), first_name);
Slice of length 5, with data Cindy

An example of &str : Panic path metadata in binaries

See the following for more info:

  • 2023 - Rust String Slicer - Binary Ninja Plugin
  • 2023 - Using panic metadata to recover source code information from Rust binaries - Blog Post

Structs, and the std::string::String type

  • 📖 Docs: std::string::String
struct String {
    vec: Vec<u8>,
}
struct Vec<u8> {
    buf: RawVec<u8>,
    len: usize,
}
struct RawVec<u8> {
    ptr: *u8, // Some details simplified here
    cap: usize,
}

The std::string::String type

Putting it all together into a C-like representation:

struct String {
    struct Vec<u8> {
        struct RawVec<u8> {
            uint8_t* ptr;
            size_t cap;
        } buf;
        size_t len;
    } vec;
};

Enums, i.e. tagged unions

enum std::result::Result<i64, String> {
    Ok(i64),
    Err(String),
}
struct Result<i64, String> {
    enum {
        Ok = 0,
        Err = 1,
    } discriminant;
    union {
        int64_t Ok_data;
        char* Err_data;
    } data;
}

Traits (i.e. “object oriented programming” in Rust)

  • 📖 Docs: core::fmt::Write
    • “A trait for writing or formatting into Unicode-accepting buffers or streams.”
trait core::fmt::Write {
    // Required method 
    fn write_str(&mut self, s: &str) -> Result<(), core::fmt::Error>;

    // Provided methods
    fn write_char(&mut self, c: char) -> Result<(), core::fmt::Error> { ... }
    fn write_fmt(&mut self, args: Arguments) -> Result<(), core::fmt::Error> { ... }
}

Traits (i.e. “object oriented programming” in Rust)

trait core::fmt::Write {
    // Required method 
    fn write_str(&mut self, s: &str) -> Result<(), core::fmt::Error>;

    // Provided methods
    fn write_char(&mut self, c: char) -> Result<(), core::fmt::Error> { ... }
    fn write_fmt(&mut self, args: Arguments) -> Result<(), core::fmt::Error> { ... }
}
impl core::fmt::Write for std::string::String {
    fn write_str(&mut self, s: &str) -> Result {
        self.push_str(s);
        Ok(())
    }

    fn write_char(&mut self, c: char) -> Result {
        self.push(c);
        Ok(())
    }
}

Dynamic dispatch using traits

To do dynamic dispatch without having a concrete type, Rust uses another type of reference: the trait object, e.g. &dyn core::fmt::Write.

fn append_woohoo(writeable_type: &mut dyn core::fmt::Write) {
    writeable_type.write_str(", woohoo").unwrap();
}

fn main() {
    let mut message = String::from("I'm at RE//verse 2025");
    println!("{}", message);

    append_woohoo(&mut message);
    println!("{}", message);
}
I'm at RE//verse 2025
I'm at RE//verse 2025, woohoo

Implementing destructors: The Drop Trait

One important trait that will be relevant for us later: The Drop trait.

trait Drop {
    // Required method
    fn drop(&mut self);
}

This is a destructor - it gets called for non-primitive types when values go out of scope!

  • 📖 The Rust Reference > Destructors

Implementing the destructor for Vec<T>: impl Drop for Vec<T>

struct Vec<T> {
    buf: RawVec<T>,
    len: usize,
}

impl<T> Drop for Vec<T> {
    fn drop(&mut self) {
        unsafe {
            ptr::drop_in_place(ptr::slice_from_raw_parts_mut(self.as_mut_ptr(), self.len))
        }
    }
}

The basic building blocks of the Rust type system: The compiler’s perspective

What Rust guarantees: Type Layouts

📖 The Rust Reference > Type Layouts

  • The sizes of primitive scalar types (bool, i64, f32, etc.) are guaranteed.
  • The sizes of references are guaranteed.
    • However, there are different types of references, each with a different (guaranteed) size!
      • References to primitive types with known sizes, such as &i64
      • Slices, such as &str, &[u8], etc.
      • Trait objects, such as &dyn core::fmt::Write

What Rust guarantees: Type Layouts

📖 The Rust Reference > Type Layouts

  • ⚠️ Structs and enums have no layout guarantees!
    • No guaranteed size
    • No guaranteed alignment
    • No guaranteed field ordering

References: Slices

These include:

  • A pointer: To the beginning of the slice
  • Metadata attached to the pointer: The length of the slice

References: Trait objects

These include:

  • A pointer: To the concrete type
  • Metadata attached to the pointer: The trait table

What Rust guarantees: Passing types between functions

⚠️ Rust’s calling convention for Rust-to-Rust function calls is neither stable, nor even defined.

  • Often the compiler will pick one of the platform’s usual calling conventions, but this is not guaranteed.

Example from our RustyClaw binary (Windows x86_32) of (sort of) __fastcall:

What Rust guarantees: Passing types between functions

Notice how a single &str is split across two registers here:

struct `&str`
{
    uint8_t* _slice_data = "called Result::unwrap() on an Err value"
    size_t _slice_len = 0x2b
};

References: Pointers and Metadata, through the compiler pipeline

Enums, and their discriminants

A C-like representation of our standard library Result type:

struct std::io::error::Result<Vec<u8>> {
    enum {
        Ok = 0,
        Err = 0x8000000000000000,
    } __discriminant;
    union {
        Vec<u8> Ok_data;
        std::io::error::Error* Err_data;
    } data;
}

Enums, and their discriminants

You will often see this idiom in decompiled Rust binaries:

struct std::io::error::Result<Vec<u8>> read_result;
std::fs::read::inner(&read_result, path_data, path_len);
uint64_t __discriminant = read_result.__discriminant;

if (__discriminant == 0x8000000000000000) {
    core::result::unwrap_failed("called `Result::unwrap()` on an `Err` value", 0x2b, &read_result);
    /* no return */
} else {
    /* Read was successful, do stuff with read_result here... */
}
  • 📖Docs: std::mem::discriminant
  • 📖 The Rust Reference > Discriminants

Example: A Result from RustyClaw

Putting basic building blocks together

Constructing the core::fmt::Arguments standard library type.

Constructing core::fmt::Arguments

Let’s construct the standard library’s core::fmt::Arguments type.

  • This is useful because it puts together every basic type concept we’ve looked at so far - structs, references, slices, enums, etc.
  • This is useful, because understanding it is required for understanding string formatting in Rust, which comes up quite often!

String formatting: Printing text with println!

From the programmer’s perspective, doing string formatting is quite easy:

println!("First number is: {}, second number is: {}", 86, 64);
First number is: 86, second number is: 64

String formatting: Printing panic strings when aborting / panicking the program

  • 📖 Example from The Rust Book > Error Handling
use std::fs::File;

fn main() {
    let greeting_file = File::open("hello.txt").unwrap();
}
thread 'main' panicked at src/main.rs:4:49:
called `Result::unwrap()` on an `Err` value: Os { code: 2, kind: NotFound, message: "No such file or directory" }

String formatting: Printing panic strings when aborting / panicking the program

  • 💽 Source: library/core/src/result.rs
fn core::result::unwrap_failed(msg: &str, error: &dyn fmt::Debug) -> ! {
    panic!("{msg}: {error:?}")
}

String formatting: Peeking inside println!

  • 📖 Docs: println!, std::io::stdio::_print
  • 💽 Source: std/src/io/stdio.rs
macro_rules! println {
    ($($arg:tt)*) => {{
        $crate::io::_print($crate::format_args_nl!($($arg)*));
    }};
}
fn std::io::stdio::_print(args: core::fmt::Arguments)

The core::fmt::Arguments Type

  • 📖 Docs: core::fmt::Arguments
  • 💽 Source: core/src/fmt/mod.rs
struct core::fmt::Arguments {
1    pieces: &[&str],
2    fmt: Option<&[core::fmt::rt::Placeholder]>,
3    args: &[core::fmt::rt::Argument],
}
1
An array slice (&[]), containing the string literals (&str) to put together: ["First number is: ", ", second number is: ", "\n"]
2
Any formatting specifications (alignment, width, etc.)
3
An array slice (&[]), containing the dynamic values (core::fmt::rt::Argument) to display as strings: [86, 64]

Exploding this: &[&str]

  • An &[&str] is:
    • An array slice (&[])
    • Pointing to a collection of string slices (&str)

Exploding this: &[&str]

Exploding this: Option<&[core::fmt::rt::Placeholder]>

  • An Option<&[core::fmt::rt::Placeholder]> is:
    • A Rust enum, i.e. 
    • tagged union (Option<>)
    • Which either contains nothing, or contains a &[core::fmt::rt::Placeholder]
  • A &[core::fmt::rt::Placeholder] is:
    • An array slice (&[])
    • Pointing to a collection of core::fmt::rt::Placeholder structs
enum Option<&[core::fmt::rt::Placeholder]> {
1    None,
2    Some(&[core::fmt::rt::Placeholder]),
}
1
The None variant of the union, when it holds no data.
2
The Some variant of the union, when it holds an actual array slice (&[]) of core::fmt::rt::Placeholder values.

Exploding this: Option<&[core::fmt::rt::Placeholder]>

Exploding this: &[core::fmt::rt::Argument]

  • A &[core::fmt::rt::Argument] is:
    • An array slice (&[])
    • Containing a set of core::fmt::rt::Argument structs

Exploding this: core::fmt::rt::Argument

struct core::fmt::rt::Argument {
1    value: &Opaque,
2    formatter: fn(_: &Opaque, _: &mut Formatter) -> Result,
}
1
A reference (&) to a value (Opaque) to format into a string.
2
A pointer to a function (fn()) which does the actual string formatting.

A C-like representation of core::fmt::rt::Argument

struct Argument<i64>
{
    void* value;
    Result* (* formatter)(void* value_to_format, Formatter* formatter);
};

Exploding this: &[core::fmt::rt::Argument]

The full explosion

struct core::fmt::Arguments
{
    struct &[&str] pieces
    {
        struct &str* data_ptr;
        usize length;
    },
    struct Option<&[core::fmt::rt::Placeholder]> fmt
    {
        __discriminant_type discriminant;
        union
        {
            &[core::fmt::rt::Placeholder] Some;
            struct {} None; 
        }
    }
    struct &[core::fmt::rt::Argument] args
    {
        struct core::fmt::rt::Argument* data_ptr;
        usize length;
    },
};

Back to the RustyClaw core::result::unwrap_failed example

Back to the RustyClaw core::result::unwrap_failed example

Spotting some likely core::fmt::Argument variables

Defining a core::fmt::Argument

struct `core::fmt::Argument`
{
    void* value;
    void* (* formatter)(void* value, void* formatter_struct);
};

After applying the core::fmt::Argument type

Spotting a likely &[core::fmt::Argument] variable

Defining a &[core::fmt::Argument]

struct `&[core::fmt::Argument]` __packed
{
    struct `core::fmt::Argument`* _slice_data;
    usize _slice_len;
};

After applying the &[core::fmt::Argument] type

In the decompiler after applying &[core::fmt::Argument]

Features of Rust binaries that give information about type layout

Allocation functions

  • Heap allocations for standard library types, such as for std::string::String, require a global allocator to be defined.
  • The Rust standard library provides a default global allocator implementation.
    • The details of this will vary by platform.

The standard library’s global allocator implementation, on Windows

  • 💽 Source: library/std/src/sys/alloc/windows.rs
fn std::sys::pal::windows::alloc::process_heap_alloc(
    _heap: MaybeUninit<c::HANDLE>,
    flags: u32,
    bytes: usize,
) -> *mut c_void
{
    let heap = HEAP.load(Ordering::Relaxed);
    if core::intrinsics::likely(!heap.is_null()) {
        unsafe { HeapAlloc(heap, flags, bytes) }
    } else {
1        process_heap_init_and_alloc(MaybeUninit::uninit(), flags, bytes)
    }
}
1
Further calls to HeapAlloc inside this function

Deallocation functions

  • When types that required heap allocations go out of scope, their destructors are called.
    • Types that require heap deallocation implement the Drop trait
    • That is, they implement a destructor!
  • The Rust standard library provides a default global deallocator implementation.
    • The details of this will vary by platform.

The standard library’s global deallocator

1fn __rust_dealloc(ptr: *mut u8, size: usize, align: usize);


fn __rdl_dealloc(ptr: *mut u8, size: usize, align: usize) {
    unsafe { System.dealloc(ptr, Layout::from_size_align_unchecked(size, align)) }
}
1
This is just a stub; it gets replaced with __rdl_dealloc, if you’re using the default standard library allocator

The standard library’s global deallocator implementation, on Windows

  • Source: library/std/src/sys/alloc/windows.rs
unsafe fn System::dealloc(&self, ptr: *mut u8, layout: Layout) {
    let block = {
        if layout.align() <= MIN_ALIGN {
            ptr
        } else {
            // The location of the start of the block is stored in the padding before `ptr`.

            // SAFETY: Because of the contract of `System`, `ptr` is guaranteed to be non-null
            // and have a header readable directly before it. 
            unsafe { ptr::read((ptr as *mut Header).sub(1)).0 }
        }
    };

    let heap = unsafe { get_process_heap() };
    unsafe { HeapFree(heap, 0, block.cast::<c_void>()) };
}

Trait objects

Trait object tables (i.e. vtables)

Recall the &dyn core::fmt::Write trait object. It includes:

  • A pointer: To the concrete type that implements the core::fmt::Write trait
  • Metadata attached to the pointer: A table of pointers to the concrete type’s functions, which implement that trait (i.e. a vtable!)

Trait object tables (i.e. vtables)

The vtable attached to trait objects has:

  • Size, alignment, and destructor information for the concrete type.
  • A fixed layout, such that size, alignment, and destructor information are always in the same places in the table!
    • 💽 Compiler source code where the vtable layout is defined and generated

Trait object tables (i.e. vtables)

struct core::fmt::Write::_vtable alloc::string::String::_vtable = 
{
1    void* (* destructor)(void* self) = core::ptr::drop_in_place<alloc::string::String>
2    int64_t size = 0x18
3    int64_t alignment = 0x8
4    void* (* write_str)(void* self, char* str_data, uint64_t str_len) = <alloc::string::String as core::fmt::Write>::write_str
    void* (* write_char)(void* self, int32_t character) = <alloc::string::String as core::fmt::Write>::write_char
    void* (* write_fmt)(void* self, Arguments* args) = core::fmt::Write::write_fmt
}
1
A pointer to the destructor for this concrete type.
2
The size (in bytes) of the concrete type that implements this trait.
3
The alignment (in bytes) of the concrete type that implements this trait.
4
A pointer to this type’s implementation of a trait method.

Finding trait object tables

  • Find a function pointer, followed by 2 usize constants, followed by a function pointer
  • Ensure that the first function pointer is a destructor; that is, make sure it eventually calls __rust_dealloc / __rdl_dealloc.

Example likely vtable from an x86_64 binary with symbols

Example: Reversing the core::result::unwrap_failed function

  • 💽 Source: library/core/src/result.rs
fn core::result::unwrap_failed(msg: &str, error: &dyn fmt::Debug) -> ! {
    panic!("{msg}: {error:?}")
}

Printing a debug representation of a struct

You can implement the fmt::Debug trait for your type, to produce a convenient string representation of your type when debugging.

  • This can be done via the printing macros (println!, panic!, etc.), via the {variable_name:?} syntax.
  • You can also get the compiler to just generate a suitable one for you, by slapping the #[derive(Debug)] onto your type:
#[derive(Debug)]
struct Coordinates {
    x: i64,
    y: i64,
}

fn main() {
    let cursor_position = Coordinates { x: -100, y: 120 };
    println!("{cursor_position:?}");
}
Coordinates { x: -100, y: 120 }

The &dyn fmt::Debug trait object

struct &dyn fmt::Debug
{
    void* _concrete_type_data;
    struct fmt::Debug::_vtable* _vtable;
};

Call to core::result::unwrap_failed in RustyClaw

Notice how our metadata-bearing pointer (&dyn fmt::Debug) is split across two variables again!

void core::result::unwrap_failed(
    void* error_concrete_type_data,                 // `&dyn fmt::Debug`._concrete_type_data 
    struct fmt::Debug::_vtable* error_vtable,       // `&dyn fmt::Debug._vtable
    struct core::panic::Location* panic_location,
    void* msg_data_ptr @ ecx,                       // `&str`._slice_data
    void* msg_len @ edx                             // `&str`._slice_len
) { [...] }
core::result::unwrap_failed(
    &unwrapped_err,                                 // error_concrete_type_data (`&dyn fmt::Debug`._concrete_type_data)
    &unwrapped_err_vtable,                          // error_vtable (`&dyn fmt::Debug._vtable)
    &panic_location_"src\is_windows7_or_below.rs"_line_37_col_59, // panic_location
    "called `Result::unwrap()` on an `Err` value",  // msg_data_ptr (`&str`._slice_data)
    0x2b                                            // msg_len (`&str`._slice_len)
);

Examining the vtable in this call

Note how this vtable likely only has one entry!

Examining the vtable in this call: Defining a vtable type

The fmt::Debug trait only requires the implementation of one method:

trait Debug {
    fn fmt(&self, f: &Formatter) -> Result;
}

The vtable type will therefore look something like this:

struct fmt::Debug::_vtable __packed
{
    void* (* destructor)(void* self);
    int32_t size;
    int32_t alignment;
    int32_t (* fmt)(void* self, struct std::fmt::Formatter* formatter_specification);
};

Examining the vtable in this call: Defining the vtable

Peeking inside the fmt implementation

Defining std::fmt::Formatter

struct `std::fmt::Formatter` __packed
{
    __padding char _0[0x14];
    __padding char _14[4];
    __padding char _18[4];
};

Defining std::fmt::Formatter

  • 📖 Docs: core::fmt::Formatter
struct Formatter {
    flags: u32,
    fill: char,
    align: Alignment,
    width: Option<usize>,
    precision: Option<usize>,
    buf: &dyn core::fmt::Write,
}

Note our trait object, &dyn core::fmt::Write, here!

Looking at &dyn core::fmt::Write

Looking at &dyn core::fmt::Write

struct `&dyn fmt::Write` __packed
{
    void* _concrete_type_data;
    struct `fmt::Write::_vtable`* _vtable;
};

Looking at &dyn core::fmt::Write

struct fmt::Write::_vtable __packed
{
    void* (* destructor)(void* self);
    uint32_t size;
    uint32_t alignment;
    int32_t (* write_str)(void* self, char* str_data, usize str_length);
};

Defining std::fmt::Formatter: The buf: &dyn core::fmt::Write field

struct `std::fmt::Formatter` __packed
{
    __padding char _0[0x14];
    struct `&dyn fmt::Write` buf;
};

Peeking inside the fmt implementation again

After defining Formatter, &dyn Write, and the Write vtable: We can now see an &str being passed to write_str!

Taking advantage of the default fmt::Debug trait implementation

Recall that you can just use the #[derive(Debug)] to get the compiler to generate a sensible fmt::Debug representation:

#[derive(Debug)]
struct Coordinates {
    x: i64,
    y: i64,
}

fn main() {
    let cursor_position = Coordinates { x: -100, y: 120 };
    println!("{cursor_position:?}");
}
Coordinates { x: -100, y: 120 }

This prints the name of the type, and all its fields!

A new NulError type

Defining a new NulError type

We actually have the size and alignment of this type already, from the vtable!

Defining a new NulError type

A likely culprit: The std::ffi::NulError type

struct NulError(usize, Vec<u8>); // Struct with anonymous fields

📖 Docs: std::ffi:NulError

An error indicating that an interior nul byte was found. While Rust strings may contain nul bytes in the middle, C strings can’t, as that byte would effectively truncate the string. This error is created by the new method on CString.

A likely culprit: The std::ffi::NulError type

use std::ffi::{CString, NulError};

fn main() {
    let err: NulError = CString::new(b"f\0oo".to_vec()).unwrap_err();
    println!("{err:?}")
}
NulError(1, [102, 0, 111, 111])
struct NulError(
    usize,   // Position of null byte in data
    Vec<u8>  // The rest of the data
); // Struct with anonymous fields

Defining the std::ffi::NulError type

struct `std::ffi::NulError` __packed
{
    struct `std::vec::Vec<u8>` _string_data;
    uint32_t _position_of_null_byte_in_string_data;
};

Defining the std::ffi::NulError type

The _<impl fmt::Debug for std::ffi::NulError>::fmt function

The full vtable for std::ffi::NulError

Our one block of code, nicely annotated

A primer on how to explore Rust internals

There is quite a lot you can figure out just by reading!

  • The Rust compiler is open source, and the Rust standard library is open source.
  • There is only one production-ready compiler implementation, and only one standard library implementation.
  • The Rust compiler is documented: 📖 The rustc book, 📖 Rust Compiler Development Guide
  • The Rust standard library is documented: 📖 doc.rust-lang.org/std/, 📖 stdrs.dev
  • The Rust language is documented: 📖 The Rust Reference, 📖 The Unsafe Rust Book (Rustonomicon)

Questions

You can also find me at:

  • 🐘 Mastodon: @cxiao@infosec.exchange
  • 🦋 Bluesky: @cxiao.net
  • 🌐 Website: cxiao.net

illustration of how we all feel

Acknowledgements

Thank you to:

  • My colleagues Ken, Josh, Lilly, and Jörg for listening to drafts of this presentation.
  • The RE//verse review board for feedback from the dry run, which significantly improved the presentation.

The slide template used here is Grant McDermott’s quarto-revealjs-clean.

Resources

This presentation would not be possible without the huge amount of documentation, blogs, tutorials and public resources published by the Rust community.

- Matt Oswalt: Polymorphism in Rust: https://oswalt.dev/2021/06/polymorphism-in-rust/
- Marco Amann: Rust Dynamic Dispatching deep-dive: https://medium.com/digitalfrontiers/rust-dynamic-dispatching-deep-dive-236a5896e49b
- Raph Levien: Rust container cheat sheet: https://docs.google.com/presentation/d/1q-c7UAyrUlM-eZyTo1pd8SZ0qwA_wYxmPZVOQkoDmH4/edit#slide=id.p
- Mara Bos: Behind the Scenes of Rust String Formatting: format_args!(): https://blog.m-ou.se/format-args/
- Rust to Assembly: Understanding the Inner Workings of Rust: https://www.eventhelix.com/rust/
- fasterthanlime - Peeking inside a Rust Enum: https://fasterthanli.me/articles/peeking-inside-a-rust-enum
- Rust Language Cheat Sheet: https://cheats.rs/

- Primitive Type fn: ABI Compatibility of Rust-to-Rust calls: https://doc.rust-lang.org/core/primitive.fn.html#abi-compatibility

- The Rust Reference: Dynamically Sized Types: https://doc.rust-lang.org/reference/dynamically-sized-types.html
- The Rust Reference: Type Layout: https://doc.rust-lang.org/reference/type-layout.html
- The Rust Reference: Destructors: https://doc.rust-lang.org/reference/destructors.html
- Changes to `u128`/`i128` layout in 1.77 and 1.78: https://blog.rust-lang.org/2024/03/30/i128-layout-update.html

- The Rustonomicon: https://doc.rust-lang.org/nightly/nomicon/
- Exploring dynamic dispatch in Rust: https://alschwalm.com/blog/static/2017/03/07/exploring-dynamic-dispatch-in-rust/
- Rust Deep Dive: Borked Vtables and Barking Cats: https://geo-ant.github.io/blog/2023/rust-dyn-trait-objects-fat-pointers/
- About `vtable_allocation_provider`: https://www.reddit.com/r/rust/comments/11okz75/comment/jbt969m/
- https://github.com/rust-lang/rust/pull/86461/files
- https://github.com/rust-lang/rust/blob/1.83.0/compiler/rustc_middle/src/ty/vtable.rs

- How is `__rust_dealloc` function connected to `__rdl_dealloc` function?: https://users.rust-lang.org/t/how-is-rust-dealloc-function-connectted-to-rdl-dealloc-function/122159
- What is difference between a unit struct and an enum with 0 variants?: https://www.reddit.com/r/rust/comments/1hw19el/what_is_difference_between_a_unit_struct_and_an/

1 / 109
Reconstructing Rust Types A Practical Guide for Reverse Engineers Cindy Xiao (she/her, they/them) Senior Security Researcher, CrowdStrike February 28, 2025

  1. Slides

  2. Tools

  3. Close
  • Reconstructing Rust Types
  • My Background
  • The State of Rust Reversing In 2025
  • The State of Rust Reversing In 2025
  • The State of Rust Reversing in 2025
  • What we’ll cover today
  • Our example: The RustyClaw malware
  • Call to core::result::unwrap_failed
  • Call to core::result::unwrap_failed
  • Inside core::result::unwrap_failed
  • Inside core::result::unwrap_failed
  • The basic building blocks of the Rust type system: The programmer’s perspective
  • Learning a little bit of Rust
  • Reading Rust
  • Reading Rust
  • Basic types
  • Reading Rust
  • Things we won’t talk about
  • Slices
  • Slices
  • Strings
  • The primitive &str type
  • An example of &str : Panic path metadata in binaries
  • Structs, and the std::string::String type
  • The std::string::String type
  • Enums, i.e. tagged unions
  • Traits (i.e. “object oriented programming” in Rust)
  • Traits (i.e. “object oriented programming” in Rust)
  • Dynamic dispatch using traits
  • Implementing destructors: The Drop Trait
  • Implementing the destructor for Vec: impl Drop for Vec
  • The basic building blocks of the Rust type system: The compiler’s perspective
  • What Rust guarantees: Type Layouts
  • What Rust guarantees: Type Layouts
  • References: Slices
  • References: Trait objects
  • What Rust guarantees: Passing types between functions
  • What Rust guarantees: Passing types between functions
  • References: Pointers and Metadata, through the compiler pipeline
  • Enums, and their discriminants
  • Enums, and their discriminants
  • Example: A Result from RustyClaw
  • Putting basic building blocks together
  • Constructing core::fmt::Arguments
  • String formatting: Printing text with println!
  • String formatting: Printing panic strings when aborting / panicking the program
  • String formatting: Printing panic strings when aborting / panicking the program
  • String formatting: Peeking inside println!
  • The core::fmt::Arguments Type
  • Exploding this: &[&str]
  • Exploding this: &[&str]
  • Exploding this: Option<&[core::fmt::rt::Placeholder]>
  • Exploding this: Option<&[core::fmt::rt::Placeholder]>
  • Exploding this: &[core::fmt::rt::Argument]
  • Exploding this: core::fmt::rt::Argument
  • A C-like representation of core::fmt::rt::Argument
  • Exploding this: &[core::fmt::rt::Argument]
  • The full explosion
  • Slide 59
  • Back to the RustyClaw core::result::unwrap_failed example
  • Back to the RustyClaw core::result::unwrap_failed example
  • Spotting some likely core::fmt::Argument variables
  • Defining a core::fmt::Argument
  • After applying the core::fmt::Argument type
  • Spotting a likely &[core::fmt::Argument] variable
  • Defining a &[core::fmt::Argument]
  • After applying the &[core::fmt::Argument] type
  • In the decompiler after applying &[core::fmt::Argument]
  • Features of Rust binaries that give information about type layout
  • Allocation functions
  • The standard library’s global allocator implementation, on Windows
  • Deallocation functions
  • The standard library’s global deallocator
  • The standard library’s global deallocator implementation, on Windows
  • Trait objects
  • Trait object tables (i.e. vtables)
  • Trait object tables (i.e. vtables)
  • Trait object tables (i.e. vtables)
  • Finding trait object tables
  • Example: Reversing the core::result::unwrap_failed function
  • Printing a debug representation of a struct
  • The &dyn fmt::Debug trait object
  • Call to core::result::unwrap_failed in RustyClaw
  • Examining the vtable in this call
  • Examining the vtable in this call: Defining a vtable type
  • Examining the vtable in this call: Defining the vtable
  • Peeking inside the fmt implementation
  • Defining std::fmt::Formatter
  • Defining std::fmt::Formatter
  • Looking at &dyn core::fmt::Write
  • Looking at &dyn core::fmt::Write
  • Looking at &dyn core::fmt::Write
  • Defining std::fmt::Formatter: The buf: &dyn core::fmt::Write field
  • Peeking inside the fmt implementation again
  • Taking advantage of the default fmt::Debug trait implementation
  • A new NulError type
  • Defining a new NulError type
  • Defining a new NulError type
  • A likely culprit: The std::ffi::NulError type
  • A likely culprit: The std::ffi::NulError type
  • Defining the std::ffi::NulError type
  • Defining the std::ffi::NulError type
  • The _::fmt function
  • The full vtable for std::ffi::NulError
  • Our one block of code, nicely annotated
  • A primer on how to explore Rust internals
  • Questions
  • Acknowledgements
  • Resources
  • f Fullscreen
  • s Speaker View
  • o Slide Overview
  • e PDF Export Mode
  • ? Keyboard Help