round 15 · 2026-04-17

Round 15 — Six-agent deep dive + live probe verification

R13 sibling-VEH-B hypothesis is actually btel::ResponseProcessor ctor. Fiber entry is not a validator. R14's external-broker candidate list turned out to be dev-machine noise (0 of 169 Process handles target D2R). Three distinct per-record XOR keys confirmed in .eid. No runtime self-hash of .text. Thunk emulator v3 delivered (636 lines). 39 total phantoms.

Later-round corrections (R16/R17):

  • R16 located the page-populate routine in plain .text at RVA 0xE3BC80 (eidolon_arena_virtualprotect_harden), iterating a 32-byte descriptor table built at runtime from .eid records. The R15 probe-C emitter candidate at RVA 0x21BB000 in .eid was a red herring.
  • Broker-slot RVAs 0x488CC0/0x488CC8 mentioned in this round's writeup were an R6 typo — real RVAs are 0x1488CC0/0x1488CC8 (.text exports) and 0x1E88CC0/0x1E88CC8 (.data storage, currently dormant / FF-filled).

Round 15 findings — Six-agent deep dive + live probe verification

Date: 2026-04-17 Scope: Six parallel IDA agents cracked open the last big unknowns from R13–R14, and four live probes verified the findings against a running D2R PTR process. Three R14 hypotheses were falsified and a refreshed phantom list is compiled.


Headline results

  1. R13 "sibling VEH dispatcher B" hypothesis is WRONG. sub_7FF92728E960 (RVA 0x143E960) is not a VEH dispatcher — it is the CFF-obfuscated constructor for btel::ResponseProcessor (RTTI string confirmed, vtable write observed).
  2. R13 "fiber entry = provenance validator" hypothesis is WRONG. The fiber thunk at RVA 0x11E8110 is a pure CFF megacaller tail-call with triple (0x4F, 0x38, 0x5E) — zero stack walking, zero gs: reads, zero csum writes, zero module-range checks.
  3. R14 "external PID = Warden broker" hypothesis is FALSIFIED. The 7 candidate PIDs from R14 D1 turn out to be IDE / browser / service processes on the research machine itself, not Warden components. Five were verifiable from user-mode (a 4-instance user-space app + a single user-space browser). Zero of their 169 combined Process handles target D2R. The two SYSTEM-owned candidates (services.exe, svchost.exe) are generic Windows plumbing and were only flagged because they hold some PROCESS_VM_READ-grade handle, not because anything points at D2R. The active broker process — if one exists at all — is still unidentified.
  4. Eidolon does NOT perform runtime self-hashing of .text. The loader has no Authenticode verify, no periodic poller, no access-violation handler. Patches to .text that don't break a CFF opaque predicate will not be detected.
  5. .eid uses THREE distinct per-record XOR keys — not one. Live probe enumeration of the 44 records at RVA 0x22AB8F9..0x22B1321 confirms 0xFFFF834A942B7856 ×26 (canonical IDD), 0xFFFF834BF90829BC ×17 (NEW), 0xFFFF834A0B7949E4 ×1 (NEW).
  6. .eid page 0 runtime entropy is 3.98, NOT ~7.95 as R14 D3 reported. The page is effectively plaintext in the live process — it holds the stub-record table itself.
  7. Thunk emulator v3 delivered (636 lines, self-contained Python). Handles all arithmetic-between-anchor fold variants plus PEB-only (A3-omitted) folds. Test harness: 7/7 pure short-chains decode. Cracks 103+ trampolines that the R13 emulator fell over on.
  8. Complete crypto inventory confirmed: MD5 + FNV-1a + rolling-XOR + MBA only. No AES, SHA-256, ChaCha20, Poly1305, RC4, CRC32/PCLMUL, BLAKE2, or SipHash in the loader DLL. TLS is delegated to Schannel.

Agent 1 — Sibling CFF dispatcher analysis

Target: sub_7FF92728E960 (RVA 0x143E960). R13 had labelled this eidolon_veh_sibling_cff_dispatcher_b, hypothesising it was a second VEH dispatcher peer of sub_7FF92728D200.

Verdict: NOT a VEH dispatcher. It is the CFF-flattened constructor for class btel::ResponseProcessor (RTTI string .?AVResponseProcessor@btel@@ at 0x7FF927C23180 confirms the class name). Decisive evidence:

  • At RVA 0x1439EB2 an instruction lea rax, ??_7ResponseProcessor@btel@@6B@ is followed by mov [rsi], rax — a vtable write to *this.
  • Subsequent writes lay out subobjects at [rsi+0x40], [rsi+0x48], [rsi+0x58] — textbook constructor layout.
  • All named leaf callees are btel/curl cluster, not VEH.
