/* -*- 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/. */ // This test makes sure mozilla::nt::PEExportSection can parse the export // section of a local process, and a remote process even though it's // modified by an external code. #include "mozilla/CmdLineAndEnvUtils.h" #include "mozilla/NativeNt.h" #include "nsWindowsDllInterceptor.h" #include #include #define EXPORT_FUNCTION_EQ(name, func) \ (GetProcAddress(imageBase, name) == reinterpret_cast(func)) #define VERIFY_EXPORT_FUNCTION(tables, name, expected, errorMessage) \ do { \ if (tables.GetProcAddress(name) != reinterpret_cast(expected)) { \ printf("TEST-FAILED | TestPEExportSection | %s", errorMessage); \ return kTestFail; \ } \ } while (0) using namespace mozilla::nt; using mozilla::interceptor::MMPolicyInProcess; using mozilla::interceptor::MMPolicyOutOfProcess; using LocalPEExportSection = PEExportSection; using RemotePEExportSection = PEExportSection; constexpr DWORD kEventTimeoutinMs = 5000; const wchar_t kProcessControlEventName[] = L"TestPEExportSection.Process.Control.Event"; enum TestResult : int { kTestSuccess = 0, kTestFail, kTestSkip, }; // These strings start with the same keyword to make sure we don't do substring // match. Moreover, kSecretFunctionInvalid is purposely longer than the // combination of the other two strings and located in between the other two // strings to effectively test binary search. const char kSecretFunction[] = "Secret"; const char kSecretFunctionInvalid[] = "Secret invalid long name"; const char kSecretFunctionWithSuffix[] = "Secret2"; const wchar_t* kNoModification = L"--NoModification"; const wchar_t* kNoExport = L"--NoExport"; const wchar_t* kModifyTableEntry = L"--ModifyTableEntry"; const wchar_t* kModifyTable = L"--ModifyTable"; const wchar_t* kModifyDirectoryEntry = L"--ModifyDirectoryEntry"; const wchar_t* kExportByOrdinal = L"--ExportByOrdinal"; // Use the global variable to pass the child process's error status to the // parent process. We don't use a process's exit code to keep the test simple. int gChildProcessStatus = 0; // These functions are exported by linker or export section tampering at // runtime. Each of function bodies needs to be different to avoid ICF. extern "C" __declspec(dllexport) int Export1() { return 0; } extern "C" __declspec(dllexport) int Export2() { return 1; } int SecretFunction1() { return 100; } int SecretFunction2() { return 101; } // This class allocates a writable region downstream of the mapped image // and prepares it as a valid export section. class ExportDirectoryPatcher final { static constexpr int kRegionAllocationTryLimit = 100; static constexpr int kNumOfTableEntries = 2; // VirtualAlloc sometimes fails if a desired base address is too small. // Define a minimum desired base to reduce the number of allocation tries. static constexpr uintptr_t kMinimumAllocationPoint = 0x8000000; struct ExportDirectory { IMAGE_EXPORT_DIRECTORY mDirectoryHeader; DWORD mExportAddressTable[kNumOfTableEntries]; DWORD mExportNameTable[kNumOfTableEntries]; WORD mExportOrdinalTable[kNumOfTableEntries]; char mNameBuffer1[sizeof(kSecretFunction)]; char mNameBuffer2[sizeof(kSecretFunctionWithSuffix)]; template static DWORD PtrToRVA(T aPtr, uintptr_t aBase) { return reinterpret_cast(aPtr) - aBase; } explicit ExportDirectory(uintptr_t aImageBase) : mDirectoryHeader{} { mDirectoryHeader.Base = 1; mExportAddressTable[0] = PtrToRVA(SecretFunction1, aImageBase); mExportAddressTable[1] = PtrToRVA(SecretFunction2, aImageBase); mExportNameTable[0] = PtrToRVA(mNameBuffer1, aImageBase); mExportNameTable[1] = PtrToRVA(mNameBuffer2, aImageBase); mExportOrdinalTable[0] = 0; mExportOrdinalTable[1] = 1; strcpy(mNameBuffer1, kSecretFunction); strcpy(mNameBuffer2, kSecretFunctionWithSuffix); } }; uintptr_t mImageBase; ExportDirectory* mNewExportDirectory; DWORD PtrToRVA(const void* aPtr) const { return reinterpret_cast(aPtr) - mImageBase; } public: explicit ExportDirectoryPatcher(HMODULE aModule) : mImageBase(PEHeaders::HModuleToBaseAddr(aModule)), mNewExportDirectory(nullptr) { SYSTEM_INFO si = {}; ::GetSystemInfo(&si); int numPagesRequired = ((sizeof(ExportDirectory) - 1) / si.dwPageSize) + 1; uintptr_t desiredBase = mImageBase + si.dwAllocationGranularity; desiredBase = std::max(desiredBase, kMinimumAllocationPoint); for (int i = 0; i < kRegionAllocationTryLimit; ++i) { void* allocated = ::VirtualAlloc(reinterpret_cast(desiredBase), numPagesRequired * si.dwPageSize, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE); if (allocated) { // Use the end of a allocated page as ExportDirectory in order to test // the boundary between a commit page and a non-commited page. allocated = reinterpret_cast(allocated) + (numPagesRequired * si.dwPageSize) - sizeof(ExportDirectory); mNewExportDirectory = new (allocated) ExportDirectory(mImageBase); return; } desiredBase += si.dwAllocationGranularity; } gChildProcessStatus = kTestSkip; printf( "TEST-SKIP | TestPEExportSection | " "Giving up finding an allocatable space following the mapped image.\n"); } ~ExportDirectoryPatcher() { // Intentionally leave mNewExportDirectory leaked to keep a patched data // available until the process is terminated. } explicit operator bool() const { return !!mNewExportDirectory; } void PopulateDirectory(IMAGE_EXPORT_DIRECTORY& aOutput) const { aOutput.NumberOfFunctions = aOutput.NumberOfNames = kNumOfTableEntries; aOutput.AddressOfFunctions = PtrToRVA(mNewExportDirectory->mExportAddressTable); aOutput.AddressOfNames = PtrToRVA(mNewExportDirectory->mExportNameTable); aOutput.AddressOfNameOrdinals = PtrToRVA(mNewExportDirectory->mExportOrdinalTable); } void PopulateDirectoryEntry(IMAGE_DATA_DIRECTORY& aOutput) const { PopulateDirectory(mNewExportDirectory->mDirectoryHeader); aOutput.VirtualAddress = PtrToRVA(&mNewExportDirectory->mDirectoryHeader); aOutput.Size = sizeof(ExportDirectory); } }; // This exports SecretFunction1 as "Export1" by replacing an entry of the // export address table. void ModifyExportAddressTableEntry() { MMPolicyInProcess policy; HMODULE imageBase = ::GetModuleHandleW(nullptr); auto ourExe = LocalPEExportSection::Get(imageBase, policy); auto addressTableEntry = const_cast(ourExe.FindExportAddressTableEntry("Export1")); if (!addressTableEntry) { gChildProcessStatus = kTestFail; return; } mozilla::AutoVirtualProtect protection( addressTableEntry, sizeof(*addressTableEntry), PAGE_READWRITE); if (!protection) { gChildProcessStatus = kTestFail; return; } *addressTableEntry = reinterpret_cast(SecretFunction1) - PEHeaders::HModuleToBaseAddr(imageBase); if (!EXPORT_FUNCTION_EQ("Export1", SecretFunction1) || !EXPORT_FUNCTION_EQ("Export2", Export2)) { gChildProcessStatus = kTestFail; } } // This switches the entire address table into one exporting SecretFunction1 // and SecretFunction2. void ModifyExportAddressTable() { MMPolicyInProcess policy; HMODULE imageBase = ::GetModuleHandleW(nullptr); auto ourExe = LocalPEExportSection::Get(imageBase, policy); auto exportDirectory = ourExe.GetExportDirectory(); if (!exportDirectory) { gChildProcessStatus = kTestFail; return; } mozilla::AutoVirtualProtect protection( exportDirectory, sizeof(*exportDirectory), PAGE_READWRITE); if (!protection) { gChildProcessStatus = kTestFail; return; } ExportDirectoryPatcher patcher(imageBase); if (!patcher) { return; } patcher.PopulateDirectory(*exportDirectory); if (GetProcAddress(imageBase, "Export1") || GetProcAddress(imageBase, "Export2") || !EXPORT_FUNCTION_EQ(kSecretFunction, SecretFunction1) || !EXPORT_FUNCTION_EQ(kSecretFunctionWithSuffix, SecretFunction2)) { gChildProcessStatus = kTestFail; } } // This hides all export functions by setting the table size to 0. void HideExportSection() { HMODULE imageBase = ::GetModuleHandleW(nullptr); PEHeaders ourExe(imageBase); auto sectionTable = ourExe.GetImageDirectoryEntryPtr(IMAGE_DIRECTORY_ENTRY_EXPORT); mozilla::AutoVirtualProtect protection(sectionTable, sizeof(*sectionTable), PAGE_READWRITE); if (!protection) { gChildProcessStatus = kTestFail; return; } sectionTable->VirtualAddress = sectionTable->Size = 0; if (GetProcAddress(imageBase, "Export1") || GetProcAddress(imageBase, "Export2")) { gChildProcessStatus = kTestFail; } } // This makes the export directory entry point to a new export section // which exports SecretFunction1 and SecretFunction2. void ModifyExportDirectoryEntry() { HMODULE imageBase = ::GetModuleHandleW(nullptr); PEHeaders ourExe(imageBase); auto sectionTable = ourExe.GetImageDirectoryEntryPtr(IMAGE_DIRECTORY_ENTRY_EXPORT); mozilla::AutoVirtualProtect protection(sectionTable, sizeof(*sectionTable), PAGE_READWRITE); if (!protection) { gChildProcessStatus = kTestFail; return; } ExportDirectoryPatcher patcher(imageBase); if (!patcher) { return; } patcher.PopulateDirectoryEntry(*sectionTable); if (GetProcAddress(imageBase, "Export1") || GetProcAddress(imageBase, "Export2") || !EXPORT_FUNCTION_EQ(kSecretFunction, SecretFunction1) || !EXPORT_FUNCTION_EQ(kSecretFunctionWithSuffix, SecretFunction2)) { gChildProcessStatus = kTestFail; } } // This exports functions only by Ordinal by hiding the export name table. void ExportByOrdinal() { ModifyExportDirectoryEntry(); if (gChildProcessStatus != kTestSuccess) { return; } MMPolicyInProcess policy; HMODULE imageBase = ::GetModuleHandleW(nullptr); auto ourExe = LocalPEExportSection::Get(imageBase, policy); auto exportDirectory = ourExe.GetExportDirectory(); if (!exportDirectory) { gChildProcessStatus = kTestFail; return; } exportDirectory->NumberOfNames = 0; if (GetProcAddress(imageBase, "Export1") || GetProcAddress(imageBase, "Export2") || GetProcAddress(imageBase, kSecretFunction) || GetProcAddress(imageBase, kSecretFunctionWithSuffix) || !EXPORT_FUNCTION_EQ(MAKEINTRESOURCE(1), SecretFunction1) || !EXPORT_FUNCTION_EQ(MAKEINTRESOURCE(2), SecretFunction2)) { gChildProcessStatus = kTestFail; } } class ChildProcess final { nsAutoHandle mChildProcess; nsAutoHandle mChildMainThread; public: static int Main(const nsAutoHandle& aEvent, const wchar_t* aOption) { if (wcscmp(aOption, kNoModification) == 0) { ; } else if (wcscmp(aOption, kNoExport) == 0) { HideExportSection(); } else if (wcscmp(aOption, kModifyTableEntry) == 0) { ModifyExportAddressTableEntry(); } else if (wcscmp(aOption, kModifyTable) == 0) { ModifyExportAddressTable(); } else if (wcscmp(aOption, kModifyDirectoryEntry) == 0) { ModifyExportDirectoryEntry(); } else if (wcscmp(aOption, kExportByOrdinal) == 0) { ExportByOrdinal(); } // Letting the parent process know the child process is ready. ::SetEvent(aEvent); // The child process does not exit itself. It's force terminated by // the parent process when all tests are done. for (;;) { ::Sleep(100); } } ChildProcess(const wchar_t* aExecutable, const wchar_t* aOption, const nsAutoHandle& aEvent, const nsAutoHandle& aJob) { 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, 0, nullptr, nullptr, &si, &pi); if (!ok) { printf( "TEST-FAILED | TestPEExportSection | " "CreateProcessW falied - %08lx.\n", GetLastError()); return; } if (aJob && !::AssignProcessToJobObject(aJob, pi.hProcess)) { printf( "TEST-FAILED | TestPEExportSection | " "AssignProcessToJobObject falied - %08lx.\n", GetLastError()); ::TerminateProcess(pi.hProcess, 1); return; } // Wait until requested modification is done in the child process. if (::WaitForSingleObject(aEvent, kEventTimeoutinMs) != WAIT_OBJECT_0) { printf( "TEST-FAILED | TestPEExportSection | " "Child process was not ready in time.\n"); return; } mChildProcess.own(pi.hProcess); mChildMainThread.own(pi.hThread); } ~ChildProcess() { ::TerminateProcess(mChildProcess, 0); } operator HANDLE() const { return mChildProcess; } TestResult GetStatus() const { TestResult status = kTestSuccess; if (!::ReadProcessMemory(mChildProcess, &gChildProcessStatus, &status, sizeof(status), nullptr)) { status = kTestFail; printf( "TEST-FAILED | TestPEExportSection | " "ReadProcessMemory failed - %08lx\n", GetLastError()); } return status; } }; template TestResult BasicTest(const MMPolicy& aMMPolicy) { const bool isAppHelpLoaded = ::GetModuleHandleW(L"apphelp.dll"); // Use ntdll.dll because it does not have any forwarder RVA. HMODULE ntdllImageBase = ::GetModuleHandleW(L"ntdll.dll"); auto ntdllExports = PEExportSection::Get(ntdllImageBase, aMMPolicy); auto exportDir = ntdllExports.GetExportDirectory(); auto tableOfNames = ntdllExports.template RVAToPtr(exportDir->AddressOfNames); for (DWORD i = 0; i < exportDir->NumberOfNames; ++i) { const auto name = ntdllExports.template RVAToPtr(tableOfNames[i]); if (isAppHelpLoaded && strcmp(name, "NtdllDefWindowProc_W") == 0) { // In this case, GetProcAddress will return // apphelp!DWM8AND16BitHook_DefWindowProcW. continue; } auto funcEntry = ntdllExports.FindExportAddressTableEntry(name); if (ntdllExports.template RVAToPtr(*funcEntry) != ::GetProcAddress(ntdllImageBase, name)) { printf( "TEST-FAILED | TestPEExportSection | " "FindExportAddressTableEntry did not resolve ntdll!%s.\n", name); return kTestFail; } } for (DWORD i = 0; i < 0x10000; i += 0x10) { if (ntdllExports.GetProcAddress(MAKEINTRESOURCE(i)) != ::GetProcAddress(ntdllImageBase, MAKEINTRESOURCE(i))) { printf( "TEST-FAILED | TestPEExportSection | " "GetProcAddress did not resolve ntdll!Ordinal#%lu.\n", i); return kTestFail; } } // Test a known forwarder RVA. auto k32Exports = PEExportSection::Get( ::GetModuleHandleW(L"kernel32.dll"), aMMPolicy); if (k32Exports.FindExportAddressTableEntry("HeapAlloc")) { printf( "TEST-FAILED | TestPEExportSection | " "kernel32!HeapAlloc should be forwarded to ntdll!RtlAllocateHeap.\n"); return kTestFail; } // Test invalid names. if (k32Exports.FindExportAddressTableEntry("Invalid name") || k32Exports.FindExportAddressTableEntry("")) { printf( "TEST-FAILED | TestPEExportSection | " "FindExportAddressTableEntry should return " "nullptr for a non-existent name.\n"); return kTestFail; } return kTestSuccess; } TestResult RunChildProcessTest( const wchar_t* aExecutable, const wchar_t* aOption, const nsAutoHandle& aEvent, const nsAutoHandle& aJob, TestResult (*aTestCallback)(const RemotePEExportSection&)) { ChildProcess childProcess(aExecutable, aOption, aEvent, aJob); if (!childProcess) { return kTestFail; } auto result = childProcess.GetStatus(); if (result != kTestSuccess) { return result; } MMPolicyOutOfProcess policy(childProcess); // One time is enough to run BasicTest in the child process. static TestResult oneTimeResult = BasicTest(policy); if (oneTimeResult != kTestSuccess) { return oneTimeResult; } auto exportTableChild = RemotePEExportSection::Get(::GetModuleHandleW(nullptr), policy); return aTestCallback(exportTableChild); } mozilla::LauncherResult> CreateJobToLimitProcessLifetime() { uint64_t version; PEHeaders ntdllHeaders(::GetModuleHandleW(L"ntdll.dll")); if (!ntdllHeaders.GetVersionInfo(version)) { printf( "TEST-FAILED | TestPEExportSection | " "Unable to obtain version information from ntdll.dll\n"); return LAUNCHER_ERROR_FROM_LAST(); } constexpr uint64_t kWin8 = 0x60002ull << 32; nsAutoHandle job; if (version < kWin8) { // Since a process can be associated only with a single job in Win7 or // older and this test program is already assigned with a job by // infrastructure, we cannot use a job. return job.out(); } job.own(::CreateJobObject(nullptr, nullptr)); if (!job) { printf( "TEST-FAILED | TestPEExportSection | " "CreateJobObject falied - %08lx.\n", GetLastError()); return LAUNCHER_ERROR_FROM_LAST(); } JOBOBJECT_EXTENDED_LIMIT_INFORMATION jobInfo = {}; jobInfo.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE; if (!::SetInformationJobObject(job, JobObjectExtendedLimitInformation, &jobInfo, sizeof(jobInfo))) { printf( "TEST-FAILED | TestPEExportSection | " "SetInformationJobObject falied - %08lx.\n", GetLastError()); return LAUNCHER_ERROR_FROM_LAST(); } return job.out(); } extern "C" int wmain(int argc, wchar_t* argv[]) { nsAutoHandle controlEvent( ::CreateEventW(nullptr, FALSE, FALSE, kProcessControlEventName)); if (argc == 2) { return ChildProcess::Main(controlEvent, argv[1]); } if (argc != 1) { printf( "TEST-FAILED | TestPEExportSection | " "Invalid arguments.\n"); return kTestFail; } MMPolicyInProcess policy; if (BasicTest(policy)) { return kTestFail; } auto exportTableSelf = LocalPEExportSection::Get(::GetModuleHandleW(nullptr), policy); if (!exportTableSelf) { printf( "TEST-FAILED | TestPEExportSection | " "LocalPEExportSection::Get failed.\n"); return kTestFail; } VERIFY_EXPORT_FUNCTION(exportTableSelf, "Export1", Export1, "Local | Export1 was not exported.\n"); VERIFY_EXPORT_FUNCTION(exportTableSelf, "Export2", Export2, "Local | Export2 was not exported.\n"); VERIFY_EXPORT_FUNCTION( exportTableSelf, "Invalid name", 0, "Local | GetProcAddress should return nullptr for an invalid name.\n"); // We'll add the child process to a job so that, in the event of a failure in // this parent process, the child process will be automatically terminated. auto probablyJob = CreateJobToLimitProcessLifetime(); if (probablyJob.isErr()) { return kTestFail; } nsAutoHandle job(probablyJob.unwrap()); auto result = RunChildProcessTest( argv[0], kNoModification, controlEvent, job, [](const RemotePEExportSection& aTables) { VERIFY_EXPORT_FUNCTION(aTables, "Export1", Export1, "NoModification | Export1 was not exported.\n"); VERIFY_EXPORT_FUNCTION(aTables, "Export2", Export2, "NoModification | Export2 was not exported.\n"); return kTestSuccess; }); if (result == kTestFail) { return result; } result = RunChildProcessTest( argv[0], kNoExport, controlEvent, job, [](const RemotePEExportSection& aTables) { VERIFY_EXPORT_FUNCTION(aTables, "Export1", 0, "NoExport | Export1 was exported.\n"); VERIFY_EXPORT_FUNCTION(aTables, "Export2", 0, "NoExport | Export2 was exported.\n"); return kTestSuccess; }); if (result == kTestFail) { return result; } result = RunChildProcessTest( argv[0], kModifyTableEntry, controlEvent, job, [](const RemotePEExportSection& aTables) { VERIFY_EXPORT_FUNCTION( aTables, "Export1", SecretFunction1, "ModifyTableEntry | SecretFunction1 was not exported.\n"); VERIFY_EXPORT_FUNCTION( aTables, "Export2", Export2, "ModifyTableEntry | Export2 was not exported.\n"); return kTestSuccess; }); if (result == kTestFail) { return result; } result = RunChildProcessTest( argv[0], kModifyTable, controlEvent, job, [](const RemotePEExportSection& aTables) { VERIFY_EXPORT_FUNCTION(aTables, "Export1", 0, "ModifyTable | Export1 was exported.\n"); VERIFY_EXPORT_FUNCTION(aTables, "Export2", 0, "ModifyTable | Export2 was exported.\n"); VERIFY_EXPORT_FUNCTION( aTables, kSecretFunction, SecretFunction1, "ModifyTable | SecretFunction1 was not exported.\n"); VERIFY_EXPORT_FUNCTION( aTables, kSecretFunctionWithSuffix, SecretFunction2, "ModifyTable | SecretFunction2 was not exported.\n"); VERIFY_EXPORT_FUNCTION( aTables, kSecretFunctionInvalid, 0, "ModifyTable | kSecretFunctionInvalid was exported.\n"); return kTestSuccess; }); if (result == kTestFail) { return result; } result = RunChildProcessTest( argv[0], kModifyDirectoryEntry, controlEvent, job, [](const RemotePEExportSection& aTables) { VERIFY_EXPORT_FUNCTION( aTables, "Export1", 0, "ModifyDirectoryEntry | Export1 was exported.\n"); VERIFY_EXPORT_FUNCTION( aTables, "Export2", 0, "ModifyDirectoryEntry | Export2 was exported.\n"); VERIFY_EXPORT_FUNCTION( aTables, kSecretFunction, SecretFunction1, "ModifyDirectoryEntry | SecretFunction1 was not exported.\n"); VERIFY_EXPORT_FUNCTION( aTables, kSecretFunctionWithSuffix, SecretFunction2, "ModifyDirectoryEntry | SecretFunction2 was not exported.\n"); VERIFY_EXPORT_FUNCTION( aTables, kSecretFunctionInvalid, 0, "ModifyDirectoryEntry | kSecretFunctionInvalid was exported.\n"); return kTestSuccess; }); if (result == kTestFail) { return result; } result = RunChildProcessTest( argv[0], kExportByOrdinal, controlEvent, job, [](const RemotePEExportSection& aTables) { VERIFY_EXPORT_FUNCTION(aTables, "Export1", 0, "ExportByOrdinal | Export1 was exported.\n"); VERIFY_EXPORT_FUNCTION(aTables, "Export2", 0, "ExportByOrdinal | Export2 was exported.\n"); VERIFY_EXPORT_FUNCTION( aTables, kSecretFunction, 0, "ModifyDirectoryEntry | kSecretFunction was exported by name.\n"); VERIFY_EXPORT_FUNCTION( aTables, kSecretFunctionWithSuffix, 0, "ModifyDirectoryEntry | " "kSecretFunctionWithSuffix was exported by name.\n"); VERIFY_EXPORT_FUNCTION( aTables, MAKEINTRESOURCE(1), SecretFunction1, "ModifyDirectoryEntry | " "kSecretFunction was not exported by ordinal.\n"); VERIFY_EXPORT_FUNCTION( aTables, MAKEINTRESOURCE(2), SecretFunction2, "ModifyDirectoryEntry | " "kSecretFunctionWithSuffix was not exported by ordinal.\n"); return kTestSuccess; }); if (result == kTestFail) { return result; } return kTestSuccess; }