Bug 1443411: Add gtests for blocking threads with LoadLibrary start address;r?aklotz draft
authorCarl Corcoran <ccorcoran@mozilla.com>
Thu, 14 Jun 2018 00:15:26 -0700
changeset 813610 679bc265de9507c05c3e23f5c9966584fa848d25
parent 813360 7d20e7fae1039720f92db1a3a72bc2c7424b5f98
push id114935
push userbmo:ccorcoran@mozilla.com
push dateTue, 03 Jul 2018 13:38:54 +0000
reviewersaklotz
bugs1443411
milestone63.0a1
Bug 1443411: Add gtests for blocking threads with LoadLibrary start address;r?aklotz MozReview-Commit-ID: 2wIUNnNoKa8
mozglue/build/WindowsDllBlocklist.cpp
mozglue/build/WindowsDllBlocklist.h
mozglue/tests/gtest/Injector/Injector.cpp
mozglue/tests/gtest/Injector/moz.build
mozglue/tests/gtest/InjectorDLL/InjectorDLL.cpp
mozglue/tests/gtest/InjectorDLL/moz.build
mozglue/tests/gtest/TestDLLEject.cpp
mozglue/tests/gtest/moz.build
mozglue/tests/moz.build
--- 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',
     ]