round 6 · 2026-04-17

Round 6 — Master Lua probe + 5 IDA agents

First full runtime probe of the live loader. Discovered 9 major findings: no in-DLL caller-provenance, no CFG enforcement, no bot-DLL scan, MD5 driver at sub_7FF9264E3480, 4 kill paths catalogued.

Later-round corrections (R16/R17):

  • The "4 kill paths" count is obsolete — R17 enumerated ≥11 kill-capable callsites.
  • The VEH inner (RVA 0x143D200) does not kill; it only dispatches. Kills route through the UEH chain.
  • The Aegis/Warden broker-via-RPM hypothesis is dead. R15 verified none of the candidate PIDs target D2R; R16 found the two callback slots are FF-filled (dormant). D2R loads no other executables, so no broker exists.
  • The broker-slot RVAs mentioned below (0x488CC0 / 0x488CC8) are an early-round typo. Real RVAs are 0x1488CC0 / 0x1488CC8 (.text exports) and 0x1E88CC0 / 0x1E88CC8 (.data storage, currently dormant).

Round 6 findings — live Lua probes + 5 IDA agents

Date: 2026-04-17 Scope: Parallel runs of 5 specialized IDA agents against the canonical IDB + 2 live Lua probes against the running D2R PTR 3.2 process. Input state: d2r_loader.dll base 0x7FF922070000 (live), D2R.exe base 0x7FF7B3810000, game running with analysis payload injected (PID 44820).


Headline results

#QuestionBefore round 6After round 6
1Where is the PTR 3.2 caller-provenance check?In d2r_loader.dll (inline)NOT in d2r_loader.dll (Agent I1, high confidence)
2Is CFG/XFG the mechanism?Primary suspect after I1No. Both binaries have GuardFlags = 0x100 only (CF_INSTRUMENTED metadata); function table empty; dispatch stub is FF E0 no-op (Lua CFG probe)
3Does Eidolon scan for bot DLLs?OpenNo (Agent I5, high confidence). No EnumProcessModules, Module32*, LdrEnum*, bot-name strings, or hash-table scans anywhere
4How many synchronous kill paths exist?UnknownOnly 4 in-DLL paths (Agent I3). Eidolon deliberately does NOT import RaiseFailFastException, NtTerminateProcess, IsDebuggerPresent, CheckRemoteDebuggerPresent
5What's the megadispatcher's actual purpose?Unknown "405 KB CFF function"Re-entrant CFF helper with 6 call sites from a "megacaller" at 0x7FF9277703C0 (Agent I2). Real work delegated to ~13 helper sub-functions, most important being sub_7FF9264E3480 (shared with IAT resolver)
6How is the IDD encrypted?UnknownSimple 8-byte rolling XOR with baked key 0xFFFF834A942B7856 (Agent I4). Not PEB.Ldr-bound — the PEB.Ldr fold happens in per-trampoline encoding only
7How does the resolver bootstrap before the synthetic IAT exists?UnknownClassic FNV-1a over PEB.Ldr module names + export-table walk (Agent I4). FNV-1a offset basis at 0x7FF926AC02DC, prime at 0x7FF926AC01BB. Cached ntdll base at 0x7FF927C3CDC0, kernel32 at 0x7FF927C3CDC8
8What was the "resolved-pointer table" really?"16-byte rows of {name_RVA, trampoline_ptr}" (round-5 claim)WRONG. Variable-length records: DLL-marker rows (marker_high = 0x01DAE188) interleaved with function rows. Field at q1 is loader-image RVA, not a heap pointer. Actual heap trampolines live elsewhere

Agent reports

Agent I1 — Caller-provenance check hunt (disconfirmed in d2r_loader.dll)

