Hello and welcome back! In our last post in this series we successfully used Rust and the Windows API to inject a payload into our own process. However, our final code contained a lot of unsafe blocks. In this post we will explore creating a safe(r) wrapper around the winapi functions. There is a great post by Jeff Hiner about writing safe Foreign Function Interfaces (FFI) here which we will be borrowing a lot of ideas from.
Let's start by creating a new library crate called winmem.
D:\projects\rust\blog\win-api>cargo new --lib winmem
Created library `winmem` package
D:\projects\rust\blog\win-api>
We'll copy over the same winapi line from our previous winapi-userland crate's Cargo.toml, since we will need all of the same winapi functions.
# Contents of Cargo.toml
[package]
name = "winmem"
version = "0.1.0"
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
winapi = { version = "0.3.9", features = ["minwindef", "winnt", "memoryapi", "processthreadsapi"]}
Now let's create a new file called handle.rs and add it to our lib.rs file.
// Contents of lib.rs
pub mod handle;
Using VirtualAllocate via the Windows API returns a pointer to a block of memory that the operating system has allocated for us. Since all of our operations revolve around this block of memory, we'll start by creating a custom struct for it. The VirtualAlloc function in winapi returns *mut c_void, so thats what we'll use as our type. We'll also add a size field to track the size of our allocated memory.
// Contents of handle.rs
pub struct MemHandle {
ptr: *mut winapi::ctypes::c_void,
size: usize
}
Before we start implementing methods, lets create a custom error type so that we can use them in our return types.
// Contents of handle.rs
use std::fmt;
#derive[(Debug)]
pub struct WinError;
impl fmt::Display for WinError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "Encountered an error!")
}
}
pub struct MemHandle {
ptr: *mut winapi::ctypes::c_void,
size: usize
}
Now lets implement a method to allocate our memory. We could use the builder pattern here to handle options such as preferred address and memory protections, but to keep things simple we will just use a lot of static selections.
// Contents of handle.rs
use std::{
fmt,
ptr,
};
use winapi::um::{
memoryapi::VirtualAlloc,
winnt::{MEM_RESERVE, MEM_COMMIT, PAGE_READWRITE},
};
// ... unchanged ...
impl MemHandle {
pub fn allocate(size: usize) -> Result<Self, WinError> {
unsafe {
let ptr = VirtualAlloc(ptr::null_mut(), size, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);
if ptr.is_null() {
Err(WinError)
} else {
Ok(MemHandle { ptr, size })
}
}
}
}
Now we can allocate memory and return our struct that holds the handle and the size. However, Rust doesn't know how to properly de-allocate this memory when the structure is no longer needed. To solve this we can implement a custom Drop function for our struct. This drop function will call VirtualFree using our handle to let the operating system know to clean up the memory. As I mentioned in the last blog post, most shellcode does not preserve registers or cleanly return to the callee which likely means that this Drop may never be reached.
impl Drop for MemHandle {
fn drop(&mut self) {
unsafe { let _r = VirtualFree(self.ptr, 0, MEM_RELEASE); }
}
}
After allocating memory, we need to write our payload to that memory. Let's implement a new function to handle the WriteProcessMemory API call.
pub fn write_mem(&self, buffer: &[u8]) -> Result<usize, WinError> {
let mut bytes_written = 0;
let len = if buffer.len() > self.size {
self.size
} else {
buffer.len()
};
let result = unsafe {
let handle = GetCurrentProcess();
WriteProcessMemory(handle, self.ptr, buffer.as_ptr() as *mut winapi::ctypes::c_void, len, &mut bytes_written)
};
if result == 0 {
Err(WinError)
} else {
Ok(bytes_written)
}
}
Notice how writing a wrapper around the winapi functions also allows us to make the functions more ergonomic to use. Now the caller of this function can pass a byte slice, &[u8], and the type conversion is handled inside of the function. We can also ensure that we never write more than we have allocated by checking the size field in our struct and we can return the amount of bytes written directly instead of having the user pass in a mutable reference.
Now let's create a function to make our memory section executable.
pub fn make_executable(&self) -> Result<u32, WinError> {
let mut old_protect = 0;
let result = unsafe {
VirtualProtect(self.ptr, self.size, PAGE_EXECUTE_READ, &mut old_protect)
};
if result == 0 {
Err(WinError)
} else {
Ok(old_protect)
}
}
This one is pretty straightforward, we attempt to make the memory executable. If the call to VirtualProtect succeeds, we return the old protection, otherwise we return an error.
Finally, we have to make a function that returns a function pointer to our allocated memory. I've elected to return a function pointer that is marked as "unsafe" since transmuting a region of bytes to a function pointer and then calling the pointer has almost no gurantees and is very likely to crash.
pub fn as_function_ptr(&self) -> unsafe extern "C" fn() {
unsafe { std::mem::transmute::<*mut winapi::ctypes::c_void, unsafe extern "C" fn()>(self.ptr)}
}
Now that we have all of the necessary functions, its time to test out the code! Let's create a new binary crate called "winmem-test" and include our library as a crate. We'll also copy over our pop_calc.bin payload from our previous project.
D:\projects\rust\blog\win-api>cargo new winmem-test
Created binary (application) `winmem-test` package
D:\projects\rust\blog\win-api>
[package]
name = "winmem-test"
version = "0.1.0"
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
winmem = { path = "../winmem" }
Now let's use our new library to inject our payload into our process.
// contents of main.rs
use winmem::handle::MemHandle;
fn main() {
let payload = include_bytes!("pop-calc.bin");
// allocate memory
let memory = MemHandle::allocate(payload.len()).unwrap();
println!("Allocated memory");
// if writing our payload to the memory succeeds
if let Ok(bytes_written) = memory.write_mem(&payload[..]) {
println!("Wrote {} bytes to memory", bytes_written);
// update protections
let _old_protect = memory.make_executable().unwrap();
println!("Memory protections updated");
// cast memory pointer to function pointer
let func = memory.as_function_ptr();
println!("Calling payload");
// call function
unsafe { func() }
}
}
That looks a lot better than our first attempt. Time to test it out..
Awesome! Looks like it works as intended. Of course this doesnt mean our library is safe necessarily, I'm sure that it can be improved. I just wanted to show an example of how you could go about implementing a safer wrapper around winapi-rs.
I hope you enjoyed this blog post! Check out part III where we call undocumented NTDLL functions!