mirror of
https://github.com/nbaertsch/pool-proxy-ng
synced 2026-08-09 12:58:25 +00:00
release
This commit is contained in:
+14
@@ -0,0 +1,14 @@
|
||||
.zig-cache/
|
||||
zig-out/
|
||||
zig-cache/
|
||||
*.exe
|
||||
*.obj
|
||||
*.o
|
||||
*.pdb
|
||||
*.log
|
||||
*.tmp
|
||||
*.test
|
||||
out.txt
|
||||
err.txt
|
||||
tmp-*.py
|
||||
scratch-*.py
|
||||
@@ -0,0 +1,107 @@
|
||||
# pool-proxy-ng
|
||||
|
||||
Next-gen rewrite of [PoolProxy](https://github.com/nbaertsch/PoolProxy) in Zig. Same core idea — proxy Win32 API calls through the thread pool so your implant never shows up on the call stack — but with multi-gadget support, automatic gadget discovery, and side-effect DLL loading. See the [PoolProxy README](https://github.com/nbaertsch/PoolProxy) for a detailed walkthrough of the technique, the sentinel trick, and how `mov [rbx], rax` gadgets work.
|
||||
|
||||
## What's different from PoolProxy
|
||||
|
||||
PoolProxy proved the concept with a single hardcoded gadget (`ntdll!RtlPcToFileHeader`, rbx, 0x40 cleanup, 8 args max). This version:
|
||||
|
||||
- Supports **9+ gadgets** across multiple DLLs and registers (rbx, rdi, rsi, r14)
|
||||
- Handles gadgets with arbitrary **pop chains** before/after the target register via a packed nibble map (`popMap`)
|
||||
- Ships a **pre-computed gadget database** with comptime verification — init takes microseconds
|
||||
- Falls back to a **PE section scanner** when RVAs shift between OS builds
|
||||
- **Side-effect loads** DLLs (COM activation, crypto APIs, device enumeration, Winsock) instead of calling `LoadLibrary`
|
||||
- Covers up to **15 arguments** (every documented user-mode Windows API)
|
||||
|
||||
## How it works
|
||||
|
||||
```
|
||||
ntdll!TppWorkpExecuteCallback ← TP dispatcher
|
||||
→ trampoline (.text) ← saves regs, builds pop slots, loads args
|
||||
→ target API ← your proxied call
|
||||
→ gadget (signed DLL) ← mov [reg],rax; add rsp,N; pops; ret
|
||||
→ ntdll!TppWorkpExecuteCallback ← clean return to TP
|
||||
```
|
||||
|
||||
The trampoline saves callee-saved registers into a scratch area on the proxy struct, builds the gadget's expected pop layout on the stack from the `popMap`, loads arguments, then does `push gadget_addr; jmp target_func`. The target's `ret` lands in the gadget, which stores `rax`, cleans up the stack, and returns straight to the TP dispatcher. No implant addresses touch the stack at any point.
|
||||
|
||||
## Gadgets
|
||||
|
||||
20 entries in the DB across 10 DLLs, 4 trampoline registers. Here are a few highlights:
|
||||
|
||||
| DLL | Reg | Cleanup | Max args | Pops |
|
||||
|-----|-----|---------|----------|------|
|
||||
| Chakra.dll | rbx | 0x78 | **15** | 4/1 |
|
||||
| Chakra.dll | rdi | 0x70 | 14 | 4/2 |
|
||||
| ntdll.dll | rbx | 0x40 | 8 | 0/0 |
|
||||
| rpcrt4.dll | rbx | 0x30 | 6 | 0/0 |
|
||||
| kernelbase.dll | rbx | 0x20 | 4 | 0/0 |
|
||||
|
||||
The ntdll gadget is the same `RtlPcToFileHeader` epilogue from the original PoolProxy — always loaded, zero extra pops, simple. The Chakra gadgets extend coverage to 15 args with more complex pop chains.
|
||||
|
||||
All gadgets are execution-verified at their full arg capacity via XOR checksums.
|
||||
|
||||
## Gadget sourcing
|
||||
|
||||
1. **Verify** pre-computed RVAs from the built-in DB (fast path).
|
||||
2. **Micro-scan** any DLL whose pinned RVA doesn't match the running build.
|
||||
3. **Side-effect load** new DLLs when the stable can't satisfy a call (see below).
|
||||
|
||||
## Side-effect loading
|
||||
|
||||
Instead of calling `LoadLibrary` directly, we invoke legitimate APIs whose dependency chains pull in the DLLs we need. `LdrLoadDll` still gets called internally (kernel image-load callbacks and ETW events still fire — the DLL load is not invisible), but the call stack at load time shows system code (combase, advapi32, etc.) rather than implant addresses. This keeps the "no implant on the stack" invariant consistent across init, not just proxied calls. It also keeps loader imports and DLL name strings out of the binary.
|
||||
|
||||
| Method | API called | Target DLL | Prerequisite |
|
||||
|--------|-----------|------------|--------------|
|
||||
| `com_chakra` | CoCreateInstance | Chakra.dll | combase.dll |
|
||||
| `com_mshtml` | CoCreateInstance | mshtml.dll | combase.dll |
|
||||
| `com_cdp` | CoCreateInstance | cdp.dll | combase.dll |
|
||||
| `com_jscript9` | CoCreateInstance | jscript9.dll | combase.dll |
|
||||
| `com_oleaut32` | CoCreateInstance | oleaut32.dll | combase.dll |
|
||||
| `com_shell32` | CoCreateInstance | shell32.dll | combase.dll |
|
||||
| `crypt_context` | CryptAcquireContextW | cryptsp/crypt32 | advapi32.dll |
|
||||
| `cm_device_list` | CM_Get_Device_ID_List_SizeW | setupapi.dll | cfgmgr32.dll |
|
||||
| `winsock_startup` | WSAStartup | mswsock.dll | ws2_32.dll |
|
||||
|
||||
Each DLL is only attempted once per stable lifetime. After the side-effect call, we verify the target appeared in the PEB before trusting it.
|
||||
|
||||
## CET / shadow stack
|
||||
|
||||
This technique does not survive CET (Control-flow Enforcement Technology). The trampoline does `push gadget_addr; jmp target_func` — when the target's `ret` fires, the hardware shadow stack comparison fails because `gadget_addr` was never pushed by a `call`. `#CP` fault, process dies. IBT (indirect branch tracking) is fine — API entry points have `ENDBR64`, and the gadget is reached via `ret` not an indirect branch.
|
||||
|
||||
CET user-mode enforcement requires the process PE to set `IMAGE_DLLCHARACTERISTICS_EX_CET_COMPAT`, a CET-capable CPU (Intel 11th gen+ / AMD Zen 3+), and HVCI enabled. Most processes don't have all three yet, but hardened targets are heading there. For CET-enabled targets, `NtContinue`-based dispatch (which legitimately manipulates the shadow stack) is the path forward.
|
||||
|
||||
## Thread safety
|
||||
|
||||
`proxyCall()` works from any thread. `init()` uses a mutex with double-checked locking. Auto-load grabs the mutex before touching the stable. Fast path (gadget already resolved) is lock-free.
|
||||
|
||||
## Build
|
||||
|
||||
```
|
||||
zig build test # unit tests
|
||||
zig build -Doptimize=ReleaseFast # release binary
|
||||
.\zig-out\bin\pool-proxy-ng.exe # test harness (loads Chakra, runs per-gadget validation)
|
||||
```
|
||||
|
||||
Zig 0.15+, `x86_64-windows`.
|
||||
|
||||
## Source layout
|
||||
|
||||
```
|
||||
src/
|
||||
pool_proxy.zig — proxyCall(), init(), config, thread-safe dispatch
|
||||
trampolines.zig — naked x64 trampolines with popMap register restoration
|
||||
gadget_db.zig — pre-computed gadget database, comptime pop_map decode
|
||||
gadget_scanner.zig — multi-register PE scanner (fallback for unknown builds)
|
||||
gadget_stable.zig — gadget collection + selection (random or deterministic)
|
||||
dll_sideload.zig — side-effect DLL loading (COM, crypto, device, Winsock)
|
||||
dm_bindings.zig — JSON serialization for introspection
|
||||
types.zig — GadgetInfo, GadgetRegister, PopMap, config types
|
||||
config.zig — PoolProxyConfig (fallback, auto-load, selection policy)
|
||||
main.zig — test harness
|
||||
platform/
|
||||
ntypes.zig — NT type definitions
|
||||
peb.zig — PEB walk + hash-based export resolution
|
||||
```
|
||||
|
||||
Tested on Windows 11 build 10.0.26200.8457.
|
||||
@@ -0,0 +1,48 @@
|
||||
const std = @import("std");
|
||||
|
||||
pub fn build(b: *std.Build) void {
|
||||
const target = b.resolveTargetQuery(.{
|
||||
.cpu_arch = .x86_64,
|
||||
.os_tag = .windows,
|
||||
});
|
||||
const optimize = b.standardOptimizeOption(.{});
|
||||
|
||||
const root_module = b.createModule(.{
|
||||
.root_source_file = b.path("src/main.zig"),
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
|
||||
const exe = b.addExecutable(.{
|
||||
.name = "pool-proxy-ng",
|
||||
.root_module = root_module,
|
||||
});
|
||||
b.installArtifact(exe);
|
||||
|
||||
const test_step = b.step("test", "Run all tests");
|
||||
const test_files = [_][]const u8{
|
||||
"src/main.zig",
|
||||
"src/types.zig",
|
||||
"src/config.zig",
|
||||
"src/gadget_db.zig",
|
||||
"src/gadget_scanner.zig",
|
||||
"src/gadget_stable.zig",
|
||||
"src/dll_sideload.zig",
|
||||
"src/trampolines.zig",
|
||||
"src/pool_proxy.zig",
|
||||
"src/dm_bindings.zig",
|
||||
"src/platform/ntypes.zig",
|
||||
"src/platform/peb.zig",
|
||||
};
|
||||
|
||||
for (test_files) |path| {
|
||||
const module = b.createModule(.{
|
||||
.root_source_file = b.path(path),
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
const module_tests = b.addTest(.{ .root_module = module });
|
||||
const run_module_tests = b.addRunArtifact(module_tests);
|
||||
test_step.dependOn(&run_module_tests.step);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
const std = @import("std");
|
||||
const types = @import("types.zig");
|
||||
|
||||
pub const PoolProxyConfig = extern struct {
|
||||
/// Strategy to use when proxy execution cannot proceed.
|
||||
fallback_strategy: types.FallbackStrategy = .tp_delegation,
|
||||
/// Allow side-effect DLL loading when the current stable has no suitable gadget.
|
||||
auto_load_enabled: u8 = 1,
|
||||
/// Select randomly from eligible gadgets instead of always taking the smallest fit.
|
||||
random_selection: u8 = 1,
|
||||
/// Track fallback events in `fallback_count` and `last_fallback_api`.
|
||||
log_fallbacks: u8 = 1,
|
||||
_pad: [1]u8 = .{0},
|
||||
/// Number of times proxy execution used the configured fallback path.
|
||||
fallback_count: u32 = 0,
|
||||
/// Best-effort text description of the last API address that fell back.
|
||||
last_fallback_api: [64]u8 = [_]u8{0} ** 64,
|
||||
/// Preferred callee-saved register, or `0xFF` to allow any supported register.
|
||||
preferred_register: u8 = 0xFF,
|
||||
_pad2: [3]u8 = .{ 0, 0, 0 },
|
||||
/// Skip auto-loaded gadgets whose cleanup is smaller than this threshold.
|
||||
min_auto_load_cleanup: u32 = 0x40,
|
||||
};
|
||||
|
||||
test "config defaults enable auto load" {
|
||||
const cfg: PoolProxyConfig = .{};
|
||||
try std.testing.expectEqual(@as(u8, 1), cfg.auto_load_enabled);
|
||||
}
|
||||
@@ -0,0 +1,262 @@
|
||||
const std = @import("std");
|
||||
const builtin = @import("builtin");
|
||||
const nt = @import("platform/ntypes.zig");
|
||||
const peb = @import("platform/peb.zig");
|
||||
const types = @import("types.zig");
|
||||
|
||||
const SideloadMethod = types.SideloadMethod;
|
||||
|
||||
const GUID = extern struct {
|
||||
Data1: u32,
|
||||
Data2: u16,
|
||||
Data3: u16,
|
||||
Data4: [8]u8,
|
||||
};
|
||||
|
||||
const CLSID_CDP: GUID = .{ .Data1 = 0xFE67FBE0, .Data2 = 0xB834, .Data3 = 0x4424, .Data4 = .{ 0x88, 0x0A, 0x52, 0xD6, 0xF7, 0x1B, 0xA9, 0x0B } };
|
||||
const CLSID_CHAKRA: GUID = .{ .Data1 = 0x1B7CD997, .Data2 = 0xE5FF, .Data3 = 0x4932, .Data4 = .{ 0xA7, 0xA6, 0x2A, 0x9E, 0x63, 0x6D, 0xA3, 0x85 } };
|
||||
const CLSID_HTMLDOC: GUID = .{ .Data1 = 0x25336920, .Data2 = 0x03F9, .Data3 = 0x11CF, .Data4 = .{ 0x8F, 0xD0, 0x00, 0xAA, 0x00, 0x68, 0x6F, 0x13 } };
|
||||
const CLSID_STDFONT: GUID = .{ .Data1 = 0x0BE35203, .Data2 = 0x8F91, .Data3 = 0x11CE, .Data4 = .{ 0x9D, 0xE3, 0x00, 0xAA, 0x00, 0x4B, 0xB8, 0x51 } };
|
||||
const CLSID_AUTOCOMPLETE: GUID = .{ .Data1 = 0x00BB2763, .Data2 = 0x6A77, .Data3 = 0x11D0, .Data4 = .{ 0xA5, 0x35, 0x00, 0xC0, 0x4F, 0xD7, 0xD0, 0x62 } };
|
||||
const CLSID_JSCRIPT9: GUID = .{ .Data1 = 0x16D51579, .Data2 = 0xA30B, .Data3 = 0x4C8B, .Data4 = .{ 0xA2, 0x76, 0x0F, 0xF4, 0xDC, 0x41, 0xE7, 0x55 } };
|
||||
|
||||
const IID_IUNKNOWN: GUID = .{ .Data1 = 0x00000000, .Data2 = 0x0000, .Data3 = 0x0000, .Data4 = .{ 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46 } };
|
||||
const CLSCTX_INPROC_SERVER: u32 = 0x1;
|
||||
|
||||
/// Optional proxy function for routing side-effect calls through a gadget.
|
||||
/// Returns the raw return value on success, or null if no gadget is available.
|
||||
pub const ProxyCallFn = *const fn (func_addr: usize, args: []const usize) ?usize;
|
||||
|
||||
/// Attempt to make `dll_name` appear in the module list without calling the loader directly.
|
||||
/// When `proxy_call` is provided, side-effect API calls are routed through a pool proxy
|
||||
/// gadget so the call stack stays clean even during DLL loading.
|
||||
pub fn ensureDllLoaded(dll_name: []const u8, method: SideloadMethod, proxy_call: ?ProxyCallFn) bool {
|
||||
if (builtin.os.tag != .windows or builtin.cpu.arch != .x86_64) return false;
|
||||
|
||||
const hash = peb.rotrHash(dll_name);
|
||||
if (peb.getModuleBase(hash) != null) return true;
|
||||
|
||||
const prereq = method.prerequisiteDll();
|
||||
if (prereq.len > 0 and peb.getModuleBase(peb.rotrHash(prereq)) == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const ok = switch (method) {
|
||||
.none => true,
|
||||
.com_cdp => comActivate(&CLSID_CDP, proxy_call),
|
||||
.com_chakra => comActivate(&CLSID_CHAKRA, proxy_call),
|
||||
.com_mshtml => comActivate(&CLSID_HTMLDOC, proxy_call),
|
||||
.com_oleaut32 => comActivate(&CLSID_STDFONT, proxy_call),
|
||||
.com_shell32 => comActivate(&CLSID_AUTOCOMPLETE, proxy_call),
|
||||
.com_jscript9 => comActivate(&CLSID_JSCRIPT9, proxy_call),
|
||||
.crypt_context => callCryptAcquireContext(proxy_call),
|
||||
.cm_device_list => callCmGetDeviceIdListSize(proxy_call),
|
||||
.winsock_startup => callWsaStartup(proxy_call),
|
||||
};
|
||||
|
||||
return ok and peb.getModuleBase(hash) != null;
|
||||
}
|
||||
|
||||
/// Trigger COM activation. The DLL staying mapped is enough for our needs, so
|
||||
/// the returned object is intentionally not released. Immediate `Release()`
|
||||
/// calls have proven unstable for some script-engine CLSIDs, and `stable`
|
||||
/// records each DLL hash so this path is exercised at most once per module.
|
||||
fn comActivate(clsid: *const GUID, proxy_call: ?ProxyCallFn) bool {
|
||||
const combase = peb.getModuleBase(comptime peb.rotrHash("combase.dll")) orelse return false;
|
||||
const co_create = peb.getExportByHash(combase, comptime peb.rotrHash("CoCreateInstance")) orelse return false;
|
||||
|
||||
var ppv: ?*anyopaque = null;
|
||||
if (proxy_call) |proxy| {
|
||||
_ = proxy(@intFromPtr(co_create), &[_]usize{
|
||||
@intFromPtr(clsid),
|
||||
0,
|
||||
CLSCTX_INPROC_SERVER,
|
||||
@intFromPtr(&IID_IUNKNOWN),
|
||||
@intFromPtr(&ppv),
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
const CoCreateInstanceFn = *const fn (
|
||||
*const GUID,
|
||||
?*anyopaque,
|
||||
u32,
|
||||
*const GUID,
|
||||
*?*anyopaque,
|
||||
) callconv(.c) i32;
|
||||
|
||||
_ = @as(CoCreateInstanceFn, @ptrCast(co_create))(
|
||||
clsid,
|
||||
null,
|
||||
CLSCTX_INPROC_SERVER,
|
||||
&IID_IUNKNOWN,
|
||||
&ppv,
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
// ── Crypto side-effects ─────────────────────────────────────────────────
|
||||
|
||||
/// Call CryptAcquireContextW from advapi32 → loads cryptsp.dll → crypt32.dll chain.
|
||||
fn callCryptAcquireContext(proxy_call: ?ProxyCallFn) bool {
|
||||
const advapi32 = peb.getModuleBase(comptime peb.rotrHash("advapi32.dll")) orelse return false;
|
||||
const crypt_acquire = peb.getExportByHash(advapi32, comptime peb.rotrHash("CryptAcquireContextW")) orelse return false;
|
||||
|
||||
const PROV_RSA_FULL: u32 = 1;
|
||||
const CRYPT_VERIFYCONTEXT: u32 = 0xF0000000;
|
||||
|
||||
var hprov: usize = 0;
|
||||
var result: usize = 0;
|
||||
if (proxy_call) |proxy| {
|
||||
result = proxy(@intFromPtr(crypt_acquire), &[_]usize{
|
||||
@intFromPtr(&hprov),
|
||||
0, 0,
|
||||
PROV_RSA_FULL,
|
||||
CRYPT_VERIFYCONTEXT,
|
||||
}) orelse 0;
|
||||
} else {
|
||||
const CryptAcquireContextFn = *const fn (
|
||||
*usize, ?[*:0]const u16, ?[*:0]const u16, u32, u32,
|
||||
) callconv(.c) nt.BOOL;
|
||||
result = @intCast(@as(u32, @bitCast(@as(CryptAcquireContextFn, @ptrCast(crypt_acquire))(
|
||||
&hprov, null, null, PROV_RSA_FULL, CRYPT_VERIFYCONTEXT,
|
||||
))));
|
||||
}
|
||||
|
||||
if (result != 0 and hprov != 0) {
|
||||
const crypt_release = peb.getExportByHash(advapi32, comptime peb.rotrHash("CryptReleaseContext"));
|
||||
if (crypt_release) |release_fn| {
|
||||
const CryptReleaseContextFn = *const fn (usize, u32) callconv(.c) nt.BOOL;
|
||||
_ = @as(CryptReleaseContextFn, @ptrCast(release_fn))(hprov, 0);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// ── Device enumeration ──────────────────────────────────────────────────
|
||||
|
||||
/// Call CM_Get_Device_ID_List_SizeW from cfgmgr32.dll → loads setupapi.dll.
|
||||
fn callCmGetDeviceIdListSize(proxy_call: ?ProxyCallFn) bool {
|
||||
const cfgmgr32 = peb.getModuleBase(comptime peb.rotrHash("cfgmgr32.dll")) orelse return false;
|
||||
const cm_func = peb.getExportByHash(cfgmgr32, comptime peb.rotrHash("CM_Get_Device_ID_List_SizeW")) orelse return false;
|
||||
|
||||
var len: u32 = 0;
|
||||
if (proxy_call) |proxy| {
|
||||
_ = proxy(@intFromPtr(cm_func), &[_]usize{ @intFromPtr(&len), 0, 0 });
|
||||
return true;
|
||||
}
|
||||
|
||||
const CmGetDeviceIdListSizeFn = *const fn (*u32, ?[*:0]const u16, u32) callconv(.c) u32;
|
||||
_ = @as(CmGetDeviceIdListSizeFn, @ptrCast(cm_func))(&len, null, 0);
|
||||
return true;
|
||||
}
|
||||
|
||||
// ── Network side-effects ────────────────────────────────────────────────
|
||||
|
||||
/// Call WSAStartup from ws2_32.dll → loads mswsock.dll + LSP chain.
|
||||
fn callWsaStartup(proxy_call: ?ProxyCallFn) bool {
|
||||
const ws2_32 = peb.getModuleBase(comptime peb.rotrHash("ws2_32.dll")) orelse return false;
|
||||
const wsa_startup = peb.getExportByHash(ws2_32, comptime peb.rotrHash("WSAStartup")) orelse return false;
|
||||
const wsa_cleanup = peb.getExportByHash(ws2_32, comptime peb.rotrHash("WSACleanup"));
|
||||
|
||||
const WSADATA = extern struct { data: [408]u8 };
|
||||
var wsa_data: WSADATA = .{ .data = [_]u8{0} ** 408 };
|
||||
|
||||
var result: usize = 1; // nonzero = failure
|
||||
if (proxy_call) |proxy| {
|
||||
result = proxy(@intFromPtr(wsa_startup), &[_]usize{
|
||||
0x0202,
|
||||
@intFromPtr(&wsa_data),
|
||||
}) orelse 1;
|
||||
} else {
|
||||
const WSAStartupFn = *const fn (u16, *WSADATA) callconv(.c) i32;
|
||||
result = @intCast(@as(u32, @bitCast(@as(WSAStartupFn, @ptrCast(wsa_startup))(0x0202, &wsa_data))));
|
||||
}
|
||||
|
||||
if (result == 0) {
|
||||
if (wsa_cleanup) |cleanup_fn| {
|
||||
const WSACleanupFn = *const fn () callconv(.c) i32;
|
||||
_ = @as(WSACleanupFn, @ptrCast(cleanup_fn))();
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// ── Tests ────────────────────────────────────────────────────────────────
|
||||
|
||||
test "ensureDllLoaded is guarded on non-windows targets" {
|
||||
if (builtin.os.tag != .windows) {
|
||||
try std.testing.expect(!ensureDllLoaded("ntdll.dll", .none, null));
|
||||
}
|
||||
}
|
||||
|
||||
test "prerequisite check rejects missing prereqs on non-windows" {
|
||||
if (builtin.os.tag != .windows) {
|
||||
try std.testing.expect(!ensureDllLoaded("cdp.dll", .com_cdp, null));
|
||||
}
|
||||
}
|
||||
|
||||
test "SideloadMethod prerequisiteDll returns expected values" {
|
||||
try std.testing.expectEqualStrings("combase.dll", SideloadMethod.com_cdp.prerequisiteDll());
|
||||
try std.testing.expectEqualStrings("advapi32.dll", SideloadMethod.crypt_context.prerequisiteDll());
|
||||
try std.testing.expectEqualStrings("", SideloadMethod.none.prerequisiteDll());
|
||||
}
|
||||
|
||||
// ── Proxy callback tests ────────────────────────────────────────────────
|
||||
|
||||
var test_proxy_called: bool = false;
|
||||
var test_proxy_func_addr: usize = 0;
|
||||
var test_proxy_arg_count: usize = 0;
|
||||
|
||||
fn testProxyFn(func_addr: usize, args: []const usize) ?usize {
|
||||
test_proxy_called = true;
|
||||
test_proxy_func_addr = func_addr;
|
||||
test_proxy_arg_count = args.len;
|
||||
return 0;
|
||||
}
|
||||
|
||||
fn resetTestProxy() void {
|
||||
test_proxy_called = false;
|
||||
test_proxy_func_addr = 0;
|
||||
test_proxy_arg_count = 0;
|
||||
}
|
||||
|
||||
test "ensureDllLoaded skips proxy for already-loaded DLL" {
|
||||
if (builtin.os.tag != .windows or builtin.cpu.arch != .x86_64) return;
|
||||
|
||||
resetTestProxy();
|
||||
// ntdll.dll is always loaded — should return true immediately, no proxy call
|
||||
const result = ensureDllLoaded("ntdll.dll", .none, &testProxyFn);
|
||||
try std.testing.expect(result);
|
||||
try std.testing.expect(!test_proxy_called);
|
||||
}
|
||||
|
||||
test "proxy callback is invoked by comActivate" {
|
||||
if (builtin.os.tag != .windows or builtin.cpu.arch != .x86_64) return;
|
||||
if (peb.getModuleBase(comptime peb.rotrHash("combase.dll")) == null) return;
|
||||
|
||||
resetTestProxy();
|
||||
// Try to sideload a DLL that probably isn't loaded. The mock proxy won't
|
||||
// actually call CoCreateInstance, so the DLL won't appear in PEB, but
|
||||
// we verify the proxy was invoked with the right arg count.
|
||||
const already_loaded = peb.getModuleBase(comptime peb.rotrHash("Chakra.dll")) != null;
|
||||
if (already_loaded) return; // can't test proxy path if already loaded
|
||||
|
||||
_ = ensureDllLoaded("Chakra.dll", .com_chakra, &testProxyFn);
|
||||
try std.testing.expect(test_proxy_called);
|
||||
try std.testing.expectEqual(@as(usize, 5), test_proxy_arg_count); // CoCreateInstance = 5 args
|
||||
try std.testing.expect(test_proxy_func_addr != 0); // should be address of CoCreateInstance
|
||||
}
|
||||
|
||||
test "proxy callback receives 2 args for WSAStartup" {
|
||||
if (builtin.os.tag != .windows or builtin.cpu.arch != .x86_64) return;
|
||||
|
||||
const already_loaded = peb.getModuleBase(comptime peb.rotrHash("mswsock.dll")) != null;
|
||||
if (already_loaded) return;
|
||||
if (peb.getModuleBase(comptime peb.rotrHash("ws2_32.dll")) == null) return;
|
||||
|
||||
resetTestProxy();
|
||||
_ = ensureDllLoaded("mswsock.dll", .winsock_startup, &testProxyFn);
|
||||
try std.testing.expect(test_proxy_called);
|
||||
try std.testing.expectEqual(@as(usize, 2), test_proxy_arg_count);
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
const std = @import("std");
|
||||
const types = @import("types.zig");
|
||||
const stable_mod = @import("gadget_stable.zig");
|
||||
const config_mod = @import("config.zig");
|
||||
const pool_proxy = @import("pool_proxy.zig");
|
||||
|
||||
const GadgetInfo = types.GadgetInfo;
|
||||
const GadgetRegister = types.GadgetRegister;
|
||||
const GadgetStable = stable_mod.GadgetStable;
|
||||
const PoolProxyConfig = config_mod.PoolProxyConfig;
|
||||
|
||||
pub fn serializeStable(stable: *const GadgetStable, allocator: std.mem.Allocator) ![]u8 {
|
||||
var list = std.ArrayList(u8){};
|
||||
errdefer list.deinit(allocator);
|
||||
const writer = list.writer(allocator);
|
||||
|
||||
try writer.writeAll("{");
|
||||
try writer.print("\"gadget_count\":{d},\"max_supported_args\":{d},\"loaded_dll_count\":{d},\"gadgets\":[", .{
|
||||
stable.count,
|
||||
stable.maxSupportedArgs(),
|
||||
stable.loaded_dll_count,
|
||||
});
|
||||
|
||||
var first = true;
|
||||
for (stable.gadgets[0..stable.count]) |entry| {
|
||||
const gadget = entry orelse continue;
|
||||
if (!first) try writer.writeAll(",");
|
||||
first = false;
|
||||
try writeGadgetJson(writer, &gadget);
|
||||
}
|
||||
|
||||
try writer.writeAll("],\"config\":");
|
||||
try writeConfigJson(writer, &pool_proxy.config);
|
||||
try writer.writeAll("}");
|
||||
return list.toOwnedSlice(allocator);
|
||||
}
|
||||
|
||||
pub fn serializeConfig(cfg: *const PoolProxyConfig, allocator: std.mem.Allocator) ![]u8 {
|
||||
var list = std.ArrayList(u8){};
|
||||
errdefer list.deinit(allocator);
|
||||
try writeConfigJson(list.writer(allocator), cfg);
|
||||
return list.toOwnedSlice(allocator);
|
||||
}
|
||||
|
||||
pub fn serializeGadget(gadget: *const GadgetInfo, allocator: std.mem.Allocator) ![]u8 {
|
||||
var list = std.ArrayList(u8){};
|
||||
errdefer list.deinit(allocator);
|
||||
try writeGadgetJson(list.writer(allocator), gadget);
|
||||
return list.toOwnedSlice(allocator);
|
||||
}
|
||||
|
||||
fn writeGadgetJson(writer: anytype, gadget: *const GadgetInfo) !void {
|
||||
try writer.writeAll("{");
|
||||
try writer.print("\"register\":\"{s}\",", .{gadget.register.name()});
|
||||
try writeJsonField(writer, "dll", gadget.dllNameSlice());
|
||||
try writer.writeAll(",");
|
||||
try writeJsonField(writer, "func", gadget.funcNameSlice());
|
||||
try writer.print(",\"gadget_rva\":\"0x{x}\",\"gadget_offset\":\"0x{x}\",\"cleanup_size\":\"0x{x}\",\"cleanup_type\":\"{s}\",\"max_args\":{d},\"pops_before\":{d},\"pops_after\":{d},\"pop_map\":\"0x{x}\"", .{
|
||||
gadget.rva(),
|
||||
gadget.gadgetOffset(),
|
||||
gadget.cleanup_size,
|
||||
@tagName(gadget.cleanup_type),
|
||||
gadget.maxArgs(),
|
||||
gadget.pops_before,
|
||||
gadget.pops_after,
|
||||
gadget.pop_map,
|
||||
});
|
||||
try writer.writeAll("}");
|
||||
}
|
||||
|
||||
fn writeConfigJson(writer: anytype, cfg: *const PoolProxyConfig) !void {
|
||||
try writer.print("{{\"fallback_strategy\":\"{s}\",\"auto_load_enabled\":{s},\"random_selection\":{s},\"log_fallbacks\":{s},\"fallback_count\":{d},\"preferred_register\":\"{s}\",\"min_auto_load_cleanup\":\"0x{x}\"}}", .{
|
||||
@tagName(cfg.fallback_strategy),
|
||||
boolJson(cfg.auto_load_enabled != 0),
|
||||
boolJson(cfg.random_selection != 0),
|
||||
boolJson(cfg.log_fallbacks != 0),
|
||||
cfg.fallback_count,
|
||||
preferredRegisterName(cfg.preferred_register),
|
||||
cfg.min_auto_load_cleanup,
|
||||
});
|
||||
}
|
||||
|
||||
fn preferredRegisterName(value: u8) []const u8 {
|
||||
return switch (value) {
|
||||
0 => GadgetRegister.rbx.name(),
|
||||
1 => GadgetRegister.rdi.name(),
|
||||
2 => GadgetRegister.rsi.name(),
|
||||
3 => GadgetRegister.rbp.name(),
|
||||
4 => GadgetRegister.r12.name(),
|
||||
5 => GadgetRegister.r13.name(),
|
||||
6 => GadgetRegister.r14.name(),
|
||||
7 => GadgetRegister.r15.name(),
|
||||
else => "any",
|
||||
};
|
||||
}
|
||||
|
||||
fn writeJsonField(writer: anytype, key: []const u8, value: []const u8) !void {
|
||||
try writer.print("\"{s}\":", .{key});
|
||||
try writeJsonString(writer, value);
|
||||
}
|
||||
|
||||
fn writeJsonString(writer: anytype, value: []const u8) !void {
|
||||
try writer.writeByte('"');
|
||||
for (value) |c| {
|
||||
switch (c) {
|
||||
'\\', '"' => {
|
||||
try writer.writeByte('\\');
|
||||
try writer.writeByte(c);
|
||||
},
|
||||
'\n' => try writer.writeAll("\\n"),
|
||||
'\r' => try writer.writeAll("\\r"),
|
||||
'\t' => try writer.writeAll("\\t"),
|
||||
else => try writer.writeByte(c),
|
||||
}
|
||||
}
|
||||
try writer.writeByte('"');
|
||||
}
|
||||
|
||||
fn boolJson(value: bool) []const u8 {
|
||||
return if (value) "true" else "false";
|
||||
}
|
||||
|
||||
test "serializeConfig emits strategy name" {
|
||||
const json = try serializeConfig(&PoolProxyConfig{}, std.testing.allocator);
|
||||
defer std.testing.allocator.free(json);
|
||||
try std.testing.expect(std.mem.indexOf(u8, json, "tp_delegation") != null);
|
||||
}
|
||||
|
||||
test "serializeGadget emits pop_map" {
|
||||
const gadget = GadgetInfo{
|
||||
.addr = 0x1000,
|
||||
.cleanup_size = 0x40,
|
||||
.cleanup_type = .imm8,
|
||||
.register = .rbx,
|
||||
.dll_base = 0x1000,
|
||||
.dll_name = [_]u8{0} ** 48,
|
||||
.func_name = [_]u8{0} ** 64,
|
||||
.func_rva = 0,
|
||||
.pops_before = 1,
|
||||
.pops_after = 1,
|
||||
.extra_pops = [_]types.GadgetRegister{.rbx} ** 8,
|
||||
.pop_map = 0x106,
|
||||
};
|
||||
const json = try serializeGadget(&gadget, std.testing.allocator);
|
||||
defer std.testing.allocator.free(json);
|
||||
try std.testing.expect(std.mem.indexOf(u8, json, "pop_map") != null);
|
||||
}
|
||||
@@ -0,0 +1,543 @@
|
||||
// Curated gadget database for the current Windows 11 test build.
|
||||
//
|
||||
// The runtime verifies each pinned byte sequence before use. If a loaded DLL no
|
||||
// longer matches the stored RVA, `pool_proxy` falls back to a targeted micro-scan
|
||||
// of that DLL instead of a full process-wide search.
|
||||
|
||||
const std = @import("std");
|
||||
const peb = @import("platform/peb.zig");
|
||||
const types = @import("types.zig");
|
||||
|
||||
const GadgetRegister = types.GadgetRegister;
|
||||
const CleanupType = types.CleanupType;
|
||||
const SideloadMethod = types.SideloadMethod;
|
||||
const GadgetInfo = types.GadgetInfo;
|
||||
|
||||
pub const PrecomputedGadget = struct {
|
||||
/// Module name used for verification and optional side-effect loading.
|
||||
dll_name: []const u8,
|
||||
/// ROTR hash of `dll_name` for fast PEB lookups.
|
||||
dll_hash: u32,
|
||||
/// Relative virtual address of the gadget.
|
||||
rva: u32,
|
||||
/// Register written by `mov [reg], rax` and later restored by the pop chain.
|
||||
register: GadgetRegister,
|
||||
/// Immediate value used by `add rsp, N`.
|
||||
cleanup_size: u16,
|
||||
/// Encoding used for the cleanup immediate.
|
||||
cleanup_type: CleanupType,
|
||||
/// Pop count before the target register is restored.
|
||||
pops_before: u8,
|
||||
/// Pop count after the target register is restored.
|
||||
pops_after: u8,
|
||||
/// Cached `maxArgs()` value for sorting and auto-load selection.
|
||||
max_args: u8,
|
||||
/// Export name associated with the gadget RVA.
|
||||
func_name: []const u8,
|
||||
/// Bytes verified at `base + rva` before the entry is accepted.
|
||||
verify_bytes: []const u8,
|
||||
/// Side-effect path that can make the DLL available if it is not already loaded.
|
||||
sideload: SideloadMethod,
|
||||
/// Packed pop-slot map consumed directly by the runtime trampoline.
|
||||
pop_map: u64 = 0,
|
||||
};
|
||||
|
||||
pub const GADGET_DB = buildDb();
|
||||
|
||||
fn buildDb() [20]PrecomputedGadget {
|
||||
var db = [_]PrecomputedGadget{
|
||||
// ── 15 args ── Chakra.dll rbx ─────────────────────────────────────
|
||||
.{
|
||||
.dll_name = "Chakra.dll",
|
||||
.dll_hash = peb.rotrHash("Chakra.dll"),
|
||||
.rva = 0x28e845,
|
||||
.register = .rbx,
|
||||
.cleanup_size = 0x78,
|
||||
.cleanup_type = .imm8,
|
||||
.pops_before = 4,
|
||||
.pops_after = 1,
|
||||
.max_args = 15,
|
||||
.func_name = "MemProtectHeapUnrootAndZero",
|
||||
.verify_bytes = &.{ 0x48, 0x89, 0x03, 0x48, 0x83, 0xc4, 0x78, 0x41, 0x5f, 0x41, 0x5e, 0x5f, 0x5e, 0x5b, 0x5d, 0xc3 },
|
||||
.sideload = .com_chakra,
|
||||
},
|
||||
// ── 15 args ── mshtml.dll rdi ─────────────────────────────────────
|
||||
.{
|
||||
.dll_name = "mshtml.dll",
|
||||
.dll_hash = peb.rotrHash("mshtml.dll"),
|
||||
.rva = 0xd5dc93,
|
||||
.register = .rdi,
|
||||
.cleanup_size = 0x78,
|
||||
.cleanup_type = .imm8,
|
||||
.pops_before = 4,
|
||||
.pops_after = 3,
|
||||
.max_args = 15,
|
||||
.func_name = "RunHTMLApplication",
|
||||
.verify_bytes = &.{ 0x48, 0x89, 0x07, 0x48, 0x83, 0xc4, 0x78, 0x41, 0x5f, 0x41, 0x5e, 0x41, 0x5d, 0x41, 0x5c, 0x5f, 0x5e, 0x5b, 0x5d, 0xc3 },
|
||||
.sideload = .com_mshtml,
|
||||
},
|
||||
// ── 14 args ── Chakra.dll rdi ─────────────────────────────────────
|
||||
.{
|
||||
.dll_name = "Chakra.dll",
|
||||
.dll_hash = peb.rotrHash("Chakra.dll"),
|
||||
.rva = 0x17acef,
|
||||
.register = .rdi,
|
||||
.cleanup_size = 0x70,
|
||||
.cleanup_type = .imm8,
|
||||
.pops_before = 4,
|
||||
.pops_after = 2,
|
||||
.max_args = 14,
|
||||
.func_name = "MemProtectHeapRootRealloc",
|
||||
.verify_bytes = &.{ 0x48, 0x89, 0x07, 0x48, 0x83, 0xc4, 0x70, 0x41, 0x5f, 0x41, 0x5e, 0x41, 0x5d, 0x41, 0x5c, 0x5f, 0x5e, 0x5b, 0xc3 },
|
||||
.sideload = .com_chakra,
|
||||
},
|
||||
// ── 13 args ── Chakra.dll rdi ─────────────────────────────────────
|
||||
.{
|
||||
.dll_name = "Chakra.dll",
|
||||
.dll_hash = peb.rotrHash("Chakra.dll"),
|
||||
.rva = 0x1299e2,
|
||||
.register = .rdi,
|
||||
.cleanup_size = 0x68,
|
||||
.cleanup_type = .imm8,
|
||||
.pops_before = 0,
|
||||
.pops_after = 3,
|
||||
.max_args = 13,
|
||||
.func_name = "MemProtectHeapRootRealloc",
|
||||
.verify_bytes = &.{ 0x48, 0x89, 0x07, 0x48, 0x83, 0xc4, 0x68, 0x5f, 0x5e, 0x5d, 0x5b, 0xc3 },
|
||||
.sideload = .com_chakra,
|
||||
},
|
||||
// ── 11 args ── mshtml.dll rdi ─────────────────────────────────────
|
||||
.{
|
||||
.dll_name = "mshtml.dll",
|
||||
.dll_hash = peb.rotrHash("mshtml.dll"),
|
||||
.rva = 0x7a6e18,
|
||||
.register = .rdi,
|
||||
.cleanup_size = 0x58,
|
||||
.cleanup_type = .imm8,
|
||||
.pops_before = 0,
|
||||
.pops_after = 3,
|
||||
.max_args = 11,
|
||||
.func_name = "mshtml_ordinal_107",
|
||||
.verify_bytes = &.{ 0x48, 0x89, 0x07, 0x48, 0x83, 0xc4, 0x58, 0x5f, 0x5e, 0x5d, 0x5b, 0xc3 },
|
||||
.sideload = .com_mshtml,
|
||||
},
|
||||
// ── 10 args ── cdp.dll rbx (zero extra pops) ──────────────────────
|
||||
.{
|
||||
.dll_name = "cdp.dll",
|
||||
.dll_hash = peb.rotrHash("cdp.dll"),
|
||||
.rva = 0xd27b0,
|
||||
.register = .rbx,
|
||||
.cleanup_size = 0x50,
|
||||
.cleanup_type = .imm8,
|
||||
.pops_before = 0,
|
||||
.pops_after = 0,
|
||||
.max_args = 10,
|
||||
.func_name = "CDPGetAccountProviderInternal",
|
||||
.verify_bytes = &.{ 0x48, 0x89, 0x03, 0x48, 0x83, 0xc4, 0x50, 0x5b, 0xc3 },
|
||||
.sideload = .com_cdp,
|
||||
},
|
||||
// ── 10 args ── setupapi.dll rdi (zero extra pops) ─────────────────
|
||||
.{
|
||||
.dll_name = "setupapi.dll",
|
||||
.dll_hash = peb.rotrHash("setupapi.dll"),
|
||||
.rva = 0xd583e,
|
||||
.register = .rdi,
|
||||
.cleanup_size = 0x50,
|
||||
.cleanup_type = .imm8,
|
||||
.pops_before = 0,
|
||||
.pops_after = 0,
|
||||
.max_args = 10,
|
||||
.func_name = "pSetupStringTableStringFromIdEx",
|
||||
.verify_bytes = &.{ 0x48, 0x89, 0x07, 0x48, 0x83, 0xc4, 0x50, 0x5f, 0xc3 },
|
||||
.sideload = .cm_device_list,
|
||||
},
|
||||
// ── 9 args ── jscript9.dll rbx ────────────────────────────────────
|
||||
.{
|
||||
.dll_name = "jscript9.dll",
|
||||
.dll_hash = peb.rotrHash("jscript9.dll"),
|
||||
.rva = 0x15e92c,
|
||||
.register = .rbx,
|
||||
.cleanup_size = 0x48,
|
||||
.cleanup_type = .imm8,
|
||||
.pops_before = 2,
|
||||
.pops_after = 1,
|
||||
.max_args = 9,
|
||||
.func_name = "JsVarAddRef",
|
||||
.verify_bytes = &.{ 0x48, 0x89, 0x03, 0x48, 0x83, 0xc4, 0x48, 0x5f, 0x5e, 0x5b, 0x5d, 0xc3 },
|
||||
.sideload = .com_jscript9,
|
||||
},
|
||||
// ── 8 args ── ntdll.dll rbx (ALWAYS loaded, zero extra pops) ──────
|
||||
.{
|
||||
.dll_name = "ntdll.dll",
|
||||
.dll_hash = peb.rotrHash("ntdll.dll"),
|
||||
.rva = 0x6ef73,
|
||||
.register = .rbx,
|
||||
.cleanup_size = 0x40,
|
||||
.cleanup_type = .imm8,
|
||||
.pops_before = 0,
|
||||
.pops_after = 0,
|
||||
.max_args = 8,
|
||||
.func_name = "RtlPcToFileHeader",
|
||||
.verify_bytes = &.{ 0x48, 0x89, 0x03, 0x48, 0x83, 0xc4, 0x40, 0x5b, 0xc3 },
|
||||
.sideload = .none,
|
||||
},
|
||||
// ── 8 args ── crypt32.dll r14 (passive — no side-effect path to crypt32) ──
|
||||
.{
|
||||
.dll_name = "crypt32.dll",
|
||||
.dll_hash = peb.rotrHash("crypt32.dll"),
|
||||
.rva = 0xd36a8,
|
||||
.register = .r14,
|
||||
.cleanup_size = 0x40,
|
||||
.cleanup_type = .imm8,
|
||||
.pops_before = 1,
|
||||
.pops_after = 1,
|
||||
.max_args = 8,
|
||||
.func_name = "CryptVerifyMessageSignatureWithKey",
|
||||
.verify_bytes = &.{ 0x49, 0x89, 0x06, 0x48, 0x83, 0xc4, 0x40, 0x41, 0x5f, 0x41, 0x5e, 0x5f, 0xc3 },
|
||||
.sideload = .none,
|
||||
},
|
||||
// ── 8 args ── mshtml.dll rdi (zero extra pops) ────────────────────
|
||||
.{
|
||||
.dll_name = "mshtml.dll",
|
||||
.dll_hash = peb.rotrHash("mshtml.dll"),
|
||||
.rva = 0xa8d897,
|
||||
.register = .rdi,
|
||||
.cleanup_size = 0x40,
|
||||
.cleanup_type = .imm8,
|
||||
.pops_before = 0,
|
||||
.pops_after = 0,
|
||||
.max_args = 8,
|
||||
.func_name = "mshtml_ordinal_105",
|
||||
.verify_bytes = &.{ 0x48, 0x89, 0x07, 0x48, 0x83, 0xc4, 0x40, 0x5f, 0xc3 },
|
||||
.sideload = .com_mshtml,
|
||||
},
|
||||
// ── 7 args ── jscript9.dll rdi ────────────────────────────────────
|
||||
.{
|
||||
.dll_name = "jscript9.dll",
|
||||
.dll_hash = peb.rotrHash("jscript9.dll"),
|
||||
.rva = 0x19c113,
|
||||
.register = .rdi,
|
||||
.cleanup_size = 0x38,
|
||||
.cleanup_type = .imm8,
|
||||
.pops_before = 0,
|
||||
.pops_after = 1,
|
||||
.max_args = 7,
|
||||
.func_name = "DllGetClassObject",
|
||||
.verify_bytes = &.{ 0x48, 0x89, 0x07, 0x48, 0x83, 0xc4, 0x38, 0x5f, 0x5b, 0xc3 },
|
||||
.sideload = .com_jscript9,
|
||||
},
|
||||
// ── 6 args ── ole32.dll rbx ───────────────────────────────────────
|
||||
.{
|
||||
.dll_name = "ole32.dll",
|
||||
.dll_hash = peb.rotrHash("ole32.dll"),
|
||||
.rva = 0x84497,
|
||||
.register = .rbx,
|
||||
.cleanup_size = 0x30,
|
||||
.cleanup_type = .imm8,
|
||||
.pops_before = 0,
|
||||
.pops_after = 0,
|
||||
.max_args = 6,
|
||||
.func_name = "CreateDataCache",
|
||||
.verify_bytes = &.{ 0x48, 0x89, 0x03, 0x48, 0x83, 0xc4, 0x30, 0x5b, 0xc3 },
|
||||
.sideload = .none,
|
||||
},
|
||||
// ── 6 args ── rpcrt4.dll rdi ──────────────────────────────────────
|
||||
.{
|
||||
.dll_name = "rpcrt4.dll",
|
||||
.dll_hash = peb.rotrHash("rpcrt4.dll"),
|
||||
.rva = 0x12786,
|
||||
.register = .rdi,
|
||||
.cleanup_size = 0x30,
|
||||
.cleanup_type = .imm8,
|
||||
.pops_before = 1,
|
||||
.pops_after = 1,
|
||||
.max_args = 6,
|
||||
.func_name = "NdrMesTypeDecode",
|
||||
.verify_bytes = &.{ 0x48, 0x89, 0x07, 0x48, 0x83, 0xc4, 0x30, 0x41, 0x5e, 0x5f, 0x5b, 0xc3 },
|
||||
.sideload = .none,
|
||||
},
|
||||
// ── 6 args ── rpcrt4.dll rbx ──────────────────────────────────────
|
||||
.{
|
||||
.dll_name = "rpcrt4.dll",
|
||||
.dll_hash = peb.rotrHash("rpcrt4.dll"),
|
||||
.rva = 0x92713,
|
||||
.register = .rbx,
|
||||
.cleanup_size = 0x30,
|
||||
.cleanup_type = .imm8,
|
||||
.pops_before = 0,
|
||||
.pops_after = 0,
|
||||
.max_args = 6,
|
||||
.func_name = "NdrMesTypeAlignSize3",
|
||||
.verify_bytes = &.{ 0x48, 0x89, 0x03, 0x48, 0x83, 0xc4, 0x30, 0x5b, 0xc3 },
|
||||
.sideload = .none,
|
||||
},
|
||||
// ── 6 args ── crypt32.dll rdi (passive — no side-effect path to crypt32) ──
|
||||
.{
|
||||
.dll_name = "crypt32.dll",
|
||||
.dll_hash = peb.rotrHash("crypt32.dll"),
|
||||
.rva = 0xd831f,
|
||||
.register = .rdi,
|
||||
.cleanup_size = 0x30,
|
||||
.cleanup_type = .imm8,
|
||||
.pops_before = 1,
|
||||
.pops_after = 1,
|
||||
.max_args = 6,
|
||||
.func_name = "CryptVerifyMessageSignatureWithKey",
|
||||
.verify_bytes = &.{ 0x48, 0x89, 0x07, 0x48, 0x83, 0xc4, 0x30, 0x41, 0x5e, 0x5f, 0x5d, 0xc3 },
|
||||
.sideload = .none,
|
||||
},
|
||||
// ── 4 args ── kernelbase.dll rbx ──────────────────────────────────
|
||||
.{
|
||||
.dll_name = "kernelbase.dll",
|
||||
.dll_hash = peb.rotrHash("kernelbase.dll"),
|
||||
.rva = 0xe0c47,
|
||||
.register = .rbx,
|
||||
.cleanup_size = 0x20,
|
||||
.cleanup_type = .imm8,
|
||||
.pops_before = 0,
|
||||
.pops_after = 0,
|
||||
.max_args = 4,
|
||||
.func_name = "PrivilegeCheck",
|
||||
.verify_bytes = &.{ 0x48, 0x89, 0x03, 0x48, 0x83, 0xc4, 0x20, 0x5b, 0xc3 },
|
||||
.sideload = .none,
|
||||
},
|
||||
// ── 4 args ── combase.dll rbx ─────────────────────────────────────
|
||||
.{
|
||||
.dll_name = "combase.dll",
|
||||
.dll_hash = peb.rotrHash("combase.dll"),
|
||||
.rva = 0x7d271,
|
||||
.register = .rbx,
|
||||
.cleanup_size = 0x20,
|
||||
.cleanup_type = .imm8,
|
||||
.pops_before = 0,
|
||||
.pops_after = 0,
|
||||
.max_args = 4,
|
||||
.func_name = "CoResumeClassObjects",
|
||||
.verify_bytes = &.{ 0x48, 0x89, 0x03, 0x48, 0x83, 0xc4, 0x20, 0x5b, 0xc3 },
|
||||
.sideload = .none,
|
||||
},
|
||||
// ── 4 args ── kernelbase.dll rdi ──────────────────────────────────
|
||||
.{
|
||||
.dll_name = "kernelbase.dll",
|
||||
.dll_hash = peb.rotrHash("kernelbase.dll"),
|
||||
.rva = 0xb9439, // may shift between builds — micro-scan will find it
|
||||
.register = .rdi,
|
||||
.cleanup_size = 0x20,
|
||||
.cleanup_type = .imm8,
|
||||
.pops_before = 0,
|
||||
.pops_after = 0,
|
||||
.max_args = 4,
|
||||
.func_name = "QueryUnbiasedInterruptTimePrecise",
|
||||
.verify_bytes = &.{ 0x48, 0x89, 0x07, 0x48, 0x83, 0xc4, 0x20, 0x5f, 0xc3 },
|
||||
.sideload = .none,
|
||||
},
|
||||
// ── 4 args ── ole32.dll rdi ───────────────────────────────────────
|
||||
.{
|
||||
.dll_name = "ole32.dll",
|
||||
.dll_hash = peb.rotrHash("ole32.dll"),
|
||||
.rva = 0x88ff6, // may shift between builds — micro-scan will find it
|
||||
.register = .rdi,
|
||||
.cleanup_size = 0x20,
|
||||
.cleanup_type = .imm8,
|
||||
.pops_before = 0,
|
||||
.pops_after = 0,
|
||||
.max_args = 4,
|
||||
.func_name = "CreateOleAdviseHolder",
|
||||
.verify_bytes = &.{ 0x48, 0x89, 0x07, 0x48, 0x83, 0xc4, 0x20, 0x5f, 0xc3 },
|
||||
.sideload = .none,
|
||||
},
|
||||
};
|
||||
|
||||
inline for (&db) |*entry| {
|
||||
entry.pop_map = decodePopMap(entry.register, entry.verify_bytes);
|
||||
}
|
||||
|
||||
return db;
|
||||
}
|
||||
|
||||
fn scratchIndex(reg: GadgetRegister) u4 {
|
||||
return switch (reg) {
|
||||
.rbx => 0,
|
||||
.rdi => 1,
|
||||
.rsi => 2,
|
||||
.rbp => 3,
|
||||
.r12 => 4,
|
||||
.r13 => 5,
|
||||
.r14 => 6,
|
||||
.r15 => 7,
|
||||
};
|
||||
}
|
||||
|
||||
fn movPatternLen(reg: GadgetRegister) usize {
|
||||
return switch (reg) {
|
||||
.rbx, .rdi, .rsi, .r14, .r15 => 3,
|
||||
.rbp, .r12, .r13 => 4,
|
||||
};
|
||||
}
|
||||
|
||||
fn addRspLen(bytes: []const u8, offset: usize) ?usize {
|
||||
if (offset + 4 <= bytes.len and bytes[offset] == 0x48 and bytes[offset + 1] == 0x83 and bytes[offset + 2] == 0xc4) {
|
||||
return 4;
|
||||
}
|
||||
if (offset + 7 <= bytes.len and bytes[offset] == 0x48 and bytes[offset + 1] == 0x81 and bytes[offset + 2] == 0xc4) {
|
||||
return 7;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const PopInfo = struct {
|
||||
reg: ?GadgetRegister,
|
||||
len: usize,
|
||||
};
|
||||
|
||||
fn parsePop(bytes: []const u8, offset: usize) ?PopInfo {
|
||||
if (offset >= bytes.len) return null;
|
||||
return switch (bytes[offset]) {
|
||||
0x58, 0x59, 0x5a => .{ .reg = null, .len = 1 },
|
||||
0x5b => .{ .reg = .rbx, .len = 1 },
|
||||
0x5d => .{ .reg = .rbp, .len = 1 },
|
||||
0x5e => .{ .reg = .rsi, .len = 1 },
|
||||
0x5f => .{ .reg = .rdi, .len = 1 },
|
||||
0x41 => blk: {
|
||||
if (offset + 1 >= bytes.len) break :blk null;
|
||||
break :blk switch (bytes[offset + 1]) {
|
||||
0x5c => .{ .reg = .r12, .len = 2 },
|
||||
0x5d => .{ .reg = .r13, .len = 2 },
|
||||
0x5e => .{ .reg = .r14, .len = 2 },
|
||||
0x5f => .{ .reg = .r15, .len = 2 },
|
||||
else => null,
|
||||
};
|
||||
},
|
||||
else => null,
|
||||
};
|
||||
}
|
||||
|
||||
/// Decode the pop sequence embedded in a verified gadget byte pattern into the
|
||||
/// packed nibble map consumed by the trampoline at runtime.
|
||||
fn decodePopMap(target_reg: GadgetRegister, bytes: []const u8) u64 {
|
||||
const after_mov = movPatternLen(target_reg);
|
||||
const add_len = addRspLen(bytes, after_mov) orelse unreachable;
|
||||
var cursor = after_mov + add_len;
|
||||
var found_target = false;
|
||||
var map: u64 = 0;
|
||||
var shift: u6 = 0;
|
||||
|
||||
while (cursor < bytes.len) {
|
||||
if (bytes[cursor] == 0xc3) break;
|
||||
const pop = parsePop(bytes, cursor) orelse unreachable;
|
||||
if (pop.reg) |pop_reg| {
|
||||
map |= @as(u64, scratchIndex(pop_reg)) << shift;
|
||||
shift += 4;
|
||||
if (pop_reg == target_reg and !found_target) found_target = true;
|
||||
}
|
||||
cursor += pop.len;
|
||||
}
|
||||
|
||||
if (!found_target) unreachable;
|
||||
return map;
|
||||
}
|
||||
|
||||
fn decodeExtraPopsFromVerifyBytes(target_reg: GadgetRegister, bytes: []const u8) [8]GadgetRegister {
|
||||
var extra_pops = [_]GadgetRegister{.rbx} ** 8;
|
||||
const after_mov = movPatternLen(target_reg);
|
||||
const add_len = addRspLen(bytes, after_mov) orelse unreachable;
|
||||
var cursor = after_mov + add_len;
|
||||
var found_target = false;
|
||||
var stored: usize = 0;
|
||||
|
||||
while (cursor < bytes.len and stored < extra_pops.len) {
|
||||
if (bytes[cursor] == 0xc3) break;
|
||||
const pop = parsePop(bytes, cursor) orelse unreachable;
|
||||
if (pop.reg) |pop_reg| {
|
||||
if (pop_reg == target_reg and !found_target) {
|
||||
found_target = true;
|
||||
} else {
|
||||
extra_pops[stored] = pop_reg;
|
||||
stored += 1;
|
||||
}
|
||||
}
|
||||
cursor += pop.len;
|
||||
}
|
||||
|
||||
if (!found_target) unreachable;
|
||||
return extra_pops;
|
||||
}
|
||||
|
||||
/// Verify a pre-computed gadget: check that the bytes at base+rva match the expected pattern.
|
||||
pub fn verifyGadget(entry: *const PrecomputedGadget, dll_base: [*]const u8) bool {
|
||||
const gadget_ptr = dll_base + entry.rva;
|
||||
return std.mem.eql(u8, gadget_ptr[0..entry.verify_bytes.len], entry.verify_bytes);
|
||||
}
|
||||
|
||||
/// Convert a verified PrecomputedGadget to a runtime GadgetInfo.
|
||||
pub fn toGadgetInfo(entry: *const PrecomputedGadget, dll_base: [*]const u8) GadgetInfo {
|
||||
var dll_name_buf = [_]u8{0} ** 48;
|
||||
var func_name_buf = [_]u8{0} ** 64;
|
||||
const dll_len = @min(entry.dll_name.len, dll_name_buf.len - 1);
|
||||
const func_len = @min(entry.func_name.len, func_name_buf.len - 1);
|
||||
@memcpy(dll_name_buf[0..dll_len], entry.dll_name[0..dll_len]);
|
||||
@memcpy(func_name_buf[0..func_len], entry.func_name[0..func_len]);
|
||||
|
||||
return GadgetInfo{
|
||||
.addr = @intFromPtr(dll_base) + entry.rva,
|
||||
.cleanup_size = entry.cleanup_size,
|
||||
.cleanup_type = entry.cleanup_type,
|
||||
.register = entry.register,
|
||||
.dll_base = @intFromPtr(dll_base),
|
||||
.dll_name = dll_name_buf,
|
||||
.func_name = func_name_buf,
|
||||
.func_rva = entry.rva,
|
||||
.pops_before = entry.pops_before,
|
||||
.pops_after = entry.pops_after,
|
||||
.extra_pops = decodeExtraPopsFromVerifyBytes(entry.register, entry.verify_bytes),
|
||||
.pop_map = entry.pop_map,
|
||||
};
|
||||
}
|
||||
|
||||
// ── Tests ────────────────────────────────────────────────────────────────
|
||||
|
||||
test "GADGET_DB is sorted by max_args descending" {
|
||||
var prev_args: u8 = 255;
|
||||
for (GADGET_DB) |entry| {
|
||||
try std.testing.expect(entry.max_args <= prev_args);
|
||||
prev_args = entry.max_args;
|
||||
}
|
||||
}
|
||||
|
||||
test "all DB entries have supported registers" {
|
||||
const trampolines = @import("trampolines.zig");
|
||||
for (GADGET_DB) |entry| {
|
||||
try std.testing.expect(trampolines.TrampolineId.forRegister(entry.register) != null);
|
||||
}
|
||||
}
|
||||
|
||||
test "verify_bytes length is reasonable" {
|
||||
for (GADGET_DB) |entry| {
|
||||
try std.testing.expect(entry.verify_bytes.len >= 7);
|
||||
try std.testing.expect(entry.verify_bytes.len <= 24);
|
||||
// Must end with 0xc3 (ret)
|
||||
try std.testing.expectEqual(@as(u8, 0xc3), entry.verify_bytes[entry.verify_bytes.len - 1]);
|
||||
}
|
||||
}
|
||||
|
||||
test "known pop maps are decoded correctly" {
|
||||
try std.testing.expectEqual(@as(u64, 0x302167), decodePopMap(.rbx, &.{ 0x48, 0x89, 0x03, 0x48, 0x83, 0xc4, 0x78, 0x41, 0x5f, 0x41, 0x5e, 0x5f, 0x5e, 0x5b, 0x5d, 0xc3 }));
|
||||
try std.testing.expectEqual(@as(u64, 0x0214567), decodePopMap(.rdi, &.{ 0x48, 0x89, 0x07, 0x48, 0x83, 0xc4, 0x70, 0x41, 0x5f, 0x41, 0x5e, 0x41, 0x5d, 0x41, 0x5c, 0x5f, 0x5e, 0x5b, 0xc3 }));
|
||||
try std.testing.expectEqual(@as(u64, 0x0321), decodePopMap(.rdi, &.{ 0x48, 0x89, 0x07, 0x48, 0x83, 0xc4, 0x68, 0x5f, 0x5e, 0x5d, 0x5b, 0xc3 }));
|
||||
try std.testing.expectEqual(@as(u64, 0x167), decodePopMap(.r14, &.{ 0x49, 0x89, 0x06, 0x48, 0x83, 0xc4, 0x40, 0x41, 0x5f, 0x41, 0x5e, 0x5f, 0xc3 }));
|
||||
try std.testing.expectEqual(@as(u64, 0x16), decodePopMap(.rdi, &.{ 0x48, 0x89, 0x07, 0x48, 0x83, 0xc4, 0x30, 0x41, 0x5e, 0x5f, 0x5b, 0xc3 }));
|
||||
}
|
||||
|
||||
test "sideload=none entries stay in expected modules" {
|
||||
for (GADGET_DB) |entry| {
|
||||
if (entry.sideload == .none) {
|
||||
const is_expected = std.mem.eql(u8, entry.dll_name, "ntdll.dll") or
|
||||
std.mem.eql(u8, entry.dll_name, "kernelbase.dll") or
|
||||
std.mem.eql(u8, entry.dll_name, "combase.dll") or
|
||||
std.mem.eql(u8, entry.dll_name, "ole32.dll") or
|
||||
std.mem.eql(u8, entry.dll_name, "rpcrt4.dll") or
|
||||
std.mem.eql(u8, entry.dll_name, "crypt32.dll");
|
||||
try std.testing.expect(is_expected);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,362 @@
|
||||
const std = @import("std");
|
||||
const builtin = @import("builtin");
|
||||
const nt = @import("platform/ntypes.zig");
|
||||
const peb = @import("platform/peb.zig");
|
||||
const types = @import("types.zig");
|
||||
|
||||
const CleanupType = types.CleanupType;
|
||||
const DllTarget = types.DllTarget;
|
||||
const GadgetInfo = types.GadgetInfo;
|
||||
const GadgetRegister = types.GadgetRegister;
|
||||
const MAX_GADGETS = types.MAX_GADGETS;
|
||||
|
||||
pub const MAX_RESULTS = MAX_GADGETS;
|
||||
|
||||
const zero_extra_pops = [_]GadgetRegister{.rbx} ** 8;
|
||||
|
||||
/// Scan one already-mapped DLL for gadgets that match the supported pattern set.
|
||||
pub fn scanDll(base: [*]const u8, dll_name: []const u8) [MAX_RESULTS]?GadgetInfo {
|
||||
var results = [_]?GadgetInfo{null} ** MAX_RESULTS;
|
||||
if (builtin.os.tag != .windows or builtin.cpu.arch != .x86_64) return results;
|
||||
|
||||
const dos: *align(1) const nt.IMAGE_DOS_HEADER = @ptrCast(base);
|
||||
if (dos.e_magic != nt.IMAGE_DOS_SIGNATURE or dos.e_lfanew < 0) return results;
|
||||
|
||||
const nt_offset: usize = @intCast(dos.e_lfanew);
|
||||
const nt_headers: *align(1) const nt.IMAGE_NT_HEADERS64 = @ptrCast(base + nt_offset);
|
||||
if (nt_headers.Signature != nt.IMAGE_NT_SIGNATURE) return results;
|
||||
|
||||
const size_of_image: usize = nt_headers.OptionalHeader.SizeOfImage;
|
||||
const section_ptr: [*]align(1) const nt.IMAGE_SECTION_HEADER = @ptrCast(base + nt_offset + @sizeOf(nt.IMAGE_NT_HEADERS64));
|
||||
var result_count: usize = 0;
|
||||
|
||||
var section_index: usize = 0;
|
||||
while (section_index < nt_headers.FileHeader.NumberOfSections and result_count < MAX_RESULTS) : (section_index += 1) {
|
||||
const section = section_ptr[section_index];
|
||||
if ((section.Characteristics & nt.IMAGE_SCN_MEM_EXECUTE) == 0) continue;
|
||||
|
||||
const virtual_size: usize = if (section.MiscVirtualSize != 0) section.MiscVirtualSize else section.SizeOfRawData;
|
||||
if (virtual_size == 0) continue;
|
||||
|
||||
const section_rva: usize = section.VirtualAddress;
|
||||
const section_end = std.math.add(usize, section_rva, virtual_size) catch continue;
|
||||
if (section_end > size_of_image) continue;
|
||||
|
||||
const section_bytes = base[section_rva..section_end];
|
||||
var offset: usize = 0;
|
||||
while (offset + 12 < section_bytes.len and result_count < MAX_RESULTS) : (offset += 1) {
|
||||
// Fast prefix filter: mov [REG], rax starts with 0x48 0x89 or 0x49 0x89
|
||||
const b0 = section_bytes[offset];
|
||||
if (b0 != 0x48 and b0 != 0x49) continue;
|
||||
if (section_bytes[offset + 1] != 0x89) continue;
|
||||
|
||||
for ([_]GadgetRegister{ .rbx, .rdi, .rsi, .rbp, .r12, .r13, .r14, .r15 }) |reg| {
|
||||
const maybe_gadget = scanRegisterAtOffset(base, dll_name, section.VirtualAddress, section_bytes, offset, reg) orelse continue;
|
||||
results[result_count] = maybe_gadget;
|
||||
result_count += 1;
|
||||
if (result_count >= MAX_RESULTS) break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sortByCleanupDesc(&results);
|
||||
return results;
|
||||
}
|
||||
|
||||
/// Scan a curated list of loaded DLLs and merge the results into one array.
|
||||
pub fn scanLoadedDlls(dll_targets: []const DllTarget) [MAX_GADGETS]?GadgetInfo {
|
||||
var results = [_]?GadgetInfo{null} ** MAX_GADGETS;
|
||||
if (builtin.os.tag != .windows or builtin.cpu.arch != .x86_64) return results;
|
||||
|
||||
var out_count: usize = 0;
|
||||
for (dll_targets) |target| {
|
||||
const base = peb.getModuleBase(target.hash) orelse continue;
|
||||
const scan = scanDll(base, target.name);
|
||||
for (scan) |entry| {
|
||||
const gadget = entry orelse continue;
|
||||
if (out_count >= results.len) break;
|
||||
results[out_count] = gadget;
|
||||
out_count += 1;
|
||||
}
|
||||
if (out_count >= results.len) break;
|
||||
}
|
||||
|
||||
sortByCleanupDesc(&results);
|
||||
return results;
|
||||
}
|
||||
|
||||
/// Count non-null gadget slots in a scan result.
|
||||
pub fn countGadgets(gadgets: []const ?GadgetInfo) usize {
|
||||
var count: usize = 0;
|
||||
for (gadgets) |entry| {
|
||||
if (entry != null) count += 1;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
fn scanRegisterAtOffset(base: [*]const u8, dll_name: []const u8, section_rva: u32, section_bytes: []const u8, offset: usize, reg: GadgetRegister) ?GadgetInfo {
|
||||
const mov_pattern = movPattern(reg);
|
||||
if (!matchBytes(section_bytes, offset, mov_pattern)) return null;
|
||||
|
||||
const after_mov = offset + mov_pattern.len;
|
||||
const add_info = parseAddRsp(section_bytes, after_mov) orelse return null;
|
||||
if (add_info.cleanup_size < 0x20 or add_info.cleanup_size >= 0x10000) return null;
|
||||
|
||||
var cursor = after_mov + add_info.len;
|
||||
var pops_before: u8 = 0;
|
||||
var pops_after: u8 = 0;
|
||||
var extra_pops = zero_extra_pops;
|
||||
var stored: usize = 0;
|
||||
var found_target = false;
|
||||
var found_ret = false;
|
||||
|
||||
// Scan pop sequence: pops before target, target pop, pops after target, ret
|
||||
while (cursor < section_bytes.len and (pops_before + pops_after) < 8) {
|
||||
// Check for ret
|
||||
if (section_bytes[cursor] == 0xc3) {
|
||||
if (found_target) found_ret = true;
|
||||
break;
|
||||
}
|
||||
const pop = parsePop(section_bytes, cursor) orelse break;
|
||||
if (pop.reg) |pop_reg| {
|
||||
if (pop_reg == reg and !found_target) {
|
||||
found_target = true;
|
||||
cursor += pop.len;
|
||||
continue;
|
||||
}
|
||||
if (stored < extra_pops.len) {
|
||||
extra_pops[stored] = pop_reg;
|
||||
stored += 1;
|
||||
}
|
||||
}
|
||||
if (found_target) {
|
||||
pops_after += 1;
|
||||
} else {
|
||||
pops_before += 1;
|
||||
}
|
||||
cursor += pop.len;
|
||||
}
|
||||
|
||||
if (!found_target or !found_ret) return null;
|
||||
|
||||
var dll_name_buf = [_]u8{0} ** 48;
|
||||
var func_name_buf = [_]u8{0} ** 64;
|
||||
copyZTerm(&dll_name_buf, dll_name);
|
||||
var func_rva: u32 = 0;
|
||||
const gadget_rva_usize = @as(usize, section_rva) + offset;
|
||||
if (gadget_rva_usize > std.math.maxInt(u32)) return null;
|
||||
resolveContainingExport(base, @intCast(gadget_rva_usize), &func_name_buf, &func_rva);
|
||||
|
||||
var info = GadgetInfo{
|
||||
.addr = @intFromPtr(base) + section_rva + offset,
|
||||
.cleanup_size = add_info.cleanup_size,
|
||||
.cleanup_type = add_info.cleanup_type,
|
||||
.register = reg,
|
||||
.dll_base = @intFromPtr(base),
|
||||
.dll_name = dll_name_buf,
|
||||
.func_name = func_name_buf,
|
||||
.func_rva = func_rva,
|
||||
.pops_before = pops_before,
|
||||
.pops_after = pops_after,
|
||||
.extra_pops = extra_pops,
|
||||
.pop_map = 0,
|
||||
};
|
||||
info.pop_map = computePopMap(&info);
|
||||
return info;
|
||||
}
|
||||
|
||||
/// Convert the decoded pop order into the packed nibble map consumed by the
|
||||
/// trampoline. Each nibble stores the scratch-slot index for one pop slot.
|
||||
pub fn computePopMap(g: *const GadgetInfo) u64 {
|
||||
var map: u64 = 0;
|
||||
var shift: u6 = 0;
|
||||
|
||||
var i: usize = 0;
|
||||
while (i < g.pops_before and i < g.extra_pops.len) : (i += 1) {
|
||||
map |= @as(u64, scratchIndex(g.extra_pops[i])) << shift;
|
||||
shift += 4;
|
||||
}
|
||||
|
||||
map |= @as(u64, scratchIndex(g.register)) << shift;
|
||||
shift += 4;
|
||||
|
||||
var j: usize = 0;
|
||||
while (j < g.pops_after and (i + j) < g.extra_pops.len) : (j += 1) {
|
||||
map |= @as(u64, scratchIndex(g.extra_pops[i + j])) << shift;
|
||||
shift += 4;
|
||||
}
|
||||
|
||||
return map;
|
||||
}
|
||||
|
||||
fn scratchIndex(reg: GadgetRegister) u4 {
|
||||
return switch (reg) {
|
||||
.rbx => 0,
|
||||
.rdi => 1,
|
||||
.rsi => 2,
|
||||
.rbp => 3,
|
||||
.r12 => 4,
|
||||
.r13 => 5,
|
||||
.r14 => 6,
|
||||
.r15 => 7,
|
||||
};
|
||||
}
|
||||
|
||||
fn AddInfo() type {
|
||||
return struct {
|
||||
cleanup_size: usize,
|
||||
cleanup_type: CleanupType,
|
||||
len: usize,
|
||||
};
|
||||
}
|
||||
|
||||
fn parseAddRsp(bytes: []const u8, offset: usize) ?AddInfo() {
|
||||
if (offset + 4 <= bytes.len and bytes[offset] == 0x48 and bytes[offset + 1] == 0x83 and bytes[offset + 2] == 0xc4) {
|
||||
return .{
|
||||
.cleanup_size = bytes[offset + 3],
|
||||
.cleanup_type = .imm8,
|
||||
.len = 4,
|
||||
};
|
||||
}
|
||||
if (offset + 7 <= bytes.len and bytes[offset] == 0x48 and bytes[offset + 1] == 0x81 and bytes[offset + 2] == 0xc4) {
|
||||
return .{
|
||||
.cleanup_size = std.mem.readInt(u32, bytes[offset + 3 .. offset + 7][0..4], .little),
|
||||
.cleanup_type = .imm32,
|
||||
.len = 7,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const PopInfo = struct {
|
||||
reg: ?GadgetRegister,
|
||||
len: usize,
|
||||
};
|
||||
|
||||
fn parsePop(bytes: []const u8, offset: usize) ?PopInfo {
|
||||
if (offset >= bytes.len) return null;
|
||||
return switch (bytes[offset]) {
|
||||
0x58, 0x59, 0x5a => .{ .reg = null, .len = 1 },
|
||||
0x5b => .{ .reg = .rbx, .len = 1 },
|
||||
0x5d => .{ .reg = .rbp, .len = 1 },
|
||||
0x5e => .{ .reg = .rsi, .len = 1 },
|
||||
0x5f => .{ .reg = .rdi, .len = 1 },
|
||||
0x41 => blk: {
|
||||
if (offset + 1 >= bytes.len) break :blk null;
|
||||
break :blk switch (bytes[offset + 1]) {
|
||||
0x5c => .{ .reg = .r12, .len = 2 },
|
||||
0x5d => .{ .reg = .r13, .len = 2 },
|
||||
0x5e => .{ .reg = .r14, .len = 2 },
|
||||
0x5f => .{ .reg = .r15, .len = 2 },
|
||||
else => null,
|
||||
};
|
||||
},
|
||||
else => null,
|
||||
};
|
||||
}
|
||||
|
||||
fn movPattern(reg: GadgetRegister) []const u8 {
|
||||
return switch (reg) {
|
||||
.rbx => &[_]u8{ 0x48, 0x89, 0x03 },
|
||||
.rdi => &[_]u8{ 0x48, 0x89, 0x07 },
|
||||
.rsi => &[_]u8{ 0x48, 0x89, 0x06 },
|
||||
.rbp => &[_]u8{ 0x48, 0x89, 0x45, 0x00 },
|
||||
.r12 => &[_]u8{ 0x49, 0x89, 0x04, 0x24 },
|
||||
.r13 => &[_]u8{ 0x49, 0x89, 0x45, 0x00 },
|
||||
.r14 => &[_]u8{ 0x49, 0x89, 0x06 },
|
||||
.r15 => &[_]u8{ 0x49, 0x89, 0x07 },
|
||||
};
|
||||
}
|
||||
|
||||
fn matchBytes(bytes: []const u8, offset: usize, pattern: []const u8) bool {
|
||||
if (offset + pattern.len > bytes.len) return false;
|
||||
return std.mem.eql(u8, bytes[offset .. offset + pattern.len], pattern);
|
||||
}
|
||||
|
||||
fn resolveContainingExport(base: [*]const u8, gadget_rva: u32, func_name: *[64]u8, func_rva: *u32) void {
|
||||
@memset(func_name, 0);
|
||||
func_rva.* = 0;
|
||||
|
||||
const dos: *align(1) const nt.IMAGE_DOS_HEADER = @ptrCast(base);
|
||||
if (dos.e_magic != nt.IMAGE_DOS_SIGNATURE or dos.e_lfanew < 0) return;
|
||||
|
||||
const nt_offset: usize = @intCast(dos.e_lfanew);
|
||||
const nt_headers: *align(1) const nt.IMAGE_NT_HEADERS64 = @ptrCast(base + nt_offset);
|
||||
if (nt_headers.Signature != nt.IMAGE_NT_SIGNATURE) return;
|
||||
if (nt.IMAGE_DIRECTORY_ENTRY_EXPORT >= nt_headers.OptionalHeader.NumberOfRvaAndSizes) return;
|
||||
|
||||
const export_entry = nt_headers.OptionalHeader.DataDirectory[nt.IMAGE_DIRECTORY_ENTRY_EXPORT];
|
||||
if (export_entry.VirtualAddress == 0 or export_entry.Size == 0) return;
|
||||
|
||||
const export_dir: *align(1) const nt.IMAGE_EXPORT_DIRECTORY = @ptrCast(base + export_entry.VirtualAddress);
|
||||
const names: [*]align(1) const nt.DWORD = @ptrCast(base + export_dir.AddressOfNames);
|
||||
const ordinals: [*]align(1) const nt.WORD = @ptrCast(base + export_dir.AddressOfNameOrdinals);
|
||||
const functions: [*]align(1) const nt.DWORD = @ptrCast(base + export_dir.AddressOfFunctions);
|
||||
|
||||
var best_ord: ?u32 = null;
|
||||
var best_rva: u32 = 0;
|
||||
var ordinal_index: u32 = 0;
|
||||
while (ordinal_index < export_dir.NumberOfFunctions) : (ordinal_index += 1) {
|
||||
const current_rva = functions[ordinal_index];
|
||||
if (current_rva == 0 or current_rva > gadget_rva) continue;
|
||||
if (best_ord == null or current_rva >= best_rva) {
|
||||
best_ord = ordinal_index;
|
||||
best_rva = current_rva;
|
||||
}
|
||||
}
|
||||
|
||||
const ord = best_ord orelse return;
|
||||
func_rva.* = best_rva;
|
||||
|
||||
var name_index: u32 = 0;
|
||||
while (name_index < export_dir.NumberOfNames) : (name_index += 1) {
|
||||
if (ordinals[name_index] != ord) continue;
|
||||
const name_ptr: [*:0]const u8 = @ptrCast(base + names[name_index]);
|
||||
copyZTerm(func_name, std.mem.span(name_ptr));
|
||||
return;
|
||||
}
|
||||
|
||||
var ordinal_buf: [16]u8 = [_]u8{0} ** 16;
|
||||
const ordinal_slice = std.fmt.bufPrint(&ordinal_buf, "#{d}", .{export_dir.Base + ord}) catch return;
|
||||
copyZTerm(func_name, ordinal_slice);
|
||||
}
|
||||
|
||||
fn copyZTerm(dest: anytype, src: []const u8) void {
|
||||
@memset(dest, 0);
|
||||
const limit = @min(dest.len - 1, src.len);
|
||||
@memcpy(dest[0..limit], src[0..limit]);
|
||||
}
|
||||
|
||||
fn sortByCleanupDesc(results: *[MAX_RESULTS]?GadgetInfo) void {
|
||||
const count = countGadgets(results);
|
||||
if (count < 2) return;
|
||||
|
||||
var i: usize = 1;
|
||||
while (i < count) : (i += 1) {
|
||||
const current = results[i].?;
|
||||
var j = i;
|
||||
while (j > 0 and results[j - 1].?.cleanup_size < current.cleanup_size) : (j -= 1) {
|
||||
results[j] = results[j - 1];
|
||||
}
|
||||
results[j] = current;
|
||||
}
|
||||
}
|
||||
|
||||
test "countGadgets counts only valid entries" {
|
||||
var gadgets = [_]?GadgetInfo{null} ** MAX_RESULTS;
|
||||
gadgets[0] = GadgetInfo{
|
||||
.addr = 1,
|
||||
.cleanup_size = 0x20,
|
||||
.cleanup_type = .imm8,
|
||||
.register = .rbx,
|
||||
.dll_base = 0,
|
||||
.dll_name = [_]u8{0} ** 48,
|
||||
.func_name = [_]u8{0} ** 64,
|
||||
.func_rva = 0,
|
||||
.pops_before = 0,
|
||||
.pops_after = 0,
|
||||
.extra_pops = zero_extra_pops,
|
||||
.pop_map = 0,
|
||||
};
|
||||
try std.testing.expectEqual(@as(usize, 1), countGadgets(&gadgets));
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
const std = @import("std");
|
||||
const types = @import("types.zig");
|
||||
|
||||
const GadgetInfo = types.GadgetInfo;
|
||||
const MAX_GADGETS = types.MAX_GADGETS;
|
||||
|
||||
pub const GadgetStable = struct {
|
||||
gadgets: [MAX_GADGETS]?GadgetInfo,
|
||||
count: usize,
|
||||
// Keep extra room for side-effect load attempts across larger scan cycles.
|
||||
loaded_dlls: [32]u32,
|
||||
loaded_dll_count: usize,
|
||||
|
||||
pub fn init() GadgetStable {
|
||||
return .{
|
||||
.gadgets = [_]?GadgetInfo{null} ** MAX_GADGETS,
|
||||
.count = 0,
|
||||
.loaded_dlls = [_]u32{0} ** 32,
|
||||
.loaded_dll_count = 0,
|
||||
};
|
||||
}
|
||||
|
||||
/// Add one gadget if the stable is not full and the address is not already present.
|
||||
pub fn addGadget(self: *GadgetStable, gadget: GadgetInfo) bool {
|
||||
if (self.count >= MAX_GADGETS) return false;
|
||||
|
||||
for (self.gadgets[0..self.count]) |existing| {
|
||||
if (existing) |current| {
|
||||
if (current.addr == gadget.addr) return true;
|
||||
}
|
||||
}
|
||||
|
||||
self.gadgets[self.count] = gadget;
|
||||
self.count += 1;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// Add a scan result set until the stable fills.
|
||||
pub fn addFromScan(self: *GadgetStable, scan_results: []const ?GadgetInfo) usize {
|
||||
var added: usize = 0;
|
||||
for (scan_results) |entry| {
|
||||
const gadget = entry orelse continue;
|
||||
if (self.addGadget(gadget)) {
|
||||
added += 1;
|
||||
if (self.count >= MAX_GADGETS) break;
|
||||
}
|
||||
}
|
||||
return added;
|
||||
}
|
||||
|
||||
/// Select a gadget that can satisfy `arg_count`.
|
||||
/// Random mode picks from all eligible entries; deterministic mode prefers the
|
||||
/// smallest cleanup and then the shortest pop sequence.
|
||||
pub fn selectGadget(self: *GadgetStable, arg_count: usize, random_select: bool) ?*const GadgetInfo {
|
||||
var eligible: [MAX_GADGETS]usize = undefined;
|
||||
var eligible_count: usize = 0;
|
||||
|
||||
for (self.gadgets[0..self.count], 0..) |entry, idx| {
|
||||
const gadget = entry orelse continue;
|
||||
if (gadget.maxArgs() >= arg_count) {
|
||||
eligible[eligible_count] = idx;
|
||||
eligible_count += 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (eligible_count == 0) return null;
|
||||
|
||||
if (random_select) {
|
||||
const choice = quickRandom() % @as(u32, @intCast(eligible_count));
|
||||
return &(self.gadgets[eligible[choice]].?);
|
||||
}
|
||||
|
||||
var best_idx = eligible[0];
|
||||
for (eligible[1..eligible_count]) |idx| {
|
||||
const current = self.gadgets[idx].?;
|
||||
const best = self.gadgets[best_idx].?;
|
||||
if (current.cleanup_size < best.cleanup_size or
|
||||
(current.cleanup_size == best.cleanup_size and (current.pops_before + current.pops_after) < (best.pops_before + best.pops_after)))
|
||||
{
|
||||
best_idx = idx;
|
||||
}
|
||||
}
|
||||
|
||||
return &(self.gadgets[best_idx].?);
|
||||
}
|
||||
|
||||
/// Report the widest argument count currently supported.
|
||||
pub fn maxSupportedArgs(self: *const GadgetStable) usize {
|
||||
var max_args: usize = 0;
|
||||
for (self.gadgets[0..self.count]) |entry| {
|
||||
const gadget = entry orelse continue;
|
||||
max_args = @max(max_args, gadget.maxArgs());
|
||||
}
|
||||
return max_args;
|
||||
}
|
||||
|
||||
/// Reset the stable and the list of DLLs already attempted for side-effect loading.
|
||||
pub fn clear(self: *GadgetStable) void {
|
||||
self.gadgets = [_]?GadgetInfo{null} ** MAX_GADGETS;
|
||||
self.count = 0;
|
||||
self.loaded_dlls = [_]u32{0} ** 32;
|
||||
self.loaded_dll_count = 0;
|
||||
}
|
||||
|
||||
/// Return true once a DLL hash has been recorded for this scan cycle.
|
||||
pub fn isDllLoaded(self: *const GadgetStable, dll_hash: u32) bool {
|
||||
for (self.loaded_dlls[0..self.loaded_dll_count]) |loaded_hash| {
|
||||
if (loaded_hash == dll_hash) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// Record a DLL that was already present or already attempted for side-effect loading.
|
||||
pub fn recordDllLoad(self: *GadgetStable, dll_hash: u32) void {
|
||||
if (self.isDllLoaded(dll_hash)) return;
|
||||
if (self.loaded_dll_count >= self.loaded_dlls.len) return;
|
||||
self.loaded_dlls[self.loaded_dll_count] = dll_hash;
|
||||
self.loaded_dll_count += 1;
|
||||
}
|
||||
};
|
||||
|
||||
fn quickRandom() u32 {
|
||||
var buf: [4]u8 = undefined;
|
||||
std.crypto.random.bytes(&buf);
|
||||
return std.mem.readInt(u32, &buf, .little);
|
||||
}
|
||||
|
||||
test "stable selects smallest sufficient gadget" {
|
||||
var stable = GadgetStable.init();
|
||||
var dll_name = [_]u8{0} ** 48;
|
||||
var func_name = [_]u8{0} ** 64;
|
||||
dll_name[0] = 'n';
|
||||
func_name[0] = 'f';
|
||||
try std.testing.expect(stable.addGadget(.{
|
||||
.addr = 0x1000,
|
||||
.cleanup_size = 0x40,
|
||||
.cleanup_type = .imm8,
|
||||
.register = .rbx,
|
||||
.dll_base = 0x1000,
|
||||
.dll_name = dll_name,
|
||||
.func_name = func_name,
|
||||
.func_rva = 0,
|
||||
.pops_before = 0,
|
||||
.pops_after = 0,
|
||||
.extra_pops = [_]types.GadgetRegister{.rbx} ** 8,
|
||||
.pop_map = 0,
|
||||
}));
|
||||
try std.testing.expect(stable.addGadget(.{
|
||||
.addr = 0x2000,
|
||||
.cleanup_size = 0x30,
|
||||
.cleanup_type = .imm8,
|
||||
.register = .rdi,
|
||||
.dll_base = 0x2000,
|
||||
.dll_name = dll_name,
|
||||
.func_name = func_name,
|
||||
.func_rva = 0,
|
||||
.pops_before = 0,
|
||||
.pops_after = 0,
|
||||
.extra_pops = [_]types.GadgetRegister{.rbx} ** 8,
|
||||
.pop_map = 0,
|
||||
}));
|
||||
const selected = stable.selectGadget(5, false).?;
|
||||
try std.testing.expectEqual(@as(usize, 0x2000), selected.addr);
|
||||
}
|
||||
|
||||
test "clear resets attempted DLL tracking" {
|
||||
var stable = GadgetStable.init();
|
||||
stable.recordDllLoad(0x1234);
|
||||
stable.clear();
|
||||
try std.testing.expectEqual(@as(usize, 0), stable.loaded_dll_count);
|
||||
}
|
||||
+332
@@ -0,0 +1,332 @@
|
||||
const std = @import("std");
|
||||
const builtin = @import("builtin");
|
||||
const nt = @import("platform/ntypes.zig");
|
||||
const peb = @import("platform/peb.zig");
|
||||
const types = @import("types.zig");
|
||||
const pool_proxy = @import("pool_proxy.zig");
|
||||
const dm_bindings = @import("dm_bindings.zig");
|
||||
|
||||
const TestApis = struct {
|
||||
allocate_virtual_memory: usize,
|
||||
free_virtual_memory: usize,
|
||||
};
|
||||
|
||||
const Summary = struct {
|
||||
passed: u32 = 0,
|
||||
failed: u32 = 0,
|
||||
skipped_alloc: u32 = 0,
|
||||
};
|
||||
|
||||
const CaseStatus = enum {
|
||||
pass,
|
||||
fail,
|
||||
skip,
|
||||
|
||||
fn label(self: CaseStatus) []const u8 {
|
||||
return switch (self) {
|
||||
.pass => "PASS",
|
||||
.fail => "FAIL",
|
||||
.skip => "SKIP",
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
pub fn main() !void {
|
||||
const stdout = std.fs.File.stdout().deprecatedWriter();
|
||||
try stdout.writeAll("pool-proxy-ng v0.1.0\n");
|
||||
|
||||
if (builtin.os.tag != .windows) {
|
||||
try stdout.writeAll("Windows-only (cross-compile with -Dtarget=x86_64-windows)\n");
|
||||
return;
|
||||
}
|
||||
|
||||
bootstrapTestHarness();
|
||||
pool_proxy.init();
|
||||
|
||||
try stdout.writeAll("\n== Stable state ==\n");
|
||||
try printStableState(stdout);
|
||||
|
||||
const apis = resolveTestApis() orelse return error.MissingTestApis;
|
||||
const summary = try runPerGadgetTests(stdout, apis);
|
||||
try stdout.print("\n== Summary ==\npassed: {d}\nfailed: {d}\nskipped_alloc: {d}\n", .{
|
||||
summary.passed,
|
||||
summary.failed,
|
||||
summary.skipped_alloc,
|
||||
});
|
||||
|
||||
const stable_json = try dm_bindings.serializeStable(&pool_proxy.stable, std.heap.page_allocator);
|
||||
defer std.heap.page_allocator.free(stable_json);
|
||||
|
||||
try stdout.print("\n== JSON ==\n{s}\n", .{stable_json});
|
||||
|
||||
if (summary.failed != 0) return error.TestFailures;
|
||||
}
|
||||
|
||||
/// TEST HARNESS ONLY: pre-load Chakra with LdrLoadDll so the release harness
|
||||
/// can exercise the 15-argument path without depending on external setup.
|
||||
fn bootstrapTestHarness() void {
|
||||
if (builtin.os.tag != .windows) return;
|
||||
|
||||
const ntdll_base = peb.getModuleBase(comptime peb.rotrHash("ntdll.dll")) orelse return;
|
||||
const ldr_fn = peb.getExportByHash(ntdll_base, comptime peb.rotrHash("LdrLoadDll")) orelse return;
|
||||
const LdrLoadDllFn = *const fn (?[*:0]const nt.WCHAR, ?*u32, *nt.UNICODE_STRING, *?*anyopaque) callconv(.c) nt.NTSTATUS;
|
||||
const ldr: LdrLoadDllFn = @ptrCast(ldr_fn);
|
||||
|
||||
const chakra_name = [_]nt.WCHAR{ 'C', 'h', 'a', 'k', 'r', 'a', '.', 'd', 'l', 'l', 0 };
|
||||
var chakra_us = nt.UNICODE_STRING{
|
||||
.Length = 20,
|
||||
.MaximumLength = 22,
|
||||
.Buffer = @constCast(&chakra_name),
|
||||
};
|
||||
var handle: ?*anyopaque = null;
|
||||
_ = ldr(null, null, &chakra_us, &handle);
|
||||
}
|
||||
|
||||
fn resolveTestApis() ?TestApis {
|
||||
const ntdll_base = peb.getModuleBase(comptime peb.rotrHash("ntdll.dll")) orelse return null;
|
||||
|
||||
return .{
|
||||
.allocate_virtual_memory = @intFromPtr(peb.getExportByHash(ntdll_base, comptime peb.rotrHash("NtAllocateVirtualMemory")) orelse return null),
|
||||
.free_virtual_memory = @intFromPtr(peb.getExportByHash(ntdll_base, comptime peb.rotrHash("NtFreeVirtualMemory")) orelse return null),
|
||||
};
|
||||
}
|
||||
|
||||
fn xorArgs(args: []const usize) usize {
|
||||
var expected: usize = 0;
|
||||
for (args) |arg| expected ^= arg;
|
||||
return expected;
|
||||
}
|
||||
|
||||
fn testFunc4(a0: usize, a1: usize, a2: usize, a3: usize) callconv(.c) usize {
|
||||
return xorArgs(&[_]usize{ a0, a1, a2, a3 });
|
||||
}
|
||||
|
||||
fn testFunc5(a0: usize, a1: usize, a2: usize, a3: usize, a4: usize) callconv(.c) usize {
|
||||
return xorArgs(&[_]usize{ a0, a1, a2, a3, a4 });
|
||||
}
|
||||
|
||||
fn testFunc6(a0: usize, a1: usize, a2: usize, a3: usize, a4: usize, a5: usize) callconv(.c) usize {
|
||||
return xorArgs(&[_]usize{ a0, a1, a2, a3, a4, a5 });
|
||||
}
|
||||
|
||||
fn testFunc7(a0: usize, a1: usize, a2: usize, a3: usize, a4: usize, a5: usize, a6: usize) callconv(.c) usize {
|
||||
return xorArgs(&[_]usize{ a0, a1, a2, a3, a4, a5, a6 });
|
||||
}
|
||||
|
||||
fn testFunc8(a0: usize, a1: usize, a2: usize, a3: usize, a4: usize, a5: usize, a6: usize, a7: usize) callconv(.c) usize {
|
||||
return xorArgs(&[_]usize{ a0, a1, a2, a3, a4, a5, a6, a7 });
|
||||
}
|
||||
|
||||
fn testFunc9(a0: usize, a1: usize, a2: usize, a3: usize, a4: usize, a5: usize, a6: usize, a7: usize, a8: usize) callconv(.c) usize {
|
||||
return xorArgs(&[_]usize{ a0, a1, a2, a3, a4, a5, a6, a7, a8 });
|
||||
}
|
||||
|
||||
fn testFunc10(a0: usize, a1: usize, a2: usize, a3: usize, a4: usize, a5: usize, a6: usize, a7: usize, a8: usize, a9: usize) callconv(.c) usize {
|
||||
return xorArgs(&[_]usize{ a0, a1, a2, a3, a4, a5, a6, a7, a8, a9 });
|
||||
}
|
||||
|
||||
fn testFunc11(a0: usize, a1: usize, a2: usize, a3: usize, a4: usize, a5: usize, a6: usize, a7: usize, a8: usize, a9: usize, a10: usize) callconv(.c) usize {
|
||||
return xorArgs(&[_]usize{ a0, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10 });
|
||||
}
|
||||
|
||||
fn testFunc12(a0: usize, a1: usize, a2: usize, a3: usize, a4: usize, a5: usize, a6: usize, a7: usize, a8: usize, a9: usize, a10: usize, a11: usize) callconv(.c) usize {
|
||||
return xorArgs(&[_]usize{ a0, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11 });
|
||||
}
|
||||
|
||||
fn testFunc13(a0: usize, a1: usize, a2: usize, a3: usize, a4: usize, a5: usize, a6: usize, a7: usize, a8: usize, a9: usize, a10: usize, a11: usize, a12: usize) callconv(.c) usize {
|
||||
return xorArgs(&[_]usize{ a0, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12 });
|
||||
}
|
||||
|
||||
fn testFunc14(a0: usize, a1: usize, a2: usize, a3: usize, a4: usize, a5: usize, a6: usize, a7: usize, a8: usize, a9: usize, a10: usize, a11: usize, a12: usize, a13: usize) callconv(.c) usize {
|
||||
return xorArgs(&[_]usize{ a0, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13 });
|
||||
}
|
||||
|
||||
fn testFunc15(a0: usize, a1: usize, a2: usize, a3: usize, a4: usize, a5: usize, a6: usize, a7: usize, a8: usize, a9: usize, a10: usize, a11: usize, a12: usize, a13: usize, a14: usize) callconv(.c) usize {
|
||||
return xorArgs(&[_]usize{ a0, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14 });
|
||||
}
|
||||
|
||||
fn xorTestFuncAddr(arg_count: usize) ?usize {
|
||||
return switch (arg_count) {
|
||||
4 => @intFromPtr(&testFunc4),
|
||||
5 => @intFromPtr(&testFunc5),
|
||||
6 => @intFromPtr(&testFunc6),
|
||||
7 => @intFromPtr(&testFunc7),
|
||||
8 => @intFromPtr(&testFunc8),
|
||||
9 => @intFromPtr(&testFunc9),
|
||||
10 => @intFromPtr(&testFunc10),
|
||||
11 => @intFromPtr(&testFunc11),
|
||||
12 => @intFromPtr(&testFunc12),
|
||||
13 => @intFromPtr(&testFunc13),
|
||||
14 => @intFromPtr(&testFunc14),
|
||||
15 => @intFromPtr(&testFunc15),
|
||||
else => null,
|
||||
};
|
||||
}
|
||||
|
||||
fn runXorProxyTest(func_addr: usize, args: []const usize, gadget: *const types.GadgetInfo) !usize {
|
||||
const result = try pool_proxy.proxyCallWithGadget(func_addr, args, gadget);
|
||||
return result.return_value;
|
||||
}
|
||||
|
||||
fn runNtAllocateVirtualMemoryTest(apis: TestApis, gadget: *const types.GadgetInfo) !void {
|
||||
const current_process: usize = @bitCast(@as(isize, -1));
|
||||
var base_addr: usize = 0;
|
||||
var region_size: usize = 0x1000;
|
||||
|
||||
const alloc_result = try pool_proxy.proxyCallWithGadget(apis.allocate_virtual_memory, &.{
|
||||
current_process,
|
||||
@intFromPtr(&base_addr),
|
||||
0,
|
||||
@intFromPtr(®ion_size),
|
||||
nt.MEM_COMMIT | nt.MEM_RESERVE,
|
||||
nt.PAGE_READWRITE,
|
||||
}, gadget);
|
||||
|
||||
const status: nt.NTSTATUS = @bitCast(@as(u32, @intCast(alloc_result.return_value & 0xFFFFFFFF)));
|
||||
if (status != nt.STATUS_SUCCESS or base_addr == 0) {
|
||||
return error.AllocationFailed;
|
||||
}
|
||||
|
||||
var free_size: usize = 0;
|
||||
_ = pool_proxy.proxyCallWithGadget(apis.free_virtual_memory, &.{
|
||||
current_process,
|
||||
@intFromPtr(&base_addr),
|
||||
@intFromPtr(&free_size),
|
||||
nt.MEM_RELEASE,
|
||||
}, gadget) catch {};
|
||||
}
|
||||
|
||||
fn formatXorCase(buf: []u8, arg_count: usize, status: CaseStatus, detail: []const u8) ![]const u8 {
|
||||
return if (detail.len != 0)
|
||||
std.fmt.bufPrint(buf, "xor{d}={s}({s})", .{ arg_count, status.label(), detail })
|
||||
else
|
||||
std.fmt.bufPrint(buf, "xor{d}={s}", .{ arg_count, status.label() });
|
||||
}
|
||||
|
||||
fn formatAllocCase(buf: []u8, status: CaseStatus, detail: []const u8) ![]const u8 {
|
||||
return if (detail.len != 0)
|
||||
std.fmt.bufPrint(buf, "alloc6={s}({s})", .{ status.label(), detail })
|
||||
else
|
||||
std.fmt.bufPrint(buf, "alloc6={s}", .{status.label()});
|
||||
}
|
||||
|
||||
fn runPerGadgetTests(stdout: anytype, apis: TestApis) !Summary {
|
||||
var summary: Summary = .{};
|
||||
|
||||
try stdout.writeAll("\n== Per-gadget tests ==\n");
|
||||
|
||||
var index: usize = 0;
|
||||
while (index < pool_proxy.stable.count) : (index += 1) {
|
||||
const gadget = &(pool_proxy.stable.gadgets[index] orelse continue);
|
||||
const max_args = gadget.maxArgs();
|
||||
|
||||
var xor_status: CaseStatus = .fail;
|
||||
var alloc_status: CaseStatus = if (max_args < 6) .skip else .fail;
|
||||
|
||||
var xor_detail_buf: [128]u8 = undefined;
|
||||
var alloc_detail_buf: [128]u8 = undefined;
|
||||
var xor_case_buf: [160]u8 = undefined;
|
||||
var alloc_case_buf: [160]u8 = undefined;
|
||||
var xor_detail: []const u8 = "";
|
||||
var alloc_detail: []const u8 = "";
|
||||
|
||||
var xor_args: [types.MAX_PROXY_ARGS]usize = [_]usize{0} ** types.MAX_PROXY_ARGS;
|
||||
for (0..max_args) |arg_index| {
|
||||
xor_args[arg_index] = arg_index + 1;
|
||||
}
|
||||
|
||||
var expected: usize = 0;
|
||||
for (xor_args[0..max_args]) |arg| expected ^= arg;
|
||||
|
||||
const xor_func_addr = xorTestFuncAddr(max_args) orelse blk: {
|
||||
xor_detail = try std.fmt.bufPrint(&xor_detail_buf, "unsupported arg count {d}", .{max_args});
|
||||
break :blk 0;
|
||||
};
|
||||
|
||||
if (xor_detail.len == 0) {
|
||||
const got = runXorProxyTest(xor_func_addr, xor_args[0..max_args], gadget) catch |err| blk: {
|
||||
xor_detail = try std.fmt.bufPrint(&xor_detail_buf, "{}", .{err});
|
||||
break :blk 0;
|
||||
};
|
||||
if (xor_detail.len == 0) {
|
||||
xor_detail = try std.fmt.bufPrint(&xor_detail_buf, "expected={d},got={d}", .{ expected, got });
|
||||
if (got == expected) xor_status = .pass;
|
||||
}
|
||||
}
|
||||
|
||||
if (max_args < 6) {
|
||||
summary.skipped_alloc += 1;
|
||||
} else {
|
||||
const alloc_passed = blk: {
|
||||
runNtAllocateVirtualMemoryTest(apis, gadget) catch |err| {
|
||||
alloc_detail = try std.fmt.bufPrint(&alloc_detail_buf, "{}", .{err});
|
||||
break :blk false;
|
||||
};
|
||||
break :blk true;
|
||||
};
|
||||
if (alloc_passed) {
|
||||
alloc_status = .pass;
|
||||
} else if (alloc_detail.len == 0) {
|
||||
alloc_detail = "unknown failure";
|
||||
}
|
||||
}
|
||||
|
||||
const xor_case = try formatXorCase(&xor_case_buf, max_args, xor_status, xor_detail);
|
||||
const alloc_case = try formatAllocCase(&alloc_case_buf, alloc_status, alloc_detail);
|
||||
|
||||
try stdout.print("[{s}] {s}!{s} max={d}: {s} {s}\n", .{
|
||||
gadget.register.name(),
|
||||
gadget.dllNameSlice(),
|
||||
gadget.funcNameSlice(),
|
||||
max_args,
|
||||
xor_case,
|
||||
alloc_case,
|
||||
});
|
||||
|
||||
if (xor_status == .pass and alloc_status != .fail) {
|
||||
summary.passed += 1;
|
||||
} else {
|
||||
summary.failed += 1;
|
||||
}
|
||||
}
|
||||
|
||||
return summary;
|
||||
}
|
||||
|
||||
fn printStableState(stdout: anytype) !void {
|
||||
try stdout.print("gadgets: {d}\nmax_args: {d}\nloaded_dlls: {d}\n", .{
|
||||
pool_proxy.stable.count,
|
||||
pool_proxy.stable.maxSupportedArgs(),
|
||||
pool_proxy.stable.loaded_dll_count,
|
||||
});
|
||||
|
||||
for (pool_proxy.stable.gadgets[0..pool_proxy.stable.count], 0..) |entry, index| {
|
||||
const gadget = entry orelse continue;
|
||||
try stdout.print("[{d}] {s} {s}!{s}+0x{x} cleanup=0x{x} max_args={d} pops={d}/{d} pop_map=0x{x}\n", .{
|
||||
index,
|
||||
gadget.register.name(),
|
||||
gadget.dllNameSlice(),
|
||||
gadget.funcNameSlice(),
|
||||
gadget.gadgetOffset(),
|
||||
gadget.cleanup_size,
|
||||
gadget.maxArgs(),
|
||||
gadget.pops_before,
|
||||
gadget.pops_after,
|
||||
gadget.pop_map,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
test {
|
||||
_ = @import("platform/ntypes.zig");
|
||||
_ = @import("platform/peb.zig");
|
||||
_ = @import("types.zig");
|
||||
_ = @import("config.zig");
|
||||
_ = @import("gadget_db.zig");
|
||||
_ = @import("gadget_scanner.zig");
|
||||
_ = @import("gadget_stable.zig");
|
||||
_ = @import("dll_sideload.zig");
|
||||
_ = @import("trampolines.zig");
|
||||
_ = @import("pool_proxy.zig");
|
||||
_ = @import("dm_bindings.zig");
|
||||
}
|
||||
@@ -0,0 +1,219 @@
|
||||
const std = @import("std");
|
||||
|
||||
pub const NTSTATUS = LONG;
|
||||
pub const HANDLE = ?*anyopaque;
|
||||
pub const PVOID = *anyopaque;
|
||||
pub const ULONG = u32;
|
||||
pub const USHORT = u16;
|
||||
pub const DWORD = u32;
|
||||
pub const WORD = u16;
|
||||
pub const BYTE = u8;
|
||||
pub const BOOL = i32;
|
||||
pub const WCHAR = u16;
|
||||
pub const SIZE_T = usize;
|
||||
pub const ULONGLONG = u64;
|
||||
pub const LONG = i32;
|
||||
pub const UCHAR = u8;
|
||||
pub const CHAR = i8;
|
||||
|
||||
pub const STATUS_SUCCESS: NTSTATUS = 0x00000000;
|
||||
pub const STATUS_UNSUCCESSFUL: NTSTATUS = @bitCast(@as(u32, 0xC0000001));
|
||||
pub const STATUS_INVALID_PARAMETER: NTSTATUS = @bitCast(@as(u32, 0xC000000D));
|
||||
pub const STATUS_ACCESS_DENIED: NTSTATUS = @bitCast(@as(u32, 0xC0000022));
|
||||
|
||||
pub const PAGE_NOACCESS: DWORD = 0x01;
|
||||
pub const PAGE_READONLY: DWORD = 0x02;
|
||||
pub const PAGE_READWRITE: DWORD = 0x04;
|
||||
pub const PAGE_EXECUTE: DWORD = 0x10;
|
||||
pub const PAGE_EXECUTE_READ: DWORD = 0x20;
|
||||
pub const PAGE_EXECUTE_READWRITE: DWORD = 0x40;
|
||||
|
||||
pub const MEM_COMMIT: DWORD = 0x00001000;
|
||||
pub const MEM_RESERVE: DWORD = 0x00002000;
|
||||
pub const MEM_DECOMMIT: DWORD = 0x00004000;
|
||||
pub const MEM_RELEASE: DWORD = 0x00008000;
|
||||
pub const MEM_FREE: DWORD = 0x00010000;
|
||||
pub const MEM_PRIVATE: DWORD = 0x00020000;
|
||||
|
||||
pub const IMAGE_DOS_SIGNATURE: WORD = 0x5A4D;
|
||||
pub const IMAGE_NT_SIGNATURE: DWORD = 0x00004550;
|
||||
pub const IMAGE_SCN_MEM_EXECUTE: DWORD = 0x20000000;
|
||||
pub const IMAGE_DIRECTORY_ENTRY_EXPORT: usize = 0;
|
||||
pub const IMAGE_NUMBEROF_DIRECTORY_ENTRIES: usize = 16;
|
||||
|
||||
pub const IMAGE_DOS_HEADER = extern struct {
|
||||
e_magic: WORD,
|
||||
e_cblp: WORD,
|
||||
e_cp: WORD,
|
||||
e_crlc: WORD,
|
||||
e_cparhdr: WORD,
|
||||
e_minalloc: WORD,
|
||||
e_maxalloc: WORD,
|
||||
e_ss: WORD,
|
||||
e_sp: WORD,
|
||||
e_csum: WORD,
|
||||
e_ip: WORD,
|
||||
e_cs: WORD,
|
||||
e_lfarlc: WORD,
|
||||
e_ovno: WORD,
|
||||
e_res: [4]WORD,
|
||||
e_oemid: WORD,
|
||||
e_oeminfo: WORD,
|
||||
e_res2: [10]WORD,
|
||||
e_lfanew: LONG,
|
||||
};
|
||||
|
||||
pub const IMAGE_FILE_HEADER = extern struct {
|
||||
Machine: WORD,
|
||||
NumberOfSections: WORD,
|
||||
TimeDateStamp: DWORD,
|
||||
PointerToSymbolTable: DWORD,
|
||||
NumberOfSymbols: DWORD,
|
||||
SizeOfOptionalHeader: WORD,
|
||||
Characteristics: WORD,
|
||||
};
|
||||
|
||||
pub const IMAGE_DATA_DIRECTORY = extern struct {
|
||||
VirtualAddress: DWORD,
|
||||
Size: DWORD,
|
||||
};
|
||||
|
||||
pub const IMAGE_OPTIONAL_HEADER64 = extern struct {
|
||||
Magic: WORD,
|
||||
MajorLinkerVersion: BYTE,
|
||||
MinorLinkerVersion: BYTE,
|
||||
SizeOfCode: DWORD,
|
||||
SizeOfInitializedData: DWORD,
|
||||
SizeOfUninitializedData: DWORD,
|
||||
AddressOfEntryPoint: DWORD,
|
||||
BaseOfCode: DWORD,
|
||||
ImageBase: ULONGLONG,
|
||||
SectionAlignment: DWORD,
|
||||
FileAlignment: DWORD,
|
||||
MajorOperatingSystemVersion: WORD,
|
||||
MinorOperatingSystemVersion: WORD,
|
||||
MajorImageVersion: WORD,
|
||||
MinorImageVersion: WORD,
|
||||
MajorSubsystemVersion: WORD,
|
||||
MinorSubsystemVersion: WORD,
|
||||
Win32VersionValue: DWORD,
|
||||
SizeOfImage: DWORD,
|
||||
SizeOfHeaders: DWORD,
|
||||
CheckSum: DWORD,
|
||||
Subsystem: WORD,
|
||||
DllCharacteristics: WORD,
|
||||
SizeOfStackReserve: ULONGLONG,
|
||||
SizeOfStackCommit: ULONGLONG,
|
||||
SizeOfHeapReserve: ULONGLONG,
|
||||
SizeOfHeapCommit: ULONGLONG,
|
||||
LoaderFlags: DWORD,
|
||||
NumberOfRvaAndSizes: DWORD,
|
||||
DataDirectory: [IMAGE_NUMBEROF_DIRECTORY_ENTRIES]IMAGE_DATA_DIRECTORY,
|
||||
};
|
||||
|
||||
pub const IMAGE_NT_HEADERS64 = extern struct {
|
||||
Signature: DWORD,
|
||||
FileHeader: IMAGE_FILE_HEADER,
|
||||
OptionalHeader: IMAGE_OPTIONAL_HEADER64,
|
||||
};
|
||||
|
||||
pub const IMAGE_SECTION_HEADER = extern struct {
|
||||
Name: [8]BYTE,
|
||||
MiscVirtualSize: DWORD,
|
||||
VirtualAddress: DWORD,
|
||||
SizeOfRawData: DWORD,
|
||||
PointerToRawData: DWORD,
|
||||
PointerToRelocations: DWORD,
|
||||
PointerToLinenumbers: DWORD,
|
||||
NumberOfRelocations: WORD,
|
||||
NumberOfLinenumbers: WORD,
|
||||
Characteristics: DWORD,
|
||||
};
|
||||
|
||||
pub const IMAGE_EXPORT_DIRECTORY = extern struct {
|
||||
Characteristics: DWORD,
|
||||
TimeDateStamp: DWORD,
|
||||
MajorVersion: WORD,
|
||||
MinorVersion: WORD,
|
||||
Name: DWORD,
|
||||
Base: DWORD,
|
||||
NumberOfFunctions: DWORD,
|
||||
NumberOfNames: DWORD,
|
||||
AddressOfFunctions: DWORD,
|
||||
AddressOfNames: DWORD,
|
||||
AddressOfNameOrdinals: DWORD,
|
||||
};
|
||||
|
||||
pub const UNICODE_STRING = extern struct {
|
||||
Length: USHORT,
|
||||
MaximumLength: USHORT,
|
||||
Buffer: ?[*]WCHAR,
|
||||
|
||||
pub fn toSlice(self: UNICODE_STRING) []const WCHAR {
|
||||
const buffer = self.Buffer orelse return &[_]WCHAR{};
|
||||
const len: usize = @intCast(self.Length / @sizeOf(WCHAR));
|
||||
return buffer[0..len];
|
||||
}
|
||||
};
|
||||
|
||||
pub const LIST_ENTRY = extern struct {
|
||||
Flink: ?*LIST_ENTRY,
|
||||
Blink: ?*LIST_ENTRY,
|
||||
};
|
||||
|
||||
pub const PEB_LDR_DATA = extern struct {
|
||||
Length: ULONG,
|
||||
Initialized: UCHAR,
|
||||
_padding0: [3]UCHAR,
|
||||
SsHandle: ?PVOID,
|
||||
InLoadOrderModuleList: LIST_ENTRY,
|
||||
InMemoryOrderModuleList: LIST_ENTRY,
|
||||
InInitializationOrderModuleList: LIST_ENTRY,
|
||||
};
|
||||
|
||||
pub const LDR_DATA_TABLE_ENTRY = extern struct {
|
||||
InLoadOrderLinks: LIST_ENTRY,
|
||||
InMemoryOrderLinks: LIST_ENTRY,
|
||||
InInitializationOrderLinks: LIST_ENTRY,
|
||||
DllBase: ?PVOID,
|
||||
EntryPoint: ?PVOID,
|
||||
SizeOfImage: ULONG,
|
||||
_padding0: ULONG,
|
||||
FullDllName: UNICODE_STRING,
|
||||
BaseDllName: UNICODE_STRING,
|
||||
Flags: ULONG,
|
||||
ObsoleteLoadCount: USHORT,
|
||||
TlsIndex: USHORT,
|
||||
HashLinks: LIST_ENTRY,
|
||||
TimeDateStamp: ULONG,
|
||||
_padding1: ULONG,
|
||||
_reserved: [64]u8,
|
||||
};
|
||||
|
||||
pub const PEB = extern struct {
|
||||
InheritedAddressSpace: u8,
|
||||
ReadImageFileExecOptions: u8,
|
||||
BeingDebugged: u8,
|
||||
BitField: u8,
|
||||
_padding: [4]u8,
|
||||
Mutant: ?PVOID,
|
||||
ImageBaseAddress: ?PVOID,
|
||||
Ldr: ?*PEB_LDR_DATA,
|
||||
ProcessParameters: ?*anyopaque,
|
||||
SubSystemData: ?PVOID,
|
||||
ProcessHeap: ?PVOID,
|
||||
};
|
||||
|
||||
pub const OBJECT_ATTRIBUTES = extern struct {
|
||||
Length: ULONG,
|
||||
RootDirectory: HANDLE,
|
||||
ObjectName: ?*UNICODE_STRING,
|
||||
Attributes: ULONG,
|
||||
SecurityDescriptor: ?PVOID,
|
||||
SecurityQualityOfService: ?PVOID,
|
||||
};
|
||||
|
||||
test "unicode string toSlice handles null buffer" {
|
||||
const empty = UNICODE_STRING{ .Length = 0, .MaximumLength = 0, .Buffer = null };
|
||||
try std.testing.expectEqual(@as(usize, 0), empty.toSlice().len);
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
const std = @import("std");
|
||||
const builtin = @import("builtin");
|
||||
const nt = @import("ntypes.zig");
|
||||
|
||||
pub fn rotrHash(name: []const u8) u32 {
|
||||
@setEvalBranchQuota(10000);
|
||||
var hash: u32 = 0;
|
||||
for (name) |c| {
|
||||
const upper: u32 = if (c >= 'a' and c <= 'z') c - 0x20 else c;
|
||||
hash = std.math.rotr(u32, hash, 13) ^ upper;
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
|
||||
pub fn rotrHashWide(name: []const nt.WCHAR) u32 {
|
||||
var hash: u32 = 0;
|
||||
for (name) |wc| {
|
||||
const c: u32 = @intCast(wc);
|
||||
const upper: u32 = if (c >= 'a' and c <= 'z') c - 0x20 else c;
|
||||
hash = std.math.rotr(u32, hash, 13) ^ upper;
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
|
||||
pub inline fn getPeb() *nt.PEB {
|
||||
if (builtin.os.tag != .windows or builtin.cpu.arch != .x86_64) {
|
||||
@compileError("getPeb is only available on x86_64-windows targets");
|
||||
}
|
||||
|
||||
return asm volatile (
|
||||
\\ movq %%gs:0x60, %[peb]
|
||||
: [peb] "=r" (-> *nt.PEB),
|
||||
);
|
||||
}
|
||||
|
||||
pub fn getModuleByHash(name_hash: u32) ?*nt.LDR_DATA_TABLE_ENTRY {
|
||||
if (builtin.os.tag != .windows or builtin.cpu.arch != .x86_64) return null;
|
||||
|
||||
const peb = getPeb();
|
||||
const ldr = peb.Ldr orelse return null;
|
||||
const head = &ldr.InLoadOrderModuleList;
|
||||
var current = head.Flink;
|
||||
|
||||
while (current) |node| : (current = node.Flink) {
|
||||
if (node == head) break;
|
||||
|
||||
const entry: *nt.LDR_DATA_TABLE_ENTRY = @fieldParentPtr("InLoadOrderLinks", node);
|
||||
if (rotrHashWide(entry.BaseDllName.toSlice()) == name_hash) {
|
||||
return entry;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
pub fn getModuleBase(name_hash: u32) ?[*]u8 {
|
||||
if (builtin.os.tag != .windows or builtin.cpu.arch != .x86_64) return null;
|
||||
|
||||
const entry = getModuleByHash(name_hash) orelse return null;
|
||||
const base = entry.DllBase orelse return null;
|
||||
return @ptrFromInt(@intFromPtr(base));
|
||||
}
|
||||
|
||||
pub fn getExportByHash(module_base: [*]u8, name_hash: u32) ?*anyopaque {
|
||||
if (builtin.os.tag != .windows or builtin.cpu.arch != .x86_64) return null;
|
||||
|
||||
const dos: *const nt.IMAGE_DOS_HEADER = @ptrCast(@alignCast(module_base));
|
||||
if (dos.e_magic != nt.IMAGE_DOS_SIGNATURE or dos.e_lfanew < 0) return null;
|
||||
|
||||
const nt_offset: usize = @intCast(dos.e_lfanew);
|
||||
const nt_headers: *const nt.IMAGE_NT_HEADERS64 = @ptrCast(@alignCast(module_base + nt_offset));
|
||||
if (nt_headers.Signature != nt.IMAGE_NT_SIGNATURE) return null;
|
||||
|
||||
if (nt.IMAGE_DIRECTORY_ENTRY_EXPORT >= nt_headers.OptionalHeader.NumberOfRvaAndSizes) return null;
|
||||
|
||||
const export_dir_entry = nt_headers.OptionalHeader.DataDirectory[nt.IMAGE_DIRECTORY_ENTRY_EXPORT];
|
||||
if (export_dir_entry.VirtualAddress == 0 or export_dir_entry.Size == 0) return null;
|
||||
|
||||
const export_dir: *const nt.IMAGE_EXPORT_DIRECTORY = @ptrCast(@alignCast(module_base + export_dir_entry.VirtualAddress));
|
||||
const names: [*]const nt.DWORD = @ptrCast(@alignCast(module_base + export_dir.AddressOfNames));
|
||||
const ordinals: [*]const nt.WORD = @ptrCast(@alignCast(module_base + export_dir.AddressOfNameOrdinals));
|
||||
const functions: [*]const nt.DWORD = @ptrCast(@alignCast(module_base + export_dir.AddressOfFunctions));
|
||||
|
||||
var i: usize = 0;
|
||||
while (i < export_dir.NumberOfNames) : (i += 1) {
|
||||
const name_ptr: [*:0]const u8 = @ptrCast(module_base + names[i]);
|
||||
if (rotrHash(std.mem.span(name_ptr)) != name_hash) continue;
|
||||
|
||||
const ordinal = ordinals[i];
|
||||
if (ordinal >= export_dir.NumberOfFunctions) return null;
|
||||
|
||||
const function_rva = functions[ordinal];
|
||||
if (function_rva == 0) return null;
|
||||
|
||||
const export_start = export_dir_entry.VirtualAddress;
|
||||
const export_end = export_start + export_dir_entry.Size;
|
||||
if (function_rva >= export_start and function_rva < export_end) return null;
|
||||
|
||||
return @ptrCast(module_base + function_rva);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
test "rotrHash is deterministic" {
|
||||
try std.testing.expectEqual(rotrHash("ntdll.dll"), rotrHash("ntdll.dll"));
|
||||
}
|
||||
|
||||
test "rotrHash is case insensitive" {
|
||||
try std.testing.expectEqual(rotrHash("kernel32.dll"), rotrHash("KERNEL32.DLL"));
|
||||
}
|
||||
|
||||
test "rotrHash differs for different strings" {
|
||||
try std.testing.expect(rotrHash("ntdll.dll") != rotrHash("kernel32.dll"));
|
||||
}
|
||||
|
||||
test "rotrHashWide matches ascii hash" {
|
||||
const wide = [_]nt.WCHAR{ 'n', 't', 'd', 'l', 'l', '.', 'd', 'l', 'l' };
|
||||
try std.testing.expectEqual(rotrHash("ntdll.dll"), rotrHashWide(&wide));
|
||||
}
|
||||
@@ -0,0 +1,528 @@
|
||||
const std = @import("std");
|
||||
const builtin = @import("builtin");
|
||||
const peb = @import("platform/peb.zig");
|
||||
const types = @import("types.zig");
|
||||
const config_mod = @import("config.zig");
|
||||
const gadget_db = @import("gadget_db.zig");
|
||||
const gadget_scanner = @import("gadget_scanner.zig");
|
||||
const gadget_stable = @import("gadget_stable.zig");
|
||||
const trampolines = @import("trampolines.zig");
|
||||
const dll_sideload = @import("dll_sideload.zig");
|
||||
|
||||
const DllTarget = types.DllTarget;
|
||||
const GadgetInfo = types.GadgetInfo;
|
||||
const GadgetRegister = types.GadgetRegister;
|
||||
const MAX_GADGETS = types.MAX_GADGETS;
|
||||
const MAX_PROXY_ARGS = types.MAX_PROXY_ARGS;
|
||||
const ProxyResult = types.ProxyResult;
|
||||
|
||||
pub const GadgetStable = gadget_stable.GadgetStable;
|
||||
pub const PoolProxyConfig = config_mod.PoolProxyConfig;
|
||||
|
||||
// ── Synchronized global state ───────────────────────────────────────────
|
||||
// All mutations to stable/config/initialized are guarded by mutex.
|
||||
// proxyCall() is safe to call from any thread — init() is lazy and
|
||||
// synchronized via std.Thread.Mutex.
|
||||
|
||||
var mutex: std.Thread.Mutex = .{};
|
||||
pub var stable: GadgetStable = GadgetStable.init();
|
||||
pub var config: PoolProxyConfig = .{};
|
||||
var initialized: bool = false;
|
||||
|
||||
// Gadget copy used by proxyForSideload — set by tryAutoLoad before the
|
||||
// sideload call and cleared immediately after. Avoids closure capture.
|
||||
threadlocal var sideload_gadget: ?GadgetInfo = null;
|
||||
|
||||
/// Route a side-effect API call through a pre-selected gadget.
|
||||
/// Called by dll_sideload as ProxyCallFn. Returns null if no gadget is available.
|
||||
fn proxyForSideload(func_addr: usize, args: []const usize) ?usize {
|
||||
const g = &(sideload_gadget orelse return null);
|
||||
const result = executeWithGadget(func_addr, args, g) catch return null;
|
||||
return result.return_value;
|
||||
}
|
||||
|
||||
/// Initialize pool proxy — verifies pre-computed gadgets in already-loaded DLLs.
|
||||
/// Thread-safe: multiple concurrent callers will block until the first completes.
|
||||
pub fn init() void {
|
||||
if (builtin.os.tag != .windows or builtin.cpu.arch != .x86_64) return;
|
||||
|
||||
mutex.lock();
|
||||
defer mutex.unlock();
|
||||
if (initialized) return;
|
||||
|
||||
stable.clear();
|
||||
verifyAndAddFromDb();
|
||||
initialized = true;
|
||||
}
|
||||
|
||||
/// Re-verify all gadgets (e.g., after a DLL load). Thread-safe.
|
||||
pub fn rescan() void {
|
||||
if (builtin.os.tag != .windows or builtin.cpu.arch != .x86_64) return;
|
||||
|
||||
mutex.lock();
|
||||
defer mutex.unlock();
|
||||
stable.clear();
|
||||
verifyAndAddFromDb();
|
||||
}
|
||||
|
||||
/// Verify pre-computed gadgets against loaded DLLs and micro-scan only the
|
||||
/// modules whose pinned RVAs no longer match the current build.
|
||||
fn verifyAndAddFromDb() void {
|
||||
var missed_dlls: [16]u32 = [_]u32{0} ** 16;
|
||||
var missed_count: usize = 0;
|
||||
|
||||
for (&gadget_db.GADGET_DB) |*entry| {
|
||||
const dll_base = peb.getModuleBase(entry.dll_hash) orelse continue;
|
||||
if (trampolines.TrampolineId.forRegister(entry.register) == null) continue;
|
||||
|
||||
if (gadget_db.verifyGadget(entry, dll_base)) {
|
||||
_ = stable.addGadget(gadget_db.toGadgetInfo(entry, dll_base));
|
||||
continue;
|
||||
}
|
||||
|
||||
var already_recorded = false;
|
||||
for (missed_dlls[0..missed_count]) |dll_hash| {
|
||||
if (dll_hash == entry.dll_hash) {
|
||||
already_recorded = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!already_recorded and missed_count < missed_dlls.len) {
|
||||
missed_dlls[missed_count] = entry.dll_hash;
|
||||
missed_count += 1;
|
||||
}
|
||||
}
|
||||
|
||||
for (missed_dlls[0..missed_count]) |dll_hash| {
|
||||
const base = peb.getModuleBase(dll_hash) orelse continue;
|
||||
const dll_name = dbDllName(dll_hash) orelse continue;
|
||||
const scan_results = gadget_scanner.scanDll(base, dll_name);
|
||||
for (&scan_results) |*maybe_gadget| {
|
||||
const gadget = maybe_gadget.* orelse continue;
|
||||
if (trampolines.TrampolineId.forRegister(gadget.register) == null) continue;
|
||||
_ = stable.addGadget(gadget);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Full PE-section scan (slow fallback for discovery on unknown OS builds).
|
||||
/// Use only when the pre-computed DB doesn't have what you need.
|
||||
pub fn fullScan() void {
|
||||
if (builtin.os.tag != .windows or builtin.cpu.arch != .x86_64) return;
|
||||
|
||||
mutex.lock();
|
||||
defer mutex.unlock();
|
||||
stable.clear();
|
||||
verifyAndAddFromDb();
|
||||
const targets = getFullScanTargets();
|
||||
const scan_results = gadget_scanner.scanLoadedDlls(&targets);
|
||||
_ = stable.addFromScan(&scan_results);
|
||||
}
|
||||
|
||||
/// Resolve a gadget for `arg_count` using the configured selection policy.
|
||||
pub fn selectGadgetForArgs(arg_count: usize) ?*const GadgetInfo {
|
||||
if (builtin.os.tag != .windows or builtin.cpu.arch != .x86_64) return null;
|
||||
if (!initialized) init();
|
||||
return resolveGadget(arg_count);
|
||||
}
|
||||
|
||||
pub fn proxyCall(func_addr: usize, args: []const usize) !ProxyResult {
|
||||
if (builtin.os.tag != .windows or builtin.cpu.arch != .x86_64) {
|
||||
return error.UnsupportedPlatform;
|
||||
}
|
||||
if (args.len > MAX_PROXY_ARGS) {
|
||||
return fallbackOrError(func_addr, args, error.TooManyArgs);
|
||||
}
|
||||
if (!initialized) init();
|
||||
|
||||
const g = resolveGadget(args.len) orelse return fallbackOrError(func_addr, args, error.GadgetNotFound);
|
||||
return executeWithGadget(func_addr, args, g);
|
||||
}
|
||||
|
||||
/// Proxy a call through a specific gadget (for testing / explicit gadget selection).
|
||||
pub fn proxyCallWithGadget(func_addr: usize, args: []const usize, gadget: *const GadgetInfo) !ProxyResult {
|
||||
if (builtin.os.tag != .windows or builtin.cpu.arch != .x86_64)
|
||||
return error.UnsupportedPlatform;
|
||||
if (args.len > gadget.maxArgs())
|
||||
return error.TooManyArgs;
|
||||
return executeWithGadget(func_addr, args, gadget);
|
||||
}
|
||||
|
||||
fn executeWithGadget(func_addr: usize, args: []const usize, g: *const GadgetInfo) !ProxyResult {
|
||||
const trampoline_id = trampolines.TrampolineId.forRegister(g.register) orelse
|
||||
return error.NoTrampoline;
|
||||
|
||||
// Validate gadget constraints.
|
||||
const total_pops: usize = @as(usize, g.pops_before) + 1 + @as(usize, g.pops_after);
|
||||
if (total_pops > 16) return error.TooManyPops;
|
||||
if (@as(usize, g.pops_before) + @as(usize, g.pops_after) > 8) return error.TooManyPops;
|
||||
|
||||
// Stack alignment for the trampoline/gadget path is intentionally left as a
|
||||
// documented invariant instead of a hard runtime check; the current verified
|
||||
// gadget set is known-good, and additional math here could reject valid gadgets.
|
||||
|
||||
// New layout: [0]sentinel [1]func [2]gadget [3]cleanup [4]totalPopBytes [5]popMap
|
||||
// [6..13]scratch(8 slots, written by trampoline) [14..]args [14+N]sentinel
|
||||
const SCRATCH_SLOTS = 8;
|
||||
const HEADER_SLOTS = 6;
|
||||
var proxy_struct = [_]usize{0} ** (HEADER_SLOTS + SCRATCH_SLOTS + MAX_PROXY_ARGS + 1);
|
||||
const sentinel = generateSentinel(func_addr, args);
|
||||
proxy_struct[0] = sentinel;
|
||||
proxy_struct[1] = func_addr;
|
||||
proxy_struct[2] = g.addr;
|
||||
proxy_struct[3] = g.cleanup_size;
|
||||
|
||||
// totalPopBytes = (pops_before + 1(target) + pops_after) * 8
|
||||
proxy_struct[4] = total_pops * 8;
|
||||
|
||||
// Use the precomputed popMap: packed nibbles encoding scratch index for each pop slot
|
||||
proxy_struct[5] = @intCast(g.pop_map);
|
||||
|
||||
// scratch[0..7] at indices 6..13 — written by the trampoline at runtime
|
||||
|
||||
// Args start at index 14 (offset 0x70)
|
||||
const args_base = HEADER_SLOTS + SCRATCH_SLOTS;
|
||||
for (args, 0..) |arg, i| {
|
||||
proxy_struct[args_base + i] = arg;
|
||||
}
|
||||
proxy_struct[args_base + args.len] = sentinel;
|
||||
|
||||
const ntdll_hash = comptime peb.rotrHash("ntdll.dll");
|
||||
const ntdll_base = peb.getModuleBase(ntdll_hash) orelse return error.NtdllNotFound;
|
||||
const tp_alloc = peb.getExportByHash(ntdll_base, comptime peb.rotrHash("TpAllocWork")) orelse return error.TpNotFound;
|
||||
const tp_post = peb.getExportByHash(ntdll_base, comptime peb.rotrHash("TpPostWork")) orelse return error.TpNotFound;
|
||||
const tp_wait = peb.getExportByHash(ntdll_base, comptime peb.rotrHash("TpWaitForWork")) orelse return error.TpNotFound;
|
||||
const tp_release = peb.getExportByHash(ntdll_base, comptime peb.rotrHash("TpReleaseWork")) orelse return error.TpNotFound;
|
||||
|
||||
const TpAllocWorkFn = *const fn (*?*anyopaque, *const anyopaque, *anyopaque, ?*anyopaque) callconv(.c) i32;
|
||||
const TpPostWorkFn = *const fn (*anyopaque) callconv(.c) void;
|
||||
const TpWaitForWorkFn = *const fn (*anyopaque, u32) callconv(.c) void;
|
||||
const TpReleaseWorkFn = *const fn (*anyopaque) callconv(.c) void;
|
||||
|
||||
var work: ?*anyopaque = null;
|
||||
const callback_fn = trampoline_id.callback();
|
||||
const status = @as(TpAllocWorkFn, @ptrCast(tp_alloc))(&work, @ptrCast(callback_fn), @ptrCast(&proxy_struct), null);
|
||||
if (status != 0 or work == null) {
|
||||
return fallbackOrError(func_addr, args, error.WorkAllocFailed);
|
||||
}
|
||||
defer @as(TpReleaseWorkFn, @ptrCast(tp_release))(work.?);
|
||||
|
||||
@as(TpPostWorkFn, @ptrCast(tp_post))(work.?);
|
||||
@as(TpWaitForWorkFn, @ptrCast(tp_wait))(work.?, 0);
|
||||
|
||||
const result = proxy_struct[0];
|
||||
@memset(std.mem.asBytes(&proxy_struct), 0);
|
||||
|
||||
return ProxyResult{ .return_value = result, .gadget_used = g };
|
||||
}
|
||||
|
||||
/// Diagnostic helper that reconstructs `g.pop_map` from the stored pop order.
|
||||
/// Runtime execution reads `g.pop_map` directly and does not call this helper.
|
||||
pub fn buildPopMap(g: *const GadgetInfo) u64 {
|
||||
var map: u64 = 0;
|
||||
var shift: u6 = 0;
|
||||
// Pops before target
|
||||
var i: usize = 0;
|
||||
while (i < g.pops_before and i < g.extra_pops.len) : (i += 1) {
|
||||
map |= @as(u64, scratchIndex(g.extra_pops[i])) << shift;
|
||||
shift += 4;
|
||||
}
|
||||
// Target register pop
|
||||
map |= @as(u64, scratchIndex(g.register)) << shift;
|
||||
shift += 4;
|
||||
// Pops after target
|
||||
var j: usize = 0;
|
||||
while (j < g.pops_after and (i + j) < g.extra_pops.len) : (j += 1) {
|
||||
map |= @as(u64, scratchIndex(g.extra_pops[i + j])) << shift;
|
||||
shift += 4;
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
fn scratchIndex(reg: GadgetRegister) u4 {
|
||||
return switch (reg) {
|
||||
.rbx => 0,
|
||||
.rdi => 1,
|
||||
.rsi => 2,
|
||||
.rbp => 3,
|
||||
.r12 => 4,
|
||||
.r13 => 5,
|
||||
.r14 => 6,
|
||||
.r15 => 7,
|
||||
};
|
||||
}
|
||||
|
||||
fn resolveGadget(arg_count: usize) ?*const GadgetInfo {
|
||||
const gadget = selectConfiguredGadget(arg_count);
|
||||
if (gadget != null or config.auto_load_enabled == 0) return gadget;
|
||||
|
||||
// Auto-load mutates stable — synchronize
|
||||
mutex.lock();
|
||||
defer mutex.unlock();
|
||||
// Re-check under lock (another thread may have loaded while we waited)
|
||||
const recheck = selectConfiguredGadget(arg_count);
|
||||
if (recheck != null) return recheck;
|
||||
if (!tryAutoLoad(arg_count)) return null;
|
||||
return selectConfiguredGadget(arg_count);
|
||||
}
|
||||
|
||||
fn selectConfiguredGadget(arg_count: usize) ?*const GadgetInfo {
|
||||
if (config.preferred_register <= 7) {
|
||||
const preferred = switch (config.preferred_register) {
|
||||
0 => GadgetRegister.rbx,
|
||||
1 => GadgetRegister.rdi,
|
||||
2 => GadgetRegister.rsi,
|
||||
3 => GadgetRegister.rbp,
|
||||
4 => GadgetRegister.r12,
|
||||
5 => GadgetRegister.r13,
|
||||
6 => GadgetRegister.r14,
|
||||
7 => GadgetRegister.r15,
|
||||
else => null,
|
||||
} orelse return stable.selectGadget(arg_count, config.random_selection != 0);
|
||||
|
||||
var preferred_hits: [MAX_GADGETS]usize = undefined;
|
||||
var preferred_count: usize = 0;
|
||||
for (stable.gadgets[0..stable.count], 0..) |entry, idx| {
|
||||
const gadget = entry orelse continue;
|
||||
if (gadget.register == preferred and gadget.maxArgs() >= arg_count) {
|
||||
preferred_hits[preferred_count] = idx;
|
||||
preferred_count += 1;
|
||||
}
|
||||
}
|
||||
if (preferred_count > 0) {
|
||||
if (config.random_selection != 0) {
|
||||
const choice = quickRandom() % @as(u32, @intCast(preferred_count));
|
||||
return &(stable.gadgets[preferred_hits[choice]].?);
|
||||
}
|
||||
var best_idx = preferred_hits[0];
|
||||
for (preferred_hits[1..preferred_count]) |idx| {
|
||||
const current = stable.gadgets[idx].?;
|
||||
const best = stable.gadgets[best_idx].?;
|
||||
const current_total_pops = current.pops_before + current.pops_after;
|
||||
const best_total_pops = best.pops_before + best.pops_after;
|
||||
if (current.cleanup_size < best.cleanup_size or
|
||||
(current.cleanup_size == best.cleanup_size and current_total_pops < best_total_pops))
|
||||
{
|
||||
best_idx = idx;
|
||||
}
|
||||
}
|
||||
return &(stable.gadgets[best_idx].?);
|
||||
}
|
||||
}
|
||||
return stable.selectGadget(arg_count, config.random_selection != 0);
|
||||
}
|
||||
|
||||
fn tryAutoLoad(needed_args: usize) bool {
|
||||
for (&gadget_db.GADGET_DB) |*entry| {
|
||||
if (entry.max_args < needed_args) continue;
|
||||
if (entry.cleanup_size < config.min_auto_load_cleanup) continue;
|
||||
if (entry.sideload == .none) continue;
|
||||
if (stable.isDllLoaded(entry.dll_hash)) continue;
|
||||
|
||||
var dll_base = peb.getModuleBase(entry.dll_hash);
|
||||
if (dll_base == null) {
|
||||
// Copy a proxy gadget by value before releasing the mutex, so the
|
||||
// sideload call itself goes through the pool proxy path.
|
||||
sideload_gadget = if (stable.selectGadget(7, false)) |g| g.* else null;
|
||||
|
||||
// Release mutex for the sideload call — the side-effect API may
|
||||
// trigger DllMain / COM class factory code that re-enters us.
|
||||
mutex.unlock();
|
||||
const loaded = dll_sideload.ensureDllLoaded(
|
||||
entry.dll_name,
|
||||
entry.sideload,
|
||||
if (sideload_gadget != null) &proxyForSideload else null,
|
||||
);
|
||||
sideload_gadget = null;
|
||||
mutex.lock();
|
||||
|
||||
if (!loaded) {
|
||||
stable.recordDllLoad(entry.dll_hash);
|
||||
continue;
|
||||
}
|
||||
dll_base = peb.getModuleBase(entry.dll_hash);
|
||||
}
|
||||
|
||||
stable.recordDllLoad(entry.dll_hash);
|
||||
const base = dll_base orelse continue;
|
||||
const dll_name = dbDllName(entry.dll_hash) orelse continue;
|
||||
|
||||
for (&gadget_db.GADGET_DB) |*db_entry| {
|
||||
if (db_entry.dll_hash != entry.dll_hash) continue;
|
||||
if (!gadget_db.verifyGadget(db_entry, base)) continue;
|
||||
if (trampolines.TrampolineId.forRegister(db_entry.register) == null) continue;
|
||||
_ = stable.addGadget(gadget_db.toGadgetInfo(db_entry, base));
|
||||
}
|
||||
|
||||
if (stable.maxSupportedArgs() < needed_args) {
|
||||
const scan_results = gadget_scanner.scanDll(base, dll_name);
|
||||
for (&scan_results) |*maybe_gadget| {
|
||||
const gadget = maybe_gadget.* orelse continue;
|
||||
if (trampolines.TrampolineId.forRegister(gadget.register) == null) continue;
|
||||
_ = stable.addGadget(gadget);
|
||||
}
|
||||
}
|
||||
|
||||
if (stable.maxSupportedArgs() >= needed_args) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
fn generateSentinel(func_addr: usize, args: []const usize) usize {
|
||||
var sentinel = func_addr ^ 0xA5A5A5A55A5A5A5A;
|
||||
if (sentinel == 0) sentinel = 0x1122334455667788;
|
||||
|
||||
while (true) {
|
||||
if (sentinel == func_addr) {
|
||||
sentinel +%= 0x9e3779b97f4a7c15;
|
||||
continue;
|
||||
}
|
||||
|
||||
var clash = false;
|
||||
for (args) |arg| {
|
||||
if (arg == sentinel) {
|
||||
clash = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!clash) return sentinel;
|
||||
sentinel +%= 0x9e3779b97f4a7c15;
|
||||
}
|
||||
}
|
||||
|
||||
fn fallbackOrError(func_addr: usize, args: []const usize, err_tag: anyerror) !ProxyResult {
|
||||
recordFallback(func_addr);
|
||||
return switch (config.fallback_strategy) {
|
||||
.err => err_tag,
|
||||
.tp_delegation, .winapi => ProxyResult{ .return_value = try directInvoke(func_addr, args), .gadget_used = null },
|
||||
};
|
||||
}
|
||||
|
||||
fn recordFallback(func_addr: usize) void {
|
||||
if (config.log_fallbacks == 0) return;
|
||||
config.fallback_count +%= 1;
|
||||
@memset(&config.last_fallback_api, 0);
|
||||
_ = std.fmt.bufPrint(&config.last_fallback_api, "0x{x}", .{func_addr}) catch {};
|
||||
}
|
||||
|
||||
fn directInvoke(func_addr: usize, args: []const usize) !usize {
|
||||
return switch (args.len) {
|
||||
0 => @as(*const fn () callconv(.c) usize, @ptrFromInt(func_addr))(),
|
||||
1 => @as(*const fn (usize) callconv(.c) usize, @ptrFromInt(func_addr))(args[0]),
|
||||
2 => @as(*const fn (usize, usize) callconv(.c) usize, @ptrFromInt(func_addr))(args[0], args[1]),
|
||||
3 => @as(*const fn (usize, usize, usize) callconv(.c) usize, @ptrFromInt(func_addr))(args[0], args[1], args[2]),
|
||||
4 => @as(*const fn (usize, usize, usize, usize) callconv(.c) usize, @ptrFromInt(func_addr))(args[0], args[1], args[2], args[3]),
|
||||
5 => @as(*const fn (usize, usize, usize, usize, usize) callconv(.c) usize, @ptrFromInt(func_addr))(args[0], args[1], args[2], args[3], args[4]),
|
||||
6 => @as(*const fn (usize, usize, usize, usize, usize, usize) callconv(.c) usize, @ptrFromInt(func_addr))(args[0], args[1], args[2], args[3], args[4], args[5]),
|
||||
7 => @as(*const fn (usize, usize, usize, usize, usize, usize, usize) callconv(.c) usize, @ptrFromInt(func_addr))(args[0], args[1], args[2], args[3], args[4], args[5], args[6]),
|
||||
8 => @as(*const fn (usize, usize, usize, usize, usize, usize, usize, usize) callconv(.c) usize, @ptrFromInt(func_addr))(args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7]),
|
||||
else => error.FallbackFailed,
|
||||
};
|
||||
}
|
||||
|
||||
fn dbDllName(dll_hash: u32) ?[]const u8 {
|
||||
for (&gadget_db.GADGET_DB) |*entry| {
|
||||
if (entry.dll_hash == dll_hash) return entry.dll_name;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
fn quickRandom() u32 {
|
||||
return @as(u32, @truncate(@as(u64, @intCast(std.time.nanoTimestamp()))));
|
||||
}
|
||||
|
||||
fn getFullScanTargets() [6]DllTarget {
|
||||
return .{
|
||||
.{ .hash = comptime peb.rotrHash("ntdll.dll"), .name = "ntdll.dll", .sideload = .none },
|
||||
.{ .hash = comptime peb.rotrHash("kernel32.dll"), .name = "kernel32.dll", .sideload = .none },
|
||||
.{ .hash = comptime peb.rotrHash("kernelbase.dll"), .name = "kernelbase.dll", .sideload = .none },
|
||||
.{ .hash = comptime peb.rotrHash("combase.dll"), .name = "combase.dll", .sideload = .none },
|
||||
.{ .hash = comptime peb.rotrHash("ole32.dll"), .name = "ole32.dll", .sideload = .none },
|
||||
.{ .hash = comptime peb.rotrHash("rpcrt4.dll"), .name = "rpcrt4.dll", .sideload = .none },
|
||||
};
|
||||
}
|
||||
|
||||
test "sentinel avoids collisions" {
|
||||
const args = [_]usize{ 1, 2, 3, 4 };
|
||||
const sentinel = generateSentinel(3, &args);
|
||||
try std.testing.expect(sentinel != 3);
|
||||
for (args) |arg| try std.testing.expect(sentinel != arg);
|
||||
}
|
||||
|
||||
test "buildPopMap matches stored pop_map" {
|
||||
const extra_pops = [_]GadgetRegister{ .r15, .r14, .rdi, .rsi, .rbp, .rbx, .rbx, .rbx };
|
||||
const gadget = GadgetInfo{
|
||||
.addr = 0x1000,
|
||||
.cleanup_size = 0x78,
|
||||
.cleanup_type = .imm8,
|
||||
.register = .rbx,
|
||||
.dll_base = 0x1000,
|
||||
.dll_name = [_]u8{0} ** 48,
|
||||
.func_name = [_]u8{0} ** 64,
|
||||
.func_rva = 0,
|
||||
.pops_before = 4,
|
||||
.pops_after = 1,
|
||||
.extra_pops = extra_pops,
|
||||
.pop_map = 0x302167,
|
||||
};
|
||||
try std.testing.expectEqual(gadget.pop_map, buildPopMap(&gadget));
|
||||
}
|
||||
|
||||
test "proxyForSideload returns null without sideload_gadget" {
|
||||
sideload_gadget = null;
|
||||
const result = proxyForSideload(0x1234, &[_]usize{ 1, 2, 3 });
|
||||
try std.testing.expect(result == null);
|
||||
}
|
||||
|
||||
test "proxyForSideload proxies GetCurrentProcessId" {
|
||||
if (builtin.os.tag != .windows or builtin.cpu.arch != .x86_64) return;
|
||||
if (!initialized) init();
|
||||
|
||||
const g = stable.selectGadget(1, false) orelse return;
|
||||
sideload_gadget = g.*;
|
||||
defer {
|
||||
sideload_gadget = null;
|
||||
}
|
||||
|
||||
const k32 = peb.getModuleBase(comptime peb.rotrHash("kernel32.dll")) orelse return;
|
||||
const get_pid = peb.getExportByHash(k32, comptime peb.rotrHash("GetCurrentProcessId")) orelse return;
|
||||
|
||||
const result = proxyForSideload(@intFromPtr(get_pid), &[_]usize{});
|
||||
try std.testing.expect(result != null);
|
||||
try std.testing.expect(result.? != 0);
|
||||
}
|
||||
|
||||
test "init populates stable with gadgets" {
|
||||
if (builtin.os.tag != .windows or builtin.cpu.arch != .x86_64) return;
|
||||
if (!initialized) init();
|
||||
|
||||
// ntdll is always loaded — we should have at least one gadget
|
||||
try std.testing.expect(stable.count > 0);
|
||||
try std.testing.expect(stable.maxSupportedArgs() >= 4);
|
||||
}
|
||||
|
||||
test "proxied sideload loads Chakra via COM" {
|
||||
if (builtin.os.tag != .windows or builtin.cpu.arch != .x86_64) return;
|
||||
if (!initialized) init();
|
||||
|
||||
// Need a gadget that can handle CoCreateInstance (5 args)
|
||||
const g = stable.selectGadget(5, false) orelse return;
|
||||
sideload_gadget = g.*;
|
||||
defer {
|
||||
sideload_gadget = null;
|
||||
}
|
||||
|
||||
// Skip if Chakra is already loaded (prior test or system state)
|
||||
const chakra_hash = comptime peb.rotrHash("Chakra.dll");
|
||||
if (peb.getModuleBase(chakra_hash) != null) return;
|
||||
|
||||
// Skip if combase isn't loaded (prerequisite for COM activation)
|
||||
if (peb.getModuleBase(comptime peb.rotrHash("combase.dll")) == null) return;
|
||||
|
||||
const loaded = dll_sideload.ensureDllLoaded("Chakra.dll", .com_chakra, &proxyForSideload);
|
||||
// Chakra COM class may not be registered on all builds — verify the proxy
|
||||
// path ran without crashing. If it did load, confirm PEB has it.
|
||||
if (loaded) {
|
||||
try std.testing.expect(peb.getModuleBase(chakra_hash) != null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
const std = @import("std");
|
||||
const types = @import("types.zig");
|
||||
|
||||
const GadgetRegister = types.GadgetRegister;
|
||||
const MAX_PROXY_ARGS = types.MAX_PROXY_ARGS;
|
||||
|
||||
pub const TpWorkCallbackFn = *const fn (?*anyopaque, ?*anyopaque, ?*anyopaque) callconv(.c) void;
|
||||
|
||||
pub const TrampolineId = enum(u8) {
|
||||
rbx = 0,
|
||||
rdi = 1,
|
||||
rsi = 2,
|
||||
r14 = 3,
|
||||
|
||||
pub fn forRegister(reg: GadgetRegister) ?TrampolineId {
|
||||
return switch (reg) {
|
||||
.rbx => .rbx,
|
||||
.rdi => .rdi,
|
||||
.rsi => .rsi,
|
||||
.r14 => .r14,
|
||||
else => null,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn callback(self: TrampolineId) TpWorkCallbackFn {
|
||||
return switch (self) {
|
||||
.rbx => @ptrCast(&tpTrampolineRbx),
|
||||
.rdi => @ptrCast(&tpTrampolineRdi),
|
||||
.rsi => @ptrCast(&tpTrampolineRsi),
|
||||
.r14 => @ptrCast(&tpTrampolineR14),
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// Clean-callstack thread-pool trampoline.
|
||||
//
|
||||
// The callback saves every callee-saved register into the proxy scratch area,
|
||||
// expands the stack for the gadget's cleanup and pop chain, and rebuilds the
|
||||
// pop slots from `pop_map`. That lets the gadget restore the right register
|
||||
// values and return directly back into the thread-pool dispatcher without
|
||||
// placing an implant return address on the stack.
|
||||
|
||||
fn trampolineAsm(comptime reg: []const u8) []const u8 {
|
||||
comptime var code: []const u8 =
|
||||
// Save callee-saved regs into proxy struct scratch [rdx+0x30..0x68]
|
||||
"mov %%rbx, 0x30(%%rdx)\n\t" ++
|
||||
"mov %%rdi, 0x38(%%rdx)\n\t" ++
|
||||
"mov %%rsi, 0x40(%%rdx)\n\t" ++
|
||||
"mov %%rbp, 0x48(%%rdx)\n\t" ++
|
||||
"mov %%r12, 0x50(%%rdx)\n\t" ++
|
||||
"mov %%r13, 0x58(%%rdx)\n\t" ++
|
||||
"mov %%r14, 0x60(%%rdx)\n\t" ++
|
||||
"mov %%r15, 0x68(%%rdx)\n\t" ++
|
||||
// Set up REG as proxy struct pointer
|
||||
"mov %%rdx, %%" ++ reg ++ "\n\t" ++
|
||||
// Pop TP return addr into r10
|
||||
"pop %%r10\n\t" ++
|
||||
// Allocate totalPopBytes + 8 (for TP ret slot above pop area)
|
||||
"mov 0x20(%%" ++ reg ++ "), %%r11\n\t" ++
|
||||
"add $8, %%r11\n\t" ++
|
||||
"sub %%r11, %%rsp\n\t" ++
|
||||
// Write TP return at top of allocated area: [RSP + totalPopBytes]
|
||||
"mov 0x20(%%" ++ reg ++ "), %%r11\n\t" ++
|
||||
"mov %%r10, (%%rsp,%%r11)\n\t" ++
|
||||
// Fill pop slots from scratch via popMap loop
|
||||
"mov 0x28(%%" ++ reg ++ "), %%rax\n\t" ++ // popMap
|
||||
"mov 0x20(%%" ++ reg ++ "), %%rcx\n\t" ++ // totalPopBytes
|
||||
"shr $3, %%rcx\n\t" ++ // pop count
|
||||
"test %%rcx, %%rcx\n\t" ++
|
||||
"jz 2f\n\t" ++
|
||||
"xor %%r8, %%r8\n\t" ++ // slot counter
|
||||
"3:\n\t" ++
|
||||
"mov %%rax, %%r9\n\t" ++
|
||||
"and $0xf, %%r9\n\t" ++ // nibble = scratch index
|
||||
"mov 0x30(%%" ++ reg ++ ",%%r9,8), %%r9\n\t" ++ // scratch[index]
|
||||
"mov %%r9, (%%rsp,%%r8,8)\n\t" ++ // popSlot[i] = value
|
||||
"shr $4, %%rax\n\t" ++
|
||||
"inc %%r8\n\t" ++
|
||||
"cmp %%rcx, %%r8\n\t" ++
|
||||
"jb 3b\n\t" ++
|
||||
"2:\n\t" ++
|
||||
// Allocate cleanupSize
|
||||
"mov 0x18(%%" ++ reg ++ "), %%r10\n\t" ++
|
||||
"sub %%r10, %%rsp\n\t" ++
|
||||
// Load sentinel and funcAddr
|
||||
"mov (%%" ++ reg ++ "), %%r11\n\t" ++
|
||||
"mov 0x08(%%" ++ reg ++ "), %%rax\n\t" ++
|
||||
// Register args (args at +0x70 after scratch area)
|
||||
"cmp %%r11, 0x70(%%" ++ reg ++ ")\n\tje 1f\n\t" ++
|
||||
"mov 0x70(%%" ++ reg ++ "), %%rcx\n\t" ++
|
||||
"cmp %%r11, 0x78(%%" ++ reg ++ ")\n\tje 1f\n\t" ++
|
||||
"mov 0x78(%%" ++ reg ++ "), %%rdx\n\t" ++
|
||||
"cmp %%r11, 0x80(%%" ++ reg ++ ")\n\tje 1f\n\t" ++
|
||||
"mov 0x80(%%" ++ reg ++ "), %%r8\n\t" ++
|
||||
"cmp %%r11, 0x88(%%" ++ reg ++ ")\n\tje 1f\n\t" ++
|
||||
"mov 0x88(%%" ++ reg ++ "), %%r9\n\t";
|
||||
|
||||
// Stack args
|
||||
inline for (4..MAX_PROXY_ARGS) |arg_index| {
|
||||
const proxy_offset = 0x70 + arg_index * 8;
|
||||
const stack_offset = 0x20 + (arg_index - 4) * 8;
|
||||
code = code ++ std.fmt.comptimePrint(
|
||||
"cmp %%r11, 0x{x}(%%{s})\n\tje 1f\n\tmov 0x{x}(%%{s}), %%r10\n\tmov %%r10, 0x{x}(%%rsp)\n\t",
|
||||
.{ proxy_offset, reg, proxy_offset, reg, stack_offset },
|
||||
);
|
||||
}
|
||||
|
||||
return code ++
|
||||
"1:\n\t" ++
|
||||
std.fmt.comptimePrint("push 0x10(%%{s})\n\tjmp *%%rax\n\t", .{reg});
|
||||
}
|
||||
|
||||
fn tpTrampolineRbx(_: ?*anyopaque, _: ?*anyopaque, _: ?*anyopaque) callconv(.naked) void {
|
||||
asm volatile (trampolineAsm("rbx"));
|
||||
}
|
||||
|
||||
fn tpTrampolineRdi(_: ?*anyopaque, _: ?*anyopaque, _: ?*anyopaque) callconv(.naked) void {
|
||||
asm volatile (trampolineAsm("rdi"));
|
||||
}
|
||||
|
||||
fn tpTrampolineRsi(_: ?*anyopaque, _: ?*anyopaque, _: ?*anyopaque) callconv(.naked) void {
|
||||
asm volatile (trampolineAsm("rsi"));
|
||||
}
|
||||
|
||||
fn tpTrampolineR14(_: ?*anyopaque, _: ?*anyopaque, _: ?*anyopaque) callconv(.naked) void {
|
||||
asm volatile (trampolineAsm("r14"));
|
||||
}
|
||||
|
||||
test "only supported registers map to trampolines" {
|
||||
try std.testing.expectEqual(TrampolineId.rbx, TrampolineId.forRegister(.rbx).?);
|
||||
try std.testing.expect(TrampolineId.forRegister(.rbp) == null);
|
||||
}
|
||||
+174
@@ -0,0 +1,174 @@
|
||||
const std = @import("std");
|
||||
|
||||
/// Callee-saved register used by a gadget to hold the proxy-structure pointer.
|
||||
/// The trampoline must match this register exactly.
|
||||
pub const GadgetRegister = enum(u8) {
|
||||
rbx = 0,
|
||||
rdi = 1,
|
||||
rsi = 2,
|
||||
rbp = 3,
|
||||
r12 = 4,
|
||||
r13 = 5,
|
||||
r14 = 6,
|
||||
r15 = 7,
|
||||
|
||||
pub fn name(self: GadgetRegister) []const u8 {
|
||||
return switch (self) {
|
||||
.rbx => "rbx",
|
||||
.rdi => "rdi",
|
||||
.rsi => "rsi",
|
||||
.rbp => "rbp",
|
||||
.r12 => "r12",
|
||||
.r13 => "r13",
|
||||
.r14 => "r14",
|
||||
.r15 => "r15",
|
||||
};
|
||||
}
|
||||
|
||||
/// Return the one-byte `pop` opcode when the register has one.
|
||||
pub fn popOpcode(self: GadgetRegister) ?u8 {
|
||||
return switch (self) {
|
||||
.rbx => 0x5b,
|
||||
.rdi => 0x5f,
|
||||
.rsi => 0x5e,
|
||||
.rbp => 0x5d,
|
||||
.r12, .r13, .r14, .r15 => null,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/// Encoding used by the gadget's `add rsp, N` instruction.
|
||||
pub const CleanupType = enum(u8) {
|
||||
imm8 = 0,
|
||||
imm32 = 1,
|
||||
};
|
||||
|
||||
/// Runtime metadata for a verified gadget.
|
||||
pub const GadgetInfo = struct {
|
||||
/// Virtual address of the gadget in the current process.
|
||||
addr: usize,
|
||||
/// Stack bytes consumed by `add rsp, N` before the pop chain executes.
|
||||
cleanup_size: usize,
|
||||
/// Encoding form of `cleanup_size` in the original instruction stream.
|
||||
cleanup_type: CleanupType,
|
||||
/// Callee-saved register that carries the proxy-structure pointer.
|
||||
register: GadgetRegister,
|
||||
/// Base address of the containing module.
|
||||
dll_base: usize,
|
||||
/// Zero-terminated module name for diagnostics and JSON export.
|
||||
dll_name: [48]u8,
|
||||
/// Zero-terminated export name associated with the gadget location.
|
||||
func_name: [64]u8,
|
||||
/// RVA of the containing export, used to report the gadget offset.
|
||||
func_rva: u32,
|
||||
/// Count of pop instructions that execute before the target register pop.
|
||||
pops_before: u8,
|
||||
/// Count of pop instructions that execute after the target register pop.
|
||||
pops_after: u8,
|
||||
/// Ordered non-target pop registers. This is kept for diagnostics and for
|
||||
/// recomputing `pop_map`; runtime trampoline selection uses `pop_map`.
|
||||
extra_pops: [8]GadgetRegister,
|
||||
/// Packed nibble map from pop-slot index to saved-register scratch slot.
|
||||
pop_map: u64,
|
||||
|
||||
/// Maximum supported argument count: `(cleanup_size - 0x20) / 8 + 4`.
|
||||
pub fn maxArgs(self: GadgetInfo) usize {
|
||||
if (self.cleanup_size < 0x20) return 4;
|
||||
return (self.cleanup_size -| 0x20) / 8 + 4;
|
||||
}
|
||||
|
||||
/// Relative virtual address of the gadget.
|
||||
pub fn rva(self: GadgetInfo) usize {
|
||||
return self.addr - self.dll_base;
|
||||
}
|
||||
|
||||
/// Offset of the gadget from the start of its containing export.
|
||||
pub fn gadgetOffset(self: GadgetInfo) usize {
|
||||
return self.rva() - @as(usize, self.func_rva);
|
||||
}
|
||||
|
||||
/// Slice view of `dll_name` up to the first terminator.
|
||||
pub fn dllNameSlice(self: *const GadgetInfo) []const u8 {
|
||||
for (self.dll_name, 0..) |c, i| {
|
||||
if (c == 0) return self.dll_name[0..i];
|
||||
}
|
||||
return &self.dll_name;
|
||||
}
|
||||
|
||||
/// Slice view of `func_name` up to the first terminator.
|
||||
pub fn funcNameSlice(self: *const GadgetInfo) []const u8 {
|
||||
for (self.func_name, 0..) |c, i| {
|
||||
if (c == 0) return self.func_name[0..i];
|
||||
}
|
||||
return &self.func_name;
|
||||
}
|
||||
};
|
||||
|
||||
/// Loader-free side-effect used to make a target DLL appear in the process.
|
||||
pub const SideloadMethod = enum(u8) {
|
||||
/// Module is expected to already be mapped.
|
||||
none = 0,
|
||||
/// CoCreateInstance on a CLSID backed by `cdp.dll`.
|
||||
com_cdp = 1,
|
||||
/// CoCreateInstance on a CLSID backed by `Chakra.dll`.
|
||||
com_chakra = 2,
|
||||
/// CoCreateInstance on a CLSID backed by `mshtml.dll`.
|
||||
com_mshtml = 3,
|
||||
/// CoCreateInstance on a CLSID backed by `oleaut32.dll`.
|
||||
com_oleaut32 = 4,
|
||||
/// CoCreateInstance on a CLSID backed by `shell32.dll`.
|
||||
com_shell32 = 5,
|
||||
/// CoCreateInstance on a CLSID backed by `jscript9.dll`.
|
||||
com_jscript9 = 6,
|
||||
/// CryptAcquireContextW provider chain.
|
||||
crypt_context = 9,
|
||||
/// CM_Get_Device_ID_List_SizeW dependency chain.
|
||||
cm_device_list = 10,
|
||||
/// Winsock startup dependency chain.
|
||||
winsock_startup = 11,
|
||||
|
||||
pub fn prerequisiteDll(self: SideloadMethod) []const u8 {
|
||||
return switch (self) {
|
||||
.none => "",
|
||||
.com_cdp, .com_chakra, .com_mshtml, .com_oleaut32, .com_shell32, .com_jscript9 => "combase.dll",
|
||||
.crypt_context => "advapi32.dll",
|
||||
.cm_device_list => "cfgmgr32.dll",
|
||||
.winsock_startup => "ws2_32.dll",
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/// One scan target and the side-effect method that can make it available.
|
||||
pub const DllTarget = struct {
|
||||
hash: u32,
|
||||
name: []const u8,
|
||||
sideload: SideloadMethod,
|
||||
};
|
||||
|
||||
/// Action to take when proxy execution cannot use a gadget.
|
||||
pub const FallbackStrategy = enum(u8) {
|
||||
/// Execute via thread pool delegation without gadget-based callstack masking.
|
||||
tp_delegation = 0,
|
||||
/// Execute via direct WinAPI call (no thread pool, no gadget).
|
||||
winapi = 1,
|
||||
/// Return error without executing. Use when silent failure is preferred.
|
||||
err = 2,
|
||||
};
|
||||
|
||||
/// Result of a proxied call.
|
||||
pub const ProxyResult = struct {
|
||||
/// Raw return value from the target function.
|
||||
return_value: usize,
|
||||
/// Gadget used for the call, if execution stayed on the proxy path.
|
||||
gadget_used: ?*const GadgetInfo,
|
||||
};
|
||||
|
||||
/// Maximum gadgets retained in the stable state.
|
||||
pub const MAX_GADGETS: usize = 64;
|
||||
|
||||
/// Maximum arguments that fit in the proxy-structure buffer.
|
||||
pub const MAX_PROXY_ARGS: usize = 48;
|
||||
|
||||
test "gadget register names are stable" {
|
||||
try std.testing.expectEqualStrings("r14", GadgetRegister.r14.name());
|
||||
}
|
||||
Reference in New Issue
Block a user