Simple Shellcode Runner in Rust
ID: T1055 Sub-techniques: T1055.001, T1055.002, T1055.003, T1055.004, T1055.005, T1055.008, T1055.009, T1055.011, T1055.012, T1055.013, T1055.014, T1055.015
ID: T1059 Sub-techniques: T1059.001, T1059.002, T1059.003, T1059.004, T1059.005, T1059.006, T1059.007, T1059.008, T1059.009, T1059.010
Understanding a Shellcode Runner in Rust: Detailed Walkthrough
In this article, we will explore a Rust program that acts as a shellcode runner. This program demonstrates how to download a binary (or shellcode) from a remote server and execute it in memory. We will break down each part of the code to understand how it works, covering everything from memory allocation to creating and managing threads in Windows.
Introduction
Shellcode runners are tools used to execute arbitrary shellcode in the memory of a host process. This is often done in security research, penetration testing, or during the exploitation of vulnerabilities. The code we are analyzing uses Rust and several Windows API functions to achieve this.
Here is the structure of the project:
Cargo.toml: The configuration file that lists the dependencies.
main.rs: The main Rust file that contains the logic for downloading and executing the shellcode.
Let's start by looking at the configuration.
Cargo.toml Configuration
The Cargo.toml
file specifies the dependencies required for the project.
winapi: This crate provides Rust bindings for Windows API functions. The features
memoryapi
,processthreadsapi
,synchapi
, andwinnt
are used to manage memory and threads.reqwest: A popular HTTP client for Rust, used here to download the shellcode from a remote server.
tokio: An asynchronous runtime for Rust, required for running asynchronous code with
reqwest
.
Main.rs: The Shellcode Runner
Now, let's dive into the main file, main.rs
.
This code can be broken down into several key steps:
1. Asynchronous Main Function
The #[tokio::main]
macro is used to declare the main
function as an asynchronous entry point. This allows us to use asynchronous operations, such as downloading the shellcode.
The Result<(), Box<dyn std::error::Error>>
return type allows the function to handle errors gracefully.
2. Downloading the Shellcode
The shellcode is downloaded from a remote server using the reqwest
library. The binary data is retrieved as a byte array.
reqwest::get(url).await: This sends a GET request to the specified URL asynchronously.
.bytes().await?: This extracts the response body as raw bytes.
3. Allocating Memory for the Shellcode
The downloaded shellcode needs to be stored in memory where it can be executed. This is done using the VirtualAlloc
function from the Windows API.
null_mut(): A null pointer is passed, indicating that the system should determine the location of the allocated memory.
bin_data.len(): The size of the memory to be allocated is the length of the binary data.
MEM_COMMIT: Allocates physical storage in memory.
PAGE_EXECUTE_READWRITE: The allocated memory is readable, writable, and executable.
4. Copying the Shellcode to Memory
Once the memory is allocated, the shellcode is copied into this memory region.
bin_data.as_ptr(): A pointer to the start of the binary data.
*func_addr as mut u8: The address where the data should be copied.
bin_data.len(): The number of bytes to copy.
5. Executing the Shellcode
To execute the shellcode, a new thread is created using CreateThread
. The entry point of this thread is set to the address where the shellcode was copied.
null_mut(): No security attributes are specified.
0: The stack size is set to the default.
Some(std::mem::transmute(func_addr)): The entry point of the thread is the address of the shellcode.
std::mem::transmute
is used to convert the function address into a type thatCreateThread
expects.null_mut(): No parameters are passed to the thread.
*&mut thread_id as mut u32: A pointer to receive the thread ID.
6. Waiting for the Thread to Finish
Finally, the program waits for the thread to finish execution using WaitForSingleObject
.
h_thread: The handle to the thread created earlier.
0xFFFFFFFF: This constant indicates that the function should wait indefinitely for the thread to complete.
Compiling the Code
To compile this Rust program, you can use the following command in your terminal. Make sure you are in the directory containing your Cargo.toml
file:
cargo build: This command compiles your Rust project.
--release: This flag tells Cargo to build the project in release mode, which optimizes the code for performance.
After running this command, the compiled executable will be located in the target/release/
directory.
Running the Executable
Once compiled, you can run the executable directly:
This will execute the program, download the shellcode from the specified URL, allocate memory for it, and execute it in a new thread.
Last updated