fune/browser/app/winlauncher/test/TestCrossProcessWin.cpp
Toshihito Kikuchi 7545ed9378 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
2021-01-13 15:13:18 +00:00

365 lines
11 KiB
C++

/* -*- 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/. */
#define MOZ_USE_LAUNCHER_ERROR
#include "freestanding/SharedSection.cpp"
#include "mozilla/CmdLineAndEnvUtils.h"
#include "mozilla/NativeNt.h"
const wchar_t kChildArg[] = L"--child";
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;
template <typename T, int N>
void PrintLauncherError(const LauncherResult<T>& aResult,
const char (&aMsg)[N]) {
const LauncherError& err = aResult.inspectErr();
printf("TEST-FAILED | TestCrossProcessWin | %s - %lx at %s:%d\n", aMsg,
err.mError.AsHResult(), err.mFile, err.mLine);
}
#define VERIFY_FUNCTION_RESOLVED(mod, exports, name) \
do { \
if (reinterpret_cast<FARPROC>(exports.m##name) != \
::GetProcAddress(mod, #name)) { \
printf( \
"TEST-FAILED | TestCrossProcessWin | " \
"Kernel32ExportsSolver::" #name " did not match.\n"); \
return false; \
} \
} while (0)
static bool VerifySharedSection(SharedSection& aSharedSection) {
LauncherResult<SharedSection::Layout*> resultView = aSharedSection.GetView();
if (resultView.isErr()) {
PrintLauncherError(resultView, "Failed to map a shared section");
return false;
}
SharedSection::Layout* view = resultView.unwrap();
// Use a local variable of RTL_RUN_ONCE to resolve Kernel32Exports every time
RTL_RUN_ONCE sRunEveryTime = RTL_RUN_ONCE_INIT;
view->mK32Exports.Resolve(sRunEveryTime);
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);
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;
}
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;
}
class ChildProcess final {
nsAutoHandle mChildProcess;
nsAutoHandle mChildMainThread;
DWORD mProcessId;
public:
// The following variables are updated from the parent process via
// WriteProcessMemory while the process is suspended as a part of
// TestWithChildProcess().
//
// Having both a non-const and a const is important because a constant
// is separately placed in the .rdata section which is read-only, so
// the region's attribute needs to be changed before modifying data via
// WriteProcessMemory.
// The keyword "volatile" is needed for a constant, otherwise the compiler
// evaluates a constant as a literal without fetching data from memory.
static HMODULE sExecutableImageBase;
static volatile const DWORD sReadOnlyProcessId;
static int Main() {
if (sExecutableImageBase != ::GetModuleHandle(nullptr)) {
printf(
"TEST-FAILED | TestCrossProcessWin | "
"sExecutableImageBase is expected to be %p, but actually was %p.\n",
::GetModuleHandle(nullptr), sExecutableImageBase);
return 1;
}
if (sReadOnlyProcessId != ::GetCurrentProcessId()) {
printf(
"TEST-FAILED | TestCrossProcessWin | "
"sReadOnlyProcessId is expected to be %lx, but actually was %lx.\n",
::GetCurrentProcessId(), sReadOnlyProcessId);
return 1;
}
auto getDependentModulePaths =
reinterpret_cast<const wchar_t (*)()>(::GetProcAddress(
::GetModuleHandleW(nullptr), "GetDependentModulePaths"));
if (!getDependentModulePaths) {
printf(
"TEST-FAILED | TestCrossProcessWin | "
"Failed to get a pointer to GetDependentModulePaths - %08lx.\n",
::GetLastError());
return 1;
}
#if !defined(DEBUG)
// GetDependentModulePaths does not allow a caller other than xul.dll.
// Skip on Debug build because it hits MOZ_ASSERT.
if (getDependentModulePaths()) {
printf(
"TEST-FAILED | TestCrossProcessWin | "
"GetDependentModulePaths should return zero if the caller is "
"not xul.dll.\n");
return 1;
}
#endif // !defined(DEBUG)
if (!VerifySharedSection(gSharedSection)) {
return 1;
}
// 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, 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;
}
return 0;
}
ChildProcess(const wchar_t* aExecutable, const wchar_t* aOption)
: mProcessId(0) {
const wchar_t* childArgv[] = {aExecutable, aOption};
auto cmdLine(
mozilla::MakeCommandLine(mozilla::ArrayLength(childArgv), childArgv));
STARTUPINFOW si = {sizeof(si)};
PROCESS_INFORMATION pi;
BOOL ok =
::CreateProcessW(aExecutable, cmdLine.get(), nullptr, nullptr, FALSE,
CREATE_SUSPENDED, nullptr, nullptr, &si, &pi);
if (!ok) {
printf(
"TEST-FAILED | TestCrossProcessWin | "
"CreateProcessW falied - %08lx.\n",
GetLastError());
return;
}
mProcessId = pi.dwProcessId;
mChildProcess.own(pi.hProcess);
mChildMainThread.own(pi.hThread);
}
~ChildProcess() { ::TerminateProcess(mChildProcess, 0); }
operator HANDLE() const { return mChildProcess; }
DWORD GetProcessId() const { return mProcessId; }
bool ResumeAndWaitUntilExit() {
if (::ResumeThread(mChildMainThread) == 0xffffffff) {
printf(
"TEST-FAILED | TestCrossProcessWin | "
"ResumeThread failed - %08lx\n",
GetLastError());
return false;
}
if (::WaitForSingleObject(mChildProcess, 60000) != WAIT_OBJECT_0) {
printf(
"TEST-FAILED | TestCrossProcessWin | "
"Unexpected result from WaitForSingleObject\n");
return false;
}
DWORD exitCode;
if (!::GetExitCodeProcess(mChildProcess, &exitCode)) {
printf(
"TEST-FAILED | TestCrossProcessWin | "
"GetExitCodeProcess failed - %08lx\n",
GetLastError());
return false;
}
return exitCode == 0;
}
};
HMODULE ChildProcess::sExecutableImageBase = 0;
volatile const DWORD ChildProcess::sReadOnlyProcessId = 0;
int wmain(int argc, wchar_t* argv[]) {
printf("Process: %-8lx Base: %p\n", ::GetCurrentProcessId(),
::GetModuleHandle(nullptr));
if (argc == 2 && wcscmp(argv[1], kChildArg) == 0) {
return ChildProcess::Main();
}
ChildProcess childProcess(argv[0], kChildArg);
if (!childProcess) {
return 1;
}
if (!TestAddString()) {
return 1;
}
LauncherResult<HMODULE> remoteImageBase =
nt::GetProcessExeModule(childProcess);
if (remoteImageBase.isErr()) {
PrintLauncherError(remoteImageBase, "nt::GetProcessExeModule failed");
return 1;
}
nt::CrossExecTransferManager transferMgr(childProcess);
if (!transferMgr) {
printf(
"TEST-FAILED | TestCrossProcessWin | "
"CrossExecTransferManager instantiation failed.\n");
return 1;
}
LauncherVoidResult result =
transferMgr.Transfer(&ChildProcess::sExecutableImageBase,
&remoteImageBase.inspect(), sizeof(HMODULE));
if (result.isErr()) {
PrintLauncherError(result, "ChildProcess::WriteData(Imagebase) failed");
return 1;
}
DWORD childPid = childProcess.GetProcessId();
DWORD* readOnlyData = const_cast<DWORD*>(&ChildProcess::sReadOnlyProcessId);
result = transferMgr.Transfer(readOnlyData, &childPid, sizeof(DWORD));
if (result.isOk()) {
printf(
"TEST-UNEXPECTED | TestCrossProcessWin | "
"A constant was located in a writable section.");
return 1;
}
AutoVirtualProtect prot =
transferMgr.Protect(readOnlyData, sizeof(uint32_t), PAGE_READWRITE);
if (!prot) {
printf(
"TEST-FAILED | TestCrossProcessWin | "
"VirtualProtect failed - %08lx\n",
prot.GetError().AsHResult());
return 1;
}
result = transferMgr.Transfer(readOnlyData, &childPid, sizeof(DWORD));
if (result.isErr()) {
PrintLauncherError(result, "ChildProcess::WriteData(PID) failed");
return 1;
}
{
// Define a scope for |sharedSection| to resume the child process after
// the section is deleted in the parent process.
result = gSharedSection.Init(transferMgr.LocalPEHeaders());
if (result.isErr()) {
PrintLauncherError(result, "SharedSection::Init failed");
return 1;
}
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;
}
}
if (!childProcess.ResumeAndWaitUntilExit()) {
return 1;
}
return 0;
}