Using CreateToolhelp32Snapshot
TL;DR
CreateToolhelp32Snapshot
allows us to capture a snapshot of the system's
process list. By iterating through this snapshot with Process32First
and
Process32Next
, we can collect information about every running process. Malware
often enumerates processes to find suitable targets for injection or to avoid
certain security tools. This example demonstrates how to traverse the snapshot,
identify the desired process, and obtain its handle for later use in other
techniques such as code injection.
Code Walkthrough
main.zig
const std = @import("std");
const windows = std.os.windows;
const print = std.debug.print;
// Windows API types
const DWORD = windows.DWORD;
const HANDLE = windows.HANDLE;
const BOOL = windows.BOOL;
const WINAPI = windows.WINAPI;
// Configuration
const TARGET_PROCESS = "svchost.exe";
// Convert UTF-8 to UTF-16 at compile time
const W = std.unicode.utf8ToUtf16LeStringLiteral;
// Constants for CreateToolhelp32Snapshot
const TH32CS_SNAPPROCESS: DWORD = 0x00000002;
const INVALID_HANDLE_VALUE: HANDLE = @as(HANDLE, @ptrFromInt(@as(usize, @bitCast(@as(isize, -1)))));
const MAX_PATH: usize = 260;
// PROCESSENTRY32W structure
const PROCESSENTRY32W = extern struct {
dwSize: DWORD,
cntUsage: DWORD,
th32ProcessID: DWORD,
th32DefaultHeapID: usize, // ULONG_PTR
th32ModuleID: DWORD,
cntThreads: DWORD,
th32ParentProcessID: DWORD,
pcPriClassBase: i32, // LONG
dwFlags: DWORD,
szExeFile: [MAX_PATH]u16, // WCHAR[MAX_PATH]
};
// External function declarations for ToolHelp32 API
extern "kernel32" fn CreateToolhelp32Snapshot(dwFlags: DWORD, th32ProcessID: DWORD) callconv(WINAPI) HANDLE;
extern "kernel32" fn Process32FirstW(hSnapshot: HANDLE, lppe: *PROCESSENTRY32W) callconv(WINAPI) BOOL;
extern "kernel32" fn Process32NextW(hSnapshot: HANDLE, lppe: *PROCESSENTRY32W) callconv(WINAPI) BOOL;
extern "kernel32" fn CloseHandle(hObject: HANDLE) callconv(WINAPI) BOOL;
extern "kernel32" fn GetLastError() callconv(WINAPI) DWORD;
extern "kernel32" fn OpenProcess(dwDesiredAccess: DWORD, bInheritHandle: BOOL, dwProcessId: DWORD) callconv(WINAPI) ?HANDLE;
// Constants
const PROCESS_ALL_ACCESS = 0x001F0FFF;
// ProcessResult structure
const ProcessResult = struct {
pid: DWORD,
handle: HANDLE,
pub fn deinit(self: ProcessResult) void {
_ = CloseHandle(self.handle);
}
};
// Helper function to convert UTF-8 string to UTF-16 (wide string)
fn convertToWideString(allocator: std.mem.Allocator, utf8_str: []const u8) ![]u16 {
const utf16_len = try std.unicode.calcUtf16LeLen(utf8_str);
var wide_string = try allocator.alloc(u16, utf16_len + 1);
_ = try std.unicode.utf8ToUtf16Le(wide_string[0..utf16_len], utf8_str);
wide_string[utf16_len] = 0; // Null terminate
return wide_string;
}
// Helper function to compare wide strings (case-insensitive)
fn compareWideStringsIgnoreCase(str1: []const u16, str2: []const u16) bool {
if (str1.len != str2.len) return false;
for (str1, str2) |c1, c2| {
// Simple case-insensitive comparison for ASCII range
const lower_c1 = if (c1 >= 'A' and c1 <= 'Z') c1 + 32 else c1;
const lower_c2 = if (c2 >= 'A' and c2 <= 'Z') c2 + 32 else c2;
if (lower_c1 != lower_c2) return false;
}
return true;
}
// Get remote process PID by name using CreateToolhelp32Snapshot (equivalent to your function)
fn getRemoteProcessPid(allocator: std.mem.Allocator, process_name: []const u8) !DWORD {
const wide_process_name = try convertToWideString(allocator, process_name);
defer allocator.free(wide_process_name);
const snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (snapshot == INVALID_HANDLE_VALUE) {
print("[!] CreateToolhelp32Snapshot Failed With Error : {d} \n", .{GetLastError()});
return error.SnapshotFailed;
}
defer _ = CloseHandle(snapshot);
var process_entry = std.mem.zeroes(PROCESSENTRY32W);
process_entry.dwSize = @sizeOf(PROCESSENTRY32W);
if (Process32FirstW(snapshot, &process_entry) == 0) {
print("[!] Process32FirstW Failed With Error : {d} \n", .{GetLastError()});
return error.ProcessEnumFailed;
}
while (true) {
// Find the length of the executable name (null-terminated)
var exe_name_len: usize = 0;
while (exe_name_len < process_entry.szExeFile.len and process_entry.szExeFile[exe_name_len] != 0) {
exe_name_len += 1;
}
const exe_name = process_entry.szExeFile[0..exe_name_len];
// Compare process names (case-insensitive)
if (compareWideStringsIgnoreCase(exe_name, wide_process_name[0 .. wide_process_name.len - 1])) { // -1 to exclude null terminator
return process_entry.th32ProcessID;
}
if (Process32NextW(snapshot, &process_entry) == 0) {
break;
}
}
print("[!] Process is Not Found \n", .{});
return error.ProcessNotFound;
}
// Enhanced version that returns ProcessResult (PID + Handle)
fn getRemoteProcessHandle(allocator: std.mem.Allocator, process_name: []const u8) !?ProcessResult {
const pid = getRemoteProcessPid(allocator, process_name) catch |err| {
switch (err) {
error.ProcessNotFound => return null,
else => return err,
}
};
const handle = OpenProcess(PROCESS_ALL_ACCESS, 0, pid) orelse {
print("[!] OpenProcess failed for PID: {} with error: {d}\n", .{ pid, GetLastError() });
return null;
};
return ProcessResult{
.pid = pid,
.handle = handle,
};
}
// Wait for user input
fn waitForInput() !void {
print("[#] Press Enter to exit...\n", .{});
const stdin = std.io.getStdIn().reader();
_ = try stdin.readByte();
}
// Main function demonstrating both approaches
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
// Get remote process handle (equivalent to Rust's match statement)
if (try getRemoteProcessHandle(allocator, TARGET_PROCESS)) |result| {
defer result.deinit();
print("[+] Found process {s} with PID: {}\n", .{ TARGET_PROCESS, result.pid });
} else {
print("[!] Could not find process {s}\n", .{TARGET_PROCESS});
}
try waitForInput();
}