All four hypothesized mechanisms ruled out with high confidence:

  1. [rsp] return-address range check — one degenerate mov rax, [rsp] pattern in the whole DLL, inside obfuscation noise at 0x7FF926A8C70C. Zero real sites.
  2. NtQueryInformationThread(ThreadQuerySetWin32StartAddress=9) — symbol absent from the synthetic IAT name table (only NtClose and RtlFreeHeap from ntdll are imported!). Absent from strings.
  3. RtlLookupFunctionEntry / RtlVirtualUnwind — absent from IAT, strings, and byte patterns.
  4. DR0..DR3 hardware breakpoints + VEH — zero mov drN, reg instructions; VEH handler at 0x7FF92728D200 does NOT filter STATUS_BREAKPOINT (0x80000003) or STATUS_SINGLE_STEP (0x80000004).

Most informative findings along the way:

  • eidolon_tls_callback_0 at 0x7FF9270AC100 fires on DLL_THREAD_ATTACH / DLL_THREAD_DETACH — i.e., on NtCreateThreadEx but NEVER on thread-hijack. This seeds the per-thread state at gs:[0x20] (TEB.NtTib.FiberData).
  • eidolon_per_thread_get_fiberdata at 0x7FF92666A840 is the canonical per-thread state getter. Main-thread's FiberData IS populated at process-attach, so simply running on the main thread isn't the problem.
  • Follow-up CFG probe (below) further ruled out CFG/XFG as the mechanism.

Implication: the PTR 3.2 hijack-injection regression is caused by something outside d2r_loader.dll. Candidates (ranked):

  1. Warden broker-side validation (separate process, reads 0x7FF927CD8CC0 / 0x7FF927CD8CC8 slots and observes process state via ReadProcessMemory).
  2. NTDLL / kernel32 changes in the Windows build the user is running — LdrpLoadDllInternal has historically been the site of provenance checks when they exist.
  3. Environmental / state mismatch unrelated to security — e.g. hijacked thread lacks fresh TLS scratch that LoadLibrary needs, SEH chain confused by forged stack frame.

The only way to know is to run the diagnostic shellcode described in the plan file (record rax, GetLastError(), [rsp] to RWX slots). Everything upstream of that is hypothesis.

Implication for the fix: Option-2 (spawn helper thread) remains the right choice — it sidesteps whatever the actual mechanism is. Option-1 (ret-gadget forgery) is not doomed by Eidolon itself but is also not sufficient.

CFG/XFG probe follow-up (written + run as probe_r6_cfg.lua)

Parsed IMAGE_LOAD_CONFIG_DIRECTORY64 at correct offsets (+0x70 GuardCFCheckFunctionPointer, +0x78 DispatchFP, +0x80 FunctionTable, +0x88 FunctionCount, +0x90 GuardFlags, +0x118/+0x120/+0x128 XFG fields):

FieldD2R.exed2r_loader.dll
GuardFlags0x100 (CF_INSTRUMENTED only)0x100 (CF_INSTRUMENTED only)
GuardCFFunctionCount00
GuardCFFunctionTable0x00x0
GuardCFCheckFunctionPointer0x7FF7B4DE82500x7FF923E1D3E0
GuardCFDispatchFunctionPointer0x7FF7B4DE82600x7FF923E1D3F0
Dispatch stub target0x7FF7B4DB5A90 = FF E0 CC CC ... (jmp rax + padding)0x7FF923DBC3A0 = FF E0 CC CC ... (same)

CFG is not enforced in either binary. The FF E0 (jmp rax) dispatch stub is the standard "NO_ENFORCEMENT" variant Windows emits when the OS/loader decides CFG should not be active. CF_INSTRUMENTED alone only means the compiler emitted CFG-aware call sites; without a function table, there's nothing to validate against.

Even the .gxfg section (new in PTR 3.2) is dormant — it's present in d2r_loader.dll at 0x7FF923F70000 (0x2830 bytes) containing XFG type-signature entries, but the XFG dispatch pointers in the load config all point at no-op stubs too.

Conclusion: CFG/XFG is not the cause of the hijack failure.

