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 810127 52597940e0a1e27dad27dd30a94eb01027b6c75d
parent 810123 4f6e597104dabedfecfafa2ab63dc79fd7f8bc7a
push id113900
push userbmo:ccorcoran@mozilla.com
push dateMon, 25 Jun 2018 11:25:30 +0000
reviewersaklotz
bugs1443411
milestone62.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(args...)
+#define CallCreateThreadHook(args...)
+#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,255 @@
+/* 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/Atomics.h"
+#include "mozilla/ScopeExit.h"
+#include "mozilla/UniquePtr.h"
+#include "mozilla/WindowsDllBlocklist.h"
+
+static mozilla::Atomic<int> sTestRunning;
+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 chars = aOther->Length / 2;
+  return nsString((const char16_t *)aOther->Buffer, chars);
+}
+
+static void
+DllLoadHook(bool aDllLoaded, NTSTATUS aStatus, HANDLE aDllBase,
+            PUNICODE_STRING aDllName)
+{
+  nsString str = makeString(aDllName);
+
+  if (sTestRunning > 0) {
+    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);
+    }
+  }
+}
+
+template<typename TgetArgsProc>
+static void
+DoTest_CreateRemoteThread_LoadLibrary(TgetArgsProc aGetArgsProc)
+{
+  sTestRunning ++;
+  auto runningFlag = mozilla::MakeScopeExit([](){
+    sTestRunning --;
+  });
+
+  DllBlocklist_SetDllLoadHook(DllLoadHook);
+  DllBlocklist_SetCreateThreadHook(CreateThreadHook);
+
+  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);
+  }
+
+  STARTUPINFOW si = { 0 };
+  si.cb = sizeof(si);
+  ::GetStartupInfoW(&si);
+  PROCESS_INFORMATION pi = { 0 };
+
+  nsString path = nsString(u"Injector.exe");
+  nsString dllPath = nsString(DLL_LEAF_NAME);
+  // {
+  //   wchar_t *ppath;
+  //   auto len = path.GetMutableData(&ppath, 1000);
+  //   ::GetModuleFileNameW(NULL, ppath, len);
+
+  //   // Remove after trailing slash
+  //   auto slash = path.RFindCharInSet(u"\\/");
+  //   if (slash != kNotFound) {
+  //     nsString t;
+  //     path.Left(t, slash + 1);
+  //     path = t;
+  //   } else {
+  //     path = u"";
+  //   }
+  // }
+  // nsString dllPath = path;
+  // dllPath.Append(DLL_LEAF_NAME);
+  // path.Append(u"Injector.exe");
+  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;
+  }
+
+  auto cleanup = mozilla::MakeScopeExit([&](){
+    CloseHandle(pi.hThread);
+    EXPECT_TRUE("Shutting down.");
+    WaitForSingleObject(pi.hProcess, INFINITE);
+    CloseHandle(pi.hProcess);
+    CloseHandle(sThreadWasAllowed);
+    CloseHandle(sThreadWasBlocked);
+    CloseHandle(sDllWasLoaded);
+  });
+
+  HANDLE handles[] = {
+    sThreadWasBlocked,
+    sThreadWasAllowed,
+    sDllWasLoaded,
+    pi.hProcess
+  };
+  int handleCount = ARRAYSIZE(handles);
+  bool keepGoing = true;
+
+  while(keepGoing) {
+    switch(WaitForMultipleObjectsEx(handleCount, handles,
+                                    FALSE, sTimeoutMS, FALSE)) {
+      case WAIT_OBJECT_0: {
+		    EXPECT_TRUE("Thread was blocked successfully.");
+		    keepGoing = false;
+		    break;
+      }
+      case WAIT_OBJECT_0 + 1: {
+        EXPECT_TRUE(!"Thread was allowed but should have been blocked.");
+        keepGoing = false;
+        break;
+      }
+      case WAIT_OBJECT_0 + 2: {
+        EXPECT_TRUE(!"DLL was loaded.");
+        keepGoing = false;
+        break;
+      }
+      case WAIT_OBJECT_0 + 3: {
+        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 == 0 || hExisting == INVALID_HANDLE_VALUE);
+
+  if (hExisting != 0 && hExisting != INVALID_HANDLE_VALUE) {
+    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',
     ]