Live-confirmed IDD XOR key works. Ruled out panic-vtable claim. Corrected op-tag hypothesis. Discovered MD5 as second integrity primitive alongside FNV-1a.
Round 7 findings — IDD decryption + 5 more IDA agents + corrections
Date: 2026-04-17 Scope: Live IDD decryption with the round-6 XOR key, thread enumeration attempt, plus 5 IDA agents (I6 IDD-decrypt, I7 megacaller-op-tags, I8 IAT-resolver-kernel, I9 panic-vtable, I10 Warden-broker-hunt).
Headline results
| Question | Round 6 belief | Round 7 truth |
|---|---|---|
Is 0x7FF927D34F98 a panic vtable slot? | "Yes — neuter it to disarm cleanest kill-switch" (Agent I3) | NO — Agent I9 proved it's the BeginAddress field of an IMAGE_RUNTIME_FUNCTION_ENTRY in .pdata. Patching it does nothing useful. |
Is sub_7FF9264E3480 the IAT resolver kernel? | "Strongest candidate" (Agent I2) | NO — Agent I8 proved it's an MD5 driver (round-1 constants confirmed at MD5_Transform 0x7FF927B65F90). It's called by the IAT resolver to hash IDD records, and by BTel to hash HTTP request bodies. |
Are the 6 megadispatcher call sites distinguished by an r8 op-tag? | "Yes" | NO — Agent I7 proved r8 = rsi (state ptr) at every site. The actual op-data is the (rcx, rdx, r9d) byte triplet. Dispatch is keyed on a mutable global cookie at dword_7FF927C71A4C (current value 0x2EB7282F), not on per-call args. |
| What encryption protects the IDD? | "Probably 8-byte rolling XOR with 0xFFFF834A942B7856" (Agent I4) | CONFIRMED by live decryption (Lua probe probe_r7_idd_decrypt.lua). 18/22 records have a fully-zero slot, which is the canonical sign of a correct rolling XOR key |
| What's the IDD record structure? | "5 × 8-byte XOR slots, ~22 records" | Layout now resolved into 3 record types: DLL header (records 0/1), per-API entry (records 2-20), table-end sentinel (record 21). Slot 4 is 0xFFFF5C__34EB276E per-record fingerprint with varying middle byte. Slot 2 is 0xD297CA__A1C5AE85 packed name-table index |
Live IDD decryption (probe_r7_idd_decrypt.lua)
Reproduced the offline result Agent I4 hypothesized — applying the baked XOR key 0xFFFF834A942B7856 (bytes 56 78 2B 94 4A 83 FF FF) to the 5 × 8-byte XOR slots in each 0x28-byte record at loader+0x22B1000 produces well-structured plaintext.
Quantitative confirmation
- 22 records × 0x28 = 0x370 bytes. Plaintext name table starts at
0x376(6-byte gap of zeros). - 18 of 22 records have a fully-zero 8-byte slot (decrypted) — exactly what we'd expect if every record contains one "always-zero" slot encrypted with the rolling key (the slot's encrypted bytes literally ARE the key, which is how Agent I4 originally identified it).
Structural recovery — three record types
Type A — DLL header (records 0, 1):
slot_0 0x000000016D2351EA (probable DLL identifier or OS-version stamp)
slot_1 0x9FB4CB5AE9C240F6 (probable DLL hash — likely MD5-trunc or FNV)
slot_2 0xD297CABEA1C5AE90 / 0xD297CAB6A1C5AE92 (only middle byte differs)
slot_3 0x329AAD1FA260F493 (constant within type)
slot_4 0xFFFF5C8ECD3A4597 / 0xFFFF5C8356074B49 (per-record fingerprint)
Type B — Per-API entry (records 2-20, the bulk):
slot_0 0x0000000000000000 (always zero in plaintext)
slot_1 0x9F81B0__XXXXXXXX (varying — probable per-API hash; high byte 9F81B0 stable)
slot_2 0xD297CA__A1C5AE85 (middle byte varies — probable name-table INDEX)
slot_3 0xXXXXXXXXA260F70A (low DWORD `A260F70A` constant — probable trampoline-arena index)
slot_4 0xFFFF5C__34EB276E (low DWORD `34EB276E` constant; middle byte = per-record fingerprint)
The middle byte of slot 2 increments by 0x08 per record (0xBE → 0xB6 → 0x46 → 0x56 → 0x6E → 0x66 → 0x7E → 0x76 → 0x0E → 0x06 → 0x1E → 0x16 → 0x2E → 0x26 → 0x3E → 0x36 → 0xCE → 0xC6 → 0xDE → 0xD6 → 0xEE → 0xFE), which is consistent with a packed name-table-offset stride of 8 bytes. This appears to be the per-API name lookup index.
Type C — Sentinel / table-end (record 21):
slot_0 0x000000009F5231B2 (different format from types A/B)
slot_1 0x9F81461945DBD855 (high byte differs — high 0x9F8146 vs Type B 0x9F81B0)
slot_2 0xD297CBFEA1C5AE85 (middle byte 0xCBFE — out of the type-B pattern)
slot_3 0x6F5AD207A260F4BA (low DWORD A260F4BA differs from type-B's A260F70A)
slot_4 0xB6AFC21AC17F3D05 (looks like a closing checksum)
Record 21's slot 4 ENCRYPTED bytes are 53 45 54 55 50 41 50 49 (literal ASCII "SETUPAPI") because the bytes at this absolute offset are actually inside the plaintext name table that begins at 0x376. The IDD's last "real" record is therefore record 20.
Fingerprint byte distribution (slot 4, middle byte)
Across records 2-20, the unique fingerprint bytes are:
0xAE, 0x80, 0xAE, 0x95, 0x94, 0x9F, 0x9C, 0x9C, 0x96, 0x90, 0x94, 0x99, 0x9C, 0x83, 0x9D, 0x9E, 0x9A, 0x9C, 0x98
Most cluster around 0x9X — these are likely 3 high bits of a category tag plus 5 bits of intra-category index, or similar packed encoding.
Open questions on IDD
- What's the API-name lookup mechanism? Slot 2's varying middle byte ALONE isn't enough to disambiguate 19 APIs without a parallel index. Either there's a separate name-table-stride index, or the byte index points into a hash-table with collision chains.
- Is slot 1 an MD5-truncated digest of the API name? Round 7 Agent I8 identified MD5 as Eidolon's integrity-tag algorithm. The high byte
0x9F81B0being constant could be the high-byte of a truncated MD5 of "category". - Does record 0/1 have a different schema entirely, or is it just a different record type with the same field layout interpreted differently? Both records share slot 0/1/3 — maybe the DLL header carries the DLL-level metadata.
Agent I7 — Megacaller op-tag recovery (Major restructure)
The "r8 op-tag" hypothesis from round 6 was wrong. Agent I7 disassembled all 6 call sites at 0x7FF9277703C0 and found:
- All 6 sites set
r8 = rsi(the saved state pointer the megacaller received as its 4th argument). - The actual differentiator is the
(rcx, rdx, r9d)triplet, three small immediates per call.
| # | Call addr | rcx | rdx | r9 | r8 |
|---|---|---|---|---|---|
| 1 | 0x7FF9277716FC | 0x3E | 0x63 | 0x13 | rsi |
| 2 | 0x7FF9277719BA | 0x30 | 0x4D | 0x19 | rsi |
| 3 | 0x7FF9277741C0 | 0x1B | 0x20 | 0x32 | rsi |
| 4 | 0x7FF92777471B | 0x64 | 0x1C | 0x53 | rsi |
| 5 | 0x7FF9277747FA | 0x1F | 0x47 | 0x4B | rsi |
| 6 | 0x7FF927776FE5 | 0x16 | 0x2E | 0x10 | rsi |
Critically: dispatch is NOT keyed on these triplets. The megadispatcher at entry reads a mutable global cookie at dword_7FF927C71A4C (live value 0x2EB7282F) and computes the dispatch state from it. The triplet is consumed only inside the arm reached for the current cookie value. This means:
- The 6 sites are not a static method table; they are 6 distinct points in a state-machine drive sequence that mutates the global cookie.
- Recovering the symbolic meaning of each operation requires a runtime trace of the global cookie's mutation across iterations.
Strongest semantic hint — site 5 (0x7FF9277747FA): the immediately-following code branches on STATUS_ILLEGAL_INSTRUCTION (0xC000001D), which is a hallmark of Eidolon's WinVerifyTrust self-verification probe (it deliberately faults to validate exception-handler integrity). (R15/R16 correction: there is no WinVerifyTrust call in the loader — the WINTRUST strings sit in a dormant import record with zero code xrefs. The STATUS_ILLEGAL_INSTRUCTION branch is real but is not a WinVerifyTrust probe.)
Megacaller execution context: the sole code caller eidolon_cff_dispatcher_clone_megacaller_thunk (0x7FF927038110) is registered as a callback by the VEH handler at 0x7FF92728DF44:
lea rdx, eidolon_cff_dispatcher_clone_megacaller_thunk
mov r8, rdi
call qword ptr cs:[byte_7FF927BFE190 + 0x40] ; AddVectoredExceptionHandler-shaped slot
mov [rdi+0x50], rax ; saved handle
So the entire CFF mega-system runs inside a VEH/fiber callback, with the per-fiber state pointer threaded through rsi. The (rcx,rdx,r9,rsi) calling convention is project-wide for Eidolon CFF helpers.
Agent I8 — sub_7FF9264E3480 is an MD5 driver, NOT the IAT resolver kernel
Round 6 Agent I2 identified sub_7FF9264E3480 as the strongest candidate for the "IAT resolver kernel" because it was the only function called by both the megadispatcher AND the IAT resolver. Round 7 Agent I8 deep-dived and proved this is wrong:
What sub_7FF9264E3480 actually is:
- An MD5-driver wrapper around
MD5_Transformat0x7FF927B65F90(round-1 constants0xD76AA478, 0xE8C7B756, 0x242070DB, 0xC1BDCEEE, 0xF57C0FAF, 0x4787C62Aand shifts7,12,17,22confirmed) - Performs MD5 padding (
0x80byte,len & 0x3F, pad-to-56 via^ 0x3F) - Emits 12-byte digest tail at
[r8]/[r8+4]/[r8+8] - 11 RDTSC anti-debug stalls (typical CFF junk)
- 452 BBs of CFF flattening around the actual MD5 work
Signature: void md5_drive_block_or_finalize(MD5_CTX *ctx /*rcx*/, const u8 block[64] /*rdx*/, u8 digest_tail[12] /*r8*/)
Callers: IAT resolver eidolon_iat_resolver_main (call site 0x7FF9260464BA) — to hash IDD records — AND BTel HTTP uplink at sub_7FF9261E50B0 — to hash request payloads.
What it DOES NOT do (refuting the round-6 hypothesis):
- Does NOT call PEB.Ldr walker (no calls to
0x7FF926AC86F0) - Does NOT read cached module bases (no reads of
0x7FF927C3CDC0/CDC8) - Does NOT touch the encrypted IDD blob (no xrefs to
0x7FF928101000..0x376) - Does NOT touch the resolved-pointer table (no xrefs to
0x7FF928101A00) - Does NOT call any allocator
- Does NOT emit trampoline bytes (no
gs:60haccess, no 16-byte byte-emission loop)
Sibling MD5 driver sub_7FF927057340 has the same shape (calls MD5_Transform twice, same caller pair). Likely a clone with different XOR-key obfuscation pentet.
Implication: Eidolon uses TWO hashing primitives:
- FNV-1a for module-name and export-name lookup (round 6 Agent I4)
- MD5 (truncated to 12 bytes) for IDD-record integrity tags AND BTel HTTP-payload hashing
This means slot 1 of the IDD records (0x9F81B0__XXXXXXXX) is likely a 96-bit MD5 truncation of the API name (or of (DLL,API) concatenation), explaining why it didn't match FNV-1a expectations in earlier examination.
Where IS the actual IAT resolver kernel? Most likely inside eidolon_iat_resolver_main (0x7FF926042AF0)'s own 9,175-instruction body — its 23 callees include allocator-shaped sub_7FF92605C000 and sub_7FF926068FE0, which are still untouched.
Strong rename: sub_7FF9264E3480 → eidolon_md5_block_finalize_cff. sub_7FF927B65F90 → eidolon_md5_transform_round1_unrolled. sub_7FF927057340 → eidolon_md5_block_finalize_cff_v2.
Agent I9 — Panic vtable analysis: ROUND-6 CLAIM REFUTED
Round 6 Agent I3 identified 0x7FF927D34F98 as a "panic vtable slot" pointing to sub_7FF9268F1140 (which calls ExitProcess), and recommended overwriting the slot with a no-op ret stub. This is wrong.
What 0x7FF927D34F98 actually is: the BeginAddress field of an IMAGE_RUNTIME_FUNCTION_ENTRY in .pdata:
struct RUNTIME_FUNCTION { DWORD BeginRVA; DWORD EndRVA; DWORD UnwindInfoRVA; };
Verified from raw bytes:
BeginRVA = 0x00AA1140→0x7FF9268F1140(start ofsub_7FF9268F1140)EndRVA = 0x00AA1162→0x7FF9268F1162(start + 0x22 = 34 bytes; matches function size exactly)UnwindInfoRVA = 0x01DB014C→0x7FF927C2014C(.rdataUNWIND_INFO blob)
This is purely SEH/unwind metadata read by RtlLookupFunctionEntry when an exception unwinds through sub_7FF9268F1140. No Eidolon code dispatches via this slot. Patching it would do nothing useful and might actually trip the IDB's eidolon_runtime_pdata_reconstructor (which validates this region).
The actual kill switch is sub_7FF9268F1140 itself, reached by direct call/jmp from .text — not via this slot.
Correct neutering targets (alternatives):
| Option | Action | Risk |
|---|---|---|
| A | Overwrite sub_7FF9268F1140's 5-byte preamble with 48 31 C0 C3 (xor eax, eax; ret) | Low — function is __noreturn, callers don't expect a return; tail-callers fall through into next instructions (risky) |
| B | Atomically swap the IAT thunk at byte_7FF927BFE190 + 0xB0 (the obfuscated kernel32!ExitProcess slot) to a no-op ret stub | Medium — neuters every ExitProcess call routed through this thunk, not just this panic |
| C | Find direct callers of sub_7FF9268F1140 in .text via find_calls_to, patch the conditions | High effort, lowest collateral damage |
Caveat: confirming option B requires identifying what import byte_7FF927BFE190 + 0xB0 resolves to (likely ExitProcess based on sub_7FF9268F1140's call shape, but unconfirmed).
Renames:
0x7FF9268F1140→eidolon_panic_exit_27_20_830x7FF92634C720→eidolon_format_exit_status(8.4 KB / 397 BB switch-cascade exit-code formatter — still untraced)0x7FF927D34F98→ label aspdata_RFE_eidolon_panic_exit_27_20_83to flag its.pdatanature and prevent future agents from making this mistake again
Live thread enumeration probe — BLOCKED on obfuscated globals
Wrote probe_r7_threads.lua to enumerate all D2R threads via CreateToolhelp32Snapshot + Thread32First/Next + NtQueryInformationThread(ThreadQuerySetWin32StartAddress). Strategy: resolve kernel32/ntdll APIs via the cached module-base globals at 0x7FF927C3CDC0 / 0x7FF927C3CDC8.
Result: these globals are themselves obfuscated with PEB.Ldr-style folding:
kernel32_base_cached_live = 0xBFA554FF2A841A63— clearly NOT a valid module base (top 16 bits aren't 0)ntdll_base_cached_live = 0xDA077F72427B86B8— same
Implication: Eidolon doesn't store plaintext module bases anywhere. Even its own internal cache is folded with PEB.Ldr, meaning every dereference does the inline decode. We can't shortcut module-base lookup via these globals.
Workaround paths for round 8:
- MZ-scan downward from a known kernel32 export. We don't have one yet.
- Walk D2R.exe's
.textfor acall qword ptr [rip+disp32]to find an intact IAT slot, follow to kernel32. - Use an in-process MCP
call_functionif bound for any kernel32 function. - Patch the in-process MCP server to expose a "resolve module" primitive.
Updated phantom list
| Phantom | Disconfirmed by | Truth |
|---|---|---|
"0x7FF927D34F98 is a panic vtable slot" | Agent I9 | It's .pdata RUNTIME_FUNCTION metadata. Patching it does nothing useful |
"sub_7FF9264E3480 is the IAT resolver kernel" | Agent I8 | It's an MD5 driver. Real resolver work is inside eidolon_iat_resolver_main's own callees |
"Each megadispatcher call passes a distinct r8 op-tag" | Agent I7 | r8 = rsi (state ptr). Op-data is (rcx,rdx,r9d) triplet, but dispatch is keyed on a global cookie |
"Eidolon's cached kernel32/ntdll base globals are usable plaintext pointers" | Live probe | Obfuscated with PEB.Ldr like everything else |
Confirmed new intelligence (not phantoms)
- IDD XOR key
0xFFFF834A942B7856works in live decryption. 18/22 records have a fully-zero slot post-decrypt — gold-standard confirmation. - IDD has 3 record types: DLL header (records 0/1), per-API entry (records 2-20), table-end sentinel (record 21).
- Eidolon uses MD5 as well as FNV-1a — FNV for name lookup, MD5 (truncated to 12 bytes) for IDD-record integrity AND BTel HTTP-payload hashing. MD5_Transform at
0x7FF927B65F90. sub_7FF9264E3480andsub_7FF927057340are sibling MD5 drivers — likely clones with distinct XOR-key pentets.- The CFF mega-system runs inside a VEH/fiber callback. The thunk
eidolon_cff_dispatcher_clone_megacaller_thunk(0x7FF927038110) is registered as a Vectored Exception Handler at0x7FF92728DF44. - Megadispatcher dispatch is mutable-global-cookie-driven —
dword_7FF927C71A4C, current value0x2EB7282F. The 6 call sites are points along a state-mutation sequence, not a static method table. - Calling convention for Eidolon CFF helpers is
(rcx_byte, rdx_byte, r9d_byte, state_ptr)— project-wide. - Site 5 (
0x7FF9277747FA) is high-confidence the WinVerifyTrust / SEH-fault probe arm — branches onSTATUS_ILLEGAL_INSTRUCTION(0xC000001D) immediately after the megadispatcher returns. - Real kill-switch path is
caller → sub_7FF9268F1140 → ExitProcess via thunk[+0xB0]. Direct callers in.textare the right neutering targets, not the panic stub itself or the.pdataentry. - Eidolon does not store plaintext module bases anywhere — even its own cache globals are PEB.Ldr-folded.
Recommended doc-v3 patches (for eidolon_analysis.md)
- Strike Round 6 Agent I3's "panic vtable neuter" recommendation — it's based on a bad identification (Agent I9 corrected it).
- Strike
sub_7FF9264E3480as "IAT resolver kernel" — it's an MD5 driver (Agent I8). Add MD5 driver section. - Add the IDD XOR key + IDD record schema (3 types) to the Synthetic IAT section.
- Add the megadispatcher's actual calling convention
(rcx_byte, rdx_byte, r9d_byte, state_ptr)and the global-cookie dispatch model. - Add the "CFF mega-system runs inside a VEH/fiber callback" finding to the Anti-tamper machinery section.
- Update the kill-switch section: real path is
caller → sub_7FF9268F1140 → ExitProcess via IAT thunk[+0xB0]. Recommend Option B (swap the thunk) as least-collateral neuter. - Add the "Eidolon uses two hashing primitives (FNV-1a + MD5)" to the Anti-tamper machinery.
- Update the phantom list with round-7 debunks.
Updated recommended next investigation steps (ranked)
- Build and ship the Option-2 hijack-injector fix (
NtCreateThreadEx(StartAddress=LoadLibraryA)). Still the primary practical action. - Run diagnostic shellcode to nail down the actual hijack-failure mechanism (now confirmed not to be in
d2r_loader.dll). - Identify what import
byte_7FF927BFE190 + 0xB0resolves to at runtime (confirm it'sExitProcess). Lua probe: read the obfuscated trampoline, attempt to decode it. - Inspect
sub_7FF926042AF0's 23 callees (the IAT resolver's own callees, especiallysub_7FF92605C000andsub_7FF926068FE0). The actual resolver kernel is in there. - Single-step the global cookie at
dword_7FF927C71A4Cwith a write watchpoint. The mutation pattern reveals what each megadispatcher arm does. - Compute MD5 of known API-name strings ("WinVerifyTrust\0", "RtlFreeHeap\0", etc.) and check whether the truncated 12-byte digest matches IDD record slot 1's varying value. If yes, IDD slot-1 is the API-name MD5 tag.
- Recover the actual semantics of records 0/1 in the IDD by computing MD5 of common DLL names and matching against slot 1's
0x9FB4CB5AE9C240F6. - Round-8 thread enumeration via the
D2R.exeIAT walker (since cached globals are obfuscated).