First live-resolved Win32 API names for 17 thunks. Full 6-round MBA fusion equations recovered. BTel seal sibling separated from primary IAT path.
Round 11 findings — 17 thunk API names resolved + full fusion-math decode
Date: 2026-04-17 Scope: 4 IDA agents (I19-I22) + combined live probe that resolved 17/18 pure-decoded thunks to their actual Win32 API targets by walking live exports of kernelbase/ntdll/kernel32.
Headline results
17 thunks → Win32 API names (live-resolved)
For the first time, we have concrete proof of what specific Win32 functions Eidolon's synthetic IAT is resolving. By following each of the 18 pure-arithmetic trampolines (identified by round 9 Agent I17's emulator) and looking up the final target in live exports, we mapped:
Kernelbase (12): CreateProcessW, CreateThread, GetComputerNameW, GetCurrentProcessId, GetFinalPathNameByHandleW, GetModuleFileNameW, GetVersionExW, MapViewOfFile, SetFilePointerEx, SetLastError, SuspendThread (plus +0x730 at 0x7FFA390A2900 unresolved — possibly a newer export not in symbol range)
Ntdll (3): RtlExitUserThread, RtlReleaseSRWLockShared, RtlTryAcquireSRWLockExclusive
Kernel32 (3): CryptDestroyKey, CryptEncrypt, RegCloseKey
This is the verified runtime Win32 surface of Eidolon — no more speculation from name-table matching.
Interpretation of the resolved set
This set of APIs is telling:
- Thread control (
CreateThread, SuspendThread, RtlExitUserThread, RtlTryAcquireSRWLockExclusive, RtlReleaseSRWLockShared) — Eidolon creates and manipulates its own worker threads and synchronizes with the game's threads - File handle inspection (
GetFinalPathNameByHandleW, GetModuleFileNameW, SetFilePointerEx, MapViewOfFile) — consistent with anti-tamper file-path fingerprinting - Host info (
GetComputerNameW, GetVersionExW, GetCurrentProcessId) —HostInfofingerprinting - Crypto (
CryptDestroyKey, CryptEncrypt) — used in telemetry encryption? (Not related to the MD5/FNV we identified) - Registry (
RegCloseKey) — likely MachineGuid lookup (paired withRegOpenKeyExW,RegQueryValueExWwhich should be in the non-pure thunks) - Process creation (
CreateProcessW) — Eidolon can spawn helper processes! Worth investigating — is this for crash-reporter launch? BTel subprocess?
Round-9 I17 correction
Agent I17 guessed +0x4A8 was ReleaseSRWLockExclusive (paired with +0x40 lock-acquire). Wrong. +0x4A8 is SetLastError in kernelbase. This means +0x40 (suspected lock-acquire by round 8 I12) is probably ALSO not a lock-related API. The "lock-acquire/release pair" narrative from round 8 was a false pattern match.
Agent I19 — sub_7FF927059950 is NOT a byte-stager (negative result)
Hypothesis refuted: sub_7FF927059950 is another MBA integer mixer, not a trampoline emitter. Evidence:
- Only 6 byte immediates written to memory, none are trampoline opcodes
- 541 constants dominated by 64-bit MBA primes (
0xD6BEB5E2D562996E, etc.) - Zero callers in the IAT-resolver call graph (only called from
sub_7FF926C5BFF0, a vtable-dispatched BTel entry)
Real emitter candidate identified: sub_7FF92604CE40 (a direct callee of the IAT resolver) has the trifecta of trampoline-opcode byte writes — 0x05, 0x35, 0x65, 0xC1, 0xE0 — exactly the byte set needed to emit add rax, imm32; xor rax, imm32; 65: (PEB.Ldr prefix); rol/ror rax; jmp rax.
Secondary candidate: sub_7FF926056F00 (also IAT-resolver callee) has 0x05, 0x65, 0xE0, 0xFF.
Per-API RNG sources: dword_7FF927CA4168 (BTel-side MBA counter) and dword_7FF927C93348 (IAT-side counter). These advance per API resolved and drive opcode selection.
Agent I20 — IAT resolver prologue is CFF-only (no allocation)
Major round-9 correction: the arena allocation is NOT in the resolver prologue. Static analysis of eidolon_iat_resolver_main's prologue (0x7FF926042AF0..0x7FF92604A9A8) shows:
- Zero allocator calls (no
VirtualAlloc,NtAllocateVirtualMemory,HeapAlloc, etc. reachable) - Zero synthetic-IAT indirect calls (
call qword ptr [byte_7FF927BFE190+X]absent from the prologue) - Only 2 real callees:
sub_7FF926D0F7D0(constant-decrypt helper with MD4/MD5 IV constant01 23 45 67 89 AB CD EF FE DC BA 98 76 54 32 10atqword_7FF927BA7610) andsub_7FF92605561F(1-byte dead stub) - Resolver is single-pass, not looped — no back-edges beyond CFF dispatcher returns
The arena allocation lives in eidolon_iat_resolver_outer_wrapper at 0x7FF927571DD0 (the resolver's only caller). Need a separate pass on that 12K-instruction function to find the allocator.
Register roles clarified:
rsi= incomingrcx(per-API context from outer wrapper)r14=&qword_7FF927CD9D40(static BSS scratch region, NOT passed in)r13= per-API row pointer, set from stack spill (specific assignment obscured by CFF)qword_7FF927C2AF70,qword_7FF927C2AF80= per-API encrypted seed scalars
Iteration model: The resolver resolves ONE API per invocation. The loop over 39 DLLs / 248 APIs is in the outer wrapper. Rename suggestion: eidolon_iat_resolver_main → eidolon_resolve_one_api.
Agent I21 — Full fusion-math decode for the 6-round chain
Complete symbolic equations for all 6 rounds (called sub_7FF927057340 is the per-round MBA mixer):
Input seeds
9 IDD slots consumed: qword_7FF927C2AF80..AFD0 (stride 8, with skipped slots). Specifically reads slots at offsets 0, 8, 16, 24, 48 (0x30), 80 (0x50).
Per-round feeder math (reconstructed)
g₁(R₁,S₀)= OR/mul/XOR chain with constantsK₁₁=0x1B02AE194C243C67, K₁₂=0x05A16FBD1B2473A5, K₁₃=0x7B61D2BF5BA38ECAg₂(R₂,S₁)= MD5-style F/G/I triad withK₂₁..K₂₅(includes0x9747609F3E4F8C86, 0xF1CAC3D4AC04CB37, 0x23BB12D9C46919D7, 0xDC44ED263B96E628, 0x0EABF99C3A70E504)g₃(R₃,S₂)= polynomial withK₃₁=0x200F6DD696A203F2g₄(R₄,S₃)= linear MBA-collapse (short block, 245 bytes)g₅(R₅,S₅)= imul(·, -0xB) + sum, produces name-hash tagg₆(R₆,S₈)= 32-bit LCG; updates rolling protocol checksum atdword_7FF927C21FE0
Per-API row slots (in the scratch struct at r13)
row[+0x06](16-bit): name-hash tag, derived from R₅ + 32-bit mixer over (ecx, edx, r8d)row[+0x88](64-bit): raw round-5 mixer output — serves as decryption key / XOR mask for verificationrow[+0x90](64-bit): encrypted pointer (single-dereference)row[+0x98](64-bit): the arena trampoline pointer —R₆ ⊕ constant-mask ⊕ S₈
Output-table commit (in r14 buffer, NOT r13)
Three qwords written per API at stride 0x18 (24 bytes), NOT 0xA0 as round 9 I18 hypothesized:
lea rax, [rax + rax*2] ; rax *= 3
mov rcx, [rsi] ; from round-5 staging
mov [r14 + rax*8 + 0x00], rcx ; slot 0
mov rcx, [var_508]; mov rcx, [rcx] ; double-deref → trampoline pointer
mov [r14 + rax*8 + 0x08], rcx ; slot 1 — ARENA POINTER
mov rcx, [var_1A0] ; metadata
mov [r14 + rax*8 + 0x10], rcx ; slot 2Critical new finding: rolling protocol checksum
At 0x7FF92604CB00: mov cs:dword_7FF927C21FE0, edx after every API resolution.
This is a rolling integrity check — if any API is skipped in the resolution phase, the final checksum won't match expectations. An in-process observer must not tamper with this global; Eidolon's anti-tamper may verify it downstream.
Table sizing
248 APIs × 24 bytes/row = 5,952 bytes = 0x1740 resolved-API output table. Plus per-DLL group headers = likely 0x1800..0x2000 total table size at r14 = &qword_7FF927CD9D40.
Agent I22 — Sibling resolver is a BTel request-seal builder, not a 2nd IAT
Round 8 hypothesis refuted: sub_7FF9261E50B0 is NOT a second IAT resolver. It's a BTel request-seal digest builder.
Evidence:
- Called only from
eidolon_btel_curl_post_stage_helperat0x7FF926254E40 - Output written to a heap-allocated buffer via
operator new(??2@), NOT to a static thunk table - Only ONE call to
sub_7FF927057340(vs 6× in the primary resolver) - No xref to the primary thunk table
byte_7FF927BFE190 - Uses different seed quartet (
9031047653CC4BA6,6FCEFB89AC33B459,41F5D9C3F3AAC92E,4687FDED7B10693) than the primary resolver
Purpose: Computes a keyed MD5-style digest of outbound telemetry payloads for per-request integrity sealing. Blizzard's backend validates the seal.
Third "clone" 0x7FF926C48FF0 (previously mislabeled iat_resolver_alt_clone in round 8 I16): this is NOT an IAT resolver either. It's a VEH bootstrap stage reached only via a descriptor table at 0x7FF927D3699C. It installs the VEH by calling sub_7FF927464BC0.
Updated resolver taxonomy
Primary IAT resolver: eidolon_iat_resolver_main (0x7FF926042AF0) 6× mixer, 21-row IDD
BTel request-seal: sub_7FF9261E50B0 1× mixer, sibling IDD, heap output
VEH bootstrap: 0x7FF926C48FF0 VEH-install stage, dispatched via table
Updated address registry
Key new addresses from round 11
| Item | IDA VA | Purpose |
|---|---|---|
| Real trampoline emitter candidate (primary) | 0x7FF92604CE40 | Writes opcode bytes 0x05, 0x35, 0x65, 0xC1, 0xE0 |
| Real trampoline emitter candidate (secondary) | 0x7FF926056F00 | Writes opcode bytes 0x05, 0x65, 0xE0, 0xFF |
| IAT-side per-API MBA counter | dword_7FF927C93348 | LFSR advancing per resolved API |
| BTel-side per-API MBA counter | dword_7FF927CA4168 | Same shape, different subsystem |
| Per-API encrypted seeds | qword_7FF927C2AF70, qword_7FF927C2AF80 | Consumed by prologue |
| IDD slot array base | qword_7FF927C2AF80 | 9 slots read during 6-round resolution |
| Rolling resolve checksum | dword_7FF927C21FE0 | Updated every API; DO NOT TOUCH |
Static scratch buffer (r14) | qword_7FF927CD9D40 | Output table base, ~0x2000 bytes |
| MD4/MD5 IV constant | qword_7FF927BA7610 = 01 23 45 67 89 AB CD EF FE DC BA 98 76 54 32 10 | Used by constant-decrypt helper |
| Constant-decrypt helper | sub_7FF926D0F7D0 | Called from resolver prologue |
| Outer wrapper (arena allocator lives here) | eidolon_iat_resolver_outer_wrapper @ 0x7FF927571DD0 | 12K insn, next agent target |
| BTel request-seal | sub_7FF9261E50B0 | Per-request MD5-like seal |
| VEH bootstrap stage | 0x7FF926C48FF0 | VEH installer, dispatch-table reached |
| BTel dispatcher (common) | eidolon_btel_post_dispatch_thunk_third_channel @ 0x7FF926CC30D0 | Shared IAT-resolver + BTel-seal entry |
Live runtime addresses (ASLR-dependent but currently)
| Module | Live base |
|---|---|
| kernelbase.dll | 0x7FFA39360000 |
| ntdll.dll | 0x7FFA3AA80000 |
| kernel32.dll | 0x7FFA38810000 |
Per-API output table (at r14 during resolution)
Row stride: 0x18 bytes (24 bytes) with layout:
+0x00QWORD: round-5 staging pointer+0x08QWORD: arena trampoline pointer (DOUBLE-DEREF) — this is the one that ends up inbyte_7FF927BFE190+0x10QWORD: metadata / NT-IAT-entry pointer
Per-API scratch struct (at r13 during resolution)
+0x06WORD: name-hash tag (used for verification)+0x88QWORD: round-5 raw mixer output (decryption key / XOR mask)+0x90QWORD: encrypted pointer (single-deref)+0x98QWORD: arena trampoline pointer pre-commit
Defense plan updates
New "don't touch" addition
Rolling resolve checksum at dword_7FF927C21FE0 — if Eidolon's downstream anti-tamper verifies this value at any point, corrupting it guarantees detection. An in-process observer must not write to this global.
VEH defense (round 10 guidance still correct)
Agent I22 confirmed the real VEH is installed via sub_7FF927464BC0 from the VEH bootstrap stage at 0x7FF926C48FF0, dispatched through descriptor table at 0x7FF927D3699C. Once installed, sub_7FF926CEFDA0 (the wrapper) + eidolon_veh_handler_uses_fiberdata @ 0x7FF92728D200 (inner dispatcher) handle all Eidolon VEH work. Our defense (head-of-list VEH with tagged-fault suppression) stays unchanged. (R16 correction: the actual OS-registered VEH body is at RVA 0x222B40, installed by a single AddVectoredExceptionHandler call at RVA 0x15A3F50. sub_7FF926CEFDA0 / RVA 0xE9FDA0 is a secondary wrapper from a parallel dispatch path, never registered. The inner at RVA 0x143D200 does not kill — it only dispatches; kills route through the UEH chain.)
Confirmed: BTel dispatcher is the shared entry for IAT resolution AND telemetry
eidolon_btel_post_dispatch_thunk_third_channel @ 0x7FF926CC30D0 routes to:
eidolon_iat_resolver_outer_wrapper @ 0x7FF927571DD0→eidolon_resolve_one_api @ 0x7FF926042AF0eidolon_btel_curl_perform_post @ 0x7FF926523940→eidolon_btel_curl_post_stage_helper @ 0x7FF926254790→sub_7FF9261E50B0(request-seal)
This means blocking BTel HTTPS (hosts-file redirect) does NOT affect IAT resolution — they share only the dispatcher thunk, not the final code paths.
Open questions for round 12 (if we continue)
- Confirm the emitter at
sub_7FF92604CE40by decompiling it and finding theC6 /0 imm8byte-write loop with opcode-selection RNG driven bydword_7FF927C93348. Or dynamically via a write-watch on the arena. - Recover the remaining 31 linear-fold thunks using a live PEB.Ldr value + the I17 emulator extended to handle folds.
- Re-probe with 128-byte trampoline window to decode the 128 truncated thunks from round 9.
- Find the arena allocation call in
eidolon_iat_resolver_outer_wrapper @ 0x7FF927571DD0. Needs dedicated agent. - Verify
+0x40API identity — still unresolved (linear-fold needs PEB.Ldr). Round 8 I12 thought it was a lock-acquire; round 9 I17 couldn't confirm. Given+0x4A8 = SetLastError(not lock-release), the pairing hypothesis is dead. - Handle scan probe — identify Warden broker PID via
NtQuerySystemInformation(SystemHandleInformation)filtered toPROCESS_VM_READhandles into D2R's PID. Requires Lua access toNtQuerySystemInformationthrough kernel32/ntdll.