Bug 1443411: Add gtests for blocking threads with LoadLibrary start address;r=aklotz

MozReview-Commit-ID: 2wIUNnNoKa8

--HG--
extra : rebase_source : f1990af6cd130d9bca38ef21d64d66584d20b94e
This commit is contained in:
Carl Corcoran 2018-06-14 00:15:26 -07:00
parent a162eddf84
commit 63153c7e3b
9 changed files with 436 additions and 1 deletions

View file

@ -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);

View file

@ -9,6 +9,9 @@
#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"
@ -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 {

View file

@ -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;
}

View file

@ -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']

View file

@ -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;
}

View file

@ -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']

View file

@ -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();
});
}

View file

@ -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',
]

View file

@ -17,4 +17,5 @@ CppUnitTests([
if CONFIG['OS_ARCH'] == 'WINNT':
TEST_DIRS += [
'interceptor',
'gtest',
]