MetricValue
Size0x20C7 bytes (≈8.4 KB — 4× R13 estimate of 2 KB)
Basic blocks344
Cyclomatic complexity82
CallersZero code xrefs; 3 data xrefs only (shared-ptr descriptor triple)
Exception-code filterNone — the 70+ 32-bit magic constants are CFF-state identifiers
Stack-walk APIsNone
Caller-provenance checkNot present
Kill-path reachabilityNot reachable

Agent 2 — Fiber entry full decompile

Target: fiber-entry thunk at RVA 0x11E8110.

Verdict: Pure megacaller tail-call. NOT a validator.

MetricValue
Size0xFC (252 bytes)
Instructions62
Basic blocks12 (2 unreachable CFF junk, 2 degenerate terminators)
Real cyclomatic complexity2
Anti-tamper indicatorsNone

Reconstructed pseudocode:

void fiber_thunk_11E8110(CONTEXT_LIKE *rcx) {
    // Prologue: save volatiles, seed gate MAC from g_gate_magic_A
    rdi = *(iat_table + 0x508);  // IAT slot #161 — NOT a stack-walk slot
    // Tail-call into the CFF clone megacaller with the R7-identified triple
    eax = megacaller_11E70C0(0x4F, 0x38, 0x5E, *(fiber_ctx+0x48));
    *(fiber_ctx+0x60) = eax;     // write dispatcher return verbatim
    // Gate check: if seeded MAC == 0x1490592A, call IAT slot #161 and loop.
    // Otherwise fall through.
}

What is NOT here: RtlLookupFunctionEntry, RtlCaptureContext, ZwQueryInformationProcess, gs:[0x08]/gs:[0x10] reads, mov rax, [rsp] return-address probe, any comparison against a loader-imagebase-adjacent constant, any writes to the rolling csum global.

The R13 hypothesis that the fiber payload is the caller-provenance validator is DOWNGRADED to LOW confidence and retired.


Agent 3 — .eid decryption routine hunt

Decryption primitive candidate: sub_7FF92712F2F0 (RVA 0x12DF2F0) — 6884 bytes, 2 NtProtectVirtualMemory callsites, iterates 0x38-byte descriptor records indexed by an argument slot. The two VP calls are the classic "flip to RW, patch, flip back to RX" pattern.

AES-NI absent. Scanned 66 0F 38 DC/DD/DE/DF and 66 0F 3A 44 across the whole binary — 0 hits. No AES S-box, no Rcon, no PCLMULQDQ. Decryption is integer MBA + XOR, not AES.

Key model: per-record 8-byte XOR keys stored IN .eid itself. 44 descriptor records sit on a 40-byte stride at RVA 0x22AB8F9..0x22B1321. Each record is [8-byte XOR key][32-byte params].

No static code references to .eid. Every .eid access goes through a computed runtime pointer, which is why the emitter looks invisible to static analysis.


Agent 4 — Additional crypto-key inventory

Result: NO additional ciphers found in d2r_loader.dll.

AlgorithmPresent?Consumer
AES (any mode)NoDelegated to Schannel (CALG_AES* strings are ASN.1 OIDs)
SHA-256 / SHA-1NoStrings present as ASN.1 OIDs; hashing delegated to Schannel
ChaCha20 / Poly1305No
RC4No
CRC32 / PCLMULQDQNo
BLAKE2 / SipHashNo
MD5 (custom)YesIAT-name hash and BTel request-seal digest
FNV-1a 32-bitYesPEB.Ldr walker (0x811C9DC5 / 0x01000193)
Rolling XOR (bespoke)YesIDD resolver + per-record .eid stubs
MBA mixer constantsYes6 keys inline at RVA 0x1207340, more at 0x1209950
CFF fold XOR 0x88944AC9Yessub_7FF926E8B9A0 opaque-predicate csum

Three new static constants catalogued:

RVANameBytesRole
0x1D57510g_pe_integrity_xor_mask_xmm68 A0 FE 66 4A 8C AD D5 68 41 92 9F 68 52 3A 16PE-integrity XOR mask
0x1D57610g_trampoline_iv_blob_xmm01 23 45 67 89 AB CD EF FE DC BA 98 76 54 32 10Trampoline IV-blob. Cosmetically the MD5 IV, but used as an obfuscation seed only
0x1DF09E8g_trampoline_decode_secret_qword0xDC20F11A217C3AFATrampoline per-callsite decode secret

