mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-11-28 23:31:56 +00:00
Bug 1684532 - Detect injected dependent modules in NtMapViewOfSection. r=mhowell
This patch is to improve the way to detect an injected dependent module for automatic DLL blocking (bug 1659438). In the previous version, we created a list of dependent modules in the launcher process and shared it with other processes via the shared section. However, it was not compatible with third-party applications who tamper the Import Table and revert it in the injected module's DllMain (bug 1682834) because we parsed the Import Table in the launcher process after it was reverted. With this patch, we check the Import Table in `patched_NtMapViewOfSection`, so we can see tampering before it's reverted. More specifically, we create a list of dependent modules in the browser process as below. 1. The launcher process creates a section object and initializes the kernel32.dll's functions in it. 2. The launcher process transfers a writable handle of the shared section to the browser process. 3. In the browser process, if an injected dependent module is being mapped by `NtMapViewOfSection`, we add its NT path to the shared section and block it with `REDIRECT_TO_NOOP_ENTRYPOINT`. 4. The `main` function of the browser process converts the writable handle of the shared section into a readonly handle. 5. The browser process transfers a readonly handle of the shared section to a sandbox process. Since automatic DLL blocking may still cause a compat issue like bug 1682304, we activate it only in Nightly for now. Differential Revision: https://phabricator.services.mozilla.com/D101460
This commit is contained in:
parent
75d7922fc7
commit
7545ed9378
@ -25,6 +25,7 @@
|
||||
#include "nsCOMPtr.h"
|
||||
|
||||
#ifdef XP_WIN
|
||||
# include "freestanding/SharedSection.h"
|
||||
# include "LauncherProcessWin.h"
|
||||
# include "mozilla/WindowsDllBlocklist.h"
|
||||
|
||||
@ -320,6 +321,12 @@ int main(int argc, char* argv[], char* envp[]) {
|
||||
#endif
|
||||
|
||||
#if defined(XP_WIN)
|
||||
// Once the browser process hits the main function, we no longer need
|
||||
// a writable section handle because all dependent modules have been
|
||||
// loaded.
|
||||
mozilla::freestanding::gSharedSection.ConvertToReadOnly();
|
||||
::RtlRunOnceInitialize(&mozilla::freestanding::gK32ExportsResolveOnce);
|
||||
|
||||
mozilla::CreateAndStorePreXULSkeletonUI(GetModuleHandle(nullptr), argc, argv);
|
||||
#endif
|
||||
|
||||
|
@ -44,12 +44,6 @@ LauncherVoidResultWithLineInfo InitializeDllBlocklistOOPFromLauncher(
|
||||
static LauncherVoidResultWithLineInfo InitializeDllBlocklistOOPInternal(
|
||||
const wchar_t* aFullImagePath, nt::CrossExecTransferManager& aTransferMgr,
|
||||
const IMAGE_THUNK_DATA* aCachedNtdllThunk) {
|
||||
LauncherVoidResult transferResult =
|
||||
freestanding::gSharedSection.TransferHandle(aTransferMgr);
|
||||
if (transferResult.isErr()) {
|
||||
return transferResult.propagateErr();
|
||||
}
|
||||
|
||||
CrossProcessDllInterceptor intcpt(aTransferMgr.RemoteProcess());
|
||||
intcpt.Init(L"ntdll.dll");
|
||||
|
||||
@ -169,6 +163,14 @@ LauncherVoidResultWithLineInfo InitializeDllBlocklistOOP(
|
||||
return RestoreImportDirectory(aFullImagePath, transferMgr);
|
||||
}
|
||||
|
||||
// Transfer a readonly handle to the child processes because all information
|
||||
// are already written to the section by the launcher and main process.
|
||||
LauncherVoidResult transferResult =
|
||||
freestanding::gSharedSection.TransferHandle(transferMgr, GENERIC_READ);
|
||||
if (transferResult.isErr()) {
|
||||
return transferResult.propagateErr();
|
||||
}
|
||||
|
||||
return InitializeDllBlocklistOOPInternal(aFullImagePath, transferMgr,
|
||||
aCachedNtdllThunk);
|
||||
}
|
||||
@ -189,6 +191,15 @@ LauncherVoidResultWithLineInfo InitializeDllBlocklistOOPFromLauncher(
|
||||
return result.propagateErr();
|
||||
}
|
||||
|
||||
// Transfer a writable handle to the main process because it needs to append
|
||||
// dependent module paths to the section.
|
||||
LauncherVoidResult transferResult =
|
||||
freestanding::gSharedSection.TransferHandle(transferMgr,
|
||||
GENERIC_READ | GENERIC_WRITE);
|
||||
if (transferResult.isErr()) {
|
||||
return transferResult.propagateErr();
|
||||
}
|
||||
|
||||
auto clearInstance = MakeScopeExit([]() {
|
||||
// After transfer, the launcher process does not need the object anymore.
|
||||
freestanding::gSharedSection.Reset(nullptr);
|
||||
|
@ -242,6 +242,48 @@ struct DllBlockInfoComparator {
|
||||
|
||||
static BOOL WINAPI NoOp_DllMain(HINSTANCE, DWORD, LPVOID) { return TRUE; }
|
||||
|
||||
// This helper function checks whether a given module is included
|
||||
// in the executable's Import Table. Because an injected module's
|
||||
// DllMain may revert the Import Table to the original state, we parse
|
||||
// the Import Table every time a module is loaded without creating a cache.
|
||||
static bool IsDependentModule(
|
||||
const UNICODE_STRING& aModuleLeafName,
|
||||
mozilla::freestanding::Kernel32ExportsSolver& aK32Exports) {
|
||||
// We enable automatic DLL blocking only in Nightly for now because it caused
|
||||
// a compat issue (bug 1682304).
|
||||
#if defined(NIGHTLY_BUILD)
|
||||
aK32Exports.Resolve(mozilla::freestanding::gK32ExportsResolveOnce);
|
||||
if (!aK32Exports.IsResolved()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
mozilla::nt::PEHeaders exeHeaders(aK32Exports.mGetModuleHandleW(nullptr));
|
||||
if (!exeHeaders || !exeHeaders.IsImportDirectoryTampered()) {
|
||||
// If no tampering is detected, no need to enumerate the Import Table.
|
||||
return false;
|
||||
}
|
||||
|
||||
bool isDependent = false;
|
||||
exeHeaders.EnumImportChunks(
|
||||
[&isDependent, &aModuleLeafName, &exeHeaders](const char* aDepModule) {
|
||||
// If |aDepModule| is within the PE image, it's not an injected module
|
||||
// but a legitimate dependent module.
|
||||
if (isDependent || exeHeaders.IsWithinImage(aDepModule)) {
|
||||
return;
|
||||
}
|
||||
|
||||
UNICODE_STRING depModuleLeafName;
|
||||
mozilla::nt::AllocatedUnicodeString depModuleName(aDepModule);
|
||||
mozilla::nt::GetLeafName(&depModuleLeafName, depModuleName);
|
||||
isDependent = (::RtlCompareUnicodeString(
|
||||
&aModuleLeafName, &depModuleLeafName, TRUE) == 0);
|
||||
});
|
||||
return isDependent;
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
// Allowing a module to be loaded but detour the entrypoint to NoOp_DllMain
|
||||
// so that the module has no chance to interact with our code. We need this
|
||||
// technique to safely block a module injected by IAT tampering because
|
||||
@ -249,8 +291,7 @@ static BOOL WINAPI NoOp_DllMain(HINSTANCE, DWORD, LPVOID) { return TRUE; }
|
||||
static bool RedirectToNoOpEntryPoint(
|
||||
const mozilla::nt::PEHeaders& aModule,
|
||||
mozilla::freestanding::Kernel32ExportsSolver& aK32Exports) {
|
||||
static RTL_RUN_ONCE sRunOnce = RTL_RUN_ONCE_INIT;
|
||||
aK32Exports.Resolve(sRunOnce);
|
||||
aK32Exports.Resolve(mozilla::freestanding::gK32ExportsResolveOnce);
|
||||
if (!aK32Exports.IsResolved()) {
|
||||
return false;
|
||||
}
|
||||
@ -305,6 +346,7 @@ namespace mozilla {
|
||||
namespace freestanding {
|
||||
|
||||
CrossProcessDllInterceptor::FuncHookType<LdrLoadDllPtr> stub_LdrLoadDll;
|
||||
RTL_RUN_ONCE gK32ExportsResolveOnce = RTL_RUN_ONCE_INIT;
|
||||
|
||||
NTSTATUS NTAPI patched_LdrLoadDll(PWCHAR aDllPath, PULONG aFlags,
|
||||
PUNICODE_STRING aDllName,
|
||||
@ -360,31 +402,27 @@ NTSTATUS NTAPI patched_NtMapViewOfSection(
|
||||
return STATUS_ACCESS_DENIED;
|
||||
}
|
||||
|
||||
bool isDependent = false;
|
||||
auto resultView = mozilla::freestanding::gSharedSection.GetView();
|
||||
if (resultView.isOk()) {
|
||||
uint32_t arrayLen = resultView.inspect()->mModulePathArrayLength;
|
||||
const uint32_t* array = resultView.inspect()->mModulePathArray;
|
||||
size_t match;
|
||||
isDependent = mozilla::BinarySearchIf(
|
||||
array, 0, arrayLen,
|
||||
[§ionFileName, arrayBase = reinterpret_cast<const uint8_t*>(array)](
|
||||
const uint32_t& aOffset) {
|
||||
UNICODE_STRING str;
|
||||
::RtlInitUnicodeString(
|
||||
&str, reinterpret_cast<const wchar_t*>(arrayBase + aOffset));
|
||||
return static_cast<int>(
|
||||
::RtlCompareUnicodeString(sectionFileName, &str, TRUE));
|
||||
},
|
||||
&match);
|
||||
}
|
||||
|
||||
// Find the leaf name
|
||||
UNICODE_STRING leafOnStack;
|
||||
nt::GetLeafName(&leafOnStack, sectionFileName);
|
||||
|
||||
bool isDependent = false;
|
||||
auto resultView = freestanding::gSharedSection.GetView();
|
||||
// Small optimization: Since loading a dependent module does not involve
|
||||
// LdrLoadDll, we know isDependent is false if we hold a top frame.
|
||||
if (resultView.isOk() && !ModuleLoadFrame::ExistsTopFrame()) {
|
||||
isDependent =
|
||||
IsDependentModule(leafOnStack, resultView.inspect()->mK32Exports);
|
||||
}
|
||||
|
||||
BlockAction blockAction;
|
||||
if (isDependent) {
|
||||
// Add an NT dv\path to the shared section so that a sandbox process can
|
||||
// use it to bypass CIG. In a sandbox process, this addition fails
|
||||
// because we cannot map the section to a writable region, but it's
|
||||
// ignorable because the paths have been added by the browser process.
|
||||
Unused << freestanding::gSharedSection.AddDepenentModule(sectionFileName);
|
||||
|
||||
// For a dependent module, try redirection instead of blocking it.
|
||||
// If we fail, we reluctantly allow the module for free.
|
||||
mozilla::nt::PEHeaders headers(*aBaseAddress);
|
||||
|
@ -85,6 +85,9 @@ void ModuleLoadFrame::NotifySectionMap(
|
||||
topFrame->OnSectionMap(std::move(aSectionName), aMapBaseAddr, aMapNtStatus);
|
||||
}
|
||||
|
||||
/* static */
|
||||
bool ModuleLoadFrame::ExistsTopFrame() { return !!sTopFrame.get(); }
|
||||
|
||||
void ModuleLoadFrame::OnSectionMap(nt::AllocatedUnicodeString&& aSectionName,
|
||||
const void* aMapBaseAddr,
|
||||
NTSTATUS aMapNtStatus) {
|
||||
|
@ -37,6 +37,7 @@ class MOZ_RAII ModuleLoadFrame final {
|
||||
*/
|
||||
static void NotifySectionMap(nt::AllocatedUnicodeString&& aSectionName,
|
||||
const void* aMapBaseAddr, NTSTATUS aMapNtStatus);
|
||||
static bool ExistsTopFrame();
|
||||
|
||||
/**
|
||||
* Called by the LdrLoadDll hook to indicate the status of the load and for
|
||||
|
@ -8,6 +8,43 @@
|
||||
|
||||
#include "CheckForCaller.h"
|
||||
|
||||
namespace {
|
||||
|
||||
bool AddString(void* aBuffer, size_t aBufferSize, const UNICODE_STRING& aStr) {
|
||||
size_t offset = 0;
|
||||
while (offset < aBufferSize) {
|
||||
UNICODE_STRING uniStr;
|
||||
::RtlInitUnicodeString(&uniStr,
|
||||
reinterpret_cast<wchar_t*>(
|
||||
reinterpret_cast<uintptr_t>(aBuffer) + offset));
|
||||
|
||||
if (uniStr.Length == 0) {
|
||||
// Reached to the array's last item.
|
||||
break;
|
||||
}
|
||||
|
||||
if (::RtlCompareUnicodeString(&uniStr, &aStr, TRUE) == 0) {
|
||||
// Already included in the array.
|
||||
return true;
|
||||
}
|
||||
|
||||
// Go to the next string.
|
||||
offset += uniStr.MaximumLength;
|
||||
}
|
||||
|
||||
// Ensure enough space including the last empty string at the end.
|
||||
if (offset + aStr.MaximumLength + sizeof(wchar_t) > aBufferSize) {
|
||||
return false;
|
||||
}
|
||||
|
||||
auto newStr = reinterpret_cast<uint8_t*>(aBuffer) + offset;
|
||||
memcpy(newStr, aStr.Buffer, aStr.Length);
|
||||
memset(newStr + aStr.Length, 0, sizeof(wchar_t));
|
||||
return true;
|
||||
}
|
||||
|
||||
} // anonymous namespace
|
||||
|
||||
namespace mozilla {
|
||||
namespace freestanding {
|
||||
|
||||
@ -56,6 +93,7 @@ void Kernel32ExportsSolver::Init() {
|
||||
|
||||
// Please make sure these functions are not forwarded to another DLL.
|
||||
INIT_FUNCTION(k32Exports, FlushInstructionCache);
|
||||
INIT_FUNCTION(k32Exports, GetModuleHandleW);
|
||||
INIT_FUNCTION(k32Exports, GetSystemInfo);
|
||||
INIT_FUNCTION(k32Exports, VirtualProtect);
|
||||
|
||||
@ -81,6 +119,7 @@ void Kernel32ExportsSolver::ResolveInternal() {
|
||||
nt::PEHeaders::HModuleToBaseAddr<uintptr_t>(k32Module.unwrap());
|
||||
|
||||
RESOLVE_FUNCTION(k32Base, FlushInstructionCache);
|
||||
RESOLVE_FUNCTION(k32Base, GetModuleHandleW);
|
||||
RESOLVE_FUNCTION(k32Base, GetSystemInfo);
|
||||
RESOLVE_FUNCTION(k32Base, VirtualProtect);
|
||||
|
||||
@ -107,74 +146,37 @@ void SharedSection::Reset(HANDLE aNewSecionObject) {
|
||||
sWriteCopyView = nullptr;
|
||||
}
|
||||
|
||||
if (sSectionHandle) {
|
||||
::CloseHandle(sSectionHandle);
|
||||
if (sSectionHandle != aNewSecionObject) {
|
||||
if (sSectionHandle) {
|
||||
::CloseHandle(sSectionHandle);
|
||||
}
|
||||
sSectionHandle = aNewSecionObject;
|
||||
}
|
||||
sSectionHandle = aNewSecionObject;
|
||||
}
|
||||
|
||||
static void PackOffsetVector(const Vector<nt::MemorySectionNameOnHeap>& aSource,
|
||||
SharedSection::Layout& aDestination,
|
||||
size_t aStringBufferOffset) {
|
||||
aDestination.mModulePathArrayLength = aSource.length();
|
||||
|
||||
uint32_t* curItem = aDestination.mModulePathArray;
|
||||
uint8_t* const arrayBase = reinterpret_cast<uint8_t*>(curItem);
|
||||
for (const auto& it : aSource) {
|
||||
// Fill the current offset value
|
||||
*(curItem++) = aStringBufferOffset;
|
||||
|
||||
// Fill a string and a null character
|
||||
uint32_t lenInBytes = it.AsUnicodeString()->Length;
|
||||
memcpy(arrayBase + aStringBufferOffset, it.AsUnicodeString()->Buffer,
|
||||
lenInBytes);
|
||||
memset(arrayBase + aStringBufferOffset + lenInBytes, 0, sizeof(WCHAR));
|
||||
|
||||
// Advance the offset
|
||||
aStringBufferOffset += (lenInBytes + sizeof(WCHAR));
|
||||
void SharedSection::ConvertToReadOnly() {
|
||||
if (!sSectionHandle) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Sort the offset array so that we can binary-search it
|
||||
std::sort(aDestination.mModulePathArray,
|
||||
aDestination.mModulePathArray + aSource.length(),
|
||||
[arrayBase](uint32_t a, uint32_t b) {
|
||||
auto s1 = reinterpret_cast<const wchar_t*>(arrayBase + a);
|
||||
auto s2 = reinterpret_cast<const wchar_t*>(arrayBase + b);
|
||||
return wcsicmp(s1, s2) < 0;
|
||||
});
|
||||
HANDLE readonlyHandle;
|
||||
if (!::DuplicateHandle(nt::kCurrentProcess, sSectionHandle,
|
||||
nt::kCurrentProcess, &readonlyHandle, GENERIC_READ,
|
||||
FALSE, 0)) {
|
||||
return;
|
||||
}
|
||||
|
||||
Reset(readonlyHandle);
|
||||
}
|
||||
|
||||
LauncherVoidResult SharedSection::Init(const nt::PEHeaders& aPEHeaders) {
|
||||
size_t stringBufferSize = 0;
|
||||
Vector<nt::MemorySectionNameOnHeap> modules;
|
||||
static_assert(
|
||||
kSharedViewSize >= sizeof(Layout),
|
||||
"kSharedViewSize is too small to represent SharedSection::Layout.");
|
||||
|
||||
// We enable automatic DLL blocking only in Nightly for now because it caused
|
||||
// a compat issue (bug 1682304).
|
||||
#if defined(NIGHTLY_BUILD)
|
||||
aPEHeaders.EnumImportChunks(
|
||||
[&stringBufferSize, &modules, &aPEHeaders](const char* aModule) {
|
||||
# if defined(DONT_SKIP_DEFAULT_DEPENDENT_MODULES)
|
||||
Unused << aPEHeaders;
|
||||
# else
|
||||
if (aPEHeaders.IsWithinImage(aModule)) {
|
||||
return;
|
||||
}
|
||||
# endif
|
||||
HMODULE module = ::GetModuleHandleA(aModule);
|
||||
nt::MemorySectionNameOnHeap ntPath =
|
||||
nt::MemorySectionNameOnHeap::GetBackingFilePath(nt::kCurrentProcess,
|
||||
module);
|
||||
stringBufferSize += (ntPath.AsUnicodeString()->Length + sizeof(WCHAR));
|
||||
Unused << modules.emplaceBack(std::move(ntPath));
|
||||
});
|
||||
#endif
|
||||
|
||||
size_t arraySize = modules.length() * sizeof(Layout::mModulePathArray[0]);
|
||||
size_t totalSize =
|
||||
sizeof(Kernel32ExportsSolver) + arraySize + stringBufferSize;
|
||||
|
||||
HANDLE section = ::CreateFileMappingW(INVALID_HANDLE_VALUE, nullptr,
|
||||
PAGE_READWRITE, 0, totalSize, nullptr);
|
||||
HANDLE section =
|
||||
::CreateFileMappingW(INVALID_HANDLE_VALUE, nullptr, PAGE_READWRITE, 0,
|
||||
kSharedViewSize, nullptr);
|
||||
if (!section) {
|
||||
return LAUNCHER_ERROR_FROM_LAST();
|
||||
}
|
||||
@ -191,7 +193,21 @@ LauncherVoidResult SharedSection::Init(const nt::PEHeaders& aPEHeaders) {
|
||||
|
||||
SharedSection::Layout* view = writableView.as<SharedSection::Layout>();
|
||||
view->mK32Exports.Init();
|
||||
PackOffsetVector(modules, *view, arraySize);
|
||||
|
||||
return Ok();
|
||||
}
|
||||
|
||||
LauncherVoidResult SharedSection::AddDepenentModule(PCUNICODE_STRING aNtPath) {
|
||||
nt::AutoMappedView writableView(sSectionHandle, PAGE_READWRITE);
|
||||
if (!writableView) {
|
||||
return LAUNCHER_ERROR_FROM_WIN32(::RtlGetLastWin32Error());
|
||||
}
|
||||
|
||||
SharedSection::Layout* view = writableView.as<SharedSection::Layout>();
|
||||
if (!AddString(view->mModulePathArray,
|
||||
kSharedViewSize - sizeof(Kernel32ExportsSolver), *aNtPath)) {
|
||||
return LAUNCHER_ERROR_FROM_WIN32(ERROR_INSUFFICIENT_BUFFER);
|
||||
}
|
||||
|
||||
return Ok();
|
||||
}
|
||||
@ -200,7 +216,7 @@ LauncherResult<SharedSection::Layout*> SharedSection::GetView() {
|
||||
if (!sWriteCopyView) {
|
||||
nt::AutoMappedView view(sSectionHandle, PAGE_WRITECOPY);
|
||||
if (!view) {
|
||||
return LAUNCHER_ERROR_FROM_LAST();
|
||||
return LAUNCHER_ERROR_FROM_WIN32(::RtlGetLastWin32Error());
|
||||
}
|
||||
sWriteCopyView = view.release();
|
||||
}
|
||||
@ -208,11 +224,12 @@ LauncherResult<SharedSection::Layout*> SharedSection::GetView() {
|
||||
}
|
||||
|
||||
LauncherVoidResult SharedSection::TransferHandle(
|
||||
nt::CrossExecTransferManager& aTransferMgr, HANDLE* aDestinationAddress) {
|
||||
nt::CrossExecTransferManager& aTransferMgr, DWORD aDesiredAccess,
|
||||
HANDLE* aDestinationAddress) {
|
||||
HANDLE remoteHandle;
|
||||
if (!::DuplicateHandle(nt::kCurrentProcess, sSectionHandle,
|
||||
aTransferMgr.RemoteProcess(), &remoteHandle,
|
||||
GENERIC_READ, FALSE, 0)) {
|
||||
aDesiredAccess, FALSE, 0)) {
|
||||
return LAUNCHER_ERROR_FROM_LAST();
|
||||
}
|
||||
|
||||
@ -220,33 +237,29 @@ LauncherVoidResult SharedSection::TransferHandle(
|
||||
sizeof(remoteHandle));
|
||||
}
|
||||
|
||||
extern "C" MOZ_EXPORT uint32_t GetDependentModulePaths(uint32_t** aOutArray) {
|
||||
if (aOutArray) {
|
||||
*aOutArray = nullptr;
|
||||
}
|
||||
|
||||
// This exported function is invoked by SandboxBroker of xul.dll
|
||||
// in order to add dependent modules to the CIG exception list.
|
||||
extern "C" MOZ_EXPORT const wchar_t* GetDependentModulePaths() {
|
||||
// We enable pre-spawn CIG only in Nightly for now because it caused
|
||||
// a compat issue (bug 1682304).
|
||||
#if defined(NIGHTLY_BUILD)
|
||||
const bool isCallerXul = CheckForAddress(RETURN_ADDRESS(), L"xul.dll");
|
||||
MOZ_ASSERT(isCallerXul);
|
||||
if (!isCallerXul) {
|
||||
return 0;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Remap a write-copy section to take the latest update in mModulePathArray.
|
||||
gSharedSection.Reset();
|
||||
|
||||
LauncherResult<SharedSection::Layout*> resultView = gSharedSection.GetView();
|
||||
if (resultView.isErr()) {
|
||||
return 0;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if (aOutArray) {
|
||||
// Return a valid address even if the array length is zero
|
||||
// to distinguish it from an error case.
|
||||
*aOutArray = resultView.inspect()->mModulePathArray;
|
||||
}
|
||||
return resultView.inspect()->mModulePathArrayLength;
|
||||
return resultView.inspect()->mModulePathArray;
|
||||
#else
|
||||
return 0;
|
||||
return nullptr;
|
||||
#endif
|
||||
}
|
||||
|
||||
|
@ -45,31 +45,27 @@ class MOZ_TRIVIAL_CTOR_DTOR Kernel32ExportsSolver final
|
||||
};
|
||||
|
||||
// This class manages a section which is created in the launcher process and
|
||||
// mapped in the browser process and the sandboxed processes as a copy-on-write
|
||||
// region. The section's layout is represented as SharedSection::Layout.
|
||||
// mapped in the browser process and the sandboxed processes. The section's
|
||||
// layout is represented as SharedSection::Layout.
|
||||
//
|
||||
// (1) Kernel32's functions required for MMPolicyInProcessEarlyStage
|
||||
// Formatted as Kernel32ExportsSolver.
|
||||
//
|
||||
// (2) Array of NT paths of the executable's dependent modules
|
||||
// Array item is an offset to a string buffer of a module's
|
||||
// NT path, relative to the beginning of the array.
|
||||
// Array is case-insensitive sorted.
|
||||
// Formatted as a null-delimited wide-character string set ending with
|
||||
// an empty string.
|
||||
//
|
||||
// +--------------------------------------------------------------+
|
||||
// | (1) | FlushInstructionCache |
|
||||
// | | GetModuleHandleW |
|
||||
// | | GetSystemInfo |
|
||||
// | | VirtualProtect |
|
||||
// | | State [Uninitialized|Initialized|Resolved] |
|
||||
// +--------------------------------------------------------------+
|
||||
// | (2) | The length of the offset array |
|
||||
// | | Offset1 to String1 |
|
||||
// | | * Offset is relative to the beginning of the array |
|
||||
// | | * String is an NT path in wchar_t |
|
||||
// | | Offset2 to String2 |
|
||||
// | (2) | L"NT path 1" |
|
||||
// | | L"NT path 2" |
|
||||
// | | ... |
|
||||
// | | OffsetN to StringN |
|
||||
// | | String1, 2, ..., N (null delimited strings) |
|
||||
// | | L"" |
|
||||
// +--------------------------------------------------------------+
|
||||
class MOZ_TRIVIAL_CTOR_DTOR SharedSection final {
|
||||
// As we define a global variable of this class and use it in our blocklist
|
||||
@ -81,25 +77,40 @@ class MOZ_TRIVIAL_CTOR_DTOR SharedSection final {
|
||||
static HANDLE sSectionHandle;
|
||||
static void* sWriteCopyView;
|
||||
|
||||
static constexpr size_t kSharedViewSize = 0x1000;
|
||||
|
||||
public:
|
||||
struct Layout final {
|
||||
Kernel32ExportsSolver mK32Exports;
|
||||
uint32_t mModulePathArrayLength;
|
||||
uint32_t mModulePathArray[1];
|
||||
wchar_t mModulePathArray[1];
|
||||
|
||||
Layout() = delete; // disallow instantiation
|
||||
};
|
||||
|
||||
static void Reset(HANDLE aNewSecionObject);
|
||||
// Replace |sSectionHandle| with a given handle.
|
||||
static void Reset(HANDLE aNewSecionObject = sSectionHandle);
|
||||
|
||||
// Replace |sSectionHandle| with a new readonly handle.
|
||||
static void ConvertToReadOnly();
|
||||
|
||||
// Create a new writable section and initialize the Kernel32ExportsSolver
|
||||
// part.
|
||||
static LauncherVoidResult Init(const nt::PEHeaders& aPEHeaders);
|
||||
|
||||
// Append a new string to the |sSectionHandle|
|
||||
static LauncherVoidResult AddDepenentModule(PCUNICODE_STRING aNtPath);
|
||||
|
||||
// Map |sSectionHandle| to a copy-on-write page and return its address.
|
||||
static LauncherResult<Layout*> GetView();
|
||||
|
||||
// Transfer |sSectionHandle| to a process associated with |aTransferMgr|.
|
||||
static LauncherVoidResult TransferHandle(
|
||||
nt::CrossExecTransferManager& aTransferMgr,
|
||||
nt::CrossExecTransferManager& aTransferMgr, DWORD aDesiredAccess,
|
||||
HANDLE* aDestinationAddress = &sSectionHandle);
|
||||
};
|
||||
|
||||
extern SharedSection gSharedSection;
|
||||
extern RTL_RUN_ONCE gK32ExportsResolveOnce;
|
||||
|
||||
} // namespace freestanding
|
||||
} // namespace mozilla
|
||||
|
@ -5,28 +5,20 @@
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
#define MOZ_USE_LAUNCHER_ERROR
|
||||
#define DONT_SKIP_DEFAULT_DEPENDENT_MODULES
|
||||
|
||||
#include "freestanding/SharedSection.cpp"
|
||||
#include "mozilla/CmdLineAndEnvUtils.h"
|
||||
#include "mozilla/NativeNt.h"
|
||||
|
||||
const wchar_t kChildArg[] = L"--child";
|
||||
|
||||
#if !defined(__MINGW32__)
|
||||
// MinGW includes an old winternl.h that defines FILE_BASIC_INFORMATION.
|
||||
typedef struct FILE_BASIC_INFORMATION {
|
||||
LARGE_INTEGER CreationTime;
|
||||
LARGE_INTEGER LastAccessTime;
|
||||
LARGE_INTEGER LastWriteTime;
|
||||
LARGE_INTEGER ChangeTime;
|
||||
ULONG FileAttributes;
|
||||
} FILE_BASIC_INFORMATION, *PFILE_BASIC_INFORMATION;
|
||||
#endif // !defined(__MINGW32__)
|
||||
|
||||
extern "C" NTSTATUS NTAPI
|
||||
NtQueryAttributesFile(POBJECT_ATTRIBUTES aObjectAttributes,
|
||||
PFILE_BASIC_INFORMATION aFileInformation);
|
||||
const char kTestStrings[][6] = {
|
||||
"a b c", "A B C", "a b c", "A B C", "X Y Z",
|
||||
};
|
||||
const wchar_t kTestStringsMerged[] =
|
||||
L"a b c"
|
||||
L"\0"
|
||||
L"X Y Z"
|
||||
L"\0";
|
||||
|
||||
using namespace mozilla;
|
||||
using namespace mozilla::freestanding;
|
||||
@ -65,32 +57,55 @@ static bool VerifySharedSection(SharedSection& aSharedSection) {
|
||||
|
||||
HMODULE k32mod = ::GetModuleHandleW(L"kernel32.dll");
|
||||
VERIFY_FUNCTION_RESOLVED(k32mod, view->mK32Exports, FlushInstructionCache);
|
||||
VERIFY_FUNCTION_RESOLVED(k32mod, view->mK32Exports, GetModuleHandleW);
|
||||
VERIFY_FUNCTION_RESOLVED(k32mod, view->mK32Exports, GetSystemInfo);
|
||||
VERIFY_FUNCTION_RESOLVED(k32mod, view->mK32Exports, VirtualProtect);
|
||||
|
||||
const uint8_t* const arrayBase =
|
||||
reinterpret_cast<const uint8_t*>(view->mModulePathArray);
|
||||
for (uint32_t i = 0; i < view->mModulePathArrayLength; ++i) {
|
||||
uint32_t offset = view->mModulePathArray[i];
|
||||
// Use NtQueryAttributesFile to check the validity of an NT path.
|
||||
UNICODE_STRING ntpath;
|
||||
::RtlInitUnicodeString(
|
||||
&ntpath, reinterpret_cast<const wchar_t*>(arrayBase + offset));
|
||||
OBJECT_ATTRIBUTES oa;
|
||||
InitializeObjectAttributes(&oa, &ntpath, OBJ_CASE_INSENSITIVE, nullptr,
|
||||
nullptr);
|
||||
FILE_BASIC_INFORMATION info;
|
||||
NTSTATUS status = ::NtQueryAttributesFile(&oa, &info);
|
||||
if (!NT_SUCCESS(status)) {
|
||||
printf(
|
||||
"TEST-FAILED | TestCrossProcessWin | "
|
||||
"Invalid path %ls - %08lx.\n",
|
||||
ntpath.Buffer, status);
|
||||
return false;
|
||||
bool matched = memcmp(view->mModulePathArray, kTestStringsMerged,
|
||||
sizeof(kTestStringsMerged)) == 0;
|
||||
if (!matched) {
|
||||
// Print actual strings on error
|
||||
for (const wchar_t* p = view->mModulePathArray; *p;) {
|
||||
printf("%p: %ls\n", p, p);
|
||||
while (*p) {
|
||||
++p;
|
||||
}
|
||||
++p;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
printf("%p: %ls\n", &offset,
|
||||
reinterpret_cast<const wchar_t*>(arrayBase + offset));
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool TestAddString() {
|
||||
wchar_t testBuffer[3] = {0};
|
||||
UNICODE_STRING ustr;
|
||||
|
||||
// This makes |testBuffer| full.
|
||||
::RtlInitUnicodeString(&ustr, L"a");
|
||||
if (!AddString(testBuffer, sizeof(testBuffer), ustr)) {
|
||||
printf(
|
||||
"TEST-FAILED | TestCrossProcessWin | "
|
||||
"AddString failed.\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Adding a string to a full buffer should fail.
|
||||
::RtlInitUnicodeString(&ustr, L"b");
|
||||
if (AddString(testBuffer, sizeof(testBuffer), ustr)) {
|
||||
printf(
|
||||
"TEST-FAILED | TestCrossProcessWin | "
|
||||
"AddString caused OOB memory access.\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
bool matched = memcmp(testBuffer, L"a\0", sizeof(testBuffer)) == 0;
|
||||
if (!matched) {
|
||||
printf(
|
||||
"TEST-FAILED | TestCrossProcessWin | "
|
||||
"AddString wrote wrong values.\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
@ -133,7 +148,7 @@ class ChildProcess final {
|
||||
}
|
||||
|
||||
auto getDependentModulePaths =
|
||||
reinterpret_cast<uint32_t (*)(uint32_t**)>(::GetProcAddress(
|
||||
reinterpret_cast<const wchar_t (*)()>(::GetProcAddress(
|
||||
::GetModuleHandleW(nullptr), "GetDependentModulePaths"));
|
||||
if (!getDependentModulePaths) {
|
||||
printf(
|
||||
@ -146,8 +161,7 @@ class ChildProcess final {
|
||||
#if !defined(DEBUG)
|
||||
// GetDependentModulePaths does not allow a caller other than xul.dll.
|
||||
// Skip on Debug build because it hits MOZ_ASSERT.
|
||||
uint32_t* modulePathArray;
|
||||
if (getDependentModulePaths(&modulePathArray) || modulePathArray) {
|
||||
if (getDependentModulePaths()) {
|
||||
printf(
|
||||
"TEST-FAILED | TestCrossProcessWin | "
|
||||
"GetDependentModulePaths should return zero if the caller is "
|
||||
@ -160,17 +174,27 @@ class ChildProcess final {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Test a scenario to transfer a transferred section
|
||||
// Test a scenario to transfer a transferred section as a readonly handle
|
||||
static HANDLE copiedHandle = nullptr;
|
||||
nt::CrossExecTransferManager tansferToSelf(::GetCurrentProcess());
|
||||
LauncherVoidResult result =
|
||||
gSharedSection.TransferHandle(tansferToSelf, &copiedHandle);
|
||||
LauncherVoidResult result = gSharedSection.TransferHandle(
|
||||
tansferToSelf, GENERIC_READ, &copiedHandle);
|
||||
if (result.isErr()) {
|
||||
PrintLauncherError(result, "SharedSection::TransferHandle(self) failed");
|
||||
return 1;
|
||||
}
|
||||
|
||||
gSharedSection.Reset(copiedHandle);
|
||||
|
||||
UNICODE_STRING ustr;
|
||||
::RtlInitUnicodeString(&ustr, L"test");
|
||||
result = gSharedSection.AddDepenentModule(&ustr);
|
||||
if (result.inspectErr() !=
|
||||
WindowsError::FromWin32Error(ERROR_ACCESS_DENIED)) {
|
||||
PrintLauncherError(result, "The readonly section was writable");
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (!VerifySharedSection(gSharedSection)) {
|
||||
return 1;
|
||||
}
|
||||
@ -253,6 +277,10 @@ int wmain(int argc, wchar_t* argv[]) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (!TestAddString()) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
LauncherResult<HMODULE> remoteImageBase =
|
||||
nt::GetProcessExeModule(childProcess);
|
||||
if (remoteImageBase.isErr()) {
|
||||
@ -312,7 +340,17 @@ int wmain(int argc, wchar_t* argv[]) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
result = gSharedSection.TransferHandle(transferMgr);
|
||||
for (const auto& testString : kTestStrings) {
|
||||
nt::AllocatedUnicodeString ustr(testString);
|
||||
result = gSharedSection.AddDepenentModule(ustr);
|
||||
if (result.isErr()) {
|
||||
PrintLauncherError(result, "SharedSection::AddDepenentModule failed");
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
result = gSharedSection.TransferHandle(transferMgr,
|
||||
GENERIC_READ | GENERIC_WRITE);
|
||||
if (result.isErr()) {
|
||||
PrintLauncherError(result, "SharedSection::TransferHandle failed");
|
||||
return 1;
|
||||
|
@ -97,6 +97,9 @@ VOID NTAPI RtlReleaseSRWLockShared(PSRWLOCK aLock);
|
||||
|
||||
ULONG NTAPI RtlNtStatusToDosError(NTSTATUS aStatus);
|
||||
VOID NTAPI RtlSetLastWin32Error(DWORD aError);
|
||||
DWORD NTAPI RtlGetLastWin32Error();
|
||||
|
||||
VOID NTAPI RtlRunOnceInitialize(PRTL_RUN_ONCE aRunOnce);
|
||||
|
||||
NTSTATUS NTAPI NtReadVirtualMemory(HANDLE aProcessHandle, PVOID aBaseAddress,
|
||||
PVOID aBuffer, SIZE_T aNumBytesToRead,
|
||||
@ -161,6 +164,15 @@ class AllocatedUnicodeString final {
|
||||
Duplicate(aSrc);
|
||||
}
|
||||
|
||||
explicit AllocatedUnicodeString(const char* aSrc) {
|
||||
if (!aSrc) {
|
||||
mUnicodeString = {};
|
||||
return;
|
||||
}
|
||||
|
||||
Duplicate(aSrc);
|
||||
}
|
||||
|
||||
AllocatedUnicodeString(const AllocatedUnicodeString& aOther) {
|
||||
Duplicate(&aOther.mUnicodeString);
|
||||
}
|
||||
@ -230,6 +242,19 @@ class AllocatedUnicodeString final {
|
||||
mUnicodeString = {};
|
||||
}
|
||||
}
|
||||
|
||||
void Duplicate(const char* aSrc) {
|
||||
MOZ_ASSERT(aSrc);
|
||||
|
||||
ANSI_STRING ansiStr;
|
||||
RtlInitAnsiString(&ansiStr, aSrc);
|
||||
NTSTATUS ntStatus =
|
||||
::RtlAnsiStringToUnicodeString(&mUnicodeString, &ansiStr, TRUE);
|
||||
MOZ_ASSERT(NT_SUCCESS(ntStatus));
|
||||
if (!NT_SUCCESS(ntStatus)) {
|
||||
mUnicodeString = {};
|
||||
}
|
||||
}
|
||||
#endif // !defined(MOZILLA_INTERNAL_API)
|
||||
|
||||
void Clear() {
|
||||
|
@ -614,6 +614,7 @@ class MMPolicyInProcessEarlyStage : public MMPolicyInProcessPrimitive {
|
||||
public:
|
||||
struct Kernel32Exports {
|
||||
decltype(&::FlushInstructionCache) mFlushInstructionCache;
|
||||
decltype(&::GetModuleHandleW) mGetModuleHandleW;
|
||||
decltype(&::GetSystemInfo) mGetSystemInfo;
|
||||
decltype(&::VirtualProtect) mVirtualProtect;
|
||||
};
|
||||
|
@ -392,21 +392,18 @@ static void AddCachedDirRule(sandbox::TargetPolicy* aPolicy,
|
||||
}
|
||||
}
|
||||
|
||||
// This function caches and returns a Maybe<Span> of offsets to null
|
||||
// terminated wchar_t NT paths of the executable's dependent modules.
|
||||
// The strings themselves are stored in memory directly after the Span.
|
||||
// See also the comment in browser/app/winlauncher/freestanding/SharedSection.h
|
||||
//
|
||||
// This function caches and returns an array of NT paths of the executable's
|
||||
// dependent modules.
|
||||
// If this returns Nothing(), it means the retrieval of the modules failed
|
||||
// (e.g. when the launcher process is disabled), so the process should not
|
||||
// enable pre-spawn CIG.
|
||||
static const Maybe<Span<uint32_t>>& GetPrespawnCigExceptionModules() {
|
||||
static const Maybe<Vector<const wchar_t*>>& GetPrespawnCigExceptionModules() {
|
||||
// sDependentModules points to a shared section created in the launcher
|
||||
// process and the mapped address is static in each process, so we cache
|
||||
// it as a static variable instead of retrieving it every time.
|
||||
static Maybe<Span<uint32_t>> sDependentModules =
|
||||
[]() -> Maybe<Span<uint32_t>> {
|
||||
using GetDependentModulePathsFn = uint32_t (*)(uint32_t**);
|
||||
static Maybe<Vector<const wchar_t*>> sDependentModules =
|
||||
[]() -> Maybe<Vector<const wchar_t*>> {
|
||||
using GetDependentModulePathsFn = const wchar_t* (*)();
|
||||
GetDependentModulePathsFn getDependentModulePaths =
|
||||
reinterpret_cast<GetDependentModulePathsFn>(::GetProcAddress(
|
||||
::GetModuleHandleW(nullptr), "GetDependentModulePaths"));
|
||||
@ -414,17 +411,30 @@ static const Maybe<Span<uint32_t>>& GetPrespawnCigExceptionModules() {
|
||||
return Nothing();
|
||||
}
|
||||
|
||||
uint32_t* modulePathArray = nullptr;
|
||||
uint32_t modulePathArrayLen = getDependentModulePaths(&modulePathArray);
|
||||
return modulePathArray ? Some(Span(modulePathArray, modulePathArrayLen))
|
||||
: Nothing();
|
||||
const wchar_t* arrayBase = getDependentModulePaths();
|
||||
if (!arrayBase) {
|
||||
return Nothing();
|
||||
}
|
||||
|
||||
// Convert a null-delimited string set to a string vector.
|
||||
Vector<const wchar_t*> paths;
|
||||
for (const wchar_t* p = arrayBase; *p;) {
|
||||
Unused << paths.append(p);
|
||||
while (*p) {
|
||||
++p;
|
||||
}
|
||||
++p;
|
||||
}
|
||||
|
||||
return Some(std::move(paths));
|
||||
}();
|
||||
|
||||
return sDependentModules;
|
||||
}
|
||||
|
||||
static sandbox::ResultCode InitSignedPolicyRulesToBypassCig(
|
||||
sandbox::TargetPolicy* aPolicy, const Span<uint32_t>& aExceptionModules) {
|
||||
sandbox::TargetPolicy* aPolicy,
|
||||
const Vector<const wchar_t*>& aExceptionModules) {
|
||||
// Allow modules in the directory containing the executable such as
|
||||
// mozglue.dll, nss3.dll, etc.
|
||||
nsAutoString rulePath(*sBinDir);
|
||||
@ -436,17 +446,13 @@ static sandbox::ResultCode InitSignedPolicyRulesToBypassCig(
|
||||
return result;
|
||||
}
|
||||
|
||||
if (aExceptionModules.IsEmpty()) {
|
||||
if (aExceptionModules.empty()) {
|
||||
return sandbox::SBOX_ALL_OK;
|
||||
}
|
||||
|
||||
const uint8_t* arrayBase =
|
||||
reinterpret_cast<const uint8_t*>(aExceptionModules.data());
|
||||
for (uint32_t offset : aExceptionModules) {
|
||||
result =
|
||||
aPolicy->AddRule(sandbox::TargetPolicy::SUBSYS_SIGNED_BINARY,
|
||||
sandbox::TargetPolicy::SIGNED_ALLOW_LOAD,
|
||||
reinterpret_cast<const wchar_t*>(arrayBase + offset));
|
||||
for (const wchar_t* path : aExceptionModules) {
|
||||
result = aPolicy->AddRule(sandbox::TargetPolicy::SUBSYS_SIGNED_BINARY,
|
||||
sandbox::TargetPolicy::SIGNED_ALLOW_LOAD, path);
|
||||
if (result != sandbox::SBOX_ALL_OK) {
|
||||
return result;
|
||||
}
|
||||
@ -961,7 +967,7 @@ bool SandboxBroker::SetSecurityLevelForRDDProcess() {
|
||||
sandbox::MITIGATION_DEP_NO_ATL_THUNK | sandbox::MITIGATION_DEP |
|
||||
sandbox::MITIGATION_IMAGE_LOAD_PREFER_SYS32;
|
||||
|
||||
const Maybe<Span<uint32_t>>& exceptionModules =
|
||||
const Maybe<Vector<const wchar_t*>>& exceptionModules =
|
||||
GetPrespawnCigExceptionModules();
|
||||
if (exceptionModules.isSome()) {
|
||||
mitigations |= sandbox::MITIGATION_FORCE_MS_SIGNED_BINS;
|
||||
|
Loading…
Reference in New Issue
Block a user