A Practical Guide for Reverse Engineers
(she/her, they/them)
Senior Security Researcher, CrowdStrike
February 28, 2025
Some of my previous work on Rust reversing:
Rust RE skills needed for:
Rust is becoming increasingly popular as a general purpose systems programming language.
We have:
We don’t have:
📖 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…….
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
🔗 Sample download from MalwareBazaar
This is an x86_32 Windows binary.
core::result::unwrap_failed
Specifically, we will be trying to annotate the code inside this one block as much as possible!
core::result::unwrap_failed
Here’s a preview of the nicely annotated result.
core::result::unwrap_failed
We’ll also be reversing a function called inside this block.
core::result::unwrap_failed
.core::result::unwrap_failed
Understanding Rust types, from the source code side.
We can’t learn all of Rust today, but we will need to understand some Rust source code.
&
:&
:i64
-> Signed 64-bit integeru8
-> Unsigned 8-bit integerf32
-> 32-bit floating point valueusize
, 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.Code examples in this talk are simplified for easier reading and clarity:
pub
keywordmut
keywordunsafe
Syntax: &[T]
, where T
is some type.
&[u8]
is a sized view into a collection of unsigned 8-bit integers (u8
).Suppose we have an array of bytes:
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);
And query its length:
We will be focusing on just two string types:
&str
std::string::String
&str
typeYou will see string slices (&str
) for:
const char*
):std::string::String
:&str
: Panic path metadata in binariesSee the following for more info:
std::string::String
typestd::string::String
std::string::String
typePutting it all together into a C-like representation:
core::fmt::Write
To do dynamic dispatch without having a concrete type, Rust uses another type of reference: the trait object, e.g. &dyn core::fmt::Write
.
Drop
TraitOne important trait that will be relevant for us later: The Drop
trait.
This is a destructor - it gets called for non-primitive types when values go out of scope!
Vec<T>
: impl Drop for Vec<T>
📖 The Rust Reference > Type Layouts
bool
, i64
, f32
, etc.) are guaranteed.&i64
&str
, &[u8]
, etc.&dyn core::fmt::Write
📖 The Rust Reference > Type Layouts
These include:
These include:
⚠️ Rust’s calling convention for Rust-to-Rust function calls is neither stable, nor even defined.
Example from our RustyClaw binary (Windows x86_32) of (sort of) __fastcall
:
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
};
A C-like representation of our standard library Result
type:
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... */
}
Result
from RustyClawConstructing the core::fmt::Arguments
standard library type.
core::fmt::Arguments
Let’s construct the standard library’s core::fmt::Arguments
type.
println!
From the programmer’s perspective, doing string formatting is quite easy:
library/core/src/result.rs
println!
println!
, std::io::stdio::_print
std/src/io/stdio.rs
core::fmt::Arguments
Typestruct core::fmt::Arguments {
1 pieces: &[&str],
2 fmt: Option<&[core::fmt::rt::Placeholder]>,
3 args: &[core::fmt::rt::Argument],
}
&[]
), containing the string literals (&str
) to put together: ["First number is: ", ", second number is: ", "\n"]
&[]
), containing the dynamic values (core::fmt::rt::Argument
) to display as strings: [86, 64]
&[&str]
&[&str]
is:
&[]
)&str
)&[&str]
Option<&[core::fmt::rt::Placeholder]>
Option<&[core::fmt::rt::Placeholder]>
is:
Option<>
)&[core::fmt::rt::Placeholder]
&[core::fmt::rt::Placeholder]
is:
&[]
)core::fmt::rt::Placeholder
structsNone
variant of the union, when it holds no data.
Some
variant of the union, when it holds an actual array slice (&[]
) of core::fmt::rt::Placeholder
values.
Option<&[core::fmt::rt::Placeholder]>
&[core::fmt::rt::Argument]
&[core::fmt::rt::Argument]
is:
&[]
)core::fmt::rt::Argument
structscore::fmt::rt::Argument
struct core::fmt::rt::Argument {
1 value: &Opaque,
2 formatter: fn(_: &Opaque, _: &mut Formatter) -> Result,
}
&
) to a value (Opaque
) to format into a string.
fn()
) which does the actual string formatting.
core::fmt::rt::Argument
&[core::fmt::rt::Argument]
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;
},
};
core::result::unwrap_failed
examplecore::result::unwrap_failed
examplecore::fmt::Argument
variablescore::fmt::Argument
core::fmt::Argument
type&[core::fmt::Argument]
variable&[core::fmt::Argument]
&[core::fmt::Argument]
type&[core::fmt::Argument]
std::string::String
, require a global allocator to be defined.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)
}
}
HeapAlloc
inside this function
Drop
trait1fn __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)) }
}
__rdl_dealloc
, if you’re using the default standard library allocator
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>()) };
}
Recall the &dyn core::fmt::Write
trait object. It includes:
core::fmt::Write
traitThe vtable attached to trait objects has:
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
}
usize
constants, followed by a function pointer__rust_dealloc
/ __rdl_dealloc
.Example likely vtable from an x86_64 binary with symbols
core::result::unwrap_failed
functionlibrary/core/src/result.rs
You can implement the fmt::Debug
trait for your type, to produce a convenient string representation of your type when debugging.
println!
, panic!
, etc.), via the {variable_name:?}
syntax.#[derive(Debug)]
onto your type:&dyn fmt::Debug
trait objectcore::result::unwrap_failed
in RustyClawNotice 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)
);
Note how this vtable likely only has one entry!
The fmt::Debug
trait only requires the implementation of one method:
The vtable type will therefore look something like this:
fmt
implementationstd::fmt::Formatter
struct `std::fmt::Formatter` __packed
{
__padding char _0[0x14];
__padding char _14[4];
__padding char _18[4];
};
std::fmt::Formatter
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!
&dyn core::fmt::Write
&dyn core::fmt::Write
&dyn core::fmt::Write
std::fmt::Formatter
: The buf: &dyn core::fmt::Write
fieldfmt
implementation againAfter defining Formatter
, &dyn Write
, and the Write
vtable: We can now see an &str
being passed to write_str
!
fmt::Debug
trait implementationRecall 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:?}");
}
This prints the name of the type, and all its fields!
NulError
typeNulError
typeWe actually have the size and alignment of this type already, from the vtable!
NulError
typestd::ffi::NulError
type📖 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 onCString
.
std::ffi::NulError
typestd::ffi::NulError
typestd::ffi::NulError
type_<impl fmt::Debug for std::ffi::NulError>::fmt
functionstd::ffi::NulError
There is quite a lot you can figure out just by reading!
You can also find me at:
illustration of how we all feel
Thank you to:
The slide template used here is Grant McDermott’s quarto-revealjs-clean
.
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/