mirror of
https://github.com/nbaertsch/COMegon
synced 2026-08-09 12:57:42 +00:00
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>
1739 lines
74 KiB
Zig
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", .{});
|
|
}
|