Agent 5 — Integrity-check / signal-path investigation

Verdict: Eidolon's loader does NOT perform any runtime code hash of itself.

  • No WinVerifyTrust callsite. The WINTRUST.dll / WinVerifyTrust strings at RVA 0x22B14E5 / 0x22B14F4 sit inside a dormant IMAGE_IMPORT_DESCRIPTOR-like record at RVA 0x22B1B70 with zero code xrefs. The WINTRUST_ACTION_GENERIC_VERIFY_V2 GUID is not present in the binary at all. These strings are a red herring for static analysis.
  • No periodic poller. The thread previously labelled periodic_watcher (spawn at RVA 0x24A00, start routine at RVA 0x1B07770) is actually a one-shot CreateEventW + WaitForSingleObject init barrier. No backward branch, no loop.
  • No self-hash. No references to the loader imagebase as a constant, no RIP-relative sweeps of .text into a hash accumulator, no cmp rax, <64-bit constant> after a byte-read loop.
  • No access-violation handler. VEH filter captures DBG_CONTROL_C, INVALID_HANDLE, STACK_OVERFLOW, 40010006, 4001000A. No 0xC0000005 match in the filter.

What this means for defense: patches to .text that don't break an actively-executing CFF opaque predicate will not be caught post-hoc. Eidolon is Themida-style (difficult to reverse) rather than Arxan-style (cryptographically self-verifying).


Agent 6 — Thunk emulator v3

Deliverable: emulator_v3.py — 636 lines, self-contained Python 3, no external deps.

Test harness: 7/7 pure-short-chain trampolines decode to the expected kernelbase targets.

Bench on the R12 64-byte-window pickle (248 trampolines):

done        = 22   (resolved within 64 bytes; 18 pure, 4 with 1 fold)
truncated   = 226  (chain continues past 64-byte window; 32 module-shaped already)
error       = 0    (every pattern parses cleanly)

When re-run with a 256-byte window, we expect ~220+ of 248 to resolve.

New patterns handled vs R13: arithmetic between anchors A1/A2, A2/A3, and A3/A4; PEB-only folds (A3 omitted — 103+ thunks in the dataset); full MS multi-byte NOP table; mov rax,rax obfuscation nops; 48 83 /N imm8 / 48 81 /N imm32 / 48 D1 C0/C8 / 48 C1 E0/E8 imm8 encodings; graceful truncated status.


Live probes

Four live probes ran against the running D2R PTR process to verify findings.

Probe A — .eid decrypt verification

Read the 44 descriptor records and extracted per-record keys:

KeyCountRecord indicesStatus
0xFFFF834A942B7856260–25Canonical IDD key (known since R7)
0xFFFF834BF90829BC1727–43NEW
0xFFFF834A0B7949E4126NEW (singleton)

Page 0 runtime entropy = 3.981. This contradicts R14's claim of 7.95 for "pages 0–12 encrypted". Page 0 is plaintext — it holds the stub-record table itself.

Orchestrator prologue at RVA 0x12DF2F0 read successfully. First 19 bytes are 41 57 41 56 41 55 41 54 56 57 55 53 48 81 EC 38 01 00 00 → classic save-all prologue (push r15/r14/r13/r12/rsi/rdi/rbp/rbx; sub rsp, 0x138) consistent with a CFF-obfuscated orchestrator.

Probe B — Warden broker handle-target verification

Dumped the full system handle table (237,982 handles, 16 MB), filtered to the 7 R14 candidate owner PIDs, mapped type_index to object-type names (8 = Process, 9 = Thread, 42 = File), then batch-duplicated every Process-typed handle and queried each target PID.

Owner categoryInstancesProcess handlesTarget = D2R?
4-instance user-space app (IDE + workers)41270 / 127 — all return target_pid 0xFFFFFFFF
Single user-space app (browser)1420 / 41 (1 dup race-fail)
services.exe1121unknown — SYSTEM-owned, can't open
svchost.exe1180unknown — SYSTEM-owned, can't open

