Bug 1445025: Part 5 - Implement a Native NT version of the DLL blocklist; r=mhowell

This version of the blocklist should be functionally comparable to the mozglue
based blocklist, except:

* We hook NtMapViewOfSection instead of LdrLoadDll: The former allows us to
  easily obtain the module file name being used for the load. The latter
  requires us to essentially emulate the loader's path searching, which is
  a perf hit, potentially a correctness issue, and more work to do given the
  limited native NT API set.

* Since the paths in native NT land are all unicode, and since this code is
  critical to startup performance, this version of the blocklist uses unicode
  strings instead of ASCII strings. My thoughts here are that we don't want to
  be wasting time on every DLL load doing ASCII-to-unicode conversion every
  time we want to do a blocklist string comparison.

* I am completely aware that this leaves us in a bizarre situation where we have
  two copies of the blocklist in our binaries: one unicode version in firefox.exe,
  and one ASCII version in mozglue.dll. Once we (hopefully) move to using the
  launcher process by default, the ASCII copy can go away. In the meantime, we
  need to be able to use either one depending on how Firefox was started. I am
  happy to make the Native NT blocklist Nightly-only to assuage these concerns.
This commit is contained in:
Aaron Klotz 2018-06-05 15:21:19 -06:00
Родитель d1efe98e29
Коммит 8630a054fc
3 изменённых файлов: 450 добавлений и 0 удалений

Просмотреть файл

