diff --git a/capemon.c b/capemon.c
index 8cb30234..3117318d 100644
--- a/capemon.c
+++ b/capemon.c
@@ -31,6 +31,9 @@ along with this program. If not, see .
#include "unhook.h"
#include "bson.h"
#include "Shlwapi.h"
+#ifdef _WIN64
+#include
+#endif
// Allow debug mode to be turned on at compilation time.
#ifdef CUCKOODBG
@@ -565,6 +568,136 @@ extern CRITICAL_SECTION readfile_critsec, g_mutex, g_writing_log_buffer_mutex, g
BOOLEAN g_dll_main_complete;
OSVERSIONINFOA g_osverinfo;
+//
+// Returns TRUE if the current process is a WoW64 process (i.e. a 32-bit image
+// hosted by the 64-bit kernel). Valid for both the 32-bit and the 64-bit build
+// of the monitor: ProcessWow64Information yields the PEB32 address, which is
+// non-NULL only under WoW64.
+//
+// NtQueryInformationProcess is resolved at runtime because we do not link
+// against ntdll.lib, and we cannot use the hooked Old_/New_ variants here as
+// this may run before set_hooks().
+//
+static BOOL is_wow64_process(void)
+{
+ static _NtQueryInformationProcess pQueryInfo;
+ ULONG_PTR wow64_peb = 0;
+ ULONG ret_len = 0;
+
+ if (!pQueryInfo) {
+ HMODULE ntdll = GetModuleHandleA("ntdll");
+ if (!ntdll)
+ return FALSE;
+ *(FARPROC *)&pQueryInfo = GetProcAddress(ntdll, "NtQueryInformationProcess");
+ if (!pQueryInfo)
+ return FALSE;
+ }
+
+ if (pQueryInfo(GetCurrentProcess(), ProcessWow64Information, &wow64_peb, sizeof(wow64_peb), &ret_len) < 0)
+ return FALSE;
+
+ return wow64_peb != 0;
+}
+
+#ifdef _WIN64
+//
+// The x64 build delay-loads its high-level dependencies (see DelayLoadDLLs in
+// capemon.vcxproj) so that injection does not fail during import resolution in
+// a process that has not mapped them yet.
+//
+// Delay loading on its own only moves the load to the first call, which may
+// well be from inside one of our own hooks - that re-enters the hooked
+// LoadLibrary/LdrLoadDll path and can run under a loader lock we do not
+// control. So resolve them explicitly here instead. This runs before
+// set_hooks(), which is what makes the LoadLibrary calls below safe.
+//
+// Policy for WoW64 targets: only the modules the monitor itself needs during
+// bring-up are forced in. A WoW64 process normally has no 64-bit modules
+// besides ntdll and the wow64 layer, and force-mapping the full set into it is
+// exactly what the delay-load change is meant to avoid. Anything else stays
+// lazy.
+//
+static const char *g_delay_loaded_dlls[] = {
+ "advapi32.dll",
+ "user32.dll",
+ "ws2_32.dll",
+ "crypt32.dll",
+ "shlwapi.dll",
+ "ole32.dll",
+ "shell32.dll",
+ "setupapi.dll",
+ "oleaut32.dll",
+ "netapi32.dll",
+ "bcrypt.dll",
+};
+
+// needed by hkcu_init() and log_environ() during DllMain
+static const char *g_delay_loaded_dlls_minimal[] = {
+ "advapi32.dll",
+};
+
+static const char *g_delay_load_failures[ARRAYSIZE(g_delay_loaded_dlls)];
+static unsigned int g_delay_load_failure_count;
+
+static void resolve_delay_loaded_dlls(void)
+{
+ const char **dlls;
+ unsigned int count, i;
+
+ if (is_wow64_process()) {
+ dlls = g_delay_loaded_dlls_minimal;
+ count = ARRAYSIZE(g_delay_loaded_dlls_minimal);
+ }
+ else {
+ dlls = g_delay_loaded_dlls;
+ count = ARRAYSIZE(g_delay_loaded_dlls);
+ }
+
+ for (i = 0; i < count; i++) {
+ if (GetModuleHandleA(dlls[i]))
+ continue;
+ if (!LoadLibraryA(dlls[i]) && g_delay_load_failure_count < ARRAYSIZE(g_delay_load_failures))
+ g_delay_load_failures[g_delay_load_failure_count++] = dlls[i];
+ }
+}
+
+//
+// Deferred because this runs before read_config(): DebugOutput() consults
+// g_config to decide between OutputDebugString and the log pipe, so it cannot
+// produce anything useful until the config has been parsed.
+//
+static void report_delay_load_failures(void)
+{
+ unsigned int i;
+
+ for (i = 0; i < g_delay_load_failure_count; i++)
+ DebugOutput("resolve_delay_loaded_dlls: unable to load %s\n", g_delay_load_failures[i]);
+}
+
+//
+// Reported when the delay-load helper cannot satisfy a call. Returning NULL
+// leaves the helper to raise its exception as usual - there is nothing valid to
+// return - but at least the failing module/import ends up in the log instead of
+// surfacing as an opaque crash.
+//
+static FARPROC WINAPI capemon_delayload_failure_hook(unsigned int dliNotify, PDelayLoadInfo pdli)
+{
+ if (dliNotify == dliFailLoadLib)
+ DebugOutput("delay-load: failed to load %s (error %d)\n", pdli->szDll, pdli->dwLastError);
+ else if (dliNotify == dliFailGetProc) {
+ if (pdli->dlp.fImportByName)
+ DebugOutput("delay-load: failed to resolve %s!%s (error %d)\n", pdli->szDll, pdli->dlp.szProcName, pdli->dwLastError);
+ else
+ DebugOutput("delay-load: failed to resolve %s ordinal %d (error %d)\n", pdli->szDll, pdli->dlp.dwOrdinal, pdli->dwLastError);
+ }
+
+ return NULL;
+}
+
+// picked up by delayimp.lib
+ExternC const PfnDliHook __pfnDliFailureHook2 = capemon_delayload_failure_hook;
+#endif
+
BOOL APIENTRY DllMain(HANDLE hModule, DWORD dwReason, LPVOID lpReserved)
{
lasterror_t lasterror;
@@ -587,6 +720,12 @@ BOOL APIENTRY DllMain(HANDLE hModule, DWORD dwReason, LPVOID lpReserved)
g_osverinfo.dwOSVersionInfoSize = sizeof(OSVERSIONINFOA);
GetVersionEx(&g_osverinfo);
+#ifdef _WIN64
+ // before anything that touches a delay-loaded import, including the
+ // advapi32 lookup in resolve_runtime_apis() below
+ resolve_delay_loaded_dlls();
+#endif
+
resolve_runtime_apis();
init_private_heap();
@@ -608,6 +747,11 @@ BOOL APIENTRY DllMain(HANDLE hModule, DWORD dwReason, LPVOID lpReserved)
// read the config settings
read_config();
+#ifdef _WIN64
+ // deferred from resolve_delay_loaded_dlls(): DebugOutput needs g_config
+ report_delay_load_failures();
+#endif
+
if (g_config.standalone) {
// initialize these because some hooks behave badly when they are empty
if (!g_config.w_analyzer[0]) {
@@ -667,6 +811,7 @@ BOOL APIENTRY DllMain(HANDLE hModule, DWORD dwReason, LPVOID lpReserved)
// initialise CAPE
CAPE_init();
+
// adds our own DLL range as well, since the hiding is done later
add_all_dlls_to_dll_ranges();
diff --git a/capemon.vcxproj b/capemon.vcxproj
index c914a429..25cc0bcb 100644
--- a/capemon.vcxproj
+++ b/capemon.vcxproj
@@ -138,7 +138,7 @@
$(ProjectDir)x64\Debug;$(ProjectDir)\libyara\lib
0x30000000
true
- advapi32.dll;user32.dll;ws2_32.dll;crypt32.dll;shlwapi.dll;ole32.dll;shell32.dll;oleaut32.dll;netapi32.dll;bcrypt.dll
+ advapi32.dll;user32.dll;ws2_32.dll;crypt32.dll;shlwapi.dll;ole32.dll;shell32.dll;setupapi.dll;oleaut32.dll;netapi32.dll;bcrypt.dll
@@ -206,7 +206,7 @@
true
NoErrorReport
capemon.def
- advapi32.dll;user32.dll;ws2_32.dll;crypt32.dll;shlwapi.dll;ole32.dll;shell32.dll;oleaut32.dll;netapi32.dll;bcrypt.dll
+ advapi32.dll;user32.dll;ws2_32.dll;crypt32.dll;shlwapi.dll;ole32.dll;shell32.dll;netapi32.dll;bcrypt.dll
diff --git a/config.c b/config.c
index 6255ecb7..72445951 100644
--- a/config.c
+++ b/config.c
@@ -1474,6 +1474,35 @@ void parse_config_line(char* line)
}
}
+//
+// Local equivalent of shlwapi!PathRemoveFileSpec. read_config() runs very early
+// in DllMain, before we have resolved the delay-loaded imports, so it must not
+// be the first thing to touch shlwapi. Same semantics: strips the trailing path
+// component in place, leaves a bare root ("C:\") alone.
+//
+static void strip_filespec(char *path)
+{
+ char *sep, *p;
+
+ if (!path || !*path)
+ return;
+
+ sep = NULL;
+ for (p = path; *p; p++) {
+ if (*p == '\\' || *p == '/')
+ sep = p;
+ }
+
+ if (!sep)
+ return;
+
+ // keep the separator for a root path such as "C:\"
+ if (sep == path || (sep == path + 2 && path[1] == ':'))
+ sep[1] = '\0';
+ else
+ *sep = '\0';
+}
+
void read_config(void)
{
char buf[32768], config_fname[MAX_PATH];
@@ -1517,7 +1546,7 @@ void read_config(void)
// look for the config in monitor directory
memset(g_config.analyzer, 0, MAX_PATH);
strncpy(g_config.analyzer, our_dll_path, strlen(our_dll_path));
- PathRemoveFileSpec(g_config.analyzer); // remove filename
+ strip_filespec(g_config.analyzer); // remove filename
sprintf(config_fname, "%s\\%u.ini", g_config.analyzer, GetCurrentProcessId());
strcpy(g_config.results, g_config.analyzer);
diff --git a/log.c b/log.c
index 635b3afa..d173528e 100644
--- a/log.c
+++ b/log.c
@@ -1446,8 +1446,25 @@ void log_hook_restoration(const hook_t *h)
DWORD g_log_thread_id;
DWORD g_logwatcher_thread_id;
+
+//
+// log_init() one-shot state. Note we deliberately do *not* block a concurrent
+// caller until initialization completes: log_init() can be reached from a hook
+// (see LdrLoadDll in hook_special.c), and spinning there risks deadlocking
+// against the loader lock. Losing a few early records is acceptable - every
+// consumer of g_buffer already handles it being NULL.
+//
+#define LOG_INIT_NONE 0
+#define LOG_INIT_RUNNING 1
+#define LOG_INIT_DONE 2
+
+volatile LONG g_log_initialized = LOG_INIT_NONE;
+
void log_init(int debug)
{
+ if (InterlockedCompareExchange(&g_log_initialized, LOG_INIT_RUNNING, LOG_INIT_NONE) != LOG_INIT_NONE)
+ return;
+
g_buffer = calloc(1, BUFFERSIZE);
g_log_flush = CreateEvent(NULL, FALSE, FALSE, NULL);
@@ -1460,7 +1477,7 @@ void log_init(int debug)
g_log_handle = CreateFileA(g_config.logserver, GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, 0, NULL);
if (g_log_handle == INVALID_HANDLE_VALUE) {
pipe("CRITICAL:Error initializing logging!");
- return;
+ goto out;
}
}
@@ -1470,7 +1487,7 @@ void log_init(int debug)
char* filename = GetResultsPath("API");
if (!filename) {
pipe("CRITICAL:Error initializing debug logging!");
- return;
+ goto out;
}
num_to_string(pid, sizeof(pid), GetCurrentProcessId());
strcat(filename, "\\");
@@ -1485,6 +1502,9 @@ void log_init(int debug)
log_environ();
// flushing here so host can create files / keep timestamps
log_flush();
+
+out:
+ InterlockedExchange(&g_log_initialized, LOG_INIT_DONE);
}
void log_free()
diff --git a/misc.c b/misc.c
index 1cde619a..3f1c7014 100644
--- a/misc.c
+++ b/misc.c
@@ -1648,15 +1648,23 @@ wchar_t *get_key_path(POBJECT_ATTRIBUTES ObjectAttributes, PKEY_NAME_INFORMATION
}
normal:
- if (!wcsnicmp(keybuf->KeyName, g_hkcu.hkcu_string, g_hkcu.len) && (keybuf->KeyName[g_hkcu.len] == L'\\' || keybuf->KeyName[g_hkcu.len] == L'\0')) {
+ if (!g_hkcu.hkcu_string)
+ hkcu_init();
+
+ {
+ // snapshot: hkcu_init() may publish these from another thread
+ const wchar_t *hkcu_string = g_hkcu.hkcu_string;
+ unsigned int hkcu_len = g_hkcu.len;
+
+ if (hkcu_string && !wcsnicmp(keybuf->KeyName, hkcu_string, hkcu_len) && (keybuf->KeyName[hkcu_len] == L'\\' || keybuf->KeyName[hkcu_len] == L'\0')) {
unsigned int ourlen = lstrlenW(L"HKEY_CURRENT_USER");
memcpy(keybuf->KeyName, L"HKEY_CURRENT_USER", ourlen * sizeof(WCHAR));
- memmove(keybuf->KeyName + ourlen, keybuf->KeyName + g_hkcu.len, keybuf->KeyNameLength + (1 * sizeof(WCHAR)) - ((g_hkcu.len) * sizeof(WCHAR)));
- keybuf->KeyNameLength -= (g_hkcu.len - ourlen) * sizeof(WCHAR);
+ memmove(keybuf->KeyName + ourlen, keybuf->KeyName + hkcu_len, keybuf->KeyNameLength + (1 * sizeof(WCHAR)) - ((hkcu_len) * sizeof(WCHAR)));
+ keybuf->KeyNameLength -= (hkcu_len - ourlen) * sizeof(WCHAR);
}
- else if (!wcsnicmp(keybuf->KeyName, g_hkcu.hkcu_string, g_hkcu.len) && !wcsnicmp(&keybuf->KeyName[g_hkcu.len], L"_Classes", 8)) {
+ else if (hkcu_string && !wcsnicmp(keybuf->KeyName, hkcu_string, hkcu_len) && !wcsnicmp(&keybuf->KeyName[hkcu_len], L"_Classes", 8)) {
unsigned int ourlen = lstrlenW(L"HKEY_CURRENT_USER\\Software\\Classes");
- unsigned int existlen = g_hkcu.len + 8;
+ unsigned int existlen = hkcu_len + 8;
memmove(keybuf->KeyName + ourlen, keybuf->KeyName + existlen, keybuf->KeyNameLength + (1 * sizeof(WCHAR)) - (existlen * sizeof(WCHAR)));
memcpy(keybuf->KeyName, L"HKEY_CURRENT_USER\\Software\\Classes", ourlen * sizeof(WCHAR));
keybuf->KeyNameLength -= (existlen - ourlen) * sizeof(WCHAR);
@@ -1673,6 +1681,7 @@ wchar_t *get_key_path(POBJECT_ATTRIBUTES ObjectAttributes, PKEY_NAME_INFORMATION
memcpy(keybuf->KeyName, L"HKEY_USERS", ourlen * sizeof(WCHAR));
keybuf->KeyNameLength -= (14 - ourlen) * sizeof(WCHAR);
}
+ }
goto out;
@@ -1685,12 +1694,18 @@ wchar_t *get_key_path(POBJECT_ATTRIBUTES ObjectAttributes, PKEY_NAME_INFORMATION
return keybuf->KeyName;
}
-static PSID GetSID(void)
+//
+// On success the caller owns *userinfo_out and must free() it; the returned SID
+// points into that allocation.
+//
+static PSID GetSID(PTOKEN_USER *userinfo_out)
{
HANDLE token;
DWORD retlen;
PTOKEN_USER userinfo = NULL;
+ *userinfo_out = NULL;
+
if (!OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY | TOKEN_QUERY_SOURCE, &token))
return NULL;
if (GetTokenInformation(token, TokenUser, 0, 0, &retlen) || GetLastError() != ERROR_INSUFFICIENT_BUFFER) {
@@ -1705,24 +1720,67 @@ static PSID GetSID(void)
return NULL;
}
CloseHandle(token);
+ *userinfo_out = userinfo;
return userinfo->User.Sid;
}
CloseHandle(token);
return NULL;
}
+//
+// hkcu_init() may be called from DllMain and lazily from get_key_path() on any
+// thread, so it has to be idempotent. The initializing thread publishes
+// g_hkcu.len first and g_hkcu.hkcu_string last (via an interlocked store, which
+// is a full barrier), because readers use hkcu_string as the "ready" flag.
+//
+// On failure the state is reset so a subsequent call can retry: the SID lookup
+// depends on advapi32, which may not be resolvable yet in an injected process.
+//
+#define HKCU_INIT_NONE 0
+#define HKCU_INIT_RUNNING 1
+#define HKCU_INIT_DONE 2
+
+static volatile LONG g_hkcu_state = HKCU_INIT_NONE;
+
void hkcu_init(void)
{
- PSID sid = GetSID();
- LPWSTR sidstr;
+ PTOKEN_USER userinfo = NULL;
+ LPWSTR sidstr = NULL;
+ wchar_t *hkcu_string;
+ unsigned int len;
+ PSID sid;
- ConvertSidToStringSidW(sid, &sidstr);
+ if (InterlockedCompareExchange(&g_hkcu_state, HKCU_INIT_RUNNING, HKCU_INIT_NONE) != HKCU_INIT_NONE)
+ return;
+
+ sid = GetSID(&userinfo);
+ if (!sid)
+ goto fail;
+
+ if (!ConvertSidToStringSidW(sid, &sidstr) || !sidstr)
+ goto fail;
+
+ len = lstrlenW(sidstr) + lstrlenW(L"\\REGISTRY\\USER\\");
+ hkcu_string = malloc((len + 1) * sizeof(wchar_t));
+ if (!hkcu_string)
+ goto fail;
+
+ wcscpy(hkcu_string, L"\\REGISTRY\\USER\\");
+ wcscat(hkcu_string, sidstr);
+
+ g_hkcu.len = len;
+ InterlockedExchangePointer((PVOID volatile *)&g_hkcu.hkcu_string, hkcu_string);
+ InterlockedExchange(&g_hkcu_state, HKCU_INIT_DONE);
- g_hkcu.len = lstrlenW(sidstr) + lstrlenW(L"\\REGISTRY\\USER\\");
- g_hkcu.hkcu_string = malloc((g_hkcu.len + 1) * sizeof(wchar_t));
- wcscpy(g_hkcu.hkcu_string, L"\\REGISTRY\\USER\\");
- wcscat(g_hkcu.hkcu_string, sidstr);
LocalFree(sidstr);
+ free(userinfo);
+ return;
+
+fail:
+ if (sidstr)
+ LocalFree(sidstr);
+ free(userinfo);
+ InterlockedExchange(&g_hkcu_state, HKCU_INIT_NONE);
}
extern int process_shutting_down;