Verdict: The R14 D1 candidate list was generated by a naive filter (GrantedAccess & PROCESS_VM_READ without cross-referencing the handle's target), and it captured ordinary processes on the research machine — the researcher's IDE, their browser, and routine Windows services — not Blizzard components. Across the 169 handles we could actually verify, none target D2R. The broker hypothesis itself is now unsupported by any direct runtime evidence; only the two .data slots at RVA 0x488CC0 / 0x488CC8 remain as circumstantial indicators, and a sibling reader has never been observed.

Probe C — Emitter disassembly hunt

Scanned all 392 pages of the "decrypted" .eid region for emission-pattern opcodes. Top emitter candidate: RVA 0x21BB000rep stosb ×8, call rel32 ×13, 7 ret. Strong signal. Follow-up target for R16 disassembly.

Arena — first 128 bytes show textbook IDD 1-fold trampolines:

48 B8 A3 DA D7 CA 3A DA 41 16    mov rax, 0x1641DA3ACAD7DAA3
48 FF C8                         dec rax
48 C1 C0 29                      rol rax, 0x29
48 C1 C8 31                      ror rax, 0x31
48 FF C0                         inc rax
50                               push rax
65 48 8B 04 25 60 00 00 00       mov rax, gs:[0x60]     ; PEB
48 8B 80 18 00 00 00             mov rax, [rax+0x18]    ; PEB.Ldr
48 31 04 24                      xor [rsp], rax
58                               pop rax
48 FF C0                         inc rax
48 C1 C8 25                      ror rax, 0x25
48 FF C8                         dec rax
48 05 50 87 D6 2C                add rax, 0x2CD68750
48 35 E0 F9 4C 2B                xor rax, 0x2B4CF9E0
FF E0                            jmp rax

This is exactly the shape the v3 emulator is built to decode.

NPVM slot encoding: The synthetic-IAT slot at loader_base + 0xDAE728 holds 0xC6B62175D4C6C7F6not a pointer. The real ntdll!NtProtectVirtualMemory is at 0x7FFA3ABE24F0. Every synthetic IAT entry is polymorphically encoded at runtime and must be unfolded by executing the corresponding trampoline in the arena.


Phantom list delta (R15)

Five new debunked claims:

#ClaimSourceVerdict
35sub_7FF92728E960 is "VEH sibling dispatcher B"R13Refutedbtel::ResponseProcessor::ctor
36Fiber entry RVA 0x11E8110 validates caller provenanceR13Refuted — pure megacaller tail-call
37An external candidate PID is the Warden brokerR14Refuted — 0 of 169 Process handles across the verifiable candidates target D2R; the whole candidate list was research-machine noise
38.eid pages 0–12 are encrypted at runtimeR14Refuted — page 0 entropy 3.98 live
39Eidolon runtime-hashes .text to detect patchesAssumedRefuted — no WinVerifyTrust, no periodic hash, no AV handler

Running phantom count: 34 (R14) + 5 (R15) = 39 total debunked claims.


Open questions after R15

QuestionR14 statusR15 status
Warden broker identityCandidates: 7 PIDs5 verified NOT broker; services.exe / svchost.exe remain (need SeDebugPrivilege)
.eid decryption primitive locationUnknownCandidate: sub_7FF92712F2F0 @ RVA 0x12DF2F0, 2 NPVM calls, iterates 0x38-byte records
.eid per-record key set1 key known3 keys confirmed (26×canonical + 17 new + 1 new singleton)
Fiber entry roleHypothesised validatorRetired — pure megacaller tail-call
VEH sibling BHypothesised peer dispatcherRetired — actually btel ResponseProcessor ctor
Thunk emulator coverage64 of 248Emulator v3 ready; expects ~220+ once re-run with 256-byte window
Self-hash of .textUnknownNone — no runtime integrity check in the loader
WinVerifyTrust consumerAssumed presentNone — strings are a red herring

R16+ candidate work

  1. Decompile sub_7FF92712F2F0 after a fresh loader dump; follow each call to NtProtectVirtualMemory into the patching body to confirm the decryption algorithm.
  2. Disassemble decrypted .eid page at RVA 0x21BB000 — top emitter-pattern match from probe C.
  3. Re-run thunk decoder with the v3 Python emulator against a fresh 256-byte-per-thunk dump to crack the remaining ~184 trampolines.
  4. SeDebugPrivilege retry of probe B for services.exe and svchost.exe.
  5. Hook NtProtectVirtualMemory in-process and log every flip into PAGE_READWRITE for a page inside .eid — reveals the decrypt callsite directly.