round 11 · 2026-04-17

Round 11 — 17 API thunks resolved + fusion math decoded

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) — HostInfo fingerprinting
  • Crypto (CryptDestroyKey, CryptEncrypt) — used in telemetry encryption? (Not related to the MD5/FNV we identified)
  • Registry (RegCloseKey) — likely MachineGuid lookup (paired with RegOpenKeyExW, RegQueryValueExW which 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 constant 01 23 45 67 89 AB CD EF FE DC BA 98 76 54 32 10 at qword_7FF927BA7610) and sub_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 = incoming rcx (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_maineidolon_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 constants K₁₁=0x1B02AE194C243C67, K₁₂=0x05A16FBD1B2473A5, K₁₃=0x7B61D2BF5BA38ECA
  • g₂(R₂,S₁) = MD5-style F/G/I triad with K₂₁..K₂₅ (includes 0x9747609F3E4F8C86, 0xF1CAC3D4AC04CB37, 0x23BB12D9C46919D7, 0xDC44ED263B96E628, 0x0EABF99C3A70E504)
  • g₃(R₃,S₂) = polynomial with K₃₁=0x200F6DD696A203F2
  • g₄(R₄,S₃) = linear MBA-collapse (short block, 245 bytes)
  • g₅(R₅,S₅) = imul(·, -0xB) + sum, produces name-hash tag
  • g₆(R₆,S₈) = 32-bit LCG; updates rolling protocol checksum at dword_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 verification
  • row[+0x90] (64-bit): encrypted pointer (single-dereference)
  • row[+0x98] (64-bit): the arena trampoline pointerR₆ ⊕ 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 2

Critical 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_helper at 0x7FF926254E40
  • 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

ItemIDA VAPurpose
Real trampoline emitter candidate (primary)0x7FF92604CE40Writes opcode bytes 0x05, 0x35, 0x65, 0xC1, 0xE0
Real trampoline emitter candidate (secondary)0x7FF926056F00Writes opcode bytes 0x05, 0x65, 0xE0, 0xFF
IAT-side per-API MBA counterdword_7FF927C93348LFSR advancing per resolved API
BTel-side per-API MBA counterdword_7FF927CA4168Same shape, different subsystem
Per-API encrypted seedsqword_7FF927C2AF70, qword_7FF927C2AF80Consumed by prologue
IDD slot array baseqword_7FF927C2AF809 slots read during 6-round resolution
Rolling resolve checksumdword_7FF927C21FE0Updated every API; DO NOT TOUCH
Static scratch buffer (r14)qword_7FF927CD9D40Output table base, ~0x2000 bytes
MD4/MD5 IV constantqword_7FF927BA7610 = 01 23 45 67 89 AB CD EF FE DC BA 98 76 54 32 10Used by constant-decrypt helper
Constant-decrypt helpersub_7FF926D0F7D0Called from resolver prologue
Outer wrapper (arena allocator lives here)eidolon_iat_resolver_outer_wrapper @ 0x7FF927571DD012K insn, next agent target
BTel request-sealsub_7FF9261E50B0Per-request MD5-like seal
VEH bootstrap stage0x7FF926C48FF0VEH installer, dispatch-table reached
BTel dispatcher (common)eidolon_btel_post_dispatch_thunk_third_channel @ 0x7FF926CC30D0Shared IAT-resolver + BTel-seal entry

Live runtime addresses (ASLR-dependent but currently)

ModuleLive base
kernelbase.dll0x7FFA39360000
ntdll.dll0x7FFA3AA80000
kernel32.dll0x7FFA38810000

Per-API output table (at r14 during resolution)

Row stride: 0x18 bytes (24 bytes) with layout:

  • +0x00 QWORD: round-5 staging pointer
  • +0x08 QWORD: arena trampoline pointer (DOUBLE-DEREF) — this is the one that ends up in byte_7FF927BFE190
  • +0x10 QWORD: metadata / NT-IAT-entry pointer

Per-API scratch struct (at r13 during resolution)

  • +0x06 WORD: name-hash tag (used for verification)
  • +0x88 QWORD: round-5 raw mixer output (decryption key / XOR mask)
  • +0x90 QWORD: encrypted pointer (single-deref)
  • +0x98 QWORD: 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 @ 0x7FF927571DD0eidolon_resolve_one_api @ 0x7FF926042AF0
  • eidolon_btel_curl_perform_post @ 0x7FF926523940eidolon_btel_curl_post_stage_helper @ 0x7FF926254790sub_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)

  1. Confirm the emitter at sub_7FF92604CE40 by decompiling it and finding the C6 /0 imm8 byte-write loop with opcode-selection RNG driven by dword_7FF927C93348. Or dynamically via a write-watch on the arena.
  2. Recover the remaining 31 linear-fold thunks using a live PEB.Ldr value + the I17 emulator extended to handle folds.
  3. Re-probe with 128-byte trampoline window to decode the 128 truncated thunks from round 9.
  4. Find the arena allocation call in eidolon_iat_resolver_outer_wrapper @ 0x7FF927571DD0. Needs dedicated agent.
  5. Verify +0x40 API 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.
  6. Handle scan probe — identify Warden broker PID via NtQuerySystemInformation(SystemHandleInformation) filtered to PROCESS_VM_READ handles into D2R's PID. Requires Lua access to NtQuerySystemInformation through kernel32/ntdll.