Agent I2 — CFF megadispatcher mapping

The "405 KB CFF function" at 0x7FF9264624A0:

PropertyValue
Size0x62F87 bytes
Instructions104,211
Basic blocks25,730
ret instructionsZERO — exits via tail-jmp
Dispatch mechanismBalanced binary-search tree over 32-bit state, NOT a jump table
State storageStack slot [rsp+var_B00+4], DWORD
Initial state computationLEA/XOR/SUB mix chain from global dword_7FF927C71A4C
Callereidolon_cff_dispatcher_clone_megacaller at 0x7FF9277703C0
Number of distinct call sites6 (from megacaller) + 1 data-ref from 0x7FF927D32EE0
Junk / anti-RE50+ rdtsc in the first 40K instructions, stack-watermark probes (cmp esp, imm), call $+5 push-and-fall, call-to-middle-of-instruction

Key structural insights:

  • The megadispatcher is a re-entrant shared helper, not a one-shot setup routine. Each of the 6 call sites passes a different r8 operation tag (saved at [rsp+var_810] on entry).
  • Actual semantic work happens in ~13 helper sub-functions in 0x7FF9264C0000..0x7FF9264F0000. The megadispatcher is the glue.
  • sub_7FF9264E3480 is called by BOTH the IAT resolver AND the megadispatcher → strongest candidate for "IAT resolver kernel" (renaming candidate: eidolon_helper_iat_resolver_kernel).
  • sub_7FF9264D2250 (0x59F1 bytes, 1244 BBs) is the most likely home for the trampoline-row writer.
  • The lone non-CFF-internal external call from inside the megadispatcher is at 0x7FF9264655C30x7FF97AB3C5C8 (previously-resolved trampoline target).

Agent I3 — Silent-abort catalog

Only 4 Eidolon-side paths can synchronously kill the process:

PathAddressTriggerDefense
A VEH fatal classifiersub_7FF9267C6B70 → TerminateProcess via eidolon_telem_veh_prev_handler_router at 0x7FF926860A05Exception the VEH classifies as fatalInstall the analysis process hooks AFTER Warden VEH; use direct syscalls / ROP to dodge
B Fingerprint fail-closedsub_7FF926C47950 (8 callers)Mutated state-key check fails (e.g. during flow-info serialize)Don't write to Eidolon's obfuscator state words (dword_7FF927C92FAC..FD4)
C VM static-init integritysub_7FF926DE7590 (1 caller: eidolon_vm_static_init_k_byte_off60)Early init-time integrity checkFires only during init; post-init injection is clear
D Panic vtable slotsub_7FF9268F1140 via data-only ref at 0x7FF927D34F98Any subsystem writes the slot and invokes itReplace the slot with a no-op stub — neuters the cleanest kill-switch

Eidolon deliberately does NOT import any of: RaiseFailFastException, NtTerminateProcess, NtRaiseException, NtRaiseHardError, RtlReportCriticalFailure, IsDebuggerPresent, CheckRemoteDebuggerPresent, DebugBreak, _exit, quick_exit, abort.

Notable non-terminators: cert validator eidolon_telem_cert_validator_blizzard (0x7FF926F8A5A0), IAT resolver (0x7FF926042AF0), CFF megadispatcher (0x7FF9264624A0). Pin-fail is a Standard.Alert event, not a kill.

Agent I4 — IAT resolver deep dive (0x7FF926042AF0)

The entry is itself OLLVM-CFF (same template as megadispatcher — entry computes 32-bit state via LEA/XOR chain then binary-search dispatches). Confirmed by the Lua disasm dump.

IDD (encrypted Import Descriptor Data):

  • Location: loader+0x22B1000 (live 0x7FF924321000), ~22 records, stride 0x28 bytes each, 5 × 8-byte XOR slots per record
  • Encryption: 8-byte rolling XOR with baked key 0xFFFF834A942B7856 (bytes 56 78 2B 94 4A 83 FF FF LE)
  • Key is NOT derived from PEB.Ldr — it's a stored constant
  • The PEB.Ldr fold is baked into per-trampoline encoding (stage 2), not into IDD decrypt (stage 1)

