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
.textat RVA0xE3BC80(eidolon_arena_virtualprotect_harden), iterating a 32-byte descriptor table built at runtime from.eidrecords. The R15 probe-C emitter candidate at RVA0x21BB000in.eidwas a red herring.- Broker-slot RVAs
0x488CC0/0x488CC8mentioned in this round's writeup were an R6 typo — real RVAs are0x1488CC0/0x1488CC8(.textexports) and0x1E88CC0/0x1E88CC8(.datastorage, 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
- R13 "sibling VEH dispatcher B" hypothesis is WRONG.
sub_7FF92728E960(RVA0x143E960) is not a VEH dispatcher — it is the CFF-obfuscated constructor forbtel::ResponseProcessor(RTTI string confirmed, vtable write observed). - R13 "fiber entry = provenance validator" hypothesis is WRONG. The
fiber thunk at RVA
0x11E8110is a pure CFF megacaller tail-call with triple(0x4F, 0x38, 0x5E)— zero stack walking, zerogs:reads, zero csum writes, zero module-range checks. - 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 somePROCESS_VM_READ-grade handle, not because anything points at D2R. The active broker process — if one exists at all — is still unidentified. - Eidolon does NOT perform runtime self-hashing of
.text. The loader has no Authenticode verify, no periodic poller, no access-violation handler. Patches to.textthat don't break a CFF opaque predicate will not be detected. .eiduses THREE distinct per-record XOR keys — not one. Live probe enumeration of the 44 records at RVA0x22AB8F9..0x22B1321confirms0xFFFF834A942B7856×26 (canonical IDD),0xFFFF834BF90829BC×17 (NEW),0xFFFF834A0B7949E4×1 (NEW)..eidpage 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.- 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.
- 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
0x1439EB2an instructionlea rax, ??_7ResponseProcessor@btel@@6B@is followed bymov [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.
| Metric | Value |
|---|---|
| Size | 0x20C7 bytes (≈8.4 KB — 4× R13 estimate of 2 KB) |
| Basic blocks | 344 |
| Cyclomatic complexity | 82 |
| Callers | Zero code xrefs; 3 data xrefs only (shared-ptr descriptor triple) |
| Exception-code filter | None — the 70+ 32-bit magic constants are CFF-state identifiers |
| Stack-walk APIs | None |
| Caller-provenance check | Not present |
| Kill-path reachability | Not reachable |
Agent 2 — Fiber entry full decompile
Target: fiber-entry thunk at RVA 0x11E8110.
Verdict: Pure megacaller tail-call. NOT a validator.
| Metric | Value |
|---|---|
| Size | 0xFC (252 bytes) |
| Instructions | 62 |
| Basic blocks | 12 (2 unreachable CFF junk, 2 degenerate terminators) |
| Real cyclomatic complexity | 2 |
| Anti-tamper indicators | None |
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.
| Algorithm | Present? | Consumer |
|---|---|---|
| AES (any mode) | No | Delegated to Schannel (CALG_AES* strings are ASN.1 OIDs) |
| SHA-256 / SHA-1 | No | Strings present as ASN.1 OIDs; hashing delegated to Schannel |
| ChaCha20 / Poly1305 | No | — |
| RC4 | No | — |
| CRC32 / PCLMULQDQ | No | — |
| BLAKE2 / SipHash | No | — |
| MD5 (custom) | Yes | IAT-name hash and BTel request-seal digest |
| FNV-1a 32-bit | Yes | PEB.Ldr walker (0x811C9DC5 / 0x01000193) |
| Rolling XOR (bespoke) | Yes | IDD resolver + per-record .eid stubs |
| MBA mixer constants | Yes | 6 keys inline at RVA 0x1207340, more at 0x1209950 |
CFF fold XOR 0x88944AC9 | Yes | sub_7FF926E8B9A0 opaque-predicate csum |
Three new static constants catalogued:
| RVA | Name | Bytes | Role |
|---|---|---|---|
0x1D57510 | g_pe_integrity_xor_mask_xmm | 68 A0 FE 66 4A 8C AD D5 68 41 92 9F 68 52 3A 16 | PE-integrity XOR mask |
0x1D57610 | g_trampoline_iv_blob_xmm | 01 23 45 67 89 AB CD EF FE DC BA 98 76 54 32 10 | Trampoline IV-blob. Cosmetically the MD5 IV, but used as an obfuscation seed only |
0x1DF09E8 | g_trampoline_decode_secret_qword | 0xDC20F11A217C3AFA | Trampoline 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
WinVerifyTrustcallsite. TheWINTRUST.dll/WinVerifyTruststrings at RVA0x22B14E5 / 0x22B14F4sit inside a dormantIMAGE_IMPORT_DESCRIPTOR-like record at RVA0x22B1B70with zero code xrefs. TheWINTRUST_ACTION_GENERIC_VERIFY_V2GUID 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 RVA0x24A00, start routine at RVA0x1B07770) is actually a one-shotCreateEventW+WaitForSingleObjectinit barrier. No backward branch, no loop. - No self-hash. No references to the loader imagebase as a constant, no
RIP-relative sweeps of
.textinto a hash accumulator, nocmp 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. No0xC0000005match 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:
| Key | Count | Record indices | Status |
|---|---|---|---|
0xFFFF834A942B7856 | 26 | 0–25 | Canonical IDD key (known since R7) |
0xFFFF834BF90829BC | 17 | 27–43 | NEW |
0xFFFF834A0B7949E4 | 1 | 26 | NEW (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 category | Instances | Process handles | Target = D2R? |
|---|---|---|---|
| 4-instance user-space app (IDE + workers) | 4 | 127 | 0 / 127 — all return target_pid 0xFFFFFFFF |
| Single user-space app (browser) | 1 | 42 | 0 / 41 (1 dup race-fail) |
services.exe | 1 | 121 | unknown — SYSTEM-owned, can't open |
svchost.exe | 1 | 180 | unknown — 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 0x21BB000 — rep 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 0xC6B62175D4C6C7F6 — not 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:
| # | Claim | Source | Verdict |
|---|---|---|---|
| 35 | sub_7FF92728E960 is "VEH sibling dispatcher B" | R13 | Refuted — btel::ResponseProcessor::ctor |
| 36 | Fiber entry RVA 0x11E8110 validates caller provenance | R13 | Refuted — pure megacaller tail-call |
| 37 | An external candidate PID is the Warden broker | R14 | Refuted — 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 runtime | R14 | Refuted — page 0 entropy 3.98 live |
| 39 | Eidolon runtime-hashes .text to detect patches | Assumed | Refuted — no WinVerifyTrust, no periodic hash, no AV handler |
Running phantom count: 34 (R14) + 5 (R15) = 39 total debunked claims.
Open questions after R15
| Question | R14 status | R15 status |
|---|---|---|
| Warden broker identity | Candidates: 7 PIDs | 5 verified NOT broker; services.exe / svchost.exe remain (need SeDebugPrivilege) |
.eid decryption primitive location | Unknown | Candidate: sub_7FF92712F2F0 @ RVA 0x12DF2F0, 2 NPVM calls, iterates 0x38-byte records |
.eid per-record key set | 1 key known | 3 keys confirmed (26×canonical + 17 new + 1 new singleton) |
| Fiber entry role | Hypothesised validator | Retired — pure megacaller tail-call |
| VEH sibling B | Hypothesised peer dispatcher | Retired — actually btel ResponseProcessor ctor |
| Thunk emulator coverage | 64 of 248 | Emulator v3 ready; expects ~220+ once re-run with 256-byte window |
Self-hash of .text | Unknown | None — no runtime integrity check in the loader |
WinVerifyTrust consumer | Assumed present | None — strings are a red herring |
R16+ candidate work
- Decompile
sub_7FF92712F2F0after a fresh loader dump; follow each call toNtProtectVirtualMemoryinto the patching body to confirm the decryption algorithm. - Disassemble decrypted
.eidpage at RVA0x21BB000— top emitter-pattern match from probe C. - Re-run thunk decoder with the v3 Python emulator against a fresh 256-byte-per-thunk dump to crack the remaining ~184 trampolines.
SeDebugPrivilegeretry of probe B forservices.exeandsvchost.exe.- Hook
NtProtectVirtualMemoryin-process and log every flip intoPAGE_READWRITEfor a page inside.eid— reveals the decrypt callsite directly.