@ -0,0 +1,431 @@
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
/* 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 https://mozilla.org/MPL/2.0/. */
#include "NativeNt.h"
#include "nsWindowsDllInterceptor.h"
#include "mozilla/ArrayUtils.h"
#include "mozilla/Attributes.h"
#include "mozilla/Types.h"
#include "mozilla/WindowsDllBlocklist.h"
#define DLL_BLOCKLIST_ENTRY(name, ...) \
{ L##name, __VA_ARGS__ },
#define DLL_BLOCKLIST_CHAR_TYPE wchar_t
// Restrict the blocklist definitions to Nightly-only for now
#if defined(NIGHTLY_BUILD)
#include "mozilla/WindowsDllBlocklistDefs.h"
#else
#include "mozilla/WindowsDllBlocklistCommon.h"
DLL_BLOCKLIST_DEFINITIONS_BEGIN
DLL_BLOCKLIST_DEFINITIONS_END
#endif
extern uint32_t gBlocklistInitFlags;
static const HANDLE kCurrentProcess = reinterpret_cast<HANDLE>(-1);
class MOZ_STATIC_CLASS MOZ_TRIVIAL_CTOR_DTOR NativeNtBlockSet final
{
struct NativeNtBlockSetEntry
{
NativeNtBlockSetEntry() = default;
~NativeNtBlockSetEntry() = default;
NativeNtBlockSetEntry(const wchar_t* aName, uint64_t aVersion,
NativeNtBlockSetEntry* aNext)
: mName(aName)
, mVersion(aVersion)
, mNext(aNext)
{}
const wchar_t* mName;
uint64_t mVersion;
NativeNtBlockSetEntry* mNext;
};
public:
// Constructor and destructor MUST be trivial
NativeNtBlockSet() = default;
~NativeNtBlockSet() = default;
void Add(const wchar_t* aName, uint64_t aVersion);
void Write(HANDLE aFile);
private:
static NativeNtBlockSetEntry* NewEntry(const wchar_t* aName, uint64_t aVersion,
NativeNtBlockSetEntry* aNextEntry);
private:
NativeNtBlockSetEntry* mFirstEntry;
// SRWLOCK_INIT == 0, so this is okay to use without any additional work as
// long as NativeNtBlockSet is instantiated statically
SRWLOCK mLock;
};
NativeNtBlockSet::NativeNtBlockSetEntry*
NativeNtBlockSet::NewEntry(const wchar_t* aName, uint64_t aVersion,
NativeNtBlockSet::NativeNtBlockSetEntry* aNextEntry)
{
HANDLE processHeap = mozilla::nt::RtlGetProcessHeap();
if (!processHeap) {
return nullptr;
}
PVOID memory = ::RtlAllocateHeap(processHeap, 0, sizeof(NativeNtBlockSetEntry));
if (!memory) {
return nullptr;
}
return new (memory) NativeNtBlockSetEntry(aName, aVersion, aNextEntry);
}
void
NativeNtBlockSet::Add(const wchar_t* aName, uint64_t aVersion)
{
::RtlAcquireSRWLockExclusive(&mLock);
for (NativeNtBlockSetEntry* entry = mFirstEntry; entry; entry = entry->mNext) {
// We just need to compare the string pointers, not the strings themselves,
// as we always pass in the strings statically defined in the blocklist.
if (aName == entry->mName && aVersion == entry->mVersion) {
::RtlReleaseSRWLockExclusive(&mLock);
return;
}
}
// Not present, add it
NativeNtBlockSetEntry* newEntry = NewEntry(aName, aVersion, mFirstEntry);
mFirstEntry = newEntry;
::RtlReleaseSRWLockExclusive(&mLock);
}
void
NativeNtBlockSet::Write(HANDLE aFile)
{
// NB: If this function is called, it is long after kernel32 is initialized,
// so it is safe to use Win32 calls here.
DWORD nBytes;
char buf[MAX_PATH];
// It would be nicer to use RAII here. However, its destructor
// might not run if an exception occurs, in which case we would never release
// the lock (MSVC warns about this possibility). So we acquire and release
// manually.
::AcquireSRWLockExclusive(&mLock);
MOZ_SEH_TRY {
for (auto entry = mFirstEntry; entry; entry = entry->mNext) {
int convOk = ::WideCharToMultiByte(CP_UTF8, 0, entry->mName, -1, buf,
sizeof(buf), nullptr, nullptr);
if (!convOk) {
continue;
}
// write name[,v.v.v.v];
if (!WriteFile(aFile, buf, convOk, &nBytes, nullptr)) {
continue;
}
if (entry->mVersion != ALL_VERSIONS) {
WriteFile(aFile, ",", 1, &nBytes, nullptr);
uint16_t parts[4];
parts[0] = entry->mVersion >> 48;
parts[1] = (entry->mVersion >> 32) & 0xFFFF;
parts[2] = (entry->mVersion >> 16) & 0xFFFF;
parts[3] = entry->mVersion & 0xFFFF;
for (size_t p = 0; p < mozilla::ArrayLength(parts); ++p) {
ltoa(parts[p], buf, 10);
WriteFile(aFile, buf, strlen(buf), &nBytes, nullptr);
if (p != mozilla::ArrayLength(parts) - 1) {
WriteFile(aFile, ".", 1, &nBytes, nullptr);
}
}
}
WriteFile(aFile, ";", 1, &nBytes, nullptr);
}
}
MOZ_SEH_EXCEPT (EXCEPTION_EXECUTE_HANDLER) {
}
::ReleaseSRWLockExclusive(&mLock);
}
static NativeNtBlockSet gBlockSet;
extern "C" void MOZ_EXPORT
NativeNtBlockSet_Write(HANDLE aHandle)
{
gBlockSet.Write(aHandle);
}
static bool
CheckBlockInfo(const DllBlockInfo* aInfo, void* aBaseAddress, uint64_t& aVersion)
{
aVersion = ALL_VERSIONS;
if (aInfo->flags & (DllBlockInfo::BLOCK_WIN8PLUS_ONLY | DllBlockInfo::BLOCK_WIN8_ONLY)) {
RTL_OSVERSIONINFOW osv;
NTSTATUS ntStatus = ::RtlGetVersion(&osv);
if (!NT_SUCCESS(ntStatus)) {
// huh?
return false;
}
if (osv.dwMajorVersion < 8) {
return true;
}
if ((aInfo->flags & DllBlockInfo::BLOCK_WIN8_ONLY) && (osv.dwMajorVersion > 8 ||
(osv.dwMajorVersion == 8 && osv.dwMinorVersion > 0))) {
return true;
}
}
// We're not bootstrapping child processes at this time, so this case is
// always true.
if (aInfo->flags & DllBlockInfo::CHILD_PROCESSES_ONLY) {
return true;
}
if (aInfo->maxVersion == ALL_VERSIONS) {
return false;
}
mozilla::nt::PEHeaders headers(aBaseAddress);
if (!headers) {
return false;
}
if (aInfo->flags & DllBlockInfo::USE_TIMESTAMP) {
DWORD timestamp;
if (!headers.GetTimeStamp(timestamp)) {
return false;
}
return timestamp > aInfo->maxVersion;
}
// Else we try to get the file version information. Note that we don't have
// access to GetFileVersionInfo* APIs.
if (!headers.GetVersionInfo(aVersion)) {
return false;
}
return aVersion > aInfo->maxVersion;
}
static bool
IsDllAllowed(const UNICODE_STRING& aLeafName, void* aBaseAddress)
{
if (mozilla::nt::Contains12DigitHexString(aLeafName) ||
mozilla::nt::IsFileNameAtLeast16HexDigits(aLeafName)) {
return false;
}
UNICODE_STRING testStr;
DECLARE_POINTER_TO_FIRST_DLL_BLOCKLIST_ENTRY(info);
while (info->name) {
::RtlInitUnicodeString(&testStr, info->name);
if (::RtlEqualUnicodeString(&aLeafName, &testStr, TRUE)) {
break;
}
++info;
}
uint64_t version;
if (info->name && !CheckBlockInfo(info, aBaseAddress, version)) {
gBlockSet.Add(info->name, version);
return false;
}
return true;
}
typedef decltype(&NtMapViewOfSection) NtMapViewOfSection_func;
static NtMapViewOfSection_func stub_NtMapViewOfSection;
static NTSTATUS NTAPI
patched_NtMapViewOfSection(HANDLE aSection, HANDLE aProcess, PVOID* aBaseAddress,
ULONG_PTR aZeroBits, SIZE_T aCommitSize,
PLARGE_INTEGER aSectionOffset, PSIZE_T aViewSize,
SECTION_INHERIT aInheritDisposition,
ULONG aAllocationType, ULONG aProtectionFlags)
{
// We always map first, then we check for additional info after.
NTSTATUS stubStatus = stub_NtMapViewOfSection(aSection, aProcess, aBaseAddress,
aZeroBits, aCommitSize,
aSectionOffset, aViewSize,
aInheritDisposition,
aAllocationType, aProtectionFlags);
if (!NT_SUCCESS(stubStatus)) {
return stubStatus;
}
if (aProcess != kCurrentProcess) {
// We're only interested in mapping for the current process.
return stubStatus;
}
// Do a query to see if the memory is MEM_IMAGE. If not, continue
MEMORY_BASIC_INFORMATION mbi;
NTSTATUS ntStatus = ::NtQueryVirtualMemory(aProcess, *aBaseAddress,
MemoryBasicInformation, &mbi,
sizeof(mbi), nullptr);
if (!NT_SUCCESS(ntStatus)) {
::NtUnmapViewOfSection(aProcess, *aBaseAddress);
return STATUS_ACCESS_DENIED;
}
// We don't care about mappings that aren't MEM_IMAGE
if (!(mbi.Type & MEM_IMAGE)) {
return stubStatus;
}
// Get the section name
mozilla::nt::MemorySectionNameBuf buf;
ntStatus = ::NtQueryVirtualMemory(aProcess, *aBaseAddress, MemorySectionName,
&buf, sizeof(buf), nullptr);
if (!NT_SUCCESS(ntStatus)) {
::NtUnmapViewOfSection(aProcess, *aBaseAddress);
return STATUS_ACCESS_DENIED;
}
// Find the leaf name
UNICODE_STRING leaf;
mozilla::nt::GetLeafName(&leaf, &buf.mSectionFileName);
// Check blocklist
if (IsDllAllowed(leaf, *aBaseAddress)) {
return stubStatus;
}
::NtUnmapViewOfSection(aProcess, *aBaseAddress);
return STATUS_ACCESS_DENIED;
}
namespace mozilla {
class MOZ_RAII AutoVirtualProtect final
{
public:
AutoVirtualProtect(void* aAddress, size_t aLength, DWORD aProtFlags,
HANDLE aTargetProcess = nullptr)
: mAddress(aAddress)
, mLength(aLength)
, mTargetProcess(aTargetProcess)
, mPrevProt(0)
{
::VirtualProtectEx(aTargetProcess, aAddress, aLength, aProtFlags,
&mPrevProt);
}
~AutoVirtualProtect()
{
if (!mPrevProt) {
return;
}
::VirtualProtectEx(mTargetProcess, mAddress, mLength, mPrevProt,
&mPrevProt);
}
explicit operator bool() const
{
return !!mPrevProt;
}
AutoVirtualProtect(const AutoVirtualProtect&) = delete;
AutoVirtualProtect(AutoVirtualProtect&&) = delete;
AutoVirtualProtect& operator=(const AutoVirtualProtect&) = delete;
AutoVirtualProtect& operator=(AutoVirtualProtect&&) = delete;
private:
void* mAddress;
size_t mLength;
HANDLE mTargetProcess;
DWORD mPrevProt;
};
bool
InitializeDllBlocklistOOP(HANDLE aChildProcess)
{
mozilla::CrossProcessDllInterceptor intcpt(aChildProcess);
intcpt.Init(L"ntdll.dll");
bool ok = intcpt.AddDetour("NtMapViewOfSection",
reinterpret_cast<intptr_t>(&patched_NtMapViewOfSection),
(void**) &stub_NtMapViewOfSection);
if (!ok) {
return false;
}
// Set the child process's copy of stub_NtMapViewOfSection
SIZE_T bytesWritten;
ok = !!::WriteProcessMemory(aChildProcess, &stub_NtMapViewOfSection,
&stub_NtMapViewOfSection,
sizeof(stub_NtMapViewOfSection), &bytesWritten);
if (!ok) {
return false;
}
// Because aChildProcess has just been created in a suspended state, its
// dynamic linker has not yet been initialized, thus its executable has
// not yet been linked with ntdll.dll. If the blocklist hook intercepts a
// library load prior to the link, the hook will be unable to invoke any
// ntdll.dll functions.
//
// We know that the executable for our *current* process's binary is already
// linked into ntdll, so we obtain the IAT from our own executable and graft
// it onto the child process's IAT, thus enabling the child process's hook to
// safely make its ntdll calls.
mozilla::nt::PEHeaders ourExeImage(::GetModuleHandleW(nullptr));
if (!ourExeImage) {
return false;
}
PIMAGE_IMPORT_DESCRIPTOR impDesc = ourExeImage.GetIATForModule("ntdll.dll");
if (!impDesc) {
return false;
}
// This is the pointer we need to write
auto firstIatThunk = ourExeImage.template
RVAToPtr<PIMAGE_THUNK_DATA>(impDesc->FirstThunk);
if (!firstIatThunk) {
return false;
}
// Find the length by iterating through the table until we find a null entry
PIMAGE_THUNK_DATA curIatThunk = firstIatThunk;
while (mozilla::nt::PEHeaders::IsValid(curIatThunk)) {
++curIatThunk;
}
ptrdiff_t iatLength = (curIatThunk - firstIatThunk) * sizeof(IMAGE_THUNK_DATA);
{ // Scope for prot
AutoVirtualProtect prot(firstIatThunk, iatLength, PAGE_READWRITE,
aChildProcess);
if (!prot) {
return false;
}
ok = !!::WriteProcessMemory(aChildProcess, firstIatThunk, firstIatThunk,
iatLength, &bytesWritten);
if (!ok) {
return false;
}
}
// Tell the mozglue blocklist that we have bootstrapped
uint32_t newFlags = eDllBlocklistInitFlagWasBootstrapped;
ok = !!::WriteProcessMemory(aChildProcess, &gBlocklistInitFlags, &newFlags,
sizeof(newFlags), &bytesWritten);
return ok;
}
} // namespace mozilla

Просмотреть файл

@ -0,0 +1,18 @@
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
/* 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 https://mozilla.org/MPL/2.0/. */
#ifndef mozilla_DllBlocklistWin_h
#define mozilla_DllBlocklistWin_h
#include <windows.h>
namespace mozilla {
bool InitializeDllBlocklistOOP(HANDLE aChildProcess);
} // namespace mozilla
#endif // mozilla_DllBlocklistWin_h

Просмотреть файл

@ -9,6 +9,7 @@ Library('winlauncher')
FORCE_STATIC_LIB = True
UNIFIED_SOURCES += [
'DllBlocklistWin.cpp',
'LauncherProcessWin.cpp',
'LaunchUnelevated.cpp',
]