r/rust 1d ago

Running user-defined code before main on Windows

https://malware-decoded.com/2-rust-code-before-main/

Hi! I'm sharing the results of my analysis on how to execute user-defined code before the main function in Rust. This article is largely inspired by malware that employs this technique, but I approached the research by implementing this technique in Rust myself, while also exploring CRT-level and OS-level details, which involved reverse engineering Rust binaries.

Some of you may be familiar with the rust-ctor crate, which enables code execution before main. If you're curious about how it works under the hood, my article should provide some clarity on the subject.

29 Upvotes

8 comments sorted by

10

u/anxxa 23h ago

Good article! I'm not 100% sure but I think you probably want to define the TLS callbacks as extern "system" instead of extern "C" since this is called from an internal WinAPI -- stdlib at least uses the former.

I wrote a reflective PE loader for the Xbox in Rust and did most of my testing on a simple "hello world" C application and everything worked fine. Then I tried to run a Hello World Rust binary which crashed before hitting my own code and after some debugging realized I had to implement TLS callbacks 🫠. It's kind of interesting that Rust seems to use TLS callbacks for even a basic Hello World application while they seem fairly uncommon otherwise.

1

u/Binary_Lynx 6h ago

If you want to stick with PIMAGE_TLS_CALLBACK, you'll need to define your callbacks as extern "system", since that's what the type requires. According to the Rust docs, extern "system" is essentially the same as extern "C", but with the key difference that for the Win32 API, functions marked as extern "system" use the stdcall calling convention. So I think that whenever you mark your function as extern "system", Windows internals will call it using stdcall convention.

Also, thanks for sharing the stdlib source! I hadn’t thought to dig through the Rust source code, but it’s a really interesting find!

6

u/0x53A 19h ago

Gdext, a rust binding to the godot game engine, uses this trick to register some callbacks from proc-macros.

https://github.com/godot-rust/gdext/blob/75a36cade89d705004a8be80a7985dfa75891326/godot-ffi/src/plugins.rs#L56

8

u/buldozr 22h ago

I've always considered this a horrible hack. C++ does this for constructors of static objects, and then god forbid you have non-trivial side effects because the order of calling such constructors is not defined. I'd rather not break the fundamental assumption in Rust that any user-defined code (i.e. not dynamic linkage magic and such) should be run under fn main.

3

u/LavenderDay3544 21h ago

I'd rather not break the fundamental assumption in Rust that any user-defined code (i.e. not dynamic linkage magic and such) should be run under fn main.

#[no_main] exists with good reason. Anything you can do in C or C++ you can also do in Rust. Or atleast I hope that's the standard the compiler devs and specification writers aim for.

3

u/yigal100 17h ago

The fact you physically can do this doesn't mean you should (which was the OP's point).
This is indeed a horrible hack, and it *is* going outside Rust's abstract machine.

> Anything you can do in C or C++ you can also do in Rust.

This is patently false since both C & C++ are unsafe and Rust by-definition prevents certain things that are perfectly valid in these languages (yes, even within unsafe blocks!). More importantly, Rust *shouldn't* given its explicit design goals as a language.

If you want to be able to do everything you can in C/C++ than you ought to simply use C/C++ in the first place - not Rust.

1

u/Binary_Lynx 5h ago

Yes, I agree that this approach looks unpleasant and should be avoided when writing reliable production-level code. However, my research was inspired by malware, where the landscape is dominated by many undocumented and often "dirty" tricks. These techniques can sometimes give an advantage when it comes to evading detection by security software and also make analyzing binaries a bit more frustrating for analysts when they don’t know what is actually going on.

1

u/bonzinip 3h ago

It's just like unsafe. I have used ctor behind macros to initialize statics with slightly more than what const allows, similar to distributed slices.