--- a/mozglue/build/WindowsDllBlocklist.cpp
+++ b/mozglue/build/WindowsDllBlocklist.cpp
@@ -369,16 +369,55 @@ static wchar_t* lastslash(wchar_t* s, in
for (wchar_t* c = s + len - 1; c >= s; --c) {
if (*c == L'\\' || *c == L'/') {
return c;
}
}
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)
{
// We have UCS2 (UTF16?), we want ASCII, but we also just want the filename portion
#define DLLNAME_MAX 128
char dllName[DLLNAME_MAX+1];
wchar_t *dll_part;
char *dot;
@@ -448,26 +487,28 @@ patched_LdrLoadDll (PWCHAR filePath, PUL
if (!(sInitFlags & eDllBlocklistInitFlagWasBootstrapped)) {
// Block a suspicious binary that uses various 12-digit hex strings
// e.g. MovieMode.48CA2AEFA22D.dll (bug 973138)
dot = strchr(dllName, '.');
if (dot && (strchr(dot+1, '.') == dot+13)) {
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;
}
}
// Block binaries where the filename is at least 16 hex digits
if (dot && ((dot - dllName) >= 16)) {
char * current = dllName;
while (current < dot && isxdigit(*current)) {
current++;
}
if (current == dot) {
+ CallDllLoadHook(false, STATUS_DLL_NOT_FOUND, 0, moduleFileName);
return STATUS_DLL_NOT_FOUND;
}
}
// then compare to everything on the blocklist
DECLARE_POINTER_TO_FIRST_DLL_BLOCKLIST_ENTRY(info);
while (info->name) {
if (strcmp(info->name, dllName) == 0)
@@ -505,16 +546,17 @@ patched_LdrLoadDll (PWCHAR filePath, PUL
if (sentinel.BailOut()) {
goto continue_loading;
}
full_fname = getFullPath(filePath, fname);
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;
}
if (info->flags & DllBlockInfo::USE_TIMESTAMP) {
fVersion = GetTimestamp(full_fname.get());
if (fVersion > info->maxVersion) {
load_ok = true;
}
@@ -543,16 +585,17 @@ patched_LdrLoadDll (PWCHAR filePath, PUL
}
}
}
}
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;
}
}
}
continue_loading:
#ifdef DEBUG_very_verbose
printf_stderr("LdrLoadDll: continuing load... ('%S')\n", moduleFileName->Buffer);
@@ -565,17 +608,19 @@ continue_loading:
__LINE__);
#ifdef _M_AMD64
// Prevent the stack walker from suspending this thread when LdrLoadDll
// holds the RtlLookupFunctionEntry lock.
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)
// Map of specific thread proc addresses we should block. In particular,
// LoadLibrary* APIs which indicate DLL injection
static mozilla::Vector<void*, 4>* gStartAddressesToBlock;
#endif
@@ -611,17 +656,20 @@ NopThreadProc(void* /* aThreadParam */)
return 0;
}
static MOZ_NORETURN void __fastcall
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);
}
static WindowsDllInterceptor NtDllIntercept;
static WindowsDllInterceptor Kernel32Intercept;
--- a/mozglue/build/WindowsDllBlocklist.h
+++ b/mozglue/build/WindowsDllBlocklist.h
@@ -4,32 +4,43 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#ifndef mozilla_windowsdllblocklist_h
#define mozilla_windowsdllblocklist_h
#if (defined(_MSC_VER) || defined(__MINGW32__)) && (defined(_M_IX86) || defined(_M_X64))
#include <windows.h>
+#ifdef ENABLE_TESTS
+#include <winternl.h>
+#endif // ENABLE_TESTS
#include "mozilla/Attributes.h"
#include "mozilla/Types.h"
#define HAS_DLL_BLOCKLIST
enum DllBlocklistInitFlags
{
eDllBlocklistInitFlagDefault = 0,
eDllBlocklistInitFlagIsChildProcess = 1,
eDllBlocklistInitFlagWasBootstrapped = 2
};
MFBT_API void DllBlocklist_Initialize(uint32_t aInitFlags = eDllBlocklistInitFlagDefault);
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 {
namespace detail {
class DllServicesBase;
} // namespace detail
} // namespace glue
} // namespace mozilla
new file mode 100644
--- /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 <Windows.h>
+#include <stdio.h>
+#include <stdlib.h>
+
+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;
+}
new file mode 100644
--- /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']
new file mode 100644
--- /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 <Windows.h>
+
+BOOL WINAPI DllMain(HINSTANCE hInstance, DWORD aReason, LPVOID)
+{
+ return TRUE;
+}
+
new file mode 100644
--- /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']
new file mode 100644
--- /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 <Windows.h>
+#include <winternl.h>
+#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<typename TgetArgsProc>
+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();
+ });
+}
new file mode 100644
--- /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',
+]
--- a/mozglue/tests/moz.build
+++ b/mozglue/tests/moz.build
@@ -12,9 +12,10 @@ GeckoCppUnitTests([
CppUnitTests([
'TestPrintf',
])
if CONFIG['OS_ARCH'] == 'WINNT':
TEST_DIRS += [
'interceptor',
+ 'gtest',
]