Bug 1443411: Add gtests for blocking threads with LoadLibrary start address;r=aklotz
authorCarl Corcoran <ccorcoran@mozilla.com>
Thu, 14 Jun 2018 00:15:26 -0700
changeset 483394 130e3b2dd716ae4608587cb6755cda069a405259
parent 483393 5c13bf70a3e1bac455d93c136d8079ac3be022e9
child 483395 40d1931d184b4e4bf7eb35c32220442f9ae14f28
push id1815
push userffxbld-merge
push dateMon, 15 Oct 2018 10:40:45 +0000
treeherdermozilla-release@18d4c09e9378 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersaklotz
bugs1443411
milestone63.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
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',
     ]