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;