Bootstrap path (before synthetic IAT exists):

  • FNV-1a hash over module names in PEB.Ldr.InLoadOrderModuleList → matches hard-coded hashes → caches ntdll/kernel32 bases
  • FNV offset basis literal at 0x7FF926AC02DC (mov [stk], 811C9DC5h)
  • FNV prime at 0x7FF926AC01BB (imul eax, r15d, 1000193h)
  • Cached ntdll base global: 0x7FF927C3CDC0
  • Cached kernel32 base global: 0x7FF927C3CDC8
  • Walker is at eidolon_antidbg_peb_ldr_walker_b (0x7FF926AC86F0, 15-case switch) — misnamed (it's actually the IAT resolver bootstrap, not anti-debug)

Trampoline emission: inline inside CFF states, no discrete emitter function. Each trampoline qword is built by XORing the intended target with gs:[60h]→[+0x18] at build time so the runtime fold subtracts it out.

Agent I5 — Bot-DLL enumeration hunt (verdict: no)

High-confidence finding: Eidolon does not enumerate loaded modules to scan against a bot-DLL blocklist.

Searched and not found:

  • EnumProcessModules*, Module32*W, CreateToolhelp32Snapshot, LdrEnumerate*, LdrFindEntryForAddress, LdrLockLoaderLock0 hits anywhere
  • GetModuleBaseName*W, GetModuleFileName*W, GetMappedFileName*W0 hits
  • RtlImageNtHeader, RtlImageDirectoryEntryToData0 hits
  • Bot/cheat name strings (cheat, bot, inject, hack, x64dbg, cheatengine, processhacker, ollydbg, windbg) — 0 hits
  • Case-compare helpers (wcsicmp, _wcsnicmp, RtlEqualUnicodeString, RtlPrefixString) — 0 hits
  • FNV-1a hashing-loop targeting module names — only present for IAT bootstrap, not for blocklisting
  • Path-vetting strings (system32, programfiles, windir) — 0 hits

Eidolon's PEB.Ldr walks exist only for: IAT resolver bootstrap (finding which module exports a hashed symbol), and CFF-VM key-mixing (folding PEB bits into state entropy). Neither reads BaseDllName.Buffer.

Misleading antidbg_* prefixes: the walkers _a/_b/_c/_d (at 0x7FF9262C9FB0, 0x7FF926AC86F0, 0x7FF927983F70, 0x7FF92683ACD0) are mis-named — they're CFF state-mixers, not anti-debug. Recommend rename to eidolon_cff_vm_peb_keymix_{a,b,c,d} and eidolon_iat_resolver_peb_ldr_walk_for_dynapi for walker_b.

Implication for the analysis process: PEB-unlink + PE-header erase are belt-and-suspenders against Eidolon itself, but keep them for Warden (which historically does scan modules).


Live-probe corrections to earlier rounds

Round-5 claim: ".pdata is wiped on disk and reconstructed at runtime"

Probably wrong. Runtime probe of d2r_loader.dll at 0x7FF923F50000 shows .pdata present with virt_size = 0x10C50, raw_size = 0x10E00. Earlier rounds may have been conflating the .pdata strip with a separate feature, or looking at the wrong dump. Needs a fresh look at the disk image of d2r_loader.dll to confirm whether the on-disk section is zeroed.

Round-5 claim: "Resolved-pointer table rows are 16 bytes"

Wrong for the second time. Actual structure is variable-length records with DLL-marker rows (q0.high = 0x01DAE188) interleaved with function-entry rows. The q1 field is loader-image-RVA-shaped, not a heap pointer. Earlier agents mis-interpreted consecutive qwords as "rows." The actual runtime trampolines must be resolved by following these RVAs through another indirection (probably the per-trampoline lookup indexes into the arena heap buffer, whose address isn't stored in this table at all — it's stored in a cached global).

Round-5 claim: "Trampoline arena is at heap address 0x000002XX_XXXXXXXX"

Partially wrong. The value 0x2049C330000 we kept seeing in round 3–5 was the trampoline for the D2R_loader!Ordinal_1 IAT slot in D2R.exe, not "the arena". Each trampoline is its own small heap allocation; there may not be a single contiguous "arena" at all. Needs further investigation.

Round-4 claim: "Aegis/Warden slots change per-session"

Wrong. Probed live: values 0xC181000C8D41C829 (Aegis) and 0xC801D129B3802195 (Warden) are identical to previous sessions. They're baked constants set at init, never mutated after (at least 0 mutations in ~10 ms spin-probe window). Per Agent A2 (round 5): these slots are host-side tokens consumed by the out-of-process Warden/Aegis brokers via ReadProcessMemory.


Phantom list additions

PhantomDisconfirmed byTruth
"Caller-provenance check in d2r_loader.dll"Agent I1 + CFG probeNot in this DLL. Real cause is either Warden broker-side (out-of-process) or NTDLL/Windows-side
"CFG/XFG enforcement causes the hijack failure"Lua CFG probeBoth D2R.exe and d2r_loader.dll have GuardFlags = 0x100 only, function table empty, dispatch stub is no-op FF E0. Not enforced
"Eidolon scans module list for bot DLLs"Agent I5Does not — 0 hits for every relevant API, string, and pattern
"Resolved-pointer table has 16-byte rows" (round 5 correction)Live probe at 16-byte strideActual layout is variable-length DLL-marker + function-entry records
"Trampoline arena at 0x2049C330000 is the single contiguous arena"Live probeThat's one trampoline's target, not an arena
"Eidolon has a bytecode VM" (round 4 correction now fully verified)Agents I2 + I4No VM. IAT resolver and megadispatcher are both OLLVM-CFF. Period
"IDD encryption is PEB.Ldr-keyed"Agent I4Baked constant 0xFFFF834A942B7856. PEB.Ldr is folded into per-trampoline encoding only

New findings (not phantoms, actual new intelligence)

  1. PTR 3.2 added a .gxfg section to d2r_loader.dll (2 new sections vs. live build). Not enforced, but its presence suggests Blizzard may be preparing XFG enforcement for a future build.
  2. The IDD XOR key is 0xFFFF834A942B7856. Offline decryption of the IDD blob is now possible.
  3. FNV-1a is confirmed as the import-hash algorithm at 0x7FF926AC01BB / 0x7FF926AC02DC. Hash tables can be precomputed for blocklisting / intercepting specific imports.
  4. Panic vtable slot at 0x7FF927D34F98 is the single clean kill-switch (Path D in Agent I3's catalog). Neutering this disables one of the 4 synchronous-abort paths.
  5. Main caller into the CFF megadispatcher is 0x7FF9277703C0 ("megacaller"). 27 KB, also CFF-flattened. Each of its 6 call sites to the megadispatcher probably sets a different operation tag.
  6. No ntdll import resolved except NtClose + RtlFreeHeap. Eidolon relies on the bootstrap FNV walker to reach ntdll for everything else — confirms the self-contained resolver design.
  7. TLS callback eidolon_tls_callback_0 (0x7FF9270AC100) only fires on NtCreateThreadEx, never on thread-hijack. Per-thread state (FiberData at gs:[0x20]) is what it seeds. Hijacked main thread already has its FiberData set from the process-attach pass.

  1. Strike all "CFG/XFG is the most likely cause" references — replaced by "mechanism unknown; likely Warden broker or NTDLL-side; confirm via diagnostic shellcode".
  2. Add the 0xFFFF834A942B7856 IDD XOR key + FNV-1a bootstrap hashes to the "Synthetic IAT" section.
  3. Correct the resolved-pointer table layout — third correction (it's NOT 16-byte rows; it's variable-length records).
  4. Replace the old "Aegis/Warden slot encoded values" narrative with "stable baked constants consumed out-of-process by Warden/Aegis brokers".
  5. Add the 4-path silent-abort catalog to "Anti-tamper machinery" — and add the panic-slot-neutering recommendation to "Defense implications".
  6. Update the megadispatcher subsection with Agent I2's findings (25K BBs, binary-search tree, 6 call sites, helper delegation).
  7. Update the phantom list with round-6 debunks (CFG-as-mechanism, 16-byte rows, single arena).

Next investigation steps (ranked)

  1. Run the diagnostic shellcode (record rax, GetLastError(), [rsp] to RWX slots). This is now the only way to find the real provenance check — all in-DLL mechanisms are ruled out.
  2. Build the Option-2 injector fix (NtCreateThreadEx(StartAddress=LoadLibraryA)) regardless of what the diagnostic shows — it sidesteps the unknown mechanism.
  3. Offline-decrypt the IDD using the now-known XOR key to get the full encrypted import table in plaintext.
  4. Inspect sub_7FF9264E3480 (the IAT resolver kernel shared with the megadispatcher) — probable location of the real semantic IAT-resolution work.
  5. Inspect the megacaller at 0x7FF9277703C0 — its 6 call sites into the megadispatcher each pass a different op-tag; recover the tags to understand what operations the megadispatcher actually performs.
  6. Inspect the Warden broker process (separate from d2r_loader.dll). Requires expanding d2r-toolbox MCP to enumerate / read the Warden process.
  7. Neuter the panic vtable slot at 0x7FF927D34F98 as a the analysis process defense hardening.

Updated renaming candidates (not yet applied)

From Agent I2:

  • 0x7FF9264645D4eidolon_cff_megadispatcher_dispatch_root
  • 0x7FF927C71A4Ceidolon_g_eidolon_cff_megadispatcher_seed_dword
  • 0x7FF9277703C0eidolon_cff_megacaller_six_sites
  • 0x7FF9264E3480eidolon_helper_iat_resolver_kernel (shared with IAT resolver; highest-value rename)
  • 0x7FF9264D2250eidolon_helper_resolve_or_emit_giant_a

From Agent I3:

  • 0x7FF9267C6B70eidolon_terminate_self_kernel32
  • 0x7FF926C47950eidolon_dispatch_kill_on_fingerprint_fail
  • 0x7FF926DE7590eidolon_vm_init_kill_dispatcher
  • 0x7FF9268F1140eidolon_panic_callback_exitprocess
  • 0x7FF92634C720eidolon_panic_compute_exit_code
  • 0x7FF927D34F98g_eidolon_panic_vtable_slot

From Agent I4:

  • 0x7FF926AC86F0eidolon_resolver_bootstrap_walk_peb_ldr (currently misnamed eidolon_antidbg_peb_ldr_walker_b)
  • 0x7FF927C3CDC0eidolon_g_eidolon_ntdll_base_cached
  • 0x7FF927C3CDC8eidolon_g_eidolon_kernel32_base_cached
  • 0x7FF927C5FDECeidolon_g_eidolon_iat_resolver_cff_seed
  • (data at loader+0x22B1000) → eidolon_idd_blob_encrypted
  • (constant 0xFFFF834A942B7856) → eidolon_EIDOLON_IDD_XOR_KEY

From Agent I5 (rename corrections):

  • 0x7FF9262C9FB0, 0x7FF927983F70, 0x7FF92683ACD0eidolon_cff_vm_peb_keymix_{a,c,d} (currently mis-prefixed antidbg_*)
  • 0x7FF9277C9260eidolon_peb_get_self_image_base_helper