Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Omegon
Dual-mode API dispatch framework that routes arbitrary Windows API calls through COM infrastructure, producing fully legitimate system callstacks with zero user code on any thread.
What It Does
Omegon leverages the COM runtime's own internal dispatch machinery to execute API calls on your behalf. From the OS perspective, every call originates from combase.dll → rpcrt4.dll → ntdll.dll — a callstack indistinguishable from normal COM RPC activity.
Modes
Sleep Mode (Batch Fire-and-Forget)
Queue multiple API calls, then execute them all in a single pump cycle. The calling thread's fiber is suspended while dispatch occurs on a dedicated pump fiber whose stack contains only system frames.
Use case: Sleep encryption chains — queue VirtualProtect → SystemFunction032 → NtDelayExecution → SystemFunction032 → VirtualProtect, then sleep. Your .text section can be encrypted because no user code exists on any active callstack.
Proxy Mode (Synchronous Dispatch)
Execute a single API call and retrieve the result. The call is dispatched through the same COM channel but returns synchronously to the caller.
Use case: Runtime stack spoofing — any sensitive API call gets a clean system-only callstack without persistent infrastructure changes.
Security Properties
| Property | Status |
|---|---|
| CET (Shadow Stack) compliant | ✅ All returns are legitimate |
| HSP (Hardware Stack Protection) compliant | ✅ No stack pivots or overwrites |
| No user code on callstack (sleep) | ✅ Pump fiber is pure system frames |
| No user code on STA callstack (proxy) | ✅ Dispatch via COM channel |
| .text encryption safe | ✅ No return addresses into user .text during sleep |
| No trampolines or detours | ✅ Uses COM's own vtable dispatch |
| Fiber-based context switching | ✅ CET-compliant control flow |
Architecture
Omegon abuses the COM runtime's cross-apartment RPC dispatch to execute arbitrary function pointers through a fully legitimate system callstack. No hooks, no trampolines, no shellcode — just carefully crafted MIDL metadata that tells NdrStubCall2 to call our functions as if they were COM method implementations.
Layered Design
Layer 1: RVA Resolution — Resolve combase.dll internals by offset (ModalLoop, BlockFn,
CCliModalLoopCtor, PostCall, NoOpReturn0, CCliModalLoop vtable)
Layer 2: Format Strings — Build NDR proc format strings at runtime describing each dispatch
slot's parameter layout (param count, stack sizes, Oi2 header)
Layer 3: MIDL Tables — Heap-allocated MIDL_SERVER_INFO, MIDL_STUB_DESC, RPC_SERVER_INTERFACE,
dispatch tables, format string offsets, CStdStubBuffer, stub vtable
Layer 4: COM Plumbing — IPSFactoryBuffer, IRpcProxyBuffer, IClassFactory, ISynchronize —
fake COM objects that satisfy the runtime's QueryInterface/CreateStub/
CreateProxy calls during CoMarshalInterThreadInterfaceInStream
Layer 5: STA Command Loop — Dedicated STA thread: CoInitEx(APARTMENTTHREADED), registers our
PSFactory via CoRegisterClassObject, marshals IFiberDispatch, then
enters a fiber-based command loop awaiting sleep/proxy/shutdown
Layer 6: PostCall Worker — MTA worker thread: GetBuffer → fill RPCOLEMESSAGE → PostCall for each
queued call. Writes SOleTlsData PID and fake ISynchronize per message.
Layer 7: SendReceive Worker — (WIP) MTA worker: GetBuffer → SendReceive → FreeBuffer for synchronous
single-call proxy mode
Layer 8: Public API — init(), queue(), pump() [working], invoke() [WIP], deinit()
Public API
// Initialize: spins up STA thread, registers COM plumbing, marshals channel
var ctx = try omegon.init(.{ .max_slots = 256 });
defer ctx.deinit();
// Sleep mode: queue N calls, fire them all, block until done
// No return values — fire-and-forget. Last call is auto-appended SwitchToFiber(main).
ctx.queue(fn_VirtualProtect, &.{ text_base, text_size, PAGE_RW, &old_prot });
ctx.queue(fn_SystemFunction032, &.{ &img_range, &key_range });
ctx.queue(fn_NtDelayExecution, &.{ 0, &delay });
ctx.queue(fn_SystemFunction032, &.{ &img_range, &key_range });
ctx.queue(fn_VirtualProtect, &.{ text_base, text_size, PAGE_RX, &old_prot });
ctx.pump(); // blocks until all 5 calls + SwitchToFiber(main) complete
// Proxy mode (WIP): single synchronous call, captures HRESULT return value
// Dispatches via SendReceive (synchronous) + PostCall(SwitchToFiber) for cleanup
const status = ctx.invoke(fn_NtAllocateVirtualMemory, &.{ process, &base, 0, &size, MEM_COMMIT, PAGE_RW });
| Function | Mode | Transport | Returns | Status |
|---|---|---|---|---|
queue() + pump() |
Sleep | PostCall (async) | void | ✅ Working |
invoke() |
Proxy | SendReceive (sync) | HRESULT (u32) | ❌ WIP |
The Dispatch Chain (Sleep Mode)
MTA Worker Thread STA Thread / Pump Fiber
──────────────── ──────────────────────────
GetBuffer(channel, &msg, IID)
msg.iMethod = RESERVED_METHODS + slot
PostCall(channel, &msg, &sync, pid) ──► WM_USER+0x00 posted to STA window
... repeat for each queued call ...
SwitchToFiber(pump_fiber)
│
┌──────▼──────────────────────────┐
│ combase!ModalLoop │
│ └─► combase!BlockFn │
│ ├─ WaitForMultipleObjects │
│ ├─ GetQueueStatus │
│ ├─ PeekRPCAndDDEMessage │
│ └─ DispatchMessageW │
│ └─► ThreadWndProc │
│ └─► ThreadDisp │
│ └─► CIFLAI │
│ ┌────────────┐ │
│ │CStdStub_Inv│ │
│ │ └─NdrStub │ │
│ │ └─YOUR │ │
│ │ API │ │
│ └────────────┘ │
└───────────────────────────────────┘
Full callstack during dispatch (what ETW/debugger sees):
ntdll!NtDelayExecution ← your function
rpcrt4!NdrStubCall2 ← NDR unmarshals params, calls dispatch_table[method]
combase!CStdStubBuffer_Invoke ← stub vtable[5], routes to NdrStubCall2 via MIDL_SERVER_INFO
combase!SyncStubInvoke
combase!StubInvoke
combase!ComInvokeWithLockAndIPID ← CIFLAI: finds IPID entry, resolves stub, calls Invoke
combase!ThreadDispatch
combase!ThreadWndProc ← STA COM hidden window message handler
user32!DispatchMessageW
combase!PeekRPCAndDDEMessage ← MyPeekMessage + DispatchMessageW on COM window
combase!CCliModalLoop::BlockFn ← pumps messages in a wait loop
combase!ModalLoop ← top-level RPC pump (entry point of pump fiber)
ntdll!BaseFiberStart ← CET-compliant fiber entry
Key Internal Structures
CStdStubBuffer (fake_stub, 0x48 bytes) — The structure CStdStubBuffer_Invoke receives as this:
+0x00 pvServerObject → fake_server_obj (whose [0] → server_dispatch_array)
+0x08 vtbl_ptr → stub vtable (CStdStubBuffer_QI/AddRef/Connect/.../Invoke)
+0x10 ref_count = 1
+0x18 pvServerObject2 → fake_server_obj (refreshed before each pump cycle)
+0x40 pHeader → CInterfaceStubHeader
CInterfaceStubHeader (precedes stub vtable in memory):
+0x00 piid → IID_IFiberDispatch
+0x08 pServerInfo → MIDL_SERVER_INFO
+0x10 DispatchTableCount = max_slots
+0x14 _pad = 0
+0x18 pDispatchTable = -1 ← CRITICAL: must be -1 for NDR path (0 = skip dispatch)
MIDL_SERVER_INFO — Tells NdrStubCall2 where to find everything:
+0x00 pStubDesc → MIDL_STUB_DESC (alloc/free funcs, format types)
+0x08 DispatchTable → [fn_ptr, fn_ptr, ...] ← YOUR function pointers
+0x10 ProcString → all_fmt buffer (NDR format strings per method)
+0x18 FmtStringOffset → [offset, offset, ...] ← per-method offset into ProcString
+0x20 ThunkTable = null
+0x28 pTransferSyntax → NDR 2.0 syntax GUID {8A885D04-1CEB-11C9-...}
Fake CSyncClientCall (0x120 bytes, passed to ModalLoop as fiber param):
+0xC0 inner_obj ptr → fake COM object (all vtable slots → combase!NoOpReturn0)
+0x108 pump_event handle ← signaled to make BlockFn's WaitForMultipleObjectsEx return
CCliModalLoop (constructed via CCliModalLoopCtor(buf, 0, 0x04FF, 0, 0)):
Written into the STA thread's TLS chain: SOleTlsData.pCAptCallCtrl._pTopCML → cml_buf. BlockFn reads cml_buf[0x18] for return code (RPC_S_CALLPENDING = keep pumping) and cml_buf[0x108] for the pump event handle.
NDR Format Strings
Each dispatch slot gets a dynamically-built NDR Oi2 proc format string that describes the method's parameter layout. NdrStubCall2 parses this to determine stack frame size and unmarshals parameters from the RPCOLEMESSAGE buffer into the call.
Format structure per method:
[0x32, 0x48] ← Oi2 header (handle_type, oi_flags)
[rpc_flags: u32] ← 0
[method_num: u16] ← RESERVED_METHODS + slot
[stack_size: u16] ← (n_params + 1) * 8
[0, 0, 0, 0, 0, 0] ← excount, padding
[n_params + 1, 0] ← param count (including return)
Per-param (6 bytes each):
[FC_HYPER, 0x08/0x48, stack_offset: u16, 0, 0] ← 8-byte param at offset
Return:
[FC_LONG, 0x52, stack_offset: u16, 0, 0] ← HRESULT return
[0x5B, 0x5C, 0x00] ← FC_END, FC_PAD, terminator
COM Channel Setup
- STA thread initializes apartment-threaded COM, registers a fake
IPSFactoryBufferunder a custom CLSID viaCoRegisterClassObject, then marshals a fakeIFiberDispatchobject viaCoMarshalInterThreadInterfaceInStream - During marshal, COM calls our
IPSFactoryBuffer::CreateStub— we return ourCStdStubBuffer(fake_stub) which points at our MIDL tables - Main thread (MTA) calls
CoGetInterfaceAndReleaseStreamto unmarshal — COM calls ourIPSFactoryBuffer::CreateProxy, connects theIRpcChannelBuffer(which we capture for laterGetBuffer/PostCall/SendReceivecalls) - The
IRpcChannelBufferis now a live cross-apartment channel.GetBufferallocates anRPCOLEMESSAGE,PostCallfires it as an async message to the STA's hidden COM window
Sleep Mode Pump Cycle
- Caller
queue()s N calls → stored as{method_slot, fn_ptr, n_params} pump()signals STA command loop → STA constructs CML, creates pump fiber (CreateFiber(ModalLoop, &fake_client_call)), switches to it- MTA worker thread posts all N calls via
PostCall(each writes method index intoRPCOLEMESSAGE.iMethod, sets wParam viaSOleTlsDataPID pattern, provides fakeISynchronize) ModalLoop→BlockFnpumps the STA message queue, dispatching eachWM_USERthroughThreadWndProc→ComInvokeWithLockAndIPID→CStdStubBuffer_Invoke→NdrStubCall2→dispatch_table[iMethod]→ your function- Last queued call is
SwitchToFiber(sta_main_fiber)— returns control to STA - STA destroys pump fiber (
DeleteFiberon suspended fiber — safe, no resume needed), signals completion
Build
Requires Zig (tested with 0.14.x):
zig build-exe omegon.zig -ODebug --name omegon_test
Project Structure
omegon.zig # Framework implementation
initial-pocs/
omega1.zig # Original sleep mode PoC (static MIDL tables)
sta_com_sleep.zig # Original proxy mode PoC (SendReceive path)
Status
- COM channel setup (STA/MTA marshal/unmarshal)
- MIDL table construction (dynamic slot allocation, up to 249 calls)
- Sleep mode — batch dispatch via PostCall + ModalLoop (
pump()) - Clean fiber lifecycle (ModalLoop as direct fiber proc, DeleteFiber on suspended)
- NDR format string generation (runtime Oi2 builder, variable param counts)
- Auto-appended SwitchToFiber(main) as final dispatch call
- Proxy mode — synchronous dispatch via SendReceive (
invoke()) — blocked onRPC_E_FULLSIC_REQUIRED - Debug output removal / production hardening
- VEH crash handler removal (debug-only)
Requirements
- Windows 10/11 x64
- COM runtime (combase.dll, rpcrt4.dll) — present on all Windows installations
- No additional dependencies
License
Private — not for redistribution.