Introduction to the Windows API in Rust with a DLL Loader
Introduction to using Windows API calls in Rust with a Rust DLL Loader using LoadLibraryA.
Intro
If you are looking for my post on remote process DLL injection (more advanced than this one), check my blog post here instead.
This post is to serve as an introduction to programming in Rust using the Windows (Win32) API by using the most simplest of DLL loading techniques, whereby we are loading the DLL into our own process, not another process. The post linked above deals with injecting a DLL into another process.
The project can be found here on my GitHub. If you want to skip the intro paragraphs and get to the meat, click here.
In this post, I want to make a start on Windows development in Rust. Pushing my own love for Rust aside, Rust is an excellent choice for red teamers, pentesters and ethical hackers for several reasons. If you are looking for a general purpose language, that allows low level access to memory (as is required in malware development), access to the native Windows API (Win32), and is memory safe, there is really only 1 choice - Rust. C and C++ are of course excellent choices, and those chads carry the last 20+ years of malware on their shoulders, but times are changing. Threat actors are moving to more ‘trendy’ languages (Rust, Go, Zig), and in order to keep up with our adversaries, I think using these modern technologies is key. Rust can do everything C++ can, with better memory safety, and better package management.. so, for me, its a no brainer!
That said… if you are new to offensive security I would still recommend you learn C. I think you can get away with avoiding C++, but C is in many ways essential. Obviously, if you are looking to join a red team who use and develop C/C++ tools, then learning C/C++ should be your primary objective - then come back to Rust and make your own opinion on the experience. I don’t want to shun C++, it’s an insanely powerful language, but I find the Rust experience a million times better (once you get past the learning curve famously involved with Rust).
As such, I want to make an introductory series for using the Windows API (Win32) in Rust, as you cannot realistically make a decent implant for Windows without using it. We will talk briefly about how to use the API documentation alongside the Rust documentation, to use Windows API calls to make our program do things. In this case, we will be specifically looking at LoadLibraryA
.
Legal & ethical disclaimer applies, by reading on you acknowledge that, see the legal disclaimer here. In short, you must not use the below information for any criminal or unethical purposes, and it should only be used by security professionals, or for those interested in cyber security to deepen your knowledge.
What is Rust
As this is an introduction, let’s have a quick chat about Rust. Rust is a strongly typed general purpose functional programming language which allows for ‘low level’ programming, such as access to raw memory. Looking at the state of computer security today, a large number of vulnerabilities which exist in software relate to memory mismanagement, for example, stack/heap overflows, dangling pointers and race conditions. Rust was designed in order to prevent these vulnerabilities from ever occurring which happens because of the famed (and often meme’d) borrow checker. The borrow checker will prevent you from making errors commonly found in C / C++ which lead to these memory vulnerabilities (AKA 0-days) at compile time.
If you are brand new to Rust, before trying to apply this blog post, you should go ahead and learn the basics of the language with the official Rust Book, a free online web resource. It’s fantastic. If you are coming from C / C++, then the learning curve won’t be too steep. If you are struggling with Rust, after you get past the ins and outs of the borrow checker, you will be onto smooth sailing! Stick with it, I promise it’s worth it.
Rust uses ‘Cargo’, a package manager with super powers. Cargo will run and build projects, help you understand (and fix) bugs, and allow you to easily use third party libraries (in Rust these are called Crates).
The borrow checker receives a lot of (unjust) hate online from people who try Rust and get frustrated. The borrow checker is the needy Spaniel of compilers. It wont just tell you there is an error, it will explain why there is an error and even tell you how to fix it. Compare that to something in C++, such as a segfault, and all of a sudden, you’ll fall in love with Rust. It is so much nicer to debug and fix. Just my opinion. Error handling is also forced, with errors as values, again, something which is just beautiful to work with and is sorely lacking in other languages.
The Windows API
The Windows API is Microsoft’s way of exposing the inner workings of the operating system to developers. To do anything which requires the operating system to do things for you (for example process management, interacting with windows, making cool popup boxes, screenshotting, security, identity, and many other things such as system services), you need to interact with the API. Commonly referred to as the Win32 API, the documentation can be found here.
It’s the foundation for creating performant and native applications on Windows, giving you the ability to leverage the full capabilities of the Windows operating system.
What is a DLL
As we are building a DLL injector in this example, it may be a good idea to talk about what a DLL is. A DLL, or Dynamic Linked Library, is basically a Windows program which has functions inside that can be used by external programs. So, lets say program A and program B are both math related programs, and they both need to be able to perform some complex calculation, rather than the developers having to implement their own code to do that, they can instead create the code which does the calculation as a DLL (AKA a library) then both use that function in their respective programs.
DLLs are some of my favourite things to work with in the space of offensive development, there is some evidence that EDR’s put them through slightly less rigour, and they open other attack vectors such as DLL Search Order Hijacking.
Documentation
The first step is to check out the documentation, both on the Win32 API and in the Rust Windows crate. As we will be looking at the LoadLibraryA
function, lets load them up side-by-side. Links: Win32 LoadLibraryA, Rust Crate LoadLibraryA.
In the below image, the Rust Crate is on the left, and the Win32 API is on the right. The first thing you notice is the difference in documentation; the Rust documentation only tells you the datatypes, as according to Rust, with little other information. The Win32 API on the other hand, has a wealth of information.
Before continuing, the documentation tells us that in order to use the LoadLibraryA
function, we need a PCSTR
.
A PCSTR
in C is a pointer to a constant string. In C, this looks like:
const char*
As the function is of type A
for ANSI
, we need to make sure that our string ends with a \0
null terminator. You may notice when looking at the documentation there are variations of functions, including A
, W
and Ex
.
The “A” stands for “ANSI”, which refers to functions that use the ANSI character set. ANSI functions are used for compatibility with systems that don’t support Unicode.
The “W” stands for “Wide”, which denotes functions that use the Unicode character set.
“Ex” stands for “Extended”. Functions with “Ex” appended to their names offer extended functionality or capabilities compared to their non-Ex counterparts. These can include additional parameters, extended features, or enhanced performance.
The rust code
So, with the knowledge that we need to use a PCSTR
and the function LoadLibraryA
(which returns a Result<HMODULE>
), we can start our project!
Lets create a new cargo project:
cargo new dll_loader
cd dll_loader
The first thing we need to do, is to add the Windows crate, which can be done easily with:
cargo add windows
With the package added, as we will be using LoadLibraryA
, we can edit our cargo.toml
dependencies to use the LibraryLoader
module.
[dependencies]
windows = { version = "0.54.0", features = [
"Win32_System_LibraryLoader",
] }
Now, in main.rs:
use windows::Win32::System::LibraryLoader::LoadLibraryA;
use windows::core::{PCSTR, Result};
use windows::Win32::Foundation::HMODULE;
fn main() {
// example of showing how one might manually build a PCSTR
let path_as_slice = "rust_dll.dll\0";
let _dll_file_path: PCSTR = PCSTR::from_raw(path_as_slice.as_ptr());
// A literal UTF-8 string with a trailing null terminator
// If you want to use this, make yourself a simple DLL and change the below string to point to
// your DLL.
let dll_file_path = s!("rust_dll.dll");
println!("[i] Loading string at location: {:?}", dll_file_path);
// use the Win32 API for LoadLibraryA and match the result
let hmod: Result<HMODULE> = unsafe { LoadLibraryA(dll_file_path) };
match hmod {
Ok(h) => { println!("[+] Module injected! Handle: {:?}", h); loop {} },
Err(e) => println!("Error[-] injecting module, {e}"),
}
}
The first thing to note is how we are building the PCSTR
, as we know the API needs a pointer to a constant string, we declare an immutable string slice with a null terminator, and then use the member function PCSTR::from_raw()
, passing in the string slice and using the .as_ptr()
function, which converts a string slice to a raw pointer. As strings in C are arrays of unsigned 8 bit integers, and Rust &str are a slice of u8 integers, the two types are interchangeable.
To call a Windows API function that requires a PCSTR
, you first need to convert a Rust &str
to a C-style string (AKA a CString which requires a null terminator \0
). Then we can use the .as_ptr()
method on the CString to obtain a raw pointer (*const c_char
) that can be passed to a function expecting a PCSTR.
There is also a shorthand way to do this, as this is a little convalluted, you need to import the s
macro from windows::core::s
, and then you can create the PCSTR like so:
let dll_file_path = s!("rust_dll.dll");
Next, we make an unsafe call to LoadLibraryA
passing in the PCSTR, saving the result in a Result<HMODULE>
(note this is the Windows result type, and needs importing via use windows::core::Result
as we have done).
Comparing to C
Finally, comparing this to doing the same in C:
#include <windows.h>
#include <stdio.h>
int main() {
const char* dllFilePath = "rust_dll.dll";
printf("[i] Loading DLL at location: %s\n", dllFilePath);
HMODULE hmod = LoadLibraryA(dllFilePath);
if (hmod) {
printf("[+] Module injected! Handle: %p\n", hmod);
} else {
DWORD dwError = GetLastError();
printf("Error[-] injecting module, Error Code: %lu\n", dwError);
}
return 0;
}
As you can see, there isn’t a huge difference here.
Hopefully this demonstrates that using Rust for Windows API development isn’t scary!
Taking it further
How is this useful?
Well, in itself, this technique is not OPSEC (operational security) friendly. It requires having a payload on disk which of course, is potentially outing your implant. That itself isn’t a good reason NOT to test this technique, as adversaries are using this in the wild.
That said, it can be taken a step further by having a PE which is reflected into another process make the call to LoadLibraryA
.
In the next post, we will be looking at building a simple DLL which can be used as the loaded DLL of this technique, and more to come in the future!