/* -*- 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 http://mozilla.org/MPL/2.0/. */ #ifndef NS_WINDOWS_DLL_INTERCEPTOR_H_ #define NS_WINDOWS_DLL_INTERCEPTOR_H_ #include #include #include #include #include "mozilla/ArrayUtils.h" #include "mozilla/Assertions.h" #include "mozilla/Atomics.h" #include "mozilla/Attributes.h" #include "mozilla/CheckedInt.h" #include "mozilla/DebugOnly.h" #include "mozilla/NativeNt.h" #include "mozilla/Types.h" #include "mozilla/UniquePtr.h" #include "mozilla/Vector.h" #include "mozilla/interceptor/MMPolicies.h" #include "mozilla/interceptor/PatcherDetour.h" #include "mozilla/interceptor/PatcherNopSpace.h" #include "mozilla/interceptor/VMSharingPolicies.h" #include "nsWindowsHelpers.h" /* * Simple function interception. * * We have two separate mechanisms for intercepting a function: We can use the * built-in nop space, if it exists, or we can create a detour. * * Using the built-in nop space works as follows: On x86-32, DLL functions * begin with a two-byte nop (mov edi, edi) and are preceeded by five bytes of * NOP instructions. * * When we detect a function with this prelude, we do the following: * * 1. Write a long jump to our interceptor function into the five bytes of NOPs * before the function. * * 2. Write a short jump -5 into the two-byte nop at the beginning of the * function. * * This mechanism is nice because it's thread-safe. It's even safe to do if * another thread is currently running the function we're modifying! * * When the WindowsDllNopSpacePatcher is destroyed, we overwrite the short jump * but not the long jump, so re-intercepting the same function won't work, * because its prelude won't match. * * * Unfortunately nop space patching doesn't work on functions which don't have * this magic prelude (and in particular, x86-64 never has the prelude). So * when we can't use the built-in nop space, we fall back to using a detour, * which works as follows: * * 1. Save first N bytes of OrigFunction to trampoline, where N is a * number of bytes >= 5 that are instruction aligned. * * 2. Replace first 5 bytes of OrigFunction with a jump to the Hook * function. * * 3. After N bytes of the trampoline, add a jump to OrigFunction+N to * continue original program flow. * * 4. Hook function needs to call the trampoline during its execution, * to invoke the original function (so address of trampoline is * returned). * * When the WindowsDllDetourPatcher object is destructed, OrigFunction is * patched again to jump directly to the trampoline instead of going through * the hook function. As such, re-intercepting the same function won't work, as * jump instructions are not supported. * * Note that this is not thread-safe. Sad day. * */ #if defined(_M_IX86) && defined(__clang__) && __has_declspec_attribute(guard) // On x86, nop-space patches return to the second instruction of their target. // This is a deliberate violation of Control Flow Guard, so disable the check. # define INTERCEPTOR_DISABLE_CFGUARD __declspec(guard(nocf)) #else # define INTERCEPTOR_DISABLE_CFGUARD /* nothing */ #endif namespace mozilla { namespace interceptor { template struct OriginalFunctionPtrTraits; template struct OriginalFunctionPtrTraits { using ReturnType = R; }; #if defined(_M_IX86) template struct OriginalFunctionPtrTraits { using ReturnType = R; }; template struct OriginalFunctionPtrTraits { using ReturnType = R; }; #endif // defined(_M_IX86) template class FuncHook final { public: using ThisType = FuncHook; using ReturnType = typename OriginalFunctionPtrTraits::ReturnType; constexpr FuncHook() : mOrigFunc(nullptr), mInitOnce(INIT_ONCE_STATIC_INIT) {} ~FuncHook() = default; bool Set(InterceptorT& aInterceptor, const char* aName, FuncPtrT aHookDest) { LPVOID addHookOk = nullptr; InitOnceContext ctx(this, &aInterceptor, aName, aHookDest, false); return ::InitOnceExecuteOnce(&mInitOnce, &InitOnceCallback, &ctx, &addHookOk) && addHookOk; } bool SetDetour(InterceptorT& aInterceptor, const char* aName, FuncPtrT aHookDest) { LPVOID addHookOk = nullptr; InitOnceContext ctx(this, &aInterceptor, aName, aHookDest, true); return ::InitOnceExecuteOnce(&mInitOnce, &InitOnceCallback, &ctx, &addHookOk) && addHookOk; } explicit operator bool() const { return !!mOrigFunc; } template INTERCEPTOR_DISABLE_CFGUARD ReturnType operator()(ArgsType&&... aArgs) const { return mOrigFunc(std::forward(aArgs)...); } FuncPtrT GetStub() const { return mOrigFunc; } // One-time init stuff cannot be moved or copied FuncHook(const FuncHook&) = delete; FuncHook(FuncHook&&) = delete; FuncHook& operator=(const FuncHook&) = delete; FuncHook& operator=(FuncHook&& aOther) = delete; private: struct MOZ_RAII InitOnceContext final { InitOnceContext(ThisType* aHook, InterceptorT* aInterceptor, const char* aName, FuncPtrT aHookDest, bool aForceDetour) : mHook(aHook), mInterceptor(aInterceptor), mName(aName), mHookDest(reinterpret_cast(aHookDest)), mForceDetour(aForceDetour) {} ThisType* mHook; InterceptorT* mInterceptor; const char* mName; void* mHookDest; bool mForceDetour; }; private: bool Apply(InterceptorT* aInterceptor, const char* aName, void* aHookDest) { return aInterceptor->AddHook(aName, reinterpret_cast(aHookDest), reinterpret_cast(&mOrigFunc)); } bool ApplyDetour(InterceptorT* aInterceptor, const char* aName, void* aHookDest) { return aInterceptor->AddDetour(aName, reinterpret_cast(aHookDest), reinterpret_cast(&mOrigFunc)); } static BOOL CALLBACK InitOnceCallback(PINIT_ONCE aInitOnce, PVOID aParam, PVOID* aOutContext) { MOZ_ASSERT(aOutContext); bool result; auto ctx = reinterpret_cast(aParam); if (ctx->mForceDetour) { result = ctx->mHook->ApplyDetour(ctx->mInterceptor, ctx->mName, ctx->mHookDest); } else { result = ctx->mHook->Apply(ctx->mInterceptor, ctx->mName, ctx->mHookDest); } *aOutContext = result ? reinterpret_cast(1U << INIT_ONCE_CTX_RESERVED_BITS) : nullptr; return TRUE; } private: FuncPtrT mOrigFunc; INIT_ONCE mInitOnce; }; template class MOZ_ONLY_USED_TO_AVOID_STATIC_CONSTRUCTORS FuncHookCrossProcess final { public: using ThisType = FuncHookCrossProcess; using ReturnType = typename OriginalFunctionPtrTraits::ReturnType; #if defined(DEBUG) FuncHookCrossProcess() {} #endif // defined(DEBUG) bool Set(nt::CrossExecTransferManager& aTransferMgr, InterceptorT& aInterceptor, const char* aName, FuncPtrT aHookDest) { FuncPtrT origFunc; if (!aInterceptor.AddHook(aName, reinterpret_cast(aHookDest), reinterpret_cast(&origFunc))) { return false; } return CopyStubToChildProcess(aTransferMgr, aInterceptor, origFunc); } bool SetDetour(nt::CrossExecTransferManager& aTransferMgr, InterceptorT& aInterceptor, const char* aName, FuncPtrT aHookDest) { FuncPtrT origFunc; if (!aInterceptor.AddDetour(aName, reinterpret_cast(aHookDest), reinterpret_cast(&origFunc))) { return false; } return CopyStubToChildProcess(aTransferMgr, aInterceptor, origFunc); } explicit operator bool() const { return !!mOrigFunc; } /** * NB: This operator is only meaningful when invoked in the target process! */ template ReturnType operator()(ArgsType&&... aArgs) const { return mOrigFunc(std::forward(aArgs)...); } #if defined(DEBUG) FuncHookCrossProcess(const FuncHookCrossProcess&) = delete; FuncHookCrossProcess(FuncHookCrossProcess&&) = delete; FuncHookCrossProcess& operator=(const FuncHookCrossProcess&) = delete; FuncHookCrossProcess& operator=(FuncHookCrossProcess&& aOther) = delete; #endif // defined(DEBUG) private: bool CopyStubToChildProcess(nt::CrossExecTransferManager& aTransferMgr, InterceptorT& aInterceptor, FuncPtrT aStub) { LauncherVoidResult writeResult = aTransferMgr.Transfer(&mOrigFunc, &aStub, sizeof(FuncPtrT)); if (writeResult.isErr()) { #ifdef MOZ_USE_LAUNCHER_ERROR const mozilla::WindowsError& err = writeResult.inspectErr().mError; #else const mozilla::WindowsError& err = writeResult.inspectErr(); #endif aInterceptor.SetLastDetourError(FUNCHOOKCROSSPROCESS_COPYSTUB_ERROR, err.AsHResult()); return false; } return true; } private: FuncPtrT mOrigFunc; }; template struct TypeResolver; template struct TypeResolver { template using FuncHookType = FuncHook; }; template struct TypeResolver { template using FuncHookType = FuncHookCrossProcess; }; template class WindowsDllInterceptor final : public TypeResolver> { typedef WindowsDllInterceptor ThisType; interceptor::WindowsDllDetourPatcher mDetourPatcher; #if defined(_M_IX86) interceptor::WindowsDllNopSpacePatcher mNopSpacePatcher; #endif // defined(_M_IX86) HMODULE mModule; public: template explicit WindowsDllInterceptor(Args&&... aArgs) : mDetourPatcher(std::forward(aArgs)...) #if defined(_M_IX86) , mNopSpacePatcher(std::forward(aArgs)...) #endif // defined(_M_IX86) , mModule(nullptr) { } WindowsDllInterceptor(const WindowsDllInterceptor&) = delete; WindowsDllInterceptor(WindowsDllInterceptor&&) = delete; WindowsDllInterceptor& operator=(const WindowsDllInterceptor&) = delete; WindowsDllInterceptor& operator=(WindowsDllInterceptor&&) = delete; ~WindowsDllInterceptor() { Clear(); } template void Init(const char (&aModuleName)[N]) { wchar_t moduleName[N]; for (size_t i = 0; i < N; ++i) { MOZ_ASSERT(!(aModuleName[i] & 0x80), "Use wide-character overload for non-ASCII module names"); moduleName[i] = aModuleName[i]; } Init(moduleName); } void Init(const wchar_t* aModuleName) { if (mModule) { return; } mModule = ::LoadLibraryW(aModuleName); } /** Force a specific configuration for testing purposes. NOT to be used in production code! **/ void TestOnlyDetourInit(const wchar_t* aModuleName, DetourFlags aFlags) { Init(aModuleName); mDetourPatcher.Init(aFlags); } void Clear() { if (!mModule) { return; } #if defined(_M_IX86) mNopSpacePatcher.Clear(); #endif // defined(_M_IX86) mDetourPatcher.Clear(); // NB: We intentionally leak mModule } #if defined(NIGHTLY_BUILD) const Maybe& GetLastDetourError() const { return mDetourPatcher.GetLastDetourError(); } #endif // defined(NIGHTLY_BUILD) template void SetLastDetourError(Args&&... aArgs) { return mDetourPatcher.SetLastDetourError(std::forward(aArgs)...); } constexpr static uint32_t GetWorstCaseRequiredBytesToPatch() { return WindowsDllDetourPatcherPrimitive< typename VMPolicy::MMPolicyT>::GetWorstCaseRequiredBytesToPatch(); } private: /** * Hook/detour the method aName from the DLL we set in Init so that it calls * aHookDest instead. Returns the original method pointer in aOrigFunc * and returns true if successful. * * IMPORTANT: If you use this method, please add your case to the * TestDllInterceptor in order to detect future failures. Even if this * succeeds now, updates to the hooked DLL could cause it to fail in * the future. */ bool AddHook(const char* aName, intptr_t aHookDest, void** aOrigFunc) { // Use a nop space patch if possible, otherwise fall back to a detour. // This should be the preferred method for adding hooks. if (!mModule) { mDetourPatcher.SetLastDetourError(DetourResultCode::INTERCEPTOR_MOD_NULL); return false; } if (!mDetourPatcher.IsPageAccessible( nt::PEHeaders::HModuleToBaseAddr(mModule))) { mDetourPatcher.SetLastDetourError( DetourResultCode::INTERCEPTOR_MOD_INACCESSIBLE); return false; } FARPROC proc = mDetourPatcher.GetProcAddress(mModule, aName); if (!proc) { mDetourPatcher.SetLastDetourError( DetourResultCode::INTERCEPTOR_PROC_NULL); return false; } if (!mDetourPatcher.IsPageAccessible(reinterpret_cast(proc))) { mDetourPatcher.SetLastDetourError( DetourResultCode::INTERCEPTOR_PROC_INACCESSIBLE); return false; } #if defined(_M_IX86) if (mNopSpacePatcher.AddHook(proc, aHookDest, aOrigFunc)) { return true; } #endif // defined(_M_IX86) return AddDetour(proc, aHookDest, aOrigFunc); } /** * Detour the method aName from the DLL we set in Init so that it calls * aHookDest instead. Returns the original method pointer in aOrigFunc * and returns true if successful. * * IMPORTANT: If you use this method, please add your case to the * TestDllInterceptor in order to detect future failures. Even if this * succeeds now, updates to the detoured DLL could cause it to fail in * the future. */ bool AddDetour(const char* aName, intptr_t aHookDest, void** aOrigFunc) { // Generally, code should not call this method directly. Use AddHook unless // there is a specific need to avoid nop space patches. if (!mModule) { mDetourPatcher.SetLastDetourError(DetourResultCode::INTERCEPTOR_MOD_NULL); return false; } if (!mDetourPatcher.IsPageAccessible( nt::PEHeaders::HModuleToBaseAddr(mModule))) { mDetourPatcher.SetLastDetourError( DetourResultCode::INTERCEPTOR_MOD_INACCESSIBLE); return false; } FARPROC proc = mDetourPatcher.GetProcAddress(mModule, aName); if (!proc) { mDetourPatcher.SetLastDetourError( DetourResultCode::INTERCEPTOR_PROC_NULL); return false; } if (!mDetourPatcher.IsPageAccessible(reinterpret_cast(proc))) { mDetourPatcher.SetLastDetourError( DetourResultCode::INTERCEPTOR_PROC_INACCESSIBLE); return false; } return AddDetour(proc, aHookDest, aOrigFunc); } bool AddDetour(FARPROC aProc, intptr_t aHookDest, void** aOrigFunc) { MOZ_ASSERT(mModule && aProc); if (!mDetourPatcher.Initialized()) { DetourFlags flags = DetourFlags::eDefault; #if defined(_M_X64) // NTDLL hooks should attempt to use a 10-byte patch because some // injected DLLs do the same and interfere with our stuff. bool needs10BytePatch = (mModule == ::GetModuleHandleW(L"ntdll.dll")); bool isWin8Or81 = IsWin8OrLater() && (!IsWin10OrLater()); bool isWin8 = IsWin8OrLater() && (!IsWin8Point1OrLater()); bool isKernel32Dll = (mModule == ::GetModuleHandleW(L"kernel32.dll")); bool isDuplicateHandle = (reinterpret_cast(aProc) == reinterpret_cast(&::DuplicateHandle)); // CloseHandle on Windows 8/8.1 only accomodates 10-byte patches. needs10BytePatch |= isWin8Or81 && isKernel32Dll && (reinterpret_cast(aProc) == reinterpret_cast(&CloseHandle)); // CreateFileA and DuplicateHandle on Windows 8 require 10-byte patches. needs10BytePatch |= isWin8 && isKernel32Dll && ((reinterpret_cast(aProc) == reinterpret_cast(&::CreateFileA)) || isDuplicateHandle); if (needs10BytePatch) { flags |= DetourFlags::eEnable10BytePatch; } if (isWin8 && isDuplicateHandle) { // Because we can't detour Win8's KERNELBASE!DuplicateHandle, // we detour kernel32!DuplicateHandle (See bug 1659398). flags |= DetourFlags::eDontResolveRedirection; } #endif // defined(_M_X64) mDetourPatcher.Init(flags); } return mDetourPatcher.AddHook(aProc, aHookDest, aOrigFunc); } private: template friend class FuncHook; template friend class FuncHookCrossProcess; }; /** * IAT patching is intended for use when we only want to intercept a function * call originating from a specific module. */ class WindowsIATPatcher final { public: template using FuncHookType = FuncHook; private: static bool CheckASCII(const char* aInStr) { while (*aInStr) { if (*aInStr & 0x80) { return false; } ++aInStr; } return true; } static bool AddHook(HMODULE aFromModule, const char* aToModuleName, const char* aTargetFnName, void* aHookDest, Atomic* aOutOrigFunc) { if (!aFromModule || !aToModuleName || !aTargetFnName || !aOutOrigFunc) { return false; } // PE Spec requires ASCII names for imported module names const bool isModuleNameAscii = CheckASCII(aToModuleName); MOZ_ASSERT(isModuleNameAscii); if (!isModuleNameAscii) { return false; } // PE Spec requires ASCII names for imported function names const bool isTargetFnNameAscii = CheckASCII(aTargetFnName); MOZ_ASSERT(isTargetFnNameAscii); if (!isTargetFnNameAscii) { return false; } nt::PEHeaders headers(aFromModule); if (!headers) { return false; } PIMAGE_IMPORT_DESCRIPTOR impDesc = headers.GetImportDescriptor(aToModuleName); if (!nt::PEHeaders::IsValid(impDesc)) { // Either aFromModule does not import aToModuleName at load-time, or // aToModuleName is a (currently unsupported) delay-load import. return false; } // Resolve the import name table (INT). auto firstINTThunk = headers.template RVAToPtr( impDesc->OriginalFirstThunk); if (!nt::PEHeaders::IsValid(firstINTThunk)) { return false; } Maybe thunkIndex; // Scan the INT for the location of the thunk for the function named // 'aTargetFnName'. for (PIMAGE_THUNK_DATA curINTThunk = firstINTThunk; nt::PEHeaders::IsValid(curINTThunk); ++curINTThunk) { if (IMAGE_SNAP_BY_ORDINAL(curINTThunk->u1.Ordinal)) { // Currently not supporting import by ordinal; this isn't hard to add, // but we won't bother unless necessary. continue; } PIMAGE_IMPORT_BY_NAME curThunkFnName = headers.template RVAToPtr( curINTThunk->u1.AddressOfData); MOZ_ASSERT(curThunkFnName); if (!curThunkFnName) { // Looks like we have a bad name descriptor. Try to continue. continue; } // Function name checks MUST be case-sensitive! if (!strcmp(aTargetFnName, curThunkFnName->Name)) { // We found the thunk. Save the index of this thunk, as the IAT thunk // is located at the same index in that table as in the INT. thunkIndex = Some(curINTThunk - firstINTThunk); break; } } if (thunkIndex.isNothing()) { // We never found a thunk for that function. Perhaps it's not imported? return false; } if (thunkIndex.value() < 0) { // That's just wrong. return false; } auto firstIATThunk = headers.template RVAToPtr(impDesc->FirstThunk); if (!nt::PEHeaders::IsValid(firstIATThunk)) { return false; } // Resolve the IAT thunk for the function we want PIMAGE_THUNK_DATA targetThunk = &firstIATThunk[thunkIndex.value()]; if (!nt::PEHeaders::IsValid(targetThunk)) { return false; } auto fnPtr = reinterpret_cast*>(&targetThunk->u1.Function); // Now we can just change out its pointer with our hook function. AutoVirtualProtect prot(fnPtr, sizeof(void*), PAGE_EXECUTE_READWRITE); if (!prot) { return false; } // We do the exchange this way to ensure that *aOutOrigFunc is always valid // once the atomic exchange has taken place. void* tmp; do { tmp = *fnPtr; *aOutOrigFunc = tmp; } while (!fnPtr->compareExchange(tmp, aHookDest)); return true; } template friend class FuncHook; }; template class MOZ_ONLY_USED_TO_AVOID_STATIC_CONSTRUCTORS FuncHook final { public: using ThisType = FuncHook; using ReturnType = typename OriginalFunctionPtrTraits::ReturnType; constexpr FuncHook() : mInitOnce(INIT_ONCE_STATIC_INIT), mFromModule(nullptr), mOrigFunc(nullptr) {} #if defined(DEBUG) ~FuncHook() = default; #endif // defined(DEBUG) bool Set(const wchar_t* aFromModuleName, const char* aToModuleName, const char* aFnName, FuncPtrT aHookDest) { nsModuleHandle fromModule(::LoadLibraryW(aFromModuleName)); if (!fromModule) { return false; } return Set(fromModule, aToModuleName, aFnName, aHookDest); } // We offer this overload in case the client wants finer-grained control over // loading aFromModule. bool Set(nsModuleHandle& aFromModule, const char* aToModuleName, const char* aFnName, FuncPtrT aHookDest) { LPVOID addHookOk = nullptr; InitOnceContext ctx(this, aFromModule, aToModuleName, aFnName, aHookDest); bool result = ::InitOnceExecuteOnce(&mInitOnce, &InitOnceCallback, &ctx, &addHookOk) && addHookOk; if (!result) { return result; } // If we successfully set the hook then we must retain a strong reference // to the module that we modified. mFromModule = aFromModule.disown(); return result; } explicit operator bool() const { return !!mOrigFunc; } template ReturnType operator()(ArgsType&&... aArgs) const { return mOrigFunc(std::forward(aArgs)...); } FuncPtrT GetStub() const { return mOrigFunc; } #if defined(DEBUG) // One-time init stuff cannot be moved or copied FuncHook(const FuncHook&) = delete; FuncHook(FuncHook&&) = delete; FuncHook& operator=(const FuncHook&) = delete; FuncHook& operator=(FuncHook&& aOther) = delete; #endif // defined(DEBUG) private: struct MOZ_RAII InitOnceContext final { InitOnceContext(ThisType* aHook, const nsModuleHandle& aFromModule, const char* aToModuleName, const char* aFnName, FuncPtrT aHookDest) : mHook(aHook), mFromModule(aFromModule), mToModuleName(aToModuleName), mFnName(aFnName), mHookDest(reinterpret_cast(aHookDest)) {} ThisType* mHook; const nsModuleHandle& mFromModule; const char* mToModuleName; const char* mFnName; void* mHookDest; }; private: bool Apply(const nsModuleHandle& aFromModule, const char* aToModuleName, const char* aFnName, void* aHookDest) { return WindowsIATPatcher::AddHook( aFromModule, aToModuleName, aFnName, aHookDest, reinterpret_cast*>(&mOrigFunc)); } static BOOL CALLBACK InitOnceCallback(PINIT_ONCE aInitOnce, PVOID aParam, PVOID* aOutContext) { MOZ_ASSERT(aOutContext); auto ctx = reinterpret_cast(aParam); bool result = ctx->mHook->Apply(ctx->mFromModule, ctx->mToModuleName, ctx->mFnName, ctx->mHookDest); *aOutContext = result ? reinterpret_cast(1U << INIT_ONCE_CTX_RESERVED_BITS) : nullptr; return TRUE; } private: INIT_ONCE mInitOnce; HMODULE mFromModule; // never freed FuncPtrT mOrigFunc; }; /** * This class applies an irreversible patch to jump to a target function * without backing up the original function. */ class WindowsDllEntryPointInterceptor final { using DllMainFn = BOOL(WINAPI*)(HINSTANCE, DWORD, LPVOID); using MMPolicyT = MMPolicyInProcessEarlyStage; MMPolicyT mMMPolicy; public: explicit WindowsDllEntryPointInterceptor( const MMPolicyT::Kernel32Exports& aK32Exports) : mMMPolicy(aK32Exports) {} bool Set(const nt::PEHeaders& aHeaders, DllMainFn aDestination) { if (!aHeaders) { return false; } WindowsDllDetourPatcherPrimitive patcher; return patcher.AddIrreversibleHook( mMMPolicy, aHeaders.GetEntryPoint(), reinterpret_cast(aDestination)); } }; } // namespace interceptor using WindowsDllInterceptor = interceptor::WindowsDllInterceptor<>; using CrossProcessDllInterceptor = interceptor::WindowsDllInterceptor< mozilla::interceptor::VMSharingPolicyUnique< mozilla::interceptor::MMPolicyOutOfProcess>>; using WindowsIATPatcher = interceptor::WindowsIATPatcher; } // namespace mozilla #endif /* NS_WINDOWS_DLL_INTERCEPTOR_H_ */