Introduction to Windows API in Rust: Process Enumeration

Introduction

The Windows operating system offers a robust set of tools for monitoring and managing the processes running within it. Rust, a systems programming language known for its safety and performance, provides a powerful way to interface with these tools. In this blog post, we’ll dive into the Windows ToolHelp API and see how Rust can help us obtain process id of a given process name.

At the end we will have a program which can give us the process id of “explorer.exe” process.

process enumeration example

Prerequisites

  • Familiarity with basic Rust concepts (functions, structs, error handling).
  • Understanding of Windows processes and the idea that each process has a unique ID.

Getting Started

Create a new Rust project named process_enumeration using Cargo:

cargo new --bin process_enumeration

Add the windows crate dependency to your Cargo.toml file:


[dependencies.windows]
version = "0.53.0"
features = [
"Win32_Foundation",
"Win32_System_Diagnostics_ToolHelp"]



Our Main.rs file:

//Importing Necessary Modules
use std::io::Error;
use std::mem;
use windows::Win32::Foundation::CloseHandle;
use windows::Win32::System::Diagnostics::ToolHelp::{
CreateToolhelp32Snapshot, Process32FirstW,
Process32NextW, PROCESSENTRY32W, TH32CS_SNAPPROCESS,
};
// Creating function to obtain the process id

pub fn get_process_id(process_name: &str) -> Result<Option<u32>, Error> {
unsafe {
// Create a snapshot of the current system processes
let snapshot_result = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);

let snapshot_handle = match snapshot_result {
Ok(handle) => handle,

Err(err) => return Err(err.into()),
// Convert windows::Error to std::io::Error
};

// Get the first process entry

let mut entry = PROCESSENTRY32W::default();
entry.dwSize = mem::size_of::<PROCESSENTRY32W>() as u32;

match Process32FirstW(snapshot_handle, &mut entry) {
Ok(_) => {} // Successfully retrieved the first process entry
Err(_) => {
CloseHandle(snapshot_handle);
return Err(Error::last_os_error());
}
}
// Iterate through process entries
loop {
let process_name_str = String::from_utf16_lossy(&entry.szExeFile);

if process_name_str.trim_end_matches('\0') == process_name {
CloseHandle(snapshot_handle);

return Ok(Some(entry.th32ProcessID));
}
match Process32NextW(snapshot_handle, &mut entry) {
Ok(_) => {} // Successfully retrieved the next process entry
Err(_) => break, // Stop iteration if an error occurs
}
}
// Close the snapshot handle
CloseHandle(snapshot_handle);
// Process not found
Ok(None)
}
}
// Our Main function
fn main() -> Result<(), Error> {
let process_name = "explorer.exe";
// You can change it to any process you want.

let process_id = get_process_id(&process_name)?;
match process_id {
Some(value) => {
println!("The Process id is: {:?}", value);
}
None => {
println!("Process not found");
}
}
Ok(())
}

The Windows ToolHelp API

The ToolHelp API is a core component of the Windows API, providing functions designed to take snapshots of various system elements. For our purposes, the most important functions are:

  • CreateToolhelp32Snapshot: This takes a snapshot of a specified type. We’ll use it with the TH32CS_SNAPPROCESS flag to get a snapshot of running processes.
  • Process32FirstW: Retrieves information about the first process found within the snapshot.
  • Process32NextW: Allows us to iterate through the snapshot, accessing information about subsequent processes.

Understanding Argument Choices

Let’s focus on the CreateToolhelp32Snapshot function, as its arguments determine the kind of data we obtain:

  • TH32CS_SNAPPROCESS: By using this flag, we instruct the API to provide a process snapshot. Other options exist for different system aspects (heaps, modules, etc.).
  • 0: Since we’re interested in a system-wide list of processes, the 0 argument signifies that we want information about all running processes. We could provide a specific process ID here for a more targeted query.

Rust’s Role

1. Structures and Memory Safety

Rust uses structures to interact with Windows API data. The PROCESSENTRY32W structure is meticulously designed to hold process information extracted from the snapshot. Rust’s memory safety features help prevent errors in how we manipulate this data.

  • The .default() function initializes the structure, aiding in predictable memory allocation.
  • Setting the structure’s dwSize field is crucial. Rust’s mem::size_of() and type casting ensure we pass a value in the exact format expected by the Windows API.

2. Error Handling

Rust’s emphasis on safety extends to interacting with external APIs. Using match expressions, we gracefully handle potential errors returned by the ToolHelp API functions. This lets us release resources (CloseHandle) and propagate errors for clearer diagnostics (Error::last_os_error()).

3. Converting and Iterating

Rust’s String::from_utf16_lossy() facilitates working with Windows process names. We iterate over the snapshot, comparing names until we locate our target process. When found, we extract its process ID.

Beyond the Basics

The provided code offers a foundation for finding a process ID by name. Consider enhancing it with:

  • Retrieving more detailed process data (PROCESSENTRY32W stores much more than just name and ID).
  • Implementing features like process termination or adjustment of process priorities and show it in the MessageBox from my previous blog.
  • This basic implementation could be a building block for a task manager-like application in Rust.

Windows API Types and Rust Equivalents

Windows API Types and Rust Equivalents

Leave a Comment