Files
nbaertsch-COMegon/comegon.zig
T
nbaertschandCopilot 0d346f2c1b Refactor: caller-is-STA pump (eliminate dedicated STA thread)
Major architecture change — the calling thread IS now the STA:
- init() does CoInitializeEx(APARTMENTTHREADED) on caller
- CoIncrementMTAUsage pins MTA alive (no persistent MTA thread)
- Short-lived MTA thread unmarshals, exits completely
- pump() builds ModalLoop fiber + dispatches directly on caller
- Worker posts all messages, exits before dispatch begins
- fCoWaitCalled=1 ensures ModalLoop dispatches pre-queued messages

Removed:
- Dedicated STA thread + command loop
- StaCommand enum, command_event, completion_event
- sta_ready, unmarshal_done, pump_ready_event
- sta_handle, sta_main_fiber

Added:
- caller_fiber, mta_usage_cookie fields
- CoDecrementMTAUsage in deinit()
- Re-marshal/re-unmarshal per pump cycle (fresh IPIDs)
- NtWaitForSingleObject for non-pumping waits on STA

Result: single thread during sleep, system-only call stack.
Tested: 5000ms MsgWait sleep, exit 0.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-10 17:05:57 -04:00

1739 lines
74 KiB
Zig

// COMegon — Dual-mode COM dispatch framework
//
// Sleep mode (pump): batch fire-and-forget via PostCall, zero user code on ANY stack
// Proxy mode (invoke): single synchronous call via SendReceive, clean STA callstack
//
// Both modes share COM apartments, MIDL tables, proxy/stub, and STA fiber pump.
// Pump fiber proc = combase!ModalLoop (system code). All dispatches via NdrStubCall2.
const std = @import("std");
const print = std.debug.print;
/// Set to true for verbose debug output during development.
/// Production builds should leave this false.
const DEBUG = false;
fn log(comptime fmt: []const u8, args: anytype) void {
if (DEBUG) print(fmt, args);
}
// ============================================================================
// Win32 imports
// ============================================================================
extern "kernel32" fn GetModuleHandleA(name: ?[*:0]const u8) callconv(.c) ?*anyopaque;
extern "kernel32" fn GetProcAddress(m: ?*anyopaque, n: [*:0]const u8) callconv(.c) ?*const fn () callconv(.c) isize;
extern "kernel32" fn LoadLibraryA(n: [*:0]const u8) callconv(.c) ?*anyopaque;
extern "kernel32" fn GetProcessHeap() callconv(.c) ?*anyopaque;
extern "kernel32" fn HeapAlloc(heap: ?*anyopaque, flags: u32, size: usize) callconv(.c) ?*anyopaque;
extern "kernel32" fn HeapFree(heap: ?*anyopaque, flags: u32, ptr: ?*anyopaque) callconv(.c) i32;
extern "kernel32" fn CreateThread(sa: ?*anyopaque, stack: usize, start: *const fn (?*anyopaque) callconv(.c) u32, param: ?*anyopaque, flags: u32, id: ?*u32) callconv(.c) ?*anyopaque;
extern "kernel32" fn WaitForSingleObject(h: ?*anyopaque, ms: u32) callconv(.c) u32;
extern "kernel32" fn WaitForMultipleObjects(n: u32, handles: [*]const ?*anyopaque, wait_all: i32, ms: u32) callconv(.c) u32;
extern "kernel32" fn CreateEventA(sa: ?*anyopaque, manual: i32, initial: i32, name: ?[*:0]const u8) callconv(.c) ?*anyopaque;
extern "kernel32" fn SetEvent(h: ?*anyopaque) callconv(.c) i32;
extern "kernel32" fn ResetEvent(h: ?*anyopaque) callconv(.c) i32;
extern "kernel32" fn CloseHandle(h: ?*anyopaque) callconv(.c) i32;
extern "kernel32" fn Sleep(ms: u32) callconv(.c) void;
extern "kernel32" fn ConvertThreadToFiber(param: ?*anyopaque) callconv(.c) ?*anyopaque;
extern "kernel32" fn CreateFiber(stack_size: usize, start: *const anyopaque, param: ?*anyopaque) callconv(.c) ?*anyopaque;
extern "kernel32" fn SwitchToFiber(fiber: ?*anyopaque) callconv(.c) void;
extern "kernel32" fn DeleteFiber(fiber: ?*anyopaque) callconv(.c) void;
extern "kernel32" fn ConvertFiberToThread() callconv(.c) i32;
extern "kernel32" fn GetCurrentProcessId() callconv(.c) u32;
extern "kernel32" fn GetTickCount64() callconv(.c) u64;
// ============================================================================
// Constants
// ============================================================================
const HRESULT = i32;
const S_OK: HRESULT = 0;
const E_NOINTERFACE: HRESULT = @bitCast(@as(u32, 0x80004002));
const E_FAIL: HRESULT = @bitCast(@as(u32, 0x80004005));
const REGCLS_MULTIPLEUSE: u32 = 1;
const CLSCTX_INPROC_SERVER: u32 = 1;
const COINIT_APARTMENTTHREADED: u32 = 0x2;
const COINIT_MULTITHREADED: u32 = 0x0;
const HEAP_ZERO_MEMORY: u32 = 0x08;
const INFINITE: u32 = 0xFFFFFFFF;
const FC_LONG: u8 = 0x08;
const FC_HYPER: u8 = 0x0B;
const VTBL_INVOKE: usize = 5;
const RESERVED_METHODS: u16 = 7; // COM IUnknown + padding slots 0-6
// ============================================================================
// COM GUIDs
// ============================================================================
const IID_IFiberDispatch = [16]u8{ 0xA1, 0xB2, 0xC3, 0xD4, 0xE5, 0xF6, 0x07, 0x18, 0x92, 0xA3, 0xB4, 0xC5, 0xD6, 0xE7, 0xF8, 0x09 };
// Default CLSID: {A1FA0000-A1FA-A1FA-A1FA-A1FA00A1FA00} — "ALPHA" (Alpha Legion).
// Intentionally obvious dev CLSID. This is the proxy/stub factory CLSID registered via CoRegisterClassObject during init.
// It must NOT match any CLSID registered in the host process's registry, or COM's PS
// factory resolution will load the real DLL instead of our in-process factory, causing
// E_NOINTERFACE on marshal. This rules out reusing any real registered CLSID.
//
// For stealth deployments, replace with an unregistered CLSID that blends into a
// legitimate Windows subsystem range. Verified candidate ranges (Win11 24H2):
//
// {00BB2766-6A77-11D0-A535-00C04FD7D062} shell32 (adjacent to ShellDesktop/ShellFSFolder)
// {0340F119-A598-4ED9-B0AC-6F6A12D3E756} propsys (adjacent to InMemoryPropertyStore)
// {01E04581-4EEE-11D0-BFE9-00AA005B4384} explorerframe (adjacent to ExplorerBand)
// {33C53A50-F456-4884-B049-85FD643ECFEE} msctf (adjacent to TF_ThreadMgr) — highest confidence
// {C90250F3-4D7D-4991-9B69-A5C5BC1C2AE7} actxprxy (adjacent to ActXProxy)
//
// Selection criteria: the CLSID must (1) not exist in HKCR\CLSID on the target OS,
// (2) not be in the Microsoft OLE range {000003xx-0000-0000-C000-000000000046} which
// has special internal handling in combase.dll, and (3) ideally follow the GUID pattern
// of a real system DLL to look plausible in ETW CoRegisterClassObject events.
const CLSID_FiberDispatchPS = [16]u8{ 0x00, 0x00, 0xFA, 0xA1, 0xFA, 0xA1, 0xFA, 0xA1, 0xA1, 0xFA, 0xA1, 0xFA, 0x00, 0xA1, 0xFA, 0x00 };
const IID_IUnknown = [16]u8{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46 };
const IID_IPSFactoryBuffer = [16]u8{ 0xD0, 0x69, 0xF5, 0xD5, 0x3B, 0x59, 0x1A, 0x10, 0xB5, 0x69, 0x08, 0x00, 0x2B, 0x2D, 0xBF, 0x7A };
const IID_IClassFactory = [16]u8{ 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46 };
const IID_ISynchronize = [16]u8{ 0x30, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46 };
fn guidsEqual(a: *const [16]u8, b: *const [16]u8) bool {
return std.mem.eql(u8, a, b);
}
fn resolve(dll: [*:0]const u8, name: [*:0]const u8) usize {
var mod = GetModuleHandleA(dll);
if (mod == null) mod = LoadLibraryA(dll);
const proc = GetProcAddress(mod, name);
return if (proc) |p| @intFromPtr(p) else 0;
}
fn releaseComObject(obj: ?*anyopaque) void {
if (obj) |o| {
const obj_ptr: *const usize = @ptrCast(@alignCast(o));
const vtbl_addr = obj_ptr.*;
const release_fn: *const fn (*anyopaque) callconv(.c) u32 =
@ptrFromInt(@as(*const usize, @ptrFromInt(vtbl_addr + 16)).*);
_ = release_fn(o);
}
}
inline fn getTEB() usize {
return asm volatile ("mov %gs:0x30, %[ret]"
: [ret] "=r" (-> usize),
);
}
fn waitNoPump(handle: ?*anyopaque) void {
const NtWaitForSingleObject: *const fn (?*anyopaque, u8, ?*const i64) callconv(.c) i32 =
@ptrFromInt(resolve("ntdll.dll", "NtWaitForSingleObject"));
_ = NtWaitForSingleObject(handle, 0, null);
}
fn waitComAware(handle: ?*anyopaque) HRESULT {
const CoWaitForMultipleHandles: *const fn (u32, u32, u32, [*]const ?*anyopaque, *u32) callconv(.c) HRESULT =
@ptrFromInt(resolve("ole32.dll", "CoWaitForMultipleHandles"));
const handles = [_]?*anyopaque{handle};
var index: u32 = 0;
return CoWaitForMultipleHandles(0, INFINITE, 1, &handles, &index);
}
// ============================================================================
// MIDL / RPC structures
// ============================================================================
const MIDL_STUB_DESC = extern struct {
RpcInterfaceInformation: ?*anyopaque,
pfnAllocate: ?*const fn (usize) callconv(.c) ?*anyopaque,
pfnFree: ?*const fn (?*anyopaque) callconv(.c) void,
IMPLICIT_HANDLE_INFO: usize,
apfnNdrRundownRoutines: ?*anyopaque,
aGenericBindingRoutinePairs: ?*anyopaque,
apfnExprEval: ?*anyopaque,
aXmitQuintuple: ?*anyopaque,
pFormatTypes: ?[*]const u8,
fCheckBounds: i32,
NdrVersion: u32,
pMalloc: ?*anyopaque,
MIDLVersion: u32,
_pad58: u32,
CommFaultOffsets: ?*anyopaque,
aUserMarshalQuadruple: ?*anyopaque,
NotifyRoutineTable: ?*anyopaque,
mFlags: usize,
CsRoutineTables: ?*anyopaque,
ProxyServerInfo: ?*anyopaque,
pExprInfo: ?*anyopaque,
};
const MIDL_SERVER_INFO = extern struct {
pStubDesc: *const MIDL_STUB_DESC,
DispatchTable: [*]const usize,
ProcString: [*]const u8,
FmtStringOffset: [*]const u16,
ThunkTable: ?*anyopaque,
pTransferSyntax: ?*anyopaque,
nCount: usize,
pSyntaxInfo: ?*anyopaque,
};
const CInterfaceStubHeader = extern struct {
piid: ?*const [16]u8,
pServerInfo: *const MIDL_SERVER_INFO,
DispatchTableCount: u32,
_pad: u32,
pDispatchTable: isize,
};
const RPCOLEMESSAGE = extern struct {
reserved1: ?*anyopaque,
dataRepresentation: u32,
_pad0c: u32,
Buffer: ?*anyopaque,
cbBuffer: u32,
iMethod: u32,
reserved2: [5]?*anyopaque,
rpcFlags: u32,
_pad4c: u32,
};
const EXCEPTION_RECORD = extern struct {
ExceptionCode: u32,
ExceptionFlags: u32,
ExceptionRecord: ?*EXCEPTION_RECORD,
ExceptionAddress: ?*anyopaque,
NumberParameters: u32,
_pad: u32,
ExceptionInformation: [15]usize,
};
// ============================================================================
// Combase internal function resolution (dynamic sig scan + version fallback)
// ============================================================================
// Resolved addresses — populated at init() via signature scanning
const ResolvedAddrs = struct {
ModalLoop: usize = 0,
CCliModalLoopCtor: usize = 0,
NoOpReturn0: usize = 0,
PostCall: usize = 0,
};
// Known-good versions: fallback when sig scan fails on a tested build
const KnownCombase = struct {
text_size: u32,
checksum: u32, // PE optional header checksum
rvas: ResolvedAddrs,
};
const known_versions = [_]KnownCombase{
// Win11 24H2 (10.0.26100.x) — original development target
.{ .text_size = 0x2677A0, .checksum = 0x0038B3D9, .rvas = .{
.ModalLoop = 0x97F98,
.CCliModalLoopCtor = 0x98C50,
.NoOpReturn0 = 0x99180,
.PostCall = 0x47A44,
} },
};
// PE .text section bounds
const TextSection = struct { base: usize, size: usize };
fn findTextSection(module_base: usize) ?TextSection {
const dos_e_lfanew = @as(*const u32, @ptrFromInt(module_base + 0x3C)).*;
const pe_sig = module_base + @as(usize, dos_e_lfanew);
// Verify PE\0\0
if (@as(*const u32, @ptrFromInt(pe_sig)).* != 0x00004550) return null;
const coff = pe_sig + 4;
const num_sections = @as(*const u16, @ptrFromInt(coff + 2)).*;
const size_opt = @as(*const u16, @ptrFromInt(coff + 16)).*;
const first_section = coff + 20 + @as(usize, size_opt);
// Walk IMAGE_SECTION_HEADERs (40 bytes each) looking for .text
for (0..num_sections) |i| {
const sh = first_section + i * 40;
const name_bytes: [*]const u8 = @ptrFromInt(sh);
if (name_bytes[0] == '.' and name_bytes[1] == 't' and
name_bytes[2] == 'e' and name_bytes[3] == 'x' and
name_bytes[4] == 't')
{
const vsize = @as(*const u32, @ptrFromInt(sh + 8)).*;
const vrva = @as(*const u32, @ptrFromInt(sh + 12)).*;
return TextSection{ .base = module_base + vrva, .size = vsize };
}
}
return null;
}
fn getPEChecksum(module_base: usize) u32 {
const dos_e_lfanew = @as(*const u32, @ptrFromInt(module_base + 0x3C)).*;
const pe_sig = module_base + @as(usize, dos_e_lfanew);
// OptionalHeader starts at pe_sig+24, CheckSum at +64 into OptionalHeader
return @as(*const u32, @ptrFromInt(pe_sig + 24 + 64)).*;
}
// Pattern scan with wildcard mask. mask[i]==0xFF means match, mask[i]==0x00 means wildcard.
// Returns null if not exactly one match (ambiguity = failure).
fn sigScan(text: TextSection, pattern: []const u8, mask: []const u8) ?usize {
if (pattern.len != mask.len) return null;
if (text.size < pattern.len) return null;
const scan_end = text.size - pattern.len;
var match_addr: usize = 0;
var match_count: u32 = 0;
var offset: usize = 0;
while (offset <= scan_end) : (offset += 1) {
const ptr: [*]const u8 = @ptrFromInt(text.base + offset);
var matched = true;
for (0..pattern.len) |j| {
if (mask[j] == 0xFF and ptr[j] != pattern[j]) {
matched = false;
break;
}
}
if (matched) {
match_count += 1;
if (match_count == 1) {
match_addr = text.base + offset;
} else {
// Multiple matches = ambiguous, fail
return null;
}
}
}
return if (match_count == 1) match_addr else null;
}
// Simpler scan without mask (exact match), returns first of N padded standalone functions
fn scanFirstPaddedFunc(text: TextSection, pattern: []const u8) ?usize {
if (text.size < pattern.len) return null;
const scan_end = text.size - pattern.len;
var offset: usize = 0;
while (offset <= scan_end) : (offset += 1) {
const ptr: [*]const u8 = @ptrFromInt(text.base + offset);
var matched = true;
for (0..pattern.len) |j| {
if (ptr[j] != pattern[j]) {
matched = false;
break;
}
}
if (matched) return text.base + offset;
}
return null;
}
// ── Signature definitions ──────────────────────────────────────────────
// Extracted from combase.dll disassembly via Binary Ninja.
// Each signature is the function prologue — unique within .text.
// ModalLoop: 29-byte prologue ending with TEB access (gs:0x30)
// 48 89 5c 24 18 mov [rsp+0x18], rbx
// 55 56 57 push rbp; push rsi; push rdi
// 41 54..41 57 push r12..r15
// 48 83 ec 30 sub rsp, 0x30
// 65 48 8b 04 25 30 00 00 00 mov rax, gs:0x30
const SIG_MODAL_LOOP = [_]u8{
0x48, 0x89, 0x5C, 0x24, 0x18, 0x55, 0x56, 0x57,
0x41, 0x54, 0x41, 0x55, 0x41, 0x56, 0x41, 0x57,
0x48, 0x83, 0xEC, 0x30, 0x65, 0x48, 0x8B, 0x04,
0x25, 0x30, 0x00, 0x00, 0x00,
};
const MASK_MODAL_LOOP = [_]u8{0xFF} ** 29;
// PostCall: 26-byte prologue with unique test [rcx+0x50], 0x0c800000
// 48 89 6c 24 10 mov [rsp+0x10], rbp
// 48 89 74 24 18 mov [rsp+0x18], rsi
// 57 push rdi
// 41 56 41 57 push r14; push r15
// 48 83 ec 40 sub rsp, 0x40
// f7 41 50 00 00 80 0c test [rcx+0x50], 0x0c800000
const SIG_POST_CALL = [_]u8{
0x48, 0x89, 0x6C, 0x24, 0x10, 0x48, 0x89, 0x74,
0x24, 0x18, 0x57, 0x41, 0x56, 0x41, 0x57, 0x48,
0x83, 0xEC, 0x40, 0xF7, 0x41, 0x50, 0x00, 0x00,
0x80, 0x0C,
};
const MASK_POST_CALL = [_]u8{0xFF} ** 26;
// CCliModalLoopCtor: two-stage scan
// Stage 1: find unique constant write c7 41 18 15 01 01 80
// (mov dword [rcx+0x18], 0x80010115)
// Stage 2: walk back 0x2B bytes, verify prologue starts with
// 48 89 5c 24 08 57 48 83 ec 20
const SIG_CTOR_CONSTANT = [_]u8{ 0xC7, 0x41, 0x18, 0x15, 0x01, 0x01, 0x80 };
const MASK_CTOR_CONSTANT = [_]u8{0xFF} ** 7;
const SIG_CTOR_PROLOGUE = [_]u8{ 0x48, 0x89, 0x5C, 0x24, 0x08, 0x57, 0x48, 0x83, 0xEC, 0x20 };
// NoOpReturn0: scan for cc cc cc cc 33 c0 c3 cc (heavily int3-padded standalone)
// The function itself is at offset+4 (after the padding).
// Any match works — all are functionally equivalent xor eax,eax; ret.
const SIG_NOOP_PADDED = [_]u8{ 0xCC, 0xCC, 0xCC, 0xCC, 0x33, 0xC0, 0xC3, 0xCC };
fn resolveCombaseInternals(combase_base: usize) ?ResolvedAddrs {
const text = findTextSection(combase_base) orelse return tryKnownVersion(combase_base);
var result = ResolvedAddrs{};
// ── ModalLoop ──
result.ModalLoop = sigScan(text, &SIG_MODAL_LOOP, &MASK_MODAL_LOOP) orelse 0;
// ── PostCall ──
result.PostCall = sigScan(text, &SIG_POST_CALL, &MASK_POST_CALL) orelse 0;
// ── CCliModalLoopCtor (two-stage) ──
if (sigScan(text, &SIG_CTOR_CONSTANT, &MASK_CTOR_CONSTANT)) |const_addr| {
// Walk back to function start
const candidate = const_addr - 0x2B;
if (candidate >= text.base) {
const prologue: [*]const u8 = @ptrFromInt(candidate);
var valid = true;
for (0..SIG_CTOR_PROLOGUE.len) |j| {
if (prologue[j] != SIG_CTOR_PROLOGUE[j]) {
valid = false;
break;
}
}
if (valid) result.CCliModalLoopCtor = candidate;
}
}
// ── NoOpReturn0 (first padded match) ──
if (scanFirstPaddedFunc(text, &SIG_NOOP_PADDED)) |padded_addr| {
result.NoOpReturn0 = padded_addr + 4; // skip cc cc cc cc prefix
}
// Validate all resolved
if (result.ModalLoop == 0 or result.CCliModalLoopCtor == 0 or
result.NoOpReturn0 == 0 or result.PostCall == 0)
{
// Partial scan failure — try known version fallback
return tryKnownVersion(combase_base) orelse return null;
}
return result;
}
fn tryKnownVersion(combase_base: usize) ?ResolvedAddrs {
const text = findTextSection(combase_base) orelse return null;
const checksum = getPEChecksum(combase_base);
const text_size: u32 = @truncate(text.size);
for (known_versions) |kv| {
if (kv.text_size == text_size and kv.checksum == checksum) {
// Verified known-good build — return hardcoded RVAs as absolute addresses
return ResolvedAddrs{
.ModalLoop = combase_base + kv.rvas.ModalLoop,
.CCliModalLoopCtor = combase_base + kv.rvas.CCliModalLoopCtor,
.NoOpReturn0 = combase_base + kv.rvas.NoOpReturn0,
.PostCall = combase_base + kv.rvas.PostCall,
};
}
}
return null;
}
// Research/documentation-only RVAs (not used at runtime):
// BlockFn: 0x98284 — called by ModalLoop, pumps messages
// PeekRPCAndDDEMessage: 0x99634 — called by BlockFn
// pre_dispatch_setup: 0x98DDC — sets up dispatch before NdrStubCall2
// CStdStubBuffer_Invoke: resolved via GetProcAddress (exported)
// ============================================================================
// Layer 2: Runtime proc format builder
// ============================================================================
/// Build an NDR proc format string at runtime.
/// Same byte layout as the comptime buildProcFormat from omega1.zig.
/// Returns number of bytes written.
fn buildFormatRuntime(buf: []u8, n_params: u8, method: u16) usize {
const len = @as(usize, 26) + @as(usize, n_params) * 6 + 7;
if (buf.len < len) return 0;
const stack_size: u16 = (@as(u16, n_params) + 1) * 8;
const client_buf: u16 = @as(u16, n_params) * 8;
buf[0] = 0x33; // FC_AUTO_HANDLE
buf[1] = 0x4C; // Oi_OBJECT_PROC | Oi_HAS_RPCFLAGS | Oi_USE_NEW_INIT
buf[2] = 0x00;
buf[3] = 0x00;
buf[4] = 0x00;
buf[5] = 0x00;
buf[6] = @truncate(method);
buf[7] = @truncate(method >> 8);
buf[8] = @truncate(stack_size);
buf[9] = @truncate(stack_size >> 8);
buf[10] = @truncate(client_buf);
buf[11] = @truncate(client_buf >> 8);
buf[12] = 0x08;
buf[13] = 0x00;
buf[14] = 0x44; // Oi2: has_ext | has_return
buf[15] = n_params + 1;
// Oi2 extension header
buf[16] = 0x0A;
buf[17] = 0x01;
buf[18] = 0x00;
buf[19] = 0x00;
buf[20] = 0x00;
buf[21] = 0x00;
buf[22] = 0x00;
buf[23] = 0x00;
buf[24] = 0x00;
buf[25] = 0x00;
// Parameter descriptors (FC_HYPER for each — pointer-sized)
var offset: u16 = 0x00;
for (0..n_params) |i| {
const base = 26 + i * 6;
buf[base + 0] = 0x48;
buf[base + 1] = 0x00;
buf[base + 2] = @truncate(offset);
buf[base + 3] = @truncate(offset >> 8);
buf[base + 4] = FC_HYPER;
buf[base + 5] = 0x00;
offset += 8;
}
// Return value descriptor (FC_LONG)
const ret_base = 26 + @as(usize, n_params) * 6;
buf[ret_base + 0] = 0x70;
buf[ret_base + 1] = 0x00;
buf[ret_base + 2] = @truncate(offset);
buf[ret_base + 3] = @truncate(offset >> 8);
buf[ret_base + 4] = FC_LONG;
buf[ret_base + 5] = 0x00;
buf[ret_base + 6] = 0x00;
return len;
}
// Maximum bytes a single format string can occupy (8 params = max)
const MAX_FMT_SIZE: usize = 26 + 8 * 6 + 7; // = 81
// ============================================================================
// Layer 3: Stable MIDL table manager
// ============================================================================
const MidlTables = struct {
// Heap-allocated arrays (init → deinit lifetime)
dispatch_table: [*]usize,
server_dispatch_array: [*]usize,
rpc_dispatch_fns: [*]usize,
fmt_offset_table: [*]u16,
all_fmt: [*]u8,
all_fmt_size: usize,
// Stable structures (heap-allocated, never mutated after init)
stub_desc: *MIDL_STUB_DESC,
server_info: *MIDL_SERVER_INFO,
rpc_iface: [*]u8, // 0x60 bytes
ndr_syntax: [*]u8, // 20 bytes
rpc_dispatch_table: [*]usize, // {count, fn_ptr_array, 0}
stub_vtbl_mem: [*]u8, // 256 bytes: header + vtable
fake_stub: [*]u8, // 0x48 bytes: CStdStubBuffer
fake_server_obj: [*]u8, // 16 bytes
type_format_data: [*]u8, // 2 bytes, in .bss-equivalent (heap)
max_slots: u16,
ndr_stub2_addr: usize,
csb_invoke_addr: usize,
noop_addr: usize,
fn init(heap: ?*anyopaque, max_slots: u16) ?MidlTables {
const n: usize = @as(usize, max_slots);
// Resolve critical addresses
const ndr_stub2 = resolve("rpcrt4.dll", "NdrStubCall2");
const csb_invoke = resolve("combase.dll", "CStdStubBuffer_Invoke");
if (ndr_stub2 == 0 or csb_invoke == 0) return null;
const noop: usize = if (g_ctx.resolved) |r| r.NoOpReturn0 else return null;
// Allocate all tables
const dt = heapAllocTyped(usize, heap, n) orelse return null;
const sda = heapAllocTyped(usize, heap, n) orelse return null;
const rdf = heapAllocTyped(usize, heap, n) orelse return null;
const fot = heapAllocTyped(u16, heap, n) orelse return null;
const fmt_buf_size = n * MAX_FMT_SIZE;
const fmt_buf: [*]u8 = @ptrCast(HeapAlloc(heap, HEAP_ZERO_MEMORY, fmt_buf_size) orelse return null);
// Initialize dispatch tables
for (0..n) |i| {
dt[i] = noop;
sda[i] = noop;
rdf[i] = ndr_stub2;
fot[i] = 0;
}
// Allocate stable structures
const stub_desc: *MIDL_STUB_DESC = @ptrCast(@alignCast(HeapAlloc(heap, HEAP_ZERO_MEMORY, @sizeOf(MIDL_STUB_DESC)) orelse return null));
const server_info: *MIDL_SERVER_INFO = @ptrCast(@alignCast(HeapAlloc(heap, HEAP_ZERO_MEMORY, @sizeOf(MIDL_SERVER_INFO)) orelse return null));
const rpc_iface: [*]u8 = @ptrCast(HeapAlloc(heap, HEAP_ZERO_MEMORY, 0x60) orelse return null);
const ndr_syntax: [*]u8 = @ptrCast(HeapAlloc(heap, HEAP_ZERO_MEMORY, 20) orelse return null);
const rpc_dt: [*]usize = @ptrCast(@alignCast(HeapAlloc(heap, HEAP_ZERO_MEMORY, 3 * @sizeOf(usize)) orelse return null));
const svtbl: [*]u8 = @ptrCast(HeapAlloc(heap, HEAP_ZERO_MEMORY, 256) orelse return null);
const fstub: [*]u8 = @ptrCast(HeapAlloc(heap, HEAP_ZERO_MEMORY, 0x48) orelse return null);
const fsobj: [*]u8 = @ptrCast(HeapAlloc(heap, HEAP_ZERO_MEMORY, 16) orelse return null);
const tfmt: [*]u8 = @ptrCast(HeapAlloc(heap, HEAP_ZERO_MEMORY, 2) orelse return null);
// Setup stub_desc
stub_desc.pfnAllocate = @ptrFromInt(resolve("rpcrt4.dll", "I_RpcAllocate"));
stub_desc.pfnFree = @ptrFromInt(resolve("rpcrt4.dll", "I_RpcFree"));
tfmt[0] = 0x00;
tfmt[1] = 0x00;
stub_desc.pFormatTypes = tfmt;
stub_desc.fCheckBounds = 1;
stub_desc.NdrVersion = 0x60001;
stub_desc.MIDLVersion = 0x8010000;
// NdrStubCall2 reads pStubDesc+0x38 (aXmitQuintuple) as dispatch table.
// When non-NULL, NdrStubCall2 uses aXmitQuintuple[ProcNum] directly
// instead of the OLE extension's var_90. Setting this to our dispatch_table
// (like omega1 does) gives a deterministic, correct dispatch path.
stub_desc.aXmitQuintuple = @ptrCast(dt);
// RPC interface (NDR 2.0 transfer syntax)
@as(*u32, @ptrCast(@alignCast(&rpc_iface[0]))).* = 0x60;
@as(*u32, @ptrCast(@alignCast(&rpc_iface[4]))).* = 0xDEADBEEF;
@as(*u16, @ptrCast(@alignCast(&rpc_iface[0x14]))).* = 1;
@as(*u32, @ptrCast(@alignCast(&rpc_iface[0x18]))).* = 0x8A885D04;
@as(*u16, @ptrCast(@alignCast(&rpc_iface[0x1C]))).* = 0x1CEB;
@as(*u16, @ptrCast(@alignCast(&rpc_iface[0x1E]))).* = 0x11C9;
rpc_iface[0x20] = 0x9F;
rpc_iface[0x21] = 0xE8;
rpc_iface[0x22] = 0x08;
rpc_iface[0x23] = 0x00;
rpc_iface[0x24] = 0x2B;
rpc_iface[0x25] = 0x10;
rpc_iface[0x26] = 0x48;
rpc_iface[0x27] = 0x60;
@as(*u16, @ptrCast(@alignCast(&rpc_iface[0x28]))).* = 2;
// NDR syntax GUID
@as(*u32, @ptrCast(@alignCast(&ndr_syntax[0]))).* = 0x8A885D04;
@as(*u16, @ptrCast(@alignCast(&ndr_syntax[4]))).* = 0x1CEB;
@as(*u16, @ptrCast(@alignCast(&ndr_syntax[6]))).* = 0x11C9;
ndr_syntax[8] = 0x9F;
ndr_syntax[9] = 0xE8;
ndr_syntax[10] = 0x08;
ndr_syntax[11] = 0x00;
ndr_syntax[12] = 0x2B;
ndr_syntax[13] = 0x10;
ndr_syntax[14] = 0x48;
ndr_syntax[15] = 0x60;
@as(*u16, @ptrCast(@alignCast(&ndr_syntax[16]))).* = 2;
// Server info
server_info.* = MIDL_SERVER_INFO{
.pStubDesc = stub_desc,
.DispatchTable = dt,
.ProcString = fmt_buf,
.FmtStringOffset = fot,
.ThunkTable = null,
.pTransferSyntax = @ptrCast(ndr_syntax),
.nCount = 0,
.pSyntaxInfo = null,
};
stub_desc.RpcInterfaceInformation = @ptrCast(rpc_iface);
// RPC dispatch table: {count, fn_array_ptr, 0}
rpc_dt[0] = n;
rpc_dt[1] = @intFromPtr(rdf);
rpc_dt[2] = 0;
@as(*usize, @ptrCast(@alignCast(&rpc_iface[0x30]))).* = @intFromPtr(rpc_dt);
// InterpreterInfo (MIDL_SERVER_INFO*) at +0x50 — NdrStubCall2 reads this.
// DefaultManagerEpv at +0x48 MUST be NULL — if non-NULL, NdrStubCall2
// treats it as a function pointer array (RPC_MGR_EPV*) and indexes by
// ProcNum, causing OOB reads and crashes.
@as(*usize, @ptrCast(@alignCast(&rpc_iface[0x48]))).* = 0; // DefaultManagerEpv = NULL
@as(*usize, @ptrCast(@alignCast(&rpc_iface[0x50]))).* = @intFromPtr(server_info); // InterpreterInfo
// CInterfaceStubHeader + vtable
const header: *CInterfaceStubHeader = @ptrCast(@alignCast(&svtbl[0]));
header.* = CInterfaceStubHeader{
.piid = &IID_IFiberDispatch,
.pServerInfo = server_info,
.DispatchTableCount = max_slots,
._pad = 0,
.pDispatchTable = -1, // MUST be -1: tells CStdStubBuffer_Invoke to use NDR/NdrStubCall2 path
};
// Stub vtable (28 slots, mostly NoOp)
const vtbl: [*]usize = @ptrCast(@alignCast(&svtbl[0x20]));
for (0..28) |i| vtbl[i] = noop;
vtbl[0] = resolve("rpcrt4.dll", "CStdStubBuffer_QueryInterface");
vtbl[1] = resolve("rpcrt4.dll", "CStdStubBuffer_AddRef");
vtbl[2] = resolve("rpcrt4.dll", "CStdStubBuffer_AddRef"); // AddRef as Release (avoids delegation crash)
vtbl[3] = resolve("rpcrt4.dll", "CStdStubBuffer_Connect");
vtbl[4] = resolve("rpcrt4.dll", "CStdStubBuffer_Disconnect");
vtbl[VTBL_INVOKE] = csb_invoke;
vtbl[6] = resolve("rpcrt4.dll", "CStdStubBuffer_IsIIDSupported");
vtbl[7] = resolve("rpcrt4.dll", "CStdStubBuffer_CountRefs");
vtbl[8] = resolve("rpcrt4.dll", "CStdStubBuffer_DebugServerQueryInterface");
vtbl[9] = resolve("rpcrt4.dll", "CStdStubBuffer_DebugServerRelease");
// CStdStubBuffer (fake_stub): pvServerObject, vtbl, refcount, pvServerObject2, pHeader
@as(*usize, @ptrCast(@alignCast(&fsobj[0]))).* = @intFromPtr(sda);
@as(*usize, @ptrCast(@alignCast(&fstub[0x00]))).* = @intFromPtr(fsobj);
@as(*usize, @ptrCast(@alignCast(&fstub[0x08]))).* = @intFromPtr(&svtbl[0x20]);
@as(*u32, @ptrCast(@alignCast(&fstub[0x10]))).* = 1; // refcount
@as(*usize, @ptrCast(@alignCast(&fstub[0x18]))).* = @intFromPtr(fsobj);
@as(*usize, @ptrCast(@alignCast(&fstub[0x40]))).* = @intFromPtr(header);
return MidlTables{
.dispatch_table = dt,
.server_dispatch_array = sda,
.rpc_dispatch_fns = rdf,
.fmt_offset_table = fot,
.all_fmt = fmt_buf,
.all_fmt_size = fmt_buf_size,
.stub_desc = stub_desc,
.server_info = server_info,
.rpc_iface = rpc_iface,
.ndr_syntax = ndr_syntax,
.rpc_dispatch_table = rpc_dt,
.stub_vtbl_mem = svtbl,
.fake_stub = fstub,
.fake_server_obj = fsobj,
.type_format_data = tfmt,
.max_slots = max_slots,
.ndr_stub2_addr = ndr_stub2,
.csb_invoke_addr = csb_invoke,
.noop_addr = noop,
};
}
/// Write a call's fn_ptr and format string into a specific method slot
fn writeSlot(self: *MidlTables, slot: u16, fn_ptr: usize, n_params: u8) void {
const idx = @as(usize, slot);
self.dispatch_table[idx] = fn_ptr;
self.server_dispatch_array[idx] = fn_ptr;
// Write format string into the all_fmt buffer at this slot's region
const fmt_offset = idx * MAX_FMT_SIZE;
const buf = self.all_fmt[fmt_offset .. fmt_offset + MAX_FMT_SIZE];
_ = buildFormatRuntime(buf, n_params, slot);
self.fmt_offset_table[idx] = @truncate(fmt_offset);
}
/// Re-set pvServerObject before a pump cycle (CStdStubBuffer_Disconnect may have zeroed it)
fn refreshStub(self: *MidlTables) void {
@as(*usize, @ptrCast(@alignCast(&self.fake_stub[0x18]))).* = @intFromPtr(self.fake_server_obj);
}
fn deinit(self: *MidlTables) void {
const heap = GetProcessHeap();
_ = HeapFree(heap, 0, @ptrCast(self.dispatch_table));
_ = HeapFree(heap, 0, @ptrCast(self.server_dispatch_array));
_ = HeapFree(heap, 0, @ptrCast(self.rpc_dispatch_fns));
_ = HeapFree(heap, 0, @ptrCast(self.fmt_offset_table));
_ = HeapFree(heap, 0, self.all_fmt);
_ = HeapFree(heap, 0, @ptrCast(self.stub_desc));
_ = HeapFree(heap, 0, @ptrCast(self.server_info));
_ = HeapFree(heap, 0, self.rpc_iface);
_ = HeapFree(heap, 0, self.ndr_syntax);
_ = HeapFree(heap, 0, @ptrCast(self.rpc_dispatch_table));
_ = HeapFree(heap, 0, self.stub_vtbl_mem);
_ = HeapFree(heap, 0, self.fake_stub);
_ = HeapFree(heap, 0, self.fake_server_obj);
_ = HeapFree(heap, 0, self.type_format_data);
}
};
fn heapAllocTyped(comptime T: type, heap: ?*anyopaque, count: usize) ?[*]T {
const ptr = HeapAlloc(heap, HEAP_ZERO_MEMORY, count * @sizeOf(T)) orelse return null;
return @ptrCast(@alignCast(ptr));
}
// ============================================================================
// Layer 4: COM plumbing — PSFactory, FakeProxy, FakeClassObject, ISynchronize
// ============================================================================
// --- FakeClassObject (exposes IFiberDispatch to COM) ---
const FakeClassObject = extern struct { vtbl: *const [3]usize };
fn fco_qi(self_raw: *anyopaque, riid: *const [16]u8, ppv: **anyopaque) callconv(.c) HRESULT {
if (guidsEqual(riid, &IID_IUnknown) or guidsEqual(riid, &IID_IFiberDispatch)) {
ppv.* = self_raw;
return S_OK;
}
@as(*usize, @ptrCast(ppv)).* = 0;
return E_NOINTERFACE;
}
fn fco_addref(_: *anyopaque) callconv(.c) u32 {
return 2;
}
fn fco_release(_: *anyopaque) callconv(.c) u32 {
return 1;
}
var g_fco_vtbl: [3]usize = undefined;
var g_fco = FakeClassObject{ .vtbl = &g_fco_vtbl };
fn initFcoVtbl() void {
g_fco_vtbl[0] = @intFromPtr(&fco_qi);
g_fco_vtbl[1] = @intFromPtr(&fco_addref);
g_fco_vtbl[2] = @intFromPtr(&fco_release);
}
// --- FakeProxyBuffer (IRpcProxyBuffer + custom interface) ---
const FakeRPBVtbl = extern struct {
QueryInterface: *const fn (**const FakeRPBVtbl, *const [16]u8, **anyopaque) callconv(.c) HRESULT,
AddRef: *const fn (**const FakeRPBVtbl) callconv(.c) u32,
Release: *const fn (**const FakeRPBVtbl) callconv(.c) u32,
Connect: *const fn (**const FakeRPBVtbl, *anyopaque) callconv(.c) HRESULT,
Disconnect: *const fn (**const FakeRPBVtbl) callconv(.c) void,
};
const FakeIfaceVtbl = extern struct {
QueryInterface: *const fn (**const FakeIfaceVtbl, *const [16]u8, **anyopaque) callconv(.c) HRESULT,
AddRef: *const fn (**const FakeIfaceVtbl) callconv(.c) u32,
Release: *const fn (**const FakeIfaceVtbl) callconv(.c) u32,
};
const FakeProxyBuffer = extern struct {
rpb_vtbl: *const FakeRPBVtbl,
ref_count: u32,
_pad: u32,
iface_vtbl: *const FakeIfaceVtbl,
punkOuter: ?*anyopaque,
};
fn rpb_qi(self: **const FakeRPBVtbl, riid: *const [16]u8, ppv: **anyopaque) callconv(.c) HRESULT {
if (guidsEqual(riid, &IID_IUnknown)) {
ppv.* = @ptrCast(self);
return S_OK;
}
@as(*usize, @ptrCast(ppv)).* = 0;
return E_NOINTERFACE;
}
fn rpb_addref(_: **const FakeRPBVtbl) callconv(.c) u32 {
g_fake_proxy.ref_count += 1;
return g_fake_proxy.ref_count;
}
fn rpb_release(_: **const FakeRPBVtbl) callconv(.c) u32 {
if (g_fake_proxy.ref_count > 1) g_fake_proxy.ref_count -= 1;
return g_fake_proxy.ref_count;
}
fn rpb_connect(_: **const FakeRPBVtbl, pChannel: *anyopaque) callconv(.c) HRESULT {
log("[RPB] Connect called! pChannel=0x{X}\n", .{@intFromPtr(pChannel)});
if (g_ctx.proxy_channel) |old_channel| {
if (old_channel == pChannel) return S_OK;
releaseComObject(old_channel);
}
g_ctx.proxy_channel = pChannel;
// AddRef the channel
const obj_ptr: *const usize = @ptrCast(@alignCast(pChannel));
const vtbl_addr = obj_ptr.*;
const addref_fn: *const fn (*anyopaque) callconv(.c) u32 =
@ptrFromInt(@as(*const usize, @ptrFromInt(vtbl_addr + 8)).*);
_ = addref_fn(pChannel);
return S_OK;
}
fn rpb_disconnect(_: **const FakeRPBVtbl) callconv(.c) void {}
fn iface_qi(_: **const FakeIfaceVtbl, riid: *const [16]u8, ppv: **anyopaque) callconv(.c) HRESULT {
if (g_fake_proxy.punkOuter) |outer| {
const vtbl_ptr: *const usize = @ptrCast(@alignCast(outer));
const vtbl: [*]const usize = @ptrFromInt(vtbl_ptr.*);
const qi_fn: *const fn (*anyopaque, *const [16]u8, **anyopaque) callconv(.c) HRESULT = @ptrFromInt(vtbl[0]);
return qi_fn(outer, riid, ppv);
}
if (guidsEqual(riid, &IID_IUnknown) or guidsEqual(riid, &IID_IFiberDispatch)) {
ppv.* = @ptrCast(&g_fake_proxy.iface_vtbl);
return S_OK;
}
@as(*usize, @ptrCast(ppv)).* = 0;
return E_NOINTERFACE;
}
fn iface_addref(_: **const FakeIfaceVtbl) callconv(.c) u32 {
if (g_fake_proxy.punkOuter) |outer| {
const vtbl_ptr: *const usize = @ptrCast(@alignCast(outer));
const vtbl: [*]const usize = @ptrFromInt(vtbl_ptr.*);
return @as(*const fn (*anyopaque) callconv(.c) u32, @ptrFromInt(vtbl[1]))(outer);
}
return 2;
}
fn iface_release(_: **const FakeIfaceVtbl) callconv(.c) u32 {
if (g_fake_proxy.punkOuter) |outer| {
const vtbl_ptr: *const usize = @ptrCast(@alignCast(outer));
const vtbl: [*]const usize = @ptrFromInt(vtbl_ptr.*);
return @as(*const fn (*anyopaque) callconv(.c) u32, @ptrFromInt(vtbl[2]))(outer);
}
return 1;
}
const g_rpb_vtbl = FakeRPBVtbl{ .QueryInterface = &rpb_qi, .AddRef = &rpb_addref, .Release = &rpb_release, .Connect = &rpb_connect, .Disconnect = &rpb_disconnect };
const g_iface_vtbl = FakeIfaceVtbl{ .QueryInterface = &iface_qi, .AddRef = &iface_addref, .Release = &iface_release };
var g_fake_proxy = FakeProxyBuffer{ .rpb_vtbl = &g_rpb_vtbl, .ref_count = 1, ._pad = 0, .iface_vtbl = &g_iface_vtbl, .punkOuter = null };
// --- PSFactory ---
const PSFactory = extern struct { vtbl: *const PSFactoryVtbl, ref_count: u32 };
const PSFactoryVtbl = extern struct {
QueryInterface: *const fn (*PSFactory, *const [16]u8, **anyopaque) callconv(.c) HRESULT,
AddRef: *const fn (*PSFactory) callconv(.c) u32,
Release: *const fn (*PSFactory) callconv(.c) u32,
CreateProxy: *const fn (*PSFactory, *anyopaque, *const [16]u8, **anyopaque, **anyopaque) callconv(.c) HRESULT,
CreateStub: *const fn (*PSFactory, *const [16]u8, ?*anyopaque, **anyopaque) callconv(.c) HRESULT,
};
fn psf_qi(self: *PSFactory, riid: *const [16]u8, ppv: **anyopaque) callconv(.c) HRESULT {
if (guidsEqual(riid, &IID_IUnknown) or guidsEqual(riid, &IID_IPSFactoryBuffer) or guidsEqual(riid, &IID_IClassFactory)) {
ppv.* = @ptrCast(self);
return S_OK;
}
@as(*usize, @ptrCast(ppv)).* = 0;
return E_NOINTERFACE;
}
fn psf_addref(_: *PSFactory) callconv(.c) u32 {
return 2;
}
fn psf_release(_: *PSFactory) callconv(.c) u32 {
return 1;
}
fn psf_create_proxy(self: *PSFactory, punkOuter: *anyopaque, _: *const [16]u8, ppProxy: **anyopaque, ppv: **anyopaque) callconv(.c) HRESULT {
_ = self;
log("[PSF] CreateProxy called!\n", .{});
g_fake_proxy.punkOuter = punkOuter;
// AddRef the controlling unknown (COM aggregation contract)
const vtbl_ptr: *const usize = @ptrCast(@alignCast(punkOuter));
const vtbl: [*]const usize = @ptrFromInt(vtbl_ptr.*);
_ = @as(*const fn (*anyopaque) callconv(.c) u32, @ptrFromInt(vtbl[1]))(punkOuter);
g_fake_proxy.ref_count = 1;
ppProxy.* = @ptrCast(&g_fake_proxy.rpb_vtbl);
ppv.* = @ptrCast(&g_fake_proxy.iface_vtbl);
return S_OK;
}
fn psf_create_stub(_: *PSFactory, _: *const [16]u8, _: ?*anyopaque, ppStub: **anyopaque) callconv(.c) HRESULT {
if (g_ctx.tables) |tables| {
const stub_addr = @intFromPtr(tables.fake_stub) + 0x08;
ppStub.* = @ptrFromInt(stub_addr);
log("[PSF] CreateStub: fake_stub=0x{X}, returning vtbl_ptr at 0x{X}\n", .{ @intFromPtr(tables.fake_stub), stub_addr });
// Dump fake_stub bytes
const fs = tables.fake_stub;
log("[PSF] fake_stub hex:", .{});
for (0..0x48) |i| {
if (i % 8 == 0) log("\n +0x{X:0>2}: ", .{i});
log("{X:0>2} ", .{fs[i]});
}
log("\n", .{});
// Dump vtable at [0x08]
const vtbl_p: *const usize = @ptrCast(@alignCast(&fs[0x08]));
const vtbl: [*]const usize = @ptrFromInt(vtbl_p.*);
log("[PSF] stub vtbl[0..5]:", .{});
for (0..5) |i| log(" 0x{X}", .{vtbl[i]});
log("\n", .{});
return S_OK;
}
log("[PSF] CreateStub: tables is null!\n", .{});
return E_FAIL;
}
const g_psf_vtbl = PSFactoryVtbl{
.QueryInterface = &psf_qi,
.AddRef = &psf_addref,
.Release = &psf_release,
.CreateProxy = &psf_create_proxy,
.CreateStub = &psf_create_stub,
};
var g_psf = PSFactory{ .vtbl = &g_psf_vtbl, .ref_count = 1 };
// --- ISynchronize (minimal fake — CChannelObject::Send calls Reset at vtable[5]) ---
const FakeSynchronize = extern struct {
vtbl: *const [6]usize,
ref_count: u32,
event: ?*anyopaque,
};
fn sync_qi(self_raw: *anyopaque, riid: *const [16]u8, ppv: **anyopaque) callconv(.c) HRESULT {
if (guidsEqual(riid, &IID_IUnknown) or guidsEqual(riid, &IID_ISynchronize)) {
ppv.* = self_raw;
return S_OK;
}
@as(*usize, @ptrCast(ppv)).* = 0;
return E_NOINTERFACE;
}
fn sync_addref(_: *anyopaque) callconv(.c) u32 {
return 2;
}
fn sync_release(_: *anyopaque) callconv(.c) u32 {
return 1;
}
fn sync_wait(_: *anyopaque, _: u32, _: u32) callconv(.c) HRESULT {
return S_OK;
}
fn sync_signal(self_raw: *anyopaque) callconv(.c) HRESULT {
const self: *FakeSynchronize = @ptrCast(@alignCast(self_raw));
if (self.event) |e| _ = SetEvent(e);
return S_OK;
}
fn sync_reset(_: *anyopaque) callconv(.c) HRESULT {
return S_OK;
}
var g_sync_vtbl: [6]usize = undefined;
fn initSyncVtbl() void {
g_sync_vtbl[0] = @intFromPtr(&sync_qi);
g_sync_vtbl[1] = @intFromPtr(&sync_addref);
g_sync_vtbl[2] = @intFromPtr(&sync_release);
g_sync_vtbl[3] = @intFromPtr(&sync_wait);
g_sync_vtbl[4] = @intFromPtr(&sync_signal);
g_sync_vtbl[5] = @intFromPtr(&sync_reset);
}
// ============================================================================
// Layer 5: Caller-STA helpers
// ============================================================================
const UnmarshalCtx = struct {
stream: ?*anyopaque,
hr: HRESULT = E_FAIL,
done_event: ?*anyopaque,
};
fn unmarshalProxyThread(param: ?*anyopaque) callconv(.c) u32 {
const ctx: *UnmarshalCtx = @ptrCast(@alignCast(param));
const CoInitializeEx: *const fn (?*anyopaque, u32) callconv(.c) HRESULT =
@ptrFromInt(resolve("ole32.dll", "CoInitializeEx"));
const CoRegisterPSClsid: *const fn (*const [16]u8, *const [16]u8) callconv(.c) HRESULT =
@ptrFromInt(resolve("ole32.dll", "CoRegisterPSClsid"));
const CoRegisterClassObject: *const fn (*const [16]u8, *anyopaque, u32, u32, *u32) callconv(.c) HRESULT =
@ptrFromInt(resolve("ole32.dll", "CoRegisterClassObject"));
const CoGetInterfaceAndReleaseStream: *const fn (*anyopaque, *const [16]u8, **anyopaque) callconv(.c) HRESULT =
@ptrFromInt(resolve("ole32.dll", "CoGetInterfaceAndReleaseStream"));
const hr_coinit = CoInitializeEx(null, COINIT_MULTITHREADED);
if (hr_coinit < 0) {
ctx.hr = hr_coinit;
if (ctx.done_event) |evt| _ = SetEvent(evt);
return 1;
}
// The helper MTA thread must register the PS mapping/class object in its
// own apartment so CoGetInterfaceAndReleaseStream can resolve the proxy.
_ = CoRegisterPSClsid(&IID_IFiberDispatch, &CLSID_FiberDispatchPS);
var cookie: u32 = 0;
_ = CoRegisterClassObject(&CLSID_FiberDispatchPS, @ptrCast(&g_psf), CLSCTX_INPROC_SERVER, REGCLS_MULTIPLEUSE, &cookie);
var proxy: ?*anyopaque = null;
ctx.hr = CoGetInterfaceAndReleaseStream(ctx.stream.?, &IID_IFiberDispatch, @ptrCast(&proxy));
if (ctx.done_event) |evt| _ = SetEvent(evt);
return if (ctx.hr < 0) 1 else 0;
}
fn refreshProxyChannel() bool {
const CoMarshalInterThreadInterfaceInStream: *const fn (*const [16]u8, *anyopaque, **anyopaque) callconv(.c) HRESULT =
@ptrFromInt(resolve("ole32.dll", "CoMarshalInterThreadInterfaceInStream"));
var stream: ?*anyopaque = null;
const marshal_hr = CoMarshalInterThreadInterfaceInStream(&IID_IFiberDispatch, @ptrCast(&g_fco), @ptrCast(&stream));
if (marshal_hr < 0) return false;
const done_event = CreateEventA(null, 1, 0, null) orelse return false;
defer _ = CloseHandle(done_event);
var ctx = UnmarshalCtx{
.stream = stream,
.done_event = done_event,
};
var helper_tid: u32 = 0;
const helper_handle = CreateThread(null, 0, &unmarshalProxyThread, @ptrCast(&ctx), 0, &helper_tid) orelse return false;
defer _ = CloseHandle(helper_handle);
_ = waitComAware(done_event);
waitNoPump(helper_handle);
waitNoPump(helper_handle);
return ctx.hr >= 0 and g_ctx.proxy_channel != null;
}
fn executePumpCycle() bool {
log("[STA] executePumpCycle starting ({d} calls)...\n", .{g_ctx.queue_count});
const addrs = g_ctx.resolved orelse return false;
const NoOpReturn0_addr = addrs.NoOpReturn0;
const CCliModalLoopCtor_addr = addrs.CCliModalLoopCtor;
var inner_vtable: [24]usize = undefined;
for (&inner_vtable) |*v| v.* = NoOpReturn0_addr;
var inner_obj: [0x170]u8 align(8) = [_]u8{0} ** 0x170;
@as(*usize, @ptrCast(@alignCast(&inner_obj[0]))).* = @intFromPtr(&inner_vtable);
var cancel_vtable: [16]usize = undefined;
for (&cancel_vtable) |*v| v.* = NoOpReturn0_addr;
var cancel_obj: [8]u8 align(8) = undefined;
@as(*usize, @ptrCast(@alignCast(&cancel_obj[0]))).* = @intFromPtr(&cancel_vtable);
@as(*usize, @ptrCast(@alignCast(&inner_obj[0x160]))).* = @intFromPtr(&cancel_obj);
const pump_event = CreateEventA(null, 0, 0, null) orelse return false;
defer _ = CloseHandle(pump_event);
var fake_client_call: [0x120]u8 align(8) = [_]u8{0} ** 0x120;
@as(*usize, @ptrCast(@alignCast(&fake_client_call[0xC0]))).* = @intFromPtr(&inner_obj);
@as(*usize, @ptrCast(@alignCast(&fake_client_call[0x108]))).* = @intFromPtr(pump_event);
var cml_buf: [0x200]u8 align(8) = [_]u8{0} ** 0x200;
const CCliModalLoopCtor: *const fn (*[0x200]u8, u32, u32, u32, i32) callconv(.c) void =
@ptrFromInt(CCliModalLoopCtor_addr);
CCliModalLoopCtor(&cml_buf, 0, 0x04FF, 0, 1);
if (g_ctx.tables) |*tables| tables.refreshStub();
const pump_fiber = CreateFiber(0x100000, @ptrFromInt(addrs.ModalLoop), @ptrCast(&fake_client_call)) orelse return false;
defer DeleteFiber(pump_fiber);
log("[STA] Switching to pump fiber (direct ModalLoop)...\n", .{});
SwitchToFiber(pump_fiber);
log("[STA] Returned from pump!\n", .{});
return true;
}
// Debug wrapper for SwitchToFiber — intercepts the dispatch to verify
// NdrStubCall2 passes the correct fiber pointer (not corrupted to NULL)
fn debugSwitchToFiber(fiber_ptr: usize) callconv(.c) u32 {
log("[DBG-SF] NdrStubCall2 dispatched SwitchToFiber with arg=0x{X}\n", .{fiber_ptr});
if (fiber_ptr == 0) {
log("[!!!] FIBER PTR IS NULL — NdrStubCall2 unmarshal corrupted the parameter!\n", .{});
std.process.exit(98);
}
const RealSwitchToFiber: *const fn (?*anyopaque) callconv(.c) void =
@ptrFromInt(resolve("kernel32.dll", "SwitchToFiber"));
RealSwitchToFiber(@ptrFromInt(fiber_ptr));
return 0;
}
// ============================================================================
// Layer 6: PostCall worker (sleep mode)
// ============================================================================
const SleepWorkerCtx = struct {
channel: *anyopaque,
post_call_addr: usize,
calls: [*]QueuedCall,
num_calls: u32,
done_event: ?*anyopaque,
};
fn sleepWorkerThread(param: ?*anyopaque) callconv(.c) u32 {
const ctx: *SleepWorkerCtx = @ptrCast(@alignCast(param));
const CoInitializeEx: *const fn (?*anyopaque, u32) callconv(.c) HRESULT =
@ptrFromInt(resolve("ole32.dll", "CoInitializeEx"));
_ = CoInitializeEx(null, COINIT_MULTITHREADED);
log("[MTA-sleep] Worker started, posting {d} calls\n", .{ctx.num_calls});
// Read SOleTlsData for PostCall PID store
const ole_tls_ptr = @as(*const usize, @ptrFromInt(getTEB() + 0x1758)).*;
const ch_vtbl_ptr: *const usize = @ptrCast(@alignCast(ctx.channel));
const vtbl_base: usize = ch_vtbl_ptr.*;
const getbuf_fn: *const fn (*anyopaque, *RPCOLEMESSAGE, *const [16]u8) callconv(.c) HRESULT =
@ptrFromInt(@as(*const usize, @ptrFromInt(vtbl_base + 3 * 8)).*);
const pid = GetCurrentProcessId();
for (0..ctx.num_calls) |i| {
const call = &ctx.calls[i];
var msg = std.mem.zeroes(RPCOLEMESSAGE);
msg.iMethod = call.method;
msg.cbBuffer = @truncate(call.n_params * 8);
msg.dataRepresentation = 0x00000010;
const hr = getbuf_fn(ctx.channel, &msg, &IID_IFiberDispatch);
if (hr < 0) {
log("[MTA-sleep] GetBuffer FAILED method={d}: 0x{X}\n", .{ call.method, @as(u32, @bitCast(hr)) });
continue;
}
log("[MTA-sleep] GetBuffer OK method={d}\n", .{call.method});
// Marshal params into COM buffer
const dst: [*]u8 = @ptrCast(msg.Buffer);
const src: [*]const u8 = @ptrCast(&call.params);
for (0..call.n_params * 8) |j| dst[j] = src[j];
// PostCall fire-and-forget
if (msg.reserved1) |call_obj_raw| {
const call_ptr = @intFromPtr(call_obj_raw);
const oxid = @as(*const usize, @ptrFromInt(call_ptr + 0x40)).*;
const transport = @as(*const usize, @ptrFromInt(call_ptr + 0xC0)).*;
const server_call_field: *usize = @ptrFromInt(call_ptr + 0xC8);
const server_call = server_call_field.*;
if (oxid == 0 or server_call == 0) {
log("[MTA-sleep] SKIP: oxid or server_call is zero!\n", .{});
continue;
}
// IncRefCnt on OXIDEntry
const refcnt_ptr: *volatile u32 = @ptrFromInt(oxid + 0x118);
_ = @atomicRmw(u32, refcnt_ptr, .Add, 1, .seq_cst);
// Set transport dispatch flag
if (transport != 0) {
const t_flags: *volatile u32 = @ptrFromInt(transport + 0x14);
_ = @atomicRmw(u32, t_flags, .Or, 0x800, .seq_cst);
}
// Store PID
if (ole_tls_ptr != 0) {
const pid_slot: *u32 = @ptrFromInt(ole_tls_ptr + 0x1A8);
pid_slot.* = pid;
}
// Clear ServerCall (PostCall takes ownership)
server_call_field.* = 0;
// PostCall
const postCallFn: *const fn (usize, usize, *u32) callconv(.c) HRESULT =
@ptrFromInt(ctx.post_call_addr);
var post_status: u32 = 0;
const post_hr = postCallFn(oxid, server_call, &post_status);
log("[MTA-sleep] PostCall OK method={d} hr=0x{X}\n", .{ call.method, @as(u32, @bitCast(post_hr)) });
// DecRefCnt on OXIDEntry
_ = @atomicRmw(u32, refcnt_ptr, .Sub, 1, .seq_cst);
}
}
log("[MTA-sleep] All done\n", .{});
_ = SetEvent(ctx.done_event);
return 0;
}
// ============================================================================
// Layer 7: SendReceive worker (proxy mode — two-phase dispatch)
// ============================================================================
const ProxyWorkerCtx = struct {
channel: *anyopaque,
post_call_addr: usize,
// Phase 1: real call via SendReceive
real_call: *QueuedCall,
// Phase 2: SwitchToFiber via PostCall
switch_call: *QueuedCall,
// Output
return_value: u32,
done_event: ?*anyopaque,
};
fn proxyWorkerThread(param: ?*anyopaque) callconv(.c) u32 {
const ctx: *ProxyWorkerCtx = @ptrCast(@alignCast(param));
const CoInitializeEx: *const fn (?*anyopaque, u32) callconv(.c) HRESULT =
@ptrFromInt(resolve("ole32.dll", "CoInitializeEx"));
_ = CoInitializeEx(null, COINIT_MULTITHREADED);
log("[MTA-proxy] Worker started\n", .{});
const ch_vtbl_ptr: *const usize = @ptrCast(@alignCast(ctx.channel));
const vtbl_base: usize = ch_vtbl_ptr.*;
const getbuf_fn: *const fn (*anyopaque, *RPCOLEMESSAGE, *const [16]u8) callconv(.c) HRESULT =
@ptrFromInt(@as(*const usize, @ptrFromInt(vtbl_base + 3 * 8)).*);
const sendrecv_fn: *const fn (*anyopaque, *RPCOLEMESSAGE, *u32) callconv(.c) HRESULT =
@ptrFromInt(@as(*const usize, @ptrFromInt(vtbl_base + 4 * 8)).*);
const freebuf_fn: *const fn (*anyopaque, *RPCOLEMESSAGE) callconv(.c) HRESULT =
@ptrFromInt(@as(*const usize, @ptrFromInt(vtbl_base + 5 * 8)).*);
// Phase 1: SendReceive for real call (blocks until STA dispatches)
var msg = std.mem.zeroes(RPCOLEMESSAGE);
msg.iMethod = ctx.real_call.method;
msg.cbBuffer = @truncate(ctx.real_call.n_params * 8);
msg.dataRepresentation = 0x00000010;
log("[MTA-proxy] Phase 1: GetBuffer(method={d})...\n", .{msg.iMethod});
var hr = getbuf_fn(ctx.channel, &msg, &IID_IFiberDispatch);
log("[MTA-proxy] GetBuffer returned: 0x{X}\n", .{@as(u32, @bitCast(hr))});
if (hr >= 0) {
const dst: [*]u8 = @ptrCast(msg.Buffer);
const src: [*]const u8 = @ptrCast(&ctx.real_call.params);
for (0..ctx.real_call.n_params * 8) |j| dst[j] = src[j];
var status: u32 = 0;
log("[MTA-proxy] Phase 1: SendReceive...\n", .{});
hr = sendrecv_fn(ctx.channel, &msg, &status);
log("[MTA-proxy] SendReceive returned: 0x{X}, status={d}\n", .{ @as(u32, @bitCast(hr)), status });
if (hr >= 0) {
// Read return value from NDR response buffer
ctx.return_value = if (msg.Buffer != null)
@as(*u32, @ptrCast(@alignCast(msg.Buffer))).*
else
0;
}
_ = freebuf_fn(ctx.channel, &msg);
}
// Phase 2: PostCall SwitchToFiber(main) to return pump control
log("[MTA-proxy] Phase 2: PostCall SwitchToFiber...\n", .{});
const ole_tls_ptr = @as(*const usize, @ptrFromInt(getTEB() + 0x1758)).*;
var switch_msg = std.mem.zeroes(RPCOLEMESSAGE);
switch_msg.iMethod = ctx.switch_call.method;
switch_msg.cbBuffer = @truncate(ctx.switch_call.n_params * 8);
switch_msg.dataRepresentation = 0x00000010;
hr = getbuf_fn(ctx.channel, &switch_msg, &IID_IFiberDispatch);
if (hr >= 0) {
const dst: [*]u8 = @ptrCast(switch_msg.Buffer);
const src: [*]const u8 = @ptrCast(&ctx.switch_call.params);
for (0..ctx.switch_call.n_params * 8) |j| dst[j] = src[j];
if (switch_msg.reserved1) |call_obj_raw| {
const call_ptr = @intFromPtr(call_obj_raw);
const oxid = @as(*const usize, @ptrFromInt(call_ptr + 0x40)).*;
const transport = @as(*const usize, @ptrFromInt(call_ptr + 0xC0)).*;
const server_call_field: *usize = @ptrFromInt(call_ptr + 0xC8);
const server_call = server_call_field.*;
if (oxid != 0 and server_call != 0) {
const refcnt_ptr: *volatile u32 = @ptrFromInt(oxid + 0x118);
_ = @atomicRmw(u32, refcnt_ptr, .Add, 1, .seq_cst);
if (transport != 0) {
const t_flags: *volatile u32 = @ptrFromInt(transport + 0x14);
_ = @atomicRmw(u32, t_flags, .Or, 0x800, .seq_cst);
}
if (ole_tls_ptr != 0) {
const pid_slot: *u32 = @ptrFromInt(ole_tls_ptr + 0x1A8);
pid_slot.* = GetCurrentProcessId();
}
server_call_field.* = 0;
const postCallFn: *const fn (usize, usize, *u32) callconv(.c) HRESULT =
@ptrFromInt(ctx.post_call_addr);
var post_status: u32 = 0;
_ = postCallFn(oxid, server_call, &post_status);
log("[MTA-proxy] PostCall OK (SwitchToFiber)\n", .{});
}
}
}
log("[MTA-proxy] Both phases complete\n", .{});
_ = SetEvent(ctx.done_event);
return 0;
}
// ============================================================================
// Layer 8: Public API
// ============================================================================
const MAX_PARAMS: usize = 8;
const QueuedCall = struct {
method: u32,
n_params: u32,
params: [MAX_PARAMS]usize,
};
pub const State = enum(u32) { uninitialized = 0, ready = 1, queuing = 2, executing = 3, invoking = 4 };
pub const Config = struct {
max_slots: u16 = 256,
};
// Global context (single instance per process)
var g_ctx: ContextInternal = .{};
const ContextInternal = struct {
tables: ?MidlTables = null,
resolved: ?ResolvedAddrs = null,
proxy_channel: ?*anyopaque = null,
marshal_stream: ?*anyopaque = null,
caller_fiber: ?*anyopaque = null,
mta_usage_cookie: ?*anyopaque = null,
// Queue for sleep mode
queue_buf: [256]QueuedCall = undefined,
queue_count: u32 = 0,
state: State = .uninitialized,
max_slots: u16 = 256,
};
pub const Context = struct {
/// Queue a call for sleep mode batch dispatch.
/// Params are copied — caller can free after this returns.
pub fn queue(self: *Context, fn_ptr: usize, params: []const usize) void {
_ = self;
if (g_ctx.state != .ready and g_ctx.state != .queuing) return;
if (params.len > MAX_PARAMS) return;
if (g_ctx.queue_count >= 249) return; // leave room for SwitchToFiber
const slot: u16 = RESERVED_METHODS + @as(u16, @truncate(g_ctx.queue_count));
var call = QueuedCall{ .method = slot, .n_params = @truncate(params.len), .params = [_]usize{0} ** MAX_PARAMS };
for (params, 0..) |p, i| call.params[i] = p;
// Write into MIDL tables
if (g_ctx.tables) |*tables| tables.writeSlot(slot, fn_ptr, @truncate(params.len));
g_ctx.queue_buf[g_ctx.queue_count] = call;
g_ctx.queue_count += 1;
g_ctx.state = .queuing;
}
/// Pump all queued calls via PostCall (fire-and-forget sleep mode).
/// Auto-appends SwitchToFiber(main) as the final dispatch.
/// Blocks until the full chain completes.
pub fn pump(self: *Context) void {
_ = self;
if (g_ctx.state != .queuing) return;
g_ctx.state = .executing;
defer {
g_ctx.queue_count = 0;
g_ctx.state = .ready;
}
if (!refreshProxyChannel()) return;
const caller_fiber = g_ctx.caller_fiber orelse return;
// Auto-append SwitchToFiber(caller_fiber)
const switch_slot: u16 = RESERVED_METHODS + @as(u16, @truncate(g_ctx.queue_count));
const fn_switchfiber = if (DEBUG)
@intFromPtr(&debugSwitchToFiber)
else
resolve("kernel32.dll", "SwitchToFiber");
var switch_call = QueuedCall{ .method = switch_slot, .n_params = 1, .params = [_]usize{0} ** MAX_PARAMS };
switch_call.params[0] = @intFromPtr(caller_fiber);
if (g_ctx.tables) |*tables| tables.writeSlot(switch_slot, fn_switchfiber, 1);
g_ctx.queue_buf[g_ctx.queue_count] = switch_call;
g_ctx.queue_count += 1;
// Spawn PostCall worker
const addrs2 = g_ctx.resolved orelse return;
var worker_ctx = SleepWorkerCtx{
.channel = g_ctx.proxy_channel.?,
.post_call_addr = addrs2.PostCall,
.calls = &g_ctx.queue_buf,
.num_calls = g_ctx.queue_count,
.done_event = CreateEventA(null, 1, 0, null),
};
if (worker_ctx.done_event == null) return;
defer _ = CloseHandle(worker_ctx.done_event);
var worker_tid: u32 = 0;
const worker_handle = CreateThread(null, 0, &sleepWorkerThread, @ptrCast(&worker_ctx), 0, &worker_tid) orelse return;
defer _ = CloseHandle(worker_handle);
waitNoPump(worker_ctx.done_event);
if (!executePumpCycle()) return;
waitNoPump(worker_handle);
}
/// Single synchronous call with return value capture (proxy mode).
/// Two-phase: SendReceive(real_call) + PostCall(SwitchToFiber(main)).
/// Zero user code on STA callstack. Blocks until dispatch completes.
pub fn invoke(self: *Context, fn_ptr: usize, params: []const usize) u32 {
_ = self;
if (g_ctx.state != .ready) return 0;
if (params.len > MAX_PARAMS) return 0;
g_ctx.state = .invoking;
defer g_ctx.state = .ready;
log("[invoke] Setting up slots...\n", .{});
const real_slot: u16 = RESERVED_METHODS;
const switch_slot: u16 = RESERVED_METHODS + 1;
const caller_fiber = g_ctx.caller_fiber orelse return 0;
// Write real call into slot 7
var real_call = QueuedCall{ .method = real_slot, .n_params = @truncate(params.len), .params = [_]usize{0} ** MAX_PARAMS };
for (params, 0..) |p, i| real_call.params[i] = p;
if (g_ctx.tables) |*tables| tables.writeSlot(real_slot, fn_ptr, @truncate(params.len));
// Write SwitchToFiber(main) into slot 8
const fn_switchfiber = resolve("kernel32.dll", "SwitchToFiber");
var switch_call = QueuedCall{ .method = switch_slot, .n_params = 1, .params = [_]usize{0} ** MAX_PARAMS };
switch_call.params[0] = @intFromPtr(caller_fiber);
if (g_ctx.tables) |*tables| tables.writeSlot(switch_slot, fn_switchfiber, 1);
const addrs3 = g_ctx.resolved orelse return 0;
var worker_ctx = ProxyWorkerCtx{
.channel = g_ctx.proxy_channel.?,
.post_call_addr = addrs3.PostCall,
.real_call = &real_call,
.switch_call = &switch_call,
.return_value = 0,
.done_event = CreateEventA(null, 0, 0, null),
};
if (worker_ctx.done_event == null) return 0;
defer _ = CloseHandle(worker_ctx.done_event);
var worker_tid: u32 = 0;
const worker_handle = CreateThread(null, 0, &proxyWorkerThread, @ptrCast(&worker_ctx), 0, &worker_tid) orelse return 0;
defer _ = CloseHandle(worker_handle);
if (!executePumpCycle()) return 0;
waitNoPump(worker_ctx.done_event);
waitNoPump(worker_handle);
const retval = worker_ctx.return_value;
return retval;
}
pub fn deinit(self: *Context) void {
_ = self;
if (g_ctx.state == .uninitialized) return;
if (g_ctx.caller_fiber != null) _ = ConvertFiberToThread();
releaseComObject(g_ctx.proxy_channel);
g_ctx.proxy_channel = null;
if (g_ctx.tables) |*tables| tables.deinit();
g_ctx.tables = null;
if (g_ctx.mta_usage_cookie) |cookie| {
const CoDecrementMTAUsage: *const fn (?*anyopaque) callconv(.c) HRESULT =
@ptrFromInt(resolve("ole32.dll", "CoDecrementMTAUsage"));
_ = CoDecrementMTAUsage(cookie);
g_ctx.mta_usage_cookie = null;
}
g_ctx.state = .uninitialized;
}
};
// VEH crash diagnostic handler (DEBUG only)
const CONTEXT_AMD64 = extern struct {
P1Home: u64, P2Home: u64, P3Home: u64, P4Home: u64, P5Home: u64, P6Home: u64,
ContextFlags: u32, MxCsr: u32,
SegCs: u16, SegDs: u16, SegEs: u16, SegFs: u16, SegGs: u16, SegSs: u16,
EFlags: u32,
// No padding needed — Dr0 (u64) naturally aligns to 0x48
Dr0: u64, Dr1: u64, Dr2: u64, Dr3: u64, Dr6: u64, Dr7: u64,
Rax: u64, Rcx: u64, Rdx: u64, Rbx: u64, Rsp: u64, Rbp: u64, Rsi: u64, Rdi: u64,
R8: u64, R9: u64, R10: u64, R11: u64, R12: u64, R13: u64, R14: u64, R15: u64,
Rip: u64,
};
const EXCEPTION_POINTERS2 = extern struct {
ExceptionRecord: *EXCEPTION_RECORD,
ContextRecord: *CONTEXT_AMD64,
};
fn crashHandler(info_raw: *anyopaque) callconv(.c) c_long {
const info: *EXCEPTION_POINTERS2 = @ptrCast(@alignCast(info_raw));
const rec = info.ExceptionRecord;
const ctx = info.ContextRecord;
if (rec.ExceptionCode == 0xC0000005) {
const fault_addr: u64 = if (rec.NumberParameters >= 2) rec.ExceptionInformation[1] else 0;
const access_type: u64 = if (rec.NumberParameters >= 1) rec.ExceptionInformation[0] else 99;
// Module bases for RVA calculation
const rpcrt4_h = GetModuleHandleA("rpcrt4.dll");
const combase_h2 = GetModuleHandleA("combase.dll");
const ntdll_h = GetModuleHandleA("ntdll.dll");
const kernel32_h = GetModuleHandleA("kernel32.dll");
const kernelbase_h = GetModuleHandleA("kernelbase.dll");
const rpcrt4_base: u64 = if (rpcrt4_h) |h| @intFromPtr(h) else 0;
const combase_base2: u64 = if (combase_h2) |h| @intFromPtr(h) else 0;
const ntdll_base: u64 = if (ntdll_h) |h| @intFromPtr(h) else 0;
const kernel32_base: u64 = if (kernel32_h) |h| @intFromPtr(h) else 0;
const kernelbase_base: u64 = if (kernelbase_h) |h| @intFromPtr(h) else 0;
// Find which module RIP is in
const modules = [_]struct { name: []const u8, base: u64 }{
.{ .name = "ntdll", .base = ntdll_base },
.{ .name = "kernelbase", .base = kernelbase_base },
.{ .name = "kernel32", .base = kernel32_base },
.{ .name = "rpcrt4", .base = rpcrt4_base },
.{ .name = "combase", .base = combase_base2 },
};
var crash_mod: []const u8 = "unknown";
var crash_rva: u64 = 0;
// Simple: pick module with largest base <= RIP
var best_base: u64 = 0;
for (modules) |m| {
if (m.base > 0 and m.base <= ctx.Rip and m.base > best_base) {
best_base = m.base;
crash_mod = m.name;
crash_rva = ctx.Rip - m.base;
}
}
print("\n[VEH] AV at RIP=0x{X} ({s}+0x{X}) fault=0x{X} access_type={}\n", .{ ctx.Rip, crash_mod, crash_rva, fault_addr, access_type });
print("[VEH] ntdll=0x{X} kernelbase=0x{X} kernel32=0x{X} rpcrt4=0x{X} combase=0x{X}\n", .{ ntdll_base, kernelbase_base, kernel32_base, rpcrt4_base, combase_base2 });
print("[VEH] RAX=0x{X} RCX=0x{X} RDX=0x{X} RBX=0x{X}\n", .{ ctx.Rax, ctx.Rcx, ctx.Rdx, ctx.Rbx });
print("[VEH] RSI=0x{X} RDI=0x{X} R8=0x{X} R9=0x{X}\n", .{ ctx.Rsi, ctx.Rdi, ctx.R8, ctx.R9 });
print("[VEH] R10=0x{X} R11=0x{X} R12=0x{X} R13=0x{X} RSP=0x{X}\n", .{ ctx.R10, ctx.R11, ctx.R12, ctx.R13, ctx.Rsp });
// Dump stack around RSP to see return addresses
const rsp: [*]const u64 = @ptrFromInt(@as(usize, @intCast(ctx.Rsp)));
print("[VEH] Stack: ", .{});
for (0..8) |i| {
print("0x{X} ", .{rsp[i]});
}
print("\n", .{});
// Check ExceptionInformation[0]: 0=read, 1=write, 8=DEP execute
if (access_type == 8) {
print("[VEH] *** DEP VIOLATION (execute at 0x{X}) *** \n", .{fault_addr});
}
// Trace pointer chain
if (g_ctx.tables) |*tables| {
const fstub = tables.fake_stub;
const pThis = fstub + 0x08;
print("[VEH] pThis=0x{X}\n", .{@intFromPtr(pThis)});
// Read lpVtbl from pThis
const vtbl_ptr = @as(*const usize, @ptrCast(@alignCast(pThis))).*;
print("[VEH] pThis->lpVtbl=0x{X}\n", .{vtbl_ptr});
// header = vtbl - 0x20
const header_ptr = vtbl_ptr - 0x20;
print("[VEH] header (vtbl-0x20)=0x{X}\n", .{header_ptr});
// Read pServerInfo from header+0x08
const si_in_header = @as(*const usize, @ptrCast(@alignCast(@as([*]const u8, @ptrFromInt(header_ptr + 0x08))))).*;
print("[VEH] header->pServerInfo=0x{X} (expected 0x{X})\n", .{ si_in_header, @intFromPtr(tables.server_info) });
// Read DispatchTable from server_info+0x08
if (si_in_header > 0x10000) {
const dt_in_si = @as(*const usize, @ptrCast(@alignCast(@as([*]const u8, @ptrFromInt(si_in_header + 0x08))))).*;
print("[VEH] si->DispatchTable=0x{X} (expected 0x{X})\n", .{ dt_in_si, @intFromPtr(tables.dispatch_table) });
// Read the function pointers
if (dt_in_si > 0x10000) {
const fn7 = @as(*const usize, @ptrCast(@alignCast(@as([*]const u8, @ptrFromInt(dt_in_si + 7 * 8))))).*;
const fn8 = @as(*const usize, @ptrCast(@alignCast(@as([*]const u8, @ptrFromInt(dt_in_si + 8 * 8))))).*;
const fn9 = @as(*const usize, @ptrCast(@alignCast(@as([*]const u8, @ptrFromInt(dt_in_si + 9 * 8))))).*;
print("[VEH] dt_chain[7]=0x{X} [8]=0x{X} [9]=0x{X}\n", .{ fn7, fn8, fn9 });
}
}
// Also read pDispatchTable from header+0x18
const pdt_in_header = @as(*const isize, @ptrCast(@alignCast(@as([*]const u8, @ptrFromInt(header_ptr + 0x18))))).*;
print("[VEH] header->pDispatchTable=0x{X} (expected -1)\n", .{@as(u64, @bitCast(pdt_in_header))});
// pvServerObject from pThis+0x10
const pvso = @as(*const usize, @ptrCast(@alignCast(pThis + 0x10))).*;
print("[VEH] pvServerObject=0x{X} (expected 0x{X})\n", .{ pvso, @intFromPtr(tables.fake_server_obj) });
// Check pStubDesc->aXmitQuintuple (+0x38) — NdrStubCall2 reads this as alt dispatch table
const stub_desc_addr = @intFromPtr(tables.stub_desc);
const xmit_q = @as(*const usize, @ptrCast(@alignCast(@as([*]const u8, @ptrFromInt(stub_desc_addr + 0x38))))).*;
print("[VEH] pStubDesc->aXmitQuintuple(+0x38)=0x{X} (expected dt=0x{X})\n", .{ xmit_q, @intFromPtr(tables.dispatch_table) });
}
// Dump RPCOLEMESSAGE from R8 (3rd arg to NdrStubCall2: pRpcMsg)
// RPC_MESSAGE layout: +0x1C=ProcNum, +0x28=RpcInterfaceInformation, +0x30=ReservedForRuntime
if (ctx.R8 > 0x10000) {
const r8p: [*]const u8 = @ptrFromInt(@as(usize, @intCast(ctx.R8)));
const proc_num = @as(*const u32, @ptrCast(@alignCast(r8p + 0x1C))).*;
const rpc_iface_info = @as(*const usize, @ptrCast(@alignCast(r8p + 0x28))).*;
const mgr_epv = @as(*const usize, @ptrCast(@alignCast(r8p + 0x38))).*;
print("[VEH] R8(pRpcMsg)->ProcNum=0x{X} RpcIfaceInfo=0x{X} MgrEpv=0x{X}\n", .{ proc_num, rpc_iface_info, mgr_epv });
if (rpc_iface_info > 0x10000) {
const expected_si = if (g_ctx.tables) |*t| @intFromPtr(t.server_info) else @as(usize, 0);
const ri_epv = @as(*const usize, @ptrCast(@alignCast(@as([*]const u8, @ptrFromInt(rpc_iface_info + 0x48))))).*;
print("[VEH] RpcIfaceInfo->DefaultMgrEpv=0x{X} (expected server_info 0x{X})\n", .{ ri_epv, expected_si });
} else {
print("[VEH] *** RpcInterfaceInformation is NULL/invalid! ***\n", .{});
}
}
}
// Check #2: Did COM mutate our rpc_iface[0x48] (DefaultManagerEpv)?
if (g_ctx.tables) |*tables| {
const our_rpc_iface = tables.rpc_iface;
const default_mgr_epv = @as(*const usize, @ptrCast(@alignCast(our_rpc_iface + 0x48))).*;
const interpreter_info = @as(*const usize, @ptrCast(@alignCast(our_rpc_iface + 0x50))).*;
print("[VEH] OUR rpc_iface[0x48](DefaultMgrEpv)=0x{X} (expected 0)\n", .{default_mgr_epv});
print("[VEH] OUR rpc_iface[0x50](InterpreterInfo)=0x{X} (expected server_info 0x{X})\n", .{ interpreter_info, @intFromPtr(tables.server_info) });
// Also check stub_desc.RpcInterfaceInformation still points to our rpc_iface
const sd_rpc_iface = @intFromPtr(tables.stub_desc.RpcInterfaceInformation);
print("[VEH] stub_desc.RpcIfaceInfo=0x{X} (expected 0x{X})\n", .{ sd_rpc_iface, @intFromPtr(our_rpc_iface) });
}
// Terminate immediately — don't let COM swallow the exception and hang
std.process.exit(99);
}
pub fn init(config: Config) !Context {
if (g_ctx.state != .uninitialized) return error.AlreadyInitialized;
const heap = GetProcessHeap();
// Resolve combase.dll internal functions via signature scanning
const combase_h = GetModuleHandleA("combase.dll") orelse
LoadLibraryA("combase.dll") orelse return error.CombaseNotFound;
const combase_base = @intFromPtr(combase_h);
g_ctx.resolved = resolveCombaseInternals(combase_base) orelse return error.CombaseResolveFailed;
initSyncVtbl();
initFcoVtbl();
// Initialize MIDL tables
g_ctx.tables = MidlTables.init(heap, config.max_slots) orelse return error.MidlInitFailed;
g_ctx.max_slots = config.max_slots;
// Caller thread is now the STA.
const CoInitializeEx: *const fn (?*anyopaque, u32) callconv(.c) HRESULT =
@ptrFromInt(resolve("ole32.dll", "CoInitializeEx"));
const CoRegisterPSClsid: *const fn (*const [16]u8, *const [16]u8) callconv(.c) HRESULT =
@ptrFromInt(resolve("ole32.dll", "CoRegisterPSClsid"));
const CoRegisterClassObject: *const fn (*const [16]u8, *anyopaque, u32, u32, *u32) callconv(.c) HRESULT =
@ptrFromInt(resolve("ole32.dll", "CoRegisterClassObject"));
const CoIncrementMTAUsage: *const fn (*?*anyopaque) callconv(.c) HRESULT =
@ptrFromInt(resolve("ole32.dll", "CoIncrementMTAUsage"));
const hr_coinit = CoInitializeEx(null, COINIT_APARTMENTTHREADED);
log("[+] CoInitializeEx (caller STA): 0x{X}\n", .{@as(u32, @bitCast(hr_coinit))});
{
const PeekMessageA: *const fn (*[48]u8, ?*anyopaque, u32, u32, u32) callconv(.c) i32 =
@ptrFromInt(resolve("user32.dll", "PeekMessageA"));
const DispatchMessageA: *const fn (*const [48]u8) callconv(.c) isize =
@ptrFromInt(resolve("user32.dll", "DispatchMessageA"));
var msg: [48]u8 = [_]u8{0} ** 48;
while (PeekMessageA(&msg, null, 0, 0, 0x0001) != 0) {
_ = DispatchMessageA(&msg);
}
}
const hr_psclsid = CoRegisterPSClsid(&IID_IFiberDispatch, &CLSID_FiberDispatchPS);
log("[+] CoRegisterPSClsid (caller): 0x{X}\n", .{@as(u32, @bitCast(hr_psclsid))});
var cookie: u32 = 0;
const hr_regclass = CoRegisterClassObject(&CLSID_FiberDispatchPS, @ptrCast(&g_psf), CLSCTX_INPROC_SERVER, REGCLS_MULTIPLEUSE, &cookie);
log("[+] CoRegisterClassObject (caller): 0x{X}\n", .{@as(u32, @bitCast(hr_regclass))});
// VEH crash handler — only in debug builds
if (DEBUG) {
const AddVEH: *const fn (u32, *const fn (*anyopaque) callconv(.c) c_long) callconv(.c) ?*anyopaque =
@ptrFromInt(resolve("kernel32.dll", "AddVectoredExceptionHandler"));
_ = AddVEH(1, &crashHandler);
}
var mta_cookie: ?*anyopaque = null;
const mta_hr = CoIncrementMTAUsage(&mta_cookie);
if (mta_hr < 0) return error.MtaUsageFailed;
g_ctx.mta_usage_cookie = mta_cookie;
if (!refreshProxyChannel()) return error.UnmarshalFailed;
g_ctx.caller_fiber = ConvertThreadToFiber(null);
if (g_ctx.caller_fiber == null) return error.CallerFiberFailed;
g_ctx.state = .ready;
return Context{};
}
// ============================================================================
// Test harness (remove for library use)
// ============================================================================
pub fn main() void {
print("[*] COMegon Framework Test\n", .{});
// Parse --sleep-ms <N> from command line (default 2000ms)
var sleep_ms: u64 = 2000;
const args = std.process.argsAlloc(std.heap.page_allocator) catch &[_][:0]const u8{};
var argi: usize = 1;
while (argi < args.len) : (argi += 1) {
if (std.mem.eql(u8, args[argi], "--sleep-ms")) {
argi += 1;
if (argi < args.len) {
sleep_ms = std.fmt.parseInt(u64, args[argi], 10) catch 2000;
}
}
}
print("[+] Sleep time: {d}ms per call\n", .{sleep_ms});
var ctx = init(.{}) catch |e| {
print("[-] Init failed: {}\n", .{e});
return;
};
defer ctx.deinit();
print("[+] COMegon initialized\n", .{});
print("[+] State: ready\n", .{});
const fn_msgwait = resolve("user32.dll", "MsgWaitForMultipleObjectsEx");
if (fn_msgwait == 0) {
print("[-] Failed to resolve MsgWaitForMultipleObjectsEx\n", .{});
return;
}
const sleep_ms_usize: usize = @intCast(sleep_ms);
print("[+] Queueing MsgWait + SwitchToFiber\n", .{});
ctx.queue(fn_msgwait, &[_]usize{ 0, 0, sleep_ms_usize, 0, 0 });
const start = GetTickCount64();
ctx.pump();
const elapsed = GetTickCount64() - start;
print("[+] Pump returned (elapsed={d}ms, expected ~{d}ms)\n", .{ elapsed, sleep_ms });
print("[+] COMegon test complete\n", .{});
}