diff --git a/mozglue/build/WindowsDllBlocklist.cpp b/mozglue/build/WindowsDllBlocklist.cpp index 4b7a6f7ecf6a..da72366774ab 100644 --- a/mozglue/build/WindowsDllBlocklist.cpp +++ b/mozglue/build/WindowsDllBlocklist.cpp @@ -374,6 +374,45 @@ static wchar_t* lastslash(wchar_t* s, int len) return nullptr; } + +#ifdef ENABLE_TESTS +DllLoadHookType gDllLoadHook = nullptr; + +void +DllBlocklist_SetDllLoadHook(DllLoadHookType aHook) +{ + gDllLoadHook = aHook; +} + +void +CallDllLoadHook(bool aDllLoaded, NTSTATUS aStatus, HANDLE aDllBase, PUNICODE_STRING aDllName) +{ + if (gDllLoadHook) { + gDllLoadHook(aDllLoaded, aStatus, aDllBase, aDllName); + } +} + +CreateThreadHookType gCreateThreadHook = nullptr; + +void +DllBlocklist_SetCreateThreadHook(CreateThreadHookType aHook) +{ + gCreateThreadHook = aHook; +} + +void +CallCreateThreadHook(bool aWasAllowed, void* aStartAddress) +{ + if (gCreateThreadHook) { + gCreateThreadHook(aWasAllowed, aStartAddress); + } +} + +#else // ENABLE_TESTS +#define CallDllLoadHook(...) +#define CallCreateThreadHook(...) +#endif // ENABLE_TESTS + static NTSTATUS NTAPI patched_LdrLoadDll (PWCHAR filePath, PULONG flags, PUNICODE_STRING moduleFileName, PHANDLE handle) { @@ -453,6 +492,7 @@ patched_LdrLoadDll (PWCHAR filePath, PULONG flags, PUNICODE_STRING moduleFileNam char * end = nullptr; _strtoui64(dot+1, &end, 16); if (end == dot+13) { + CallDllLoadHook(false, STATUS_DLL_NOT_FOUND, 0, moduleFileName); return STATUS_DLL_NOT_FOUND; } } @@ -463,6 +503,7 @@ patched_LdrLoadDll (PWCHAR filePath, PULONG flags, PUNICODE_STRING moduleFileNam current++; } if (current == dot) { + CallDllLoadHook(false, STATUS_DLL_NOT_FOUND, 0, moduleFileName); return STATUS_DLL_NOT_FOUND; } } @@ -510,6 +551,7 @@ patched_LdrLoadDll (PWCHAR filePath, PULONG flags, PUNICODE_STRING moduleFileNam if (!full_fname) { // uh, we couldn't find the DLL at all, so... printf_stderr("LdrLoadDll: Blocking load of '%s' (SearchPathW didn't find it?)\n", dllName); + CallDllLoadHook(false, STATUS_DLL_NOT_FOUND, 0, moduleFileName); return STATUS_DLL_NOT_FOUND; } @@ -548,6 +590,7 @@ patched_LdrLoadDll (PWCHAR filePath, PULONG flags, PUNICODE_STRING moduleFileNam if (!load_ok) { printf_stderr("LdrLoadDll: Blocking load of '%s' -- see http://www.mozilla.com/en-US/blocklist/\n", dllName); DllBlockSet::Add(info->name, fVersion); + CallDllLoadHook(false, STATUS_DLL_NOT_FOUND, 0, moduleFileName); return STATUS_DLL_NOT_FOUND; } } @@ -570,7 +613,9 @@ continue_loading: AutoSuppressStackWalking suppress; #endif - return stub_LdrLoadDll(filePath, flags, moduleFileName, handle); + NTSTATUS ret = stub_LdrLoadDll(filePath, flags, moduleFileName, handle); + CallDllLoadHook(true, ret, handle ? *handle : 0, moduleFileName); + return ret; } #if defined(NIGHTLY_BUILD) @@ -616,7 +661,10 @@ patched_BaseThreadInitThunk(BOOL aIsInitialThread, void* aStartAddress, void* aThreadParam) { if (ShouldBlockThread(aStartAddress)) { + CallCreateThreadHook(false, aStartAddress); aStartAddress = (void*)NopThreadProc; + } else { + CallCreateThreadHook(true, aStartAddress); } stub_BaseThreadInitThunk(aIsInitialThread, aStartAddress, aThreadParam); diff --git a/mozglue/build/WindowsDllBlocklist.h b/mozglue/build/WindowsDllBlocklist.h index b49d643482dc..5d599ade890b 100644 --- a/mozglue/build/WindowsDllBlocklist.h +++ b/mozglue/build/WindowsDllBlocklist.h @@ -9,6 +9,9 @@ #if (defined(_MSC_VER) || defined(__MINGW32__)) && (defined(_M_IX86) || defined(_M_X64)) #include +#ifdef ENABLE_TESTS +#include +#endif // ENABLE_TESTS #include "mozilla/Attributes.h" #include "mozilla/Types.h" @@ -25,6 +28,14 @@ MFBT_API void DllBlocklist_Initialize(uint32_t aInitFlags = eDllBlocklistInitFla MFBT_API void DllBlocklist_WriteNotes(HANDLE file); MFBT_API bool DllBlocklist_CheckStatus(); +#ifdef ENABLE_TESTS +typedef void (*DllLoadHookType)(bool aDllLoaded, NTSTATUS aNtStatus, + HANDLE aDllBase, PUNICODE_STRING aDllName); +MFBT_API void DllBlocklist_SetDllLoadHook(DllLoadHookType aHook); +typedef void (*CreateThreadHookType)(bool aWasAllowed, void *aStartAddress); +MFBT_API void DllBlocklist_SetCreateThreadHook(CreateThreadHookType aHook); +#endif // ENABLE_TESTS + // Forward declaration namespace mozilla { namespace glue { diff --git a/mozglue/tests/gtest/Injector/Injector.cpp b/mozglue/tests/gtest/Injector/Injector.cpp new file mode 100644 index 000000000000..8beb8d67da8d --- /dev/null +++ b/mozglue/tests/gtest/Injector/Injector.cpp @@ -0,0 +1,50 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include +#include +#include + +int +main(int argc, char** argv) +{ + if (argc < 4) { + fprintf(stderr, + "Not enough command line arguments.\n" + "Command line syntax:\n" + "Injector.exe [pid] [startAddr] [threadParam]\n"); + return 1; + } + + DWORD pid = strtoul(argv[1], 0, 0); +#ifdef HAVE_64BIT_BUILD + void* startAddr = (void*)strtoull(argv[2], 0, 0); + void* threadParam = (void*)strtoull(argv[3], 0, 0); +#else + void* startAddr = (void*)strtoul(argv[2], 0, 0); + void* threadParam = (void*)strtoul(argv[3], 0, 0); +#endif + HANDLE targetProc = OpenProcess(PROCESS_CREATE_THREAD | PROCESS_QUERY_INFORMATION | + PROCESS_VM_OPERATION | PROCESS_VM_WRITE | PROCESS_VM_READ, + FALSE, + pid); + if (targetProc == nullptr) { + fprintf(stderr, "Error %lu opening target process, PID %lu \n", GetLastError(), pid); + return 1; + } + + HANDLE hThread = CreateRemoteThread(targetProc, nullptr, 0, + (LPTHREAD_START_ROUTINE)startAddr, + threadParam, 0, nullptr); + if (hThread == nullptr) { + fprintf(stderr, "Error %lu in CreateRemoteThread\n", GetLastError()); + CloseHandle(targetProc); + return 1; + } + + CloseHandle(hThread); + CloseHandle(targetProc); + + return 0; +} diff --git a/mozglue/tests/gtest/Injector/moz.build b/mozglue/tests/gtest/Injector/moz.build new file mode 100644 index 000000000000..50bd8182eb1c --- /dev/null +++ b/mozglue/tests/gtest/Injector/moz.build @@ -0,0 +1,9 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +DIST_INSTALL = False + +SimplePrograms(['Injector']) + +TEST_HARNESS_FILES.gtest += ['!Injector.exe'] diff --git a/mozglue/tests/gtest/InjectorDLL/InjectorDLL.cpp b/mozglue/tests/gtest/InjectorDLL/InjectorDLL.cpp new file mode 100644 index 000000000000..485d43d5d6e1 --- /dev/null +++ b/mozglue/tests/gtest/InjectorDLL/InjectorDLL.cpp @@ -0,0 +1,11 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include + +BOOL WINAPI DllMain(HINSTANCE hInstance, DWORD aReason, LPVOID) +{ + return TRUE; +} + diff --git a/mozglue/tests/gtest/InjectorDLL/moz.build b/mozglue/tests/gtest/InjectorDLL/moz.build new file mode 100644 index 000000000000..721318920fc5 --- /dev/null +++ b/mozglue/tests/gtest/InjectorDLL/moz.build @@ -0,0 +1,14 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +DIST_INSTALL = False + +SharedLibrary('InjectorDLL') + +UNIFIED_SOURCES = [ + 'InjectorDLL.cpp', +] + +TEST_HARNESS_FILES.gtest += ['!InjectorDLL.dll'] diff --git a/mozglue/tests/gtest/TestDLLEject.cpp b/mozglue/tests/gtest/TestDLLEject.cpp new file mode 100644 index 000000000000..d240cac3f586 --- /dev/null +++ b/mozglue/tests/gtest/TestDLLEject.cpp @@ -0,0 +1,277 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include +#include +#include "gtest/gtest.h" +#include "nsReadableUtils.h" +#include "nsString.h" +#include "nsUnicharUtils.h" +#include "mozilla/ArrayUtils.h" +#include "mozilla/ScopeExit.h" +#include "mozilla/UniquePtr.h" +#include "mozilla/WindowsDllBlocklist.h" + +static HANDLE sThreadWasBlocked = 0; +static HANDLE sThreadWasAllowed = 0; +static HANDLE sDllWasLoaded = 0; +static uintptr_t sStartAddress = 0; + +static const int sTimeoutMS = 10000; + +#define DLL_LEAF_NAME (u"InjectorDLL.dll") + +static nsString +makeString(PUNICODE_STRING aOther) +{ + size_t numChars = aOther->Length / sizeof(WCHAR); + return nsString((const char16_t *)aOther->Buffer, numChars); +} + +static void +DllLoadHook(bool aDllLoaded, NTSTATUS aStatus, HANDLE aDllBase, + PUNICODE_STRING aDllName) +{ + nsString str = makeString(aDllName); + + nsString dllName = nsString(DLL_LEAF_NAME); + if (StringEndsWith(str, dllName, nsCaseInsensitiveStringComparator())) { + if (aDllLoaded) { + SetEvent(sDllWasLoaded); + } + } +} + +static void +CreateThreadHook(bool aWasAllowed, void* aStartAddress) +{ + if (sStartAddress == (uintptr_t)aStartAddress) { + if (!aWasAllowed) { + SetEvent(sThreadWasBlocked); + } else { + SetEvent(sThreadWasAllowed); + } + } +} + +/** + * This function tests that we correctly block DLLs injected into this process + * via an injection technique which calls CreateRemoteThread with LoadLibrary*() + * as the thread start address, and the path to the DLL as the thread param. + * + * We prevent this technique by blocking threads with a start address in any + * LoadLibrary*() APIs. + * + * This function launches Injector.exe which simulates a 3rd-party application + * executing this technique. + * + * @param aGetArgsProc A callable procedure that specifies the thread start + * address and thread param passed as arguments to + * Injector.exe, which are in turn passed as arguments to + * CreateRemoteThread. This procedure is defined as such: + * + * void (*aGetArgsProc)(const nsString& aDllPath, + * const nsCString& aDllPathC, + * uintptr_t& startAddress, + * uintptr_t& threadParam); + * + * aDllPath is a WCHAR-friendly path to InjectorDLL.dll. + * Its memory will persist during the injection attempt. + * + * aDllPathC is the equivalent char-friendly path. + * + * startAddress and threadParam are passed into + * CreateRemoteThread as arguments. + */ +template +static void +DoTest_CreateRemoteThread_LoadLibrary(TgetArgsProc aGetArgsProc) +{ + sThreadWasBlocked = CreateEvent(NULL, FALSE, FALSE, NULL); + if (!sThreadWasBlocked) { + EXPECT_TRUE(!"Unable to create sThreadWasBlocked event"); + ASSERT_EQ(GetLastError(), ERROR_SUCCESS); + } + + sThreadWasAllowed = CreateEvent(NULL, FALSE, FALSE, NULL); + if (!sThreadWasAllowed) { + EXPECT_TRUE(!"Unable to create sThreadWasAllowed event"); + ASSERT_EQ(GetLastError(), ERROR_SUCCESS); + } + + sDllWasLoaded = CreateEvent(NULL, FALSE, FALSE, NULL); + if (!sDllWasLoaded) { + EXPECT_TRUE(!"Unable to create sDllWasLoaded event"); + ASSERT_EQ(GetLastError(), ERROR_SUCCESS); + } + + auto closeEvents = mozilla::MakeScopeExit([&](){ + CloseHandle(sThreadWasAllowed); + CloseHandle(sThreadWasBlocked); + CloseHandle(sDllWasLoaded); + }); + + // Hook into our DLL and thread blocking routines during this test. + DllBlocklist_SetDllLoadHook(DllLoadHook); + DllBlocklist_SetCreateThreadHook(CreateThreadHook); + auto undoHooks = mozilla::MakeScopeExit([&](){ + DllBlocklist_SetDllLoadHook(nullptr); + DllBlocklist_SetCreateThreadHook(nullptr); + }); + + // Launch Injector.exe. + STARTUPINFOW si = { 0 }; + si.cb = sizeof(si); + ::GetStartupInfoW(&si); + PROCESS_INFORMATION pi = { 0 }; + + nsString path(u"Injector.exe"); + nsString dllPath(DLL_LEAF_NAME); + nsCString dllPathC = NS_ConvertUTF16toUTF8(dllPath); + + uintptr_t threadParam; + aGetArgsProc(dllPath, dllPathC, sStartAddress, threadParam); + + path.AppendPrintf(" %lu 0x%p 0x%p", GetCurrentProcessId(), sStartAddress, + threadParam); + if (::CreateProcessW(NULL, path.get(), 0, 0, FALSE, 0, NULL, NULL, + &si, &pi) == FALSE) { + EXPECT_TRUE(!"Error in CreateProcessW() launching Injector.exe"); + ASSERT_EQ(GetLastError(), ERROR_SUCCESS); + return; + } + + // Ensure Injector.exe doesn't stay running after this test finishes. + auto cleanup = mozilla::MakeScopeExit([&](){ + CloseHandle(pi.hThread); + EXPECT_TRUE("Shutting down."); + WaitForSingleObject(pi.hProcess, INFINITE); + CloseHandle(pi.hProcess); + }); + + // Wait for information to come in and complete the test. + HANDLE handles[] = { + sThreadWasBlocked, + sThreadWasAllowed, + sDllWasLoaded, + pi.hProcess + }; + int handleCount = mozilla::ArrayLength(handles); + bool keepGoing = true; // Set to false to signal that the test is over. + + while(keepGoing) { + switch(WaitForMultipleObjectsEx(handleCount, handles, + FALSE, sTimeoutMS, FALSE)) { + case WAIT_OBJECT_0: { // sThreadWasBlocked + EXPECT_TRUE("Thread was blocked successfully."); + // No need to continue testing; blocking was successful. + keepGoing = false; + break; + } + case WAIT_OBJECT_0 + 1: { // sThreadWasAllowed + EXPECT_TRUE(!"Thread was allowed but should have been blocked."); + // No need to continue testing; blocking failed. + keepGoing = false; + break; + } + case WAIT_OBJECT_0 + 2: { // sDllWasLoaded + EXPECT_TRUE(!"DLL was loaded."); + // No need to continue testing; blocking failed and the DLL was + // consequently loaded. In theory we should never see this fire, because + // the thread being allowed should already trigger a test failure. + keepGoing = false; + break; + } + case WAIT_OBJECT_0 + 3: { // pi.hProcess + // Check to see if we got an error code from Injector.exe, in which case + // fail the test and exit. + DWORD exitCode; + if (!GetExitCodeProcess(pi.hProcess, &exitCode)) { + EXPECT_TRUE(!"Injector.exe exited but we were unable to get the exit code."); + keepGoing = false; + break; + } + EXPECT_EQ(exitCode, 0); + if (exitCode != 0) { + EXPECT_TRUE(!"Injector.exe returned non-zero exit code"); + keepGoing = false; + break; + } + // Process exited successfully. This can be ignored; we expect to get an + // event whether the DLL was loaded or blocked. + EXPECT_TRUE("Process exited as expected."); + handleCount--; + break; + } + case WAIT_TIMEOUT: + default: { + EXPECT_TRUE(!"An error or timeout occurred while waiting for activity " + "from Injector.exe"); + keepGoing = false; + break; + } + } + } + + // Double-check that injectordll is not loaded. + auto hExisting = GetModuleHandleW(dllPath.get()); + EXPECT_TRUE(!hExisting); + + // If the DLL was erroneously loaded, attempt to unload it before exiting. + if (hExisting) { + FreeLibrary(hExisting); + } + + return; +} + +TEST(TestInjectEject, CreateRemoteThread_LoadLibraryA) +{ + DoTest_CreateRemoteThread_LoadLibrary([](const nsString& dllPath, + const nsCString& dllPathC, + uintptr_t& aStartAddress, + uintptr_t& aThreadParam){ + HMODULE hKernel32 = GetModuleHandleW(L"Kernel32"); + aStartAddress = (uintptr_t)GetProcAddress(hKernel32, "LoadLibraryA"); + aThreadParam = (uintptr_t)dllPathC.get(); + }); +} + +TEST(TestInjectEject, CreateRemoteThread_LoadLibraryW) +{ + DoTest_CreateRemoteThread_LoadLibrary([](const nsString& dllPath, + const nsCString& dllPathC, + uintptr_t& aStartAddress, + uintptr_t& aThreadParam){ + HMODULE hKernel32 = GetModuleHandleW(L"Kernel32"); + aStartAddress = (uintptr_t)GetProcAddress(hKernel32, "LoadLibraryW"); + aThreadParam = (uintptr_t)dllPath.get(); + }); +} + +TEST(TestInjectEject, CreateRemoteThread_LoadLibraryExW) +{ + DoTest_CreateRemoteThread_LoadLibrary([](const nsString& dllPath, + const nsCString& dllPathC, + uintptr_t& aStartAddress, + uintptr_t& aThreadParam){ + HMODULE hKernel32 = GetModuleHandleW(L"Kernel32"); + // LoadLibraryEx requires three arguments so this injection method may not + // be viable. It's certainly not an allowable thread start in any case. + aStartAddress = (uintptr_t)GetProcAddress(hKernel32, "LoadLibraryExW"); + aThreadParam = (uintptr_t)dllPath.get(); + }); +} + +TEST(TestInjectEject, CreateRemoteThread_LoadLibraryExA) +{ + DoTest_CreateRemoteThread_LoadLibrary([](const nsString& dllPath, + const nsCString& dllPathC, + uintptr_t& aStartAddress, + uintptr_t& aThreadParam){ + HMODULE hKernel32 = GetModuleHandleW(L"Kernel32"); + aStartAddress = (uintptr_t)GetProcAddress(hKernel32, "LoadLibraryExA"); + aThreadParam = (uintptr_t)dllPathC.get(); + }); +} diff --git a/mozglue/tests/gtest/moz.build b/mozglue/tests/gtest/moz.build new file mode 100644 index 000000000000..0d7ef9845f37 --- /dev/null +++ b/mozglue/tests/gtest/moz.build @@ -0,0 +1,14 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +UNIFIED_SOURCES += [ + 'TestDLLEject.cpp' +] + +FINAL_LIBRARY = 'xul-gtest' + +TEST_DIRS += [ + 'Injector', + 'InjectorDLL', +] diff --git a/mozglue/tests/moz.build b/mozglue/tests/moz.build index d0e060181411..1e64194d3483 100644 --- a/mozglue/tests/moz.build +++ b/mozglue/tests/moz.build @@ -17,4 +17,5 @@ CppUnitTests([ if CONFIG['OS_ARCH'] == 'WINNT': TEST_DIRS += [ 'interceptor', + 'gtest', ]