diff --git a/dom/plugins/ipc/FunctionHook.cpp b/dom/plugins/ipc/FunctionHook.cpp index 1ea963ea558c..d5e29ec14f18 100644 --- a/dom/plugins/ipc/FunctionHook.cpp +++ b/dom/plugins/ipc/FunctionHook.cpp @@ -58,7 +58,7 @@ FunctionHook::HookFunctions(int aQuirks) // This cache is created when a DLL is registered with a FunctionHook. // It is cleared on a call to ClearDllInterceptorCache(). It // must be freed before exit to avoid leaks. -typedef nsClassHashtable DllInterceptors; +typedef nsClassHashtable DllInterceptors; DllInterceptors* sDllInterceptorCache = nullptr; WindowsDllInterceptor* @@ -68,9 +68,13 @@ FunctionHook::GetDllInterceptorFor(const char* aModuleName) sDllInterceptorCache = new DllInterceptors(); } + MOZ_ASSERT(NS_IsAscii(aModuleName), "Non-ASCII module names are not supported"); + NS_ConvertASCIItoUTF16 moduleName(aModuleName); + WindowsDllInterceptor* ret = - sDllInterceptorCache->LookupOrAdd(nsCString(aModuleName), aModuleName); + sDllInterceptorCache->LookupOrAdd(moduleName); MOZ_ASSERT(ret); + ret->Init(moduleName.get()); return ret; } diff --git a/mozglue/misc/interceptor/MMPolicies.h b/mozglue/misc/interceptor/MMPolicies.h new file mode 100644 index 000000000000..c33bf5aba976 --- /dev/null +++ b/mozglue/misc/interceptor/MMPolicies.h @@ -0,0 +1,224 @@ +/* -*- 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_interceptor_MMPolicies_h +#define mozilla_interceptor_MMPolicies_h + +#include "mozilla/Assertions.h" +#include "mozilla/Types.h" + +#include + +namespace mozilla { +namespace interceptor { + +class MMPolicyBase +{ +public: + static DWORD ComputeAllocationSize(const uint32_t aRequestedSize) + { + MOZ_ASSERT(aRequestedSize); + DWORD result = aRequestedSize; + + const uint32_t granularity = GetAllocGranularity(); + + uint32_t mod = aRequestedSize % granularity; + if (mod) { + result += (granularity - mod); + } + + return result; + } + + static DWORD GetAllocGranularity() + { + static const DWORD kAllocGranularity = []() -> DWORD { + SYSTEM_INFO sysInfo; + ::GetSystemInfo(&sysInfo); + return sysInfo.dwAllocationGranularity; + }(); + + return kAllocGranularity; + } + + static DWORD GetPageSize() + { + static const DWORD kPageSize = []() -> DWORD { + SYSTEM_INFO sysInfo; + ::GetSystemInfo(&sysInfo); + return sysInfo.dwPageSize; + }(); + + return kPageSize; + } +}; + +class MMPolicyInProcess : public MMPolicyBase +{ +public: + typedef MMPolicyInProcess MMPolicyT; + + explicit MMPolicyInProcess() + : mBase(nullptr) + , mReservationSize(0) + , mCommitOffset(0) + { + } + + MMPolicyInProcess(const MMPolicyInProcess&) = delete; + MMPolicyInProcess& operator=(const MMPolicyInProcess&) = delete; + + MMPolicyInProcess(MMPolicyInProcess&& aOther) + : mBase(nullptr) + , mReservationSize(0) + , mCommitOffset(0) + { + *this = Move(aOther); + } + + MMPolicyInProcess& operator=(MMPolicyInProcess&& aOther) + { + mBase = aOther.mBase; + aOther.mBase = nullptr; + + mCommitOffset = aOther.mCommitOffset; + aOther.mCommitOffset = 0; + + mReservationSize = aOther.mReservationSize; + aOther.mReservationSize = 0; + + return *this; + } + + // We always leak mBase + ~MMPolicyInProcess() = default; + + explicit operator bool() const + { + return !!mBase; + } + + /** + * Should we unhook everything upon destruction? + */ + bool ShouldUnhookUponDestruction() const + { + return true; + } + + bool Read(void* aToPtr, const void* aFromPtr, size_t aLen) const + { + ::memcpy(aToPtr, aFromPtr, aLen); + return true; + } + + bool Write(void* aToPtr, const void* aFromPtr, size_t aLen) const + { + ::memcpy(aToPtr, aFromPtr, aLen); + return true; + } + + bool Protect(void* aVAddress, size_t aSize, uint32_t aProtFlags, + uint32_t* aPrevProtFlags) const + { + MOZ_ASSERT(aPrevProtFlags); + BOOL ok = ::VirtualProtect(aVAddress, aSize, aProtFlags, + reinterpret_cast(aPrevProtFlags)); + MOZ_ASSERT(ok); + return !!ok; + } + + /** + * @return true if the page that hosts aVAddress is accessible. + */ + bool IsPageAccessible(void* aVAddress) const + { + MEMORY_BASIC_INFORMATION mbi; + SIZE_T result = ::VirtualQuery(aVAddress, &mbi, sizeof(mbi)); + + return result && mbi.AllocationProtect && mbi.State == MEM_COMMIT && + mbi.Protect != PAGE_NOACCESS; + } + + bool FlushInstructionCache() const + { + return !!::FlushInstructionCache(::GetCurrentProcess(), nullptr, 0); + } + +protected: + uint8_t* GetLocalView() const + { + return mBase; + } + + uintptr_t GetRemoteView() const + { + // Same as local view for in-process + return reinterpret_cast(mBase); + } + + /** + * @return the effective number of bytes reserved, or 0 on failure + */ + uint32_t Reserve(const uint32_t aSize) + { + if (!aSize) { + return 0; + } + + if (mBase) { + MOZ_ASSERT(mReservationSize >= aSize); + return mReservationSize; + } + + mReservationSize = ComputeAllocationSize(aSize); + mBase = static_cast(::VirtualAlloc(nullptr, mReservationSize, + MEM_RESERVE, PAGE_NOACCESS)); + if (!mBase) { + return 0; + } + return mReservationSize; + } + + bool MaybeCommitNextPage(const uint32_t aRequestedOffset, + const uint32_t aRequestedLength) + { + if (!(*this)) { + return false; + } + + uint32_t limit = aRequestedOffset + aRequestedLength - 1; + if (limit < mCommitOffset) { + // No commit required + return true; + } + + MOZ_DIAGNOSTIC_ASSERT(mCommitOffset < mReservationSize); + if (mCommitOffset >= mReservationSize) { + return false; + } + + PVOID local = ::VirtualAlloc(mBase + mCommitOffset, GetPageSize(), + MEM_COMMIT, PAGE_EXECUTE_READ); + if (!local) { + return false; + } + + mCommitOffset += GetPageSize(); + return true; + } + +private: + uint8_t* mBase; + uint32_t mReservationSize; + uint32_t mCommitOffset; +}; + +} // namespace interceptor +} // namespace mozilla + +#endif // mozilla_interceptor_MMPolicies_h + diff --git a/mozglue/misc/interceptor/PatcherBase.h b/mozglue/misc/interceptor/PatcherBase.h new file mode 100644 index 000000000000..3d87cb6998de --- /dev/null +++ b/mozglue/misc/interceptor/PatcherBase.h @@ -0,0 +1,84 @@ +/* -*- 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_interceptor_PatcherBase_h +#define mozilla_interceptor_PatcherBase_h + +#include "mozilla/interceptor/TargetFunction.h" + +namespace mozilla { +namespace interceptor { + +template +class WindowsDllPatcherBase +{ +protected: + typedef typename VMPolicy::MMPolicyT MMPolicyT; + + template + explicit WindowsDllPatcherBase(Args... aArgs) + : mVMPolicy(mozilla::Forward(aArgs)...) + { + } + + ReadOnlyTargetFunction + ResolveRedirectedAddress(const void* aOriginalFunction) + { + ReadOnlyTargetFunction origFn(mVMPolicy, aOriginalFunction); + // If function entry is jmp rel8 stub to the internal implementation, we + // resolve redirected address from the jump target. + if (origFn[0] == 0xeb) { + int8_t offset = (int8_t)(origFn[1]); + if (offset <= 0) { + // Bail out for negative offset: probably already patched by some + // third-party code. + return Move(origFn); + } + + for (int8_t i = 0; i < offset; i++) { + if (origFn[2 + i] != 0x90) { + // Bail out on insufficient nop space. + return Move(origFn); + } + } + + origFn += 2 + offset; + return ReadOnlyTargetFunction(mVMPolicy, origFn.GetAddress()); + } + +#if defined(_M_IX86) + // If function entry is jmp [disp32] such as used by kernel32, + // we resolve redirected address from import table. + if (origFn[0] == 0xff && origFn[1] == 0x25) { + return ReadOnlyTargetFunction(mVMPolicy, + reinterpret_cast((origFn + 2).template ChasePointer())); + } +#elif defined(_M_X64) + // If function entry is jmp [disp32] such as used by kernel32, + // we resolve redirected address from import table. + if (origFn[0] == 0x48 && origFn[1] == 0xff && origFn[2] == 0x25) { + return ReadOnlyTargetFunction(mVMPolicy, + reinterpret_cast((origFn + 3).ChasePointerFromDisp())); + } + + if (origFn[0] == 0xe9) { + // require for TestDllInterceptor with --disable-optimize + uintptr_t abstarget = (origFn + 1).ReadDisp32AsAbsolute(); + return ReadOnlyTargetFunction(mVMPolicy, abstarget); + } +#endif + + return Move(origFn); + } + +protected: + VMPolicy mVMPolicy; +}; + +} // namespace interceptor +} // namespace mozilla + +#endif // mozilla_interceptor_PatcherBase_h diff --git a/mozglue/misc/interceptor/PatcherDetour.h b/mozglue/misc/interceptor/PatcherDetour.h new file mode 100644 index 000000000000..cb9471721872 --- /dev/null +++ b/mozglue/misc/interceptor/PatcherDetour.h @@ -0,0 +1,1029 @@ +/* -*- 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_interceptor_PatcherDetour_h +#define mozilla_interceptor_PatcherDetour_h + +#include "mozilla/interceptor/PatcherBase.h" +#include "mozilla/interceptor/Trampoline.h" + +#include "mozilla/ScopeExit.h" + +#define COPY_CODES(NBYTES) do { \ + tramp.CopyFrom(origBytes.GetAddress(), NBYTES); \ + origBytes += NBYTES; \ +} while (0) + +namespace mozilla { +namespace interceptor { + +template +class WindowsDllDetourPatcher final : public WindowsDllPatcherBase +{ +public: + template + explicit WindowsDllDetourPatcher(Args... aArgs) + : WindowsDllPatcherBase(mozilla::Forward(aArgs)...) + { + } + + ~WindowsDllDetourPatcher() + { + Clear(); + } + + WindowsDllDetourPatcher(const WindowsDllDetourPatcher&) = delete; + WindowsDllDetourPatcher(WindowsDllDetourPatcher&&) = delete; + WindowsDllDetourPatcher& operator=(const WindowsDllDetourPatcher&) = delete; + WindowsDllDetourPatcher& operator=(WindowsDllDetourPatcher&&) = delete; + + void Clear() + { + if (!mVMPolicy.ShouldUnhookUponDestruction()) { + return; + } + +#if defined(_M_IX86) + size_t nBytes = 1 + sizeof(intptr_t); +#elif defined(_M_X64) + size_t nBytes = 2 + sizeof(intptr_t); +#else +#error "Unknown processor type" +#endif + + const auto& tramps = mVMPolicy.Items(); + for (auto&& tramp : tramps) { + // First we read the pointer to the interceptor instance. + Maybe instance = tramp.ReadEncodedPointer(); + if (!instance) { + continue; + } + + if (instance.value() != reinterpret_cast(this)) { + // tramp does not belong to this interceptor instance. + continue; + } + + auto clearInstance = MakeScopeExit([&tramp]() -> void { + // Clear the instance pointer so that no future instances with the same + // |this| pointer will attempt to reset its hook. + tramp.Rewind(); + tramp.WriteEncodedPointer(nullptr); + }); + + // Now we read the pointer to the intercepted function. + Maybe interceptedFn = tramp.ReadEncodedPointer(); + if (!interceptedFn) { + continue; + } + + WritableTargetFunction origBytes(mVMPolicy, + interceptedFn.value(), nBytes); + if (!origBytes) { + continue; + } + + Maybe maybeOpcode1 = origBytes.ReadByte(); + if (!maybeOpcode1) { + continue; + } + + uint8_t opcode1 = maybeOpcode1.value(); + +#if defined(_M_IX86) + // Ensure the JMP from CreateTrampoline is where we expect it to be. + MOZ_ASSERT(opcode1 == 0xE9); + if (opcode1 != 0xE9) { + continue; + } + + intptr_t startOfTrampInstructions = + static_cast(tramp.GetCurrentRemoteAddress()); + + origBytes.WriteDisp32(startOfTrampInstructions); + if (!origBytes) { + continue; + } +#elif defined(_M_X64) + // Ensure the MOV R11 from CreateTrampoline is where we expect it to be. + MOZ_ASSERT(opcode1 == 0x49); + if (opcode1 != 0x49) { + continue; + } + + Maybe maybeOpcode2 = origBytes.ReadByte(); + if (!maybeOpcode2) { + continue; + } + + uint8_t opcode2 = maybeOpcode2.value(); + if (opcode2 != 0xBB) { + continue; + } + + origBytes.WritePointer(tramp.GetCurrentRemoteAddress()); + if (!origBytes) { + continue; + } +#else +#error "Unknown processor type" +#endif + + origBytes.Commit(); + } + + mVMPolicy.Clear(); + } + + void Init(int aNumHooks = 0) + { + if (Initialized()) { + return; + } + + if (aNumHooks == 0) { + // Win32 allocates VM addresses at a 64KiB granularity, so by default we + // might as well utilize that entire 64KiB reservation instead of + // artifically constraining ourselves to the page size. + aNumHooks = mVMPolicy.GetAllocGranularity() / kHookSize; + } + + mVMPolicy.Reserve(aNumHooks); + } + + bool Initialized() const + { + return !!mVMPolicy; + } + + bool AddHook(FARPROC aTargetFn, intptr_t aHookDest, void** aOrigFunc) + { + ReadOnlyTargetFunction target(ResolveRedirectedAddress(aTargetFn)); + + CreateTrampoline(target, aHookDest, aOrigFunc); + if (!*aOrigFunc) { + return false; + } + + return true; + } + +protected: + const static int kPageSize = 4096; + const static int kHookSize = 128; + + // rex bits + static const BYTE kMaskHighNibble = 0xF0; + static const BYTE kRexOpcode = 0x40; + static const BYTE kMaskRexW = 0x08; + static const BYTE kMaskRexR = 0x04; + static const BYTE kMaskRexX = 0x02; + static const BYTE kMaskRexB = 0x01; + + // mod r/m bits + static const BYTE kRegFieldShift = 3; + static const BYTE kMaskMod = 0xC0; + static const BYTE kMaskReg = 0x38; + static const BYTE kMaskRm = 0x07; + static const BYTE kRmNeedSib = 0x04; + static const BYTE kModReg = 0xC0; + static const BYTE kModDisp32 = 0x80; + static const BYTE kModDisp8 = 0x40; + static const BYTE kModNoRegDisp = 0x00; + static const BYTE kRmNoRegDispDisp32 = 0x05; + + // sib bits + static const BYTE kMaskSibScale = 0xC0; + static const BYTE kMaskSibIndex = 0x38; + static const BYTE kMaskSibBase = 0x07; + static const BYTE kSibBaseEbp = 0x05; + + // Register bit IDs. + static const BYTE kRegAx = 0x0; + static const BYTE kRegCx = 0x1; + static const BYTE kRegDx = 0x2; + static const BYTE kRegBx = 0x3; + static const BYTE kRegSp = 0x4; + static const BYTE kRegBp = 0x5; + static const BYTE kRegSi = 0x6; + static const BYTE kRegDi = 0x7; + + // Special ModR/M codes. These indicate operands that cannot be simply + // memcpy-ed. + // Operand is a 64-bit RIP-relative address. + static const int kModOperand64 = -2; + // Operand is not yet handled by our trampoline. + static const int kModUnknown = -1; + + /** + * Returns the number of bytes taken by the ModR/M byte, SIB (if present) + * and the instruction's operand. In special cases, the special MODRM codes + * above are returned. + * aModRm points to the ModR/M byte of the instruction. + * On return, aSubOpcode (if present) is filled with the subopcode/register + * code found in the ModR/M byte. + */ + int CountModRmSib(const ReadOnlyTargetFunction& aModRm, + BYTE* aSubOpcode = nullptr) + { + int numBytes = 1; // Start with 1 for mod r/m byte itself + switch (*aModRm & kMaskMod) { + case kModReg: + return numBytes; + case kModDisp8: + numBytes += 1; + break; + case kModDisp32: + numBytes += 4; + break; + case kModNoRegDisp: + if ((*aModRm & kMaskRm) == kRmNoRegDispDisp32) { +#if defined(_M_X64) + if (aSubOpcode) { + *aSubOpcode = (*aModRm & kMaskReg) >> kRegFieldShift; + } + return kModOperand64; +#else + // On IA-32, all ModR/M instruction modes address memory relative to 0 + numBytes += 4; +#endif + } else if (((*aModRm & kMaskRm) == kRmNeedSib && + (*(aModRm + 1) & kMaskSibBase) == kSibBaseEbp)) { + numBytes += 4; + } + break; + default: + // This should not be reachable + MOZ_ASSERT_UNREACHABLE("Impossible value for modr/m byte mod bits"); + return kModUnknown; + } + if ((*aModRm & kMaskRm) == kRmNeedSib) { + // SIB byte + numBytes += 1; + } + if (aSubOpcode) { + *aSubOpcode = (*aModRm & kMaskReg) >> kRegFieldShift; + } + return numBytes; + } + +#if defined(_M_X64) + enum class JumpType + { + Je, + Jne, + Jmp, + Call + }; + + static bool + GenerateJump(Trampoline& aTramp, uintptr_t aAbsTargetAddress, + const JumpType aType) + { + // Near call, absolute indirect, address given in r/m32 + if (aType == JumpType::Call) { + // CALL [RIP+0] + aTramp.WriteByte(0xff); + aTramp.WriteByte(0x15); + // The offset to jump destination -- 2 bytes after the current position. + aTramp.WriteInteger(2); + aTramp.WriteByte(0xeb); // JMP + 8 (jump over target address) + aTramp.WriteByte(8); + aTramp.WritePointer(aAbsTargetAddress); + return !!aTramp; + } + + if (aType == JumpType::Je) { + // JNE RIP+14 + aTramp.WriteByte(0x75); + aTramp.WriteByte(14); + } else if (aType == JumpType::Jne) { + // JE RIP+14 + aTramp.WriteByte(0x74); + aTramp.WriteByte(14); + } + + // Near jmp, absolute indirect, address given in r/m32 + // JMP [RIP+0] + aTramp.WriteByte(0xff); + aTramp.WriteByte(0x25); + // The offset to jump destination is 0 + aTramp.WriteInteger(0); + aTramp.WritePointer(aAbsTargetAddress); + + return !!aTramp; + } +#endif + + enum ePrefixGroupBits + { + eNoPrefixes = 0, + ePrefixGroup1 = (1 << 0), + ePrefixGroup2 = (1 << 1), + ePrefixGroup3 = (1 << 2), + ePrefixGroup4 = (1 << 3) + }; + + int CountPrefixBytes(const ReadOnlyTargetFunction& aBytes, + const int aBytesIndex, + unsigned char* aOutGroupBits) + { + unsigned char& groupBits = *aOutGroupBits; + groupBits = eNoPrefixes; + int index = aBytesIndex; + while (true) { + switch (aBytes[index]) { + // Group 1 + case 0xF0: // LOCK + case 0xF2: // REPNZ + case 0xF3: // REP / REPZ + if (groupBits & ePrefixGroup1) { + return -1; + } + groupBits |= ePrefixGroup1; + ++index; + break; + + // Group 2 + case 0x2E: // CS override / branch not taken + case 0x36: // SS override + case 0x3E: // DS override / branch taken + case 0x64: // FS override + case 0x65: // GS override + if (groupBits & ePrefixGroup2) { + return -1; + } + groupBits |= ePrefixGroup2; + ++index; + break; + + // Group 3 + case 0x66: // operand size override + if (groupBits & ePrefixGroup3) { + return -1; + } + groupBits |= ePrefixGroup3; + ++index; + break; + + // Group 4 + case 0x67: // Address size override + if (groupBits & ePrefixGroup4) { + return -1; + } + groupBits |= ePrefixGroup4; + ++index; + break; + + default: + return index - aBytesIndex; + } + } + } + + // Return a ModR/M byte made from the 2 Mod bits, the register used for the + // reg bits and the register used for the R/M bits. + BYTE BuildModRmByte(BYTE aModBits, BYTE aReg, BYTE aRm) + { + MOZ_ASSERT((aRm & kMaskRm) == aRm); + MOZ_ASSERT((aModBits & kMaskMod) == aModBits); + MOZ_ASSERT(((aReg << kRegFieldShift) & kMaskReg) == (aReg << kRegFieldShift)); + return aModBits | (aReg << kRegFieldShift) | aRm; + } + + void CreateTrampoline(ReadOnlyTargetFunction& origBytes, + intptr_t aDest, void** aOutTramp) + { + *aOutTramp = nullptr; + + Trampoline tramp(mVMPolicy.GetNextTrampoline()); + if (!tramp) { + return; + } + + // The beginning of the trampoline contains two pointer-width slots: + // [0]: |this|, so that we know whether the trampoline belongs to us; + // [1]: Pointer to original function, so that we can reset the hook upon + // destruction. + tramp.WriteEncodedPointer(this); + if (!tramp) { + return; + } + + auto clearInstanceOnFailure = MakeScopeExit([aOutTramp, &tramp]() -> void { + // *aOutTramp is not set until CreateTrampoline has completed successfully, + // so we can use that to check for success. + if (*aOutTramp) { + return; + } + + // Clear the instance pointer so that we don't try to reset a nonexistent + // hook. + tramp.Rewind(); + tramp.WriteEncodedPointer(nullptr); + }); + + tramp.WritePointer(origBytes.AsEncodedPtr()); + if (!tramp) { + return; + } + + tramp.StartExecutableCode(); + +#if defined(_M_IX86) + int pJmp32 = -1; + while (origBytes.GetOffset() < 5) { + // Understand some simple instructions that might be found in a + // prologue; we might need to extend this as necessary. + // + // Note! If we ever need to understand jump instructions, we'll + // need to rewrite the displacement argument. + unsigned char prefixGroups; + int numPrefixBytes = CountPrefixBytes(origBytes, origBytes.GetOffset(), &prefixGroups); + if (numPrefixBytes < 0 || (prefixGroups & (ePrefixGroup3 | ePrefixGroup4))) { + // Either the prefix sequence was bad, or there are prefixes that + // we don't currently support (groups 3 and 4) + MOZ_ASSERT_UNREACHABLE("Unrecognized opcode sequence"); + return; + } + + origBytes += numPrefixBytes; + if (*origBytes >= 0x88 && *origBytes <= 0x8B) { + // various MOVs + ++origBytes; + int len = CountModRmSib(origBytes); + if (len < 0) { + MOZ_ASSERT_UNREACHABLE("Unrecognized MOV opcode sequence"); + return; + } + origBytes += len; + } else if (*origBytes == 0x0f && + (origBytes[1] == 0x10 || + origBytes[1] == 0x11)) { + // SSE: movups xmm, xmm/m128 + // movups xmm/m128, xmm + origBytes += 2; + int len = CountModRmSib(origBytes); + if (len < 0) { + MOZ_ASSERT_UNREACHABLE("Unrecognized MOV opcode sequence"); + return; + } + origBytes += len; + } else if (*origBytes == 0xA1) { + // MOV eax, [seg:offset] + origBytes += 5; + } else if (*origBytes == 0xB8) { + // MOV 0xB8: http://ref.x86asm.net/coder32.html#xB8 + origBytes += 5; + } else if (*origBytes == 0x33 && + (origBytes[1] & kMaskMod) == kModReg) { + // XOR r32, r32 + origBytes += 2; + } else if ((*origBytes & 0xf8) == 0x40) { + // INC r32 + origBytes += 1; + } else if (*origBytes == 0x83) { + // ADD|ODR|ADC|SBB|AND|SUB|XOR|CMP r/m, imm8 + unsigned char b = origBytes[1]; + if ((b & 0xc0) == 0xc0) { + // ADD|ODR|ADC|SBB|AND|SUB|XOR|CMP r, imm8 + origBytes += 3; + } else { + // bail + MOZ_ASSERT_UNREACHABLE("Unrecognized bit opcode sequence"); + return; + } + } else if (*origBytes == 0x68) { + // PUSH with 4-byte operand + origBytes += 5; + } else if ((*origBytes & 0xf0) == 0x50) { + // 1-byte PUSH/POP + ++origBytes; + } else if (*origBytes == 0x6A) { + // PUSH imm8 + origBytes += 2; + } else if (*origBytes == 0xe9) { + pJmp32 = origBytes.GetOffset(); + // jmp 32bit offset + origBytes += 5; + } else if (*origBytes == 0xff && + origBytes[1] == 0x25) { + // jmp [disp32] + origBytes += 6; + } else if (*origBytes == 0xc2) { + // ret imm16. We can't handle this but it happens. We don't ASSERT but we do fail to hook. +#if defined(MOZILLA_INTERNAL_API) + NS_WARNING("Cannot hook method -- RET opcode found"); +#endif + return; + } else { + //printf ("Unknown x86 instruction byte 0x%02x, aborting trampoline\n", *origBytes); + MOZ_ASSERT_UNREACHABLE("Unrecognized opcode sequence"); + return; + } + } + + // The trampoline is a copy of the instructions that we just traced, + // followed by a jump that we add below. + tramp.CopyFrom(origBytes.GetBaseAddress(), origBytes.GetOffset()); + if (!tramp) { + return; + } +#elif defined(_M_X64) + bool foundJmp = false; + + while (origBytes.GetOffset() < 13) { + // If we found JMP 32bit offset, we require that the next bytes must + // be NOP or INT3. There is no reason to copy them. + // TODO: This used to trigger for Je as well. Now that I allow + // instructions after CALL and JE, I don't think I need that. + // The only real value of this condition is that if code follows a JMP + // then its _probably_ the target of a JMP somewhere else and we + // will be overwriting it, which would be tragic. This seems + // highly unlikely. + if (foundJmp) { + if (*origBytes == 0x90 || *origBytes == 0xcc) { + ++origBytes; + continue; + } + MOZ_ASSERT_UNREACHABLE("Opcode sequence includes commands after JMP"); + return; + } + if (*origBytes == 0x0f) { + COPY_CODES(1); + if (*origBytes == 0x1f) { + // nop (multibyte) + COPY_CODES(1); + if ((*origBytes & 0xc0) == 0x40 && + (*origBytes & 0x7) == 0x04) { + COPY_CODES(3); + } else { + MOZ_ASSERT_UNREACHABLE("Unrecognized opcode sequence"); + return; + } + } else if (*origBytes == 0x05) { + // syscall + COPY_CODES(1); + } else if (*origBytes == 0x10 || + *origBytes == 0x11) { + // SSE: movups xmm, xmm/m128 + // movups xmm/m128, xmm + COPY_CODES(1); + int nModRmSibBytes = CountModRmSib(origBytes); + if (nModRmSibBytes < 0) { + MOZ_ASSERT_UNREACHABLE("Unrecognized opcode sequence"); + return; + } else { + COPY_CODES(nModRmSibBytes); + } + } else if (*origBytes == 0x84) { + // je rel32 + ++origBytes; + --tramp; // overwrite the 0x0f we copied above + + if (!GenerateJump(tramp, + origBytes.ReadDisp32AsAbsolute(), + JumpType::Je)) { + return; + } + } else { + MOZ_ASSERT_UNREACHABLE("Unrecognized opcode sequence"); + return; + } + } else if (*origBytes >= 0x88 && *origBytes <= 0x8B) { + // various 32-bit MOVs + COPY_CODES(1); + int len = CountModRmSib(origBytes); + if (len < 0) { + MOZ_ASSERT_UNREACHABLE("Unrecognized MOV opcode sequence"); + return; + } + COPY_CODES(len); + } else if (*origBytes == 0x40 || + *origBytes == 0x41) { + // Plain REX or REX.B + COPY_CODES(1); + if ((*origBytes & 0xf0) == 0x50) { + // push/pop with Rx register + COPY_CODES(1); + } else if (*origBytes >= 0xb8 && *origBytes <= 0xbf) { + // mov r32, imm32 + COPY_CODES(5); + } else { + MOZ_ASSERT_UNREACHABLE("Unrecognized opcode sequence"); + return; + } + } else if (*origBytes == 0x44) { + // REX.R + COPY_CODES(1); + + // TODO: Combine with the "0x89" case below in the REX.W section + if (*origBytes == 0x89) { + // mov r/m32, r32 + COPY_CODES(1); + int len = CountModRmSib(origBytes); + if (len < 0) { + MOZ_ASSERT_UNREACHABLE("Unrecognized opcode sequence"); + return; + } + COPY_CODES(len); + } else { + MOZ_ASSERT_UNREACHABLE("Unrecognized opcode sequence"); + return; + } + } else if (*origBytes == 0x45) { + // REX.R & REX.B + COPY_CODES(1); + + if (*origBytes == 0x33) { + // xor r32, r32 + COPY_CODES(2); + } else { + MOZ_ASSERT_UNREACHABLE("Unrecognized opcode sequence"); + return; + } + } else if ((*origBytes & 0xfa) == 0x48) { + // REX.W | REX.WR | REX.WRB | REX.WB + COPY_CODES(1); + + if (*origBytes == 0x81 && + (origBytes[1] & 0xf8) == 0xe8) { + // sub r, dword + COPY_CODES(6); + } else if (*origBytes == 0x83 && + (origBytes[1] & 0xf8) == 0xe8) { + // sub r, byte + COPY_CODES(3); + } else if (*origBytes == 0x83 && + (origBytes[1] & (kMaskMod|kMaskReg)) == kModReg) { + // add r, byte + COPY_CODES(3); + } else if (*origBytes == 0x83 && + (origBytes[1] & 0xf8) == 0x60) { + // and [r+d], imm8 + COPY_CODES(5); + } else if (*origBytes == 0x2b && + (origBytes[1] & kMaskMod) == kModReg) { + // sub r64, r64 + COPY_CODES(2); + } else if (*origBytes == 0x85) { + // 85 /r => TEST r/m32, r32 + if ((origBytes[1] & 0xc0) == 0xc0) { + COPY_CODES(2); + } else { + MOZ_ASSERT_UNREACHABLE("Unrecognized opcode sequence"); + return; + } + } else if ((*origBytes & 0xfd) == 0x89) { + // MOV r/m64, r64 | MOV r64, r/m64 + BYTE reg; + int len = CountModRmSib(origBytes + 1, ®); + if (len < 0) { + MOZ_ASSERT(len == kModOperand64); + if (len != kModOperand64) { + return; + } + origBytes += 2; // skip the MOV and MOD R/M bytes + + // The instruction MOVs 64-bit data from a RIP-relative memory + // address (determined with a 32-bit offset from RIP) into a + // 64-bit register. + uintptr_t absAddr = origBytes.ReadDisp32AsAbsolute(); + + if (reg == kRegAx) { + // Destination is RAX. Encode instruction as MOVABS with a + // 64-bit absolute address as its immediate operand. + tramp.WriteByte(0xa1); + tramp.WritePointer(absAddr); + } else { + // The MOV must be done in two steps. First, we MOVABS the + // absolute 64-bit address into our target register. + // Then, we MOV from that address into the register + // using register-indirect addressing. + tramp.WriteByte(0xb8 + reg); + tramp.WritePointer(absAddr); + tramp.WriteByte(0x48); + tramp.WriteByte(0x8b); + tramp.WriteByte(BuildModRmByte(kModNoRegDisp, reg, reg)); + } + } else { + COPY_CODES(len+1); + } + } else if (*origBytes == 0xc7) { + // MOV r/m64, imm32 + if (origBytes[1] == 0x44) { + // MOV [r64+disp8], imm32 + // ModR/W + SIB + disp8 + imm32 + COPY_CODES(8); + } else { + MOZ_ASSERT_UNREACHABLE("Unrecognized opcode sequence"); + return; + } + } else if (*origBytes == 0xff) { + // JMP /4 + if ((origBytes[1] & 0xc0) == 0x0 && + (origBytes[1] & 0x07) == 0x5) { + origBytes += 2; + --tramp; // overwrite the REX.W/REX.RW we copied above + + if (!GenerateJump(tramp, origBytes.ChasePointerFromDisp(), + JumpType::Jmp)) { + return; + } + + foundJmp = true; + } else { + // not support yet! + MOZ_ASSERT_UNREACHABLE("Unrecognized opcode sequence"); + return; + } + } else if (*origBytes == 0x8d) { + // LEA reg, addr + if ((origBytes[1] & kMaskMod) == 0x0 && + (origBytes[1] & kMaskRm) == 0x5) { + // [rip+disp32] + // convert 32bit offset to 64bit direct and convert instruction + // to a simple 64-bit mov + BYTE reg = (origBytes[1] & kMaskReg) >> kRegFieldShift; + origBytes += 2; + uintptr_t absAddr = origBytes.ReadDisp32AsAbsolute(); + tramp.WriteByte(0xb8 + reg); // move + tramp.WritePointer(absAddr); + } else { + // Above we dealt with RIP-relative instructions. Any other + // operand form can simply be copied. + int len = CountModRmSib(origBytes + 1); + // We handled the kModOperand64 -- ie RIP-relative -- case above + MOZ_ASSERT(len > 0); + COPY_CODES(len + 1); + } + } else if (*origBytes == 0x63 && + (origBytes[1] & kMaskMod) == kModReg) { + // movsxd r64, r32 (move + sign extend) + COPY_CODES(2); + } else { + // not support yet! + MOZ_ASSERT_UNREACHABLE("Unrecognized opcode sequence"); + return; + } + } else if (*origBytes == 0x66) { + // operand override prefix + COPY_CODES(1); + // This is the same as the x86 version + if (*origBytes >= 0x88 && *origBytes <= 0x8B) { + // various MOVs + unsigned char b = origBytes[1]; + if (((b & 0xc0) == 0xc0) || + (((b & 0xc0) == 0x00) && + ((b & 0x07) != 0x04) && ((b & 0x07) != 0x05))) { + // REG=r, R/M=r or REG=r, R/M=[r] + COPY_CODES(2); + } else if ((b & 0xc0) == 0x40) { + if ((b & 0x07) == 0x04) { + // REG=r, R/M=[SIB + disp8] + COPY_CODES(4); + } else { + // REG=r, R/M=[r + disp8] + COPY_CODES(3); + } + } else { + // complex MOV, bail + MOZ_ASSERT_UNREACHABLE("Unrecognized MOV opcode sequence"); + return; + } + } else if (*origBytes == 0x44 && + origBytes[1] == 0x89) { + // mov word ptr [reg+disp8], reg + COPY_CODES(2); + int len = CountModRmSib(origBytes); + if (len < 0) { + // no way to support this yet. + MOZ_ASSERT_UNREACHABLE("Unrecognized opcode sequence"); + return; + } + COPY_CODES(len); + } + } else if ((*origBytes & 0xf0) == 0x50) { + // 1-byte push/pop + COPY_CODES(1); + } else if (*origBytes == 0x65) { + // GS prefix + // + // The entry of GetKeyState on Windows 10 has the following code. + // 65 48 8b 04 25 30 00 00 00 mov rax,qword ptr gs:[30h] + // (GS prefix + REX + MOV (0x8b) ...) + if (origBytes[1] == 0x48 && + (origBytes[2] >= 0x88 && origBytes[2] <= 0x8b)) { + COPY_CODES(3); + int len = CountModRmSib(origBytes); + if (len < 0) { + // no way to support this yet. + MOZ_ASSERT_UNREACHABLE("Unrecognized opcode sequence"); + return; + } + COPY_CODES(len); + } else { + MOZ_ASSERT_UNREACHABLE("Unrecognized opcode sequence"); + return; + } + } else if (*origBytes == 0x80 && + origBytes[1] == 0x3d) { + origBytes += 2; + + // cmp byte ptr [rip-relative address], imm8 + // We'll compute the absolute address and do the cmp in r11 + + // push r11 (to save the old value) + tramp.WriteByte(0x49); + tramp.WriteByte(0x53); + + uintptr_t absAddr = origBytes.ReadDisp32AsAbsolute(); + + // mov r11, absolute address + tramp.WriteByte(0x49); + tramp.WriteByte(0xbb); + tramp.WritePointer(absAddr); + + // cmp byte ptr [r11],... + tramp.WriteByte(0x41); + tramp.WriteByte(0x80); + tramp.WriteByte(0x3b); + + // ...imm8 + COPY_CODES(1); + + // pop r11 (doesn't affect the flags from the cmp) + tramp.WriteByte(0x49); + tramp.WriteByte(0x5b); + } else if (*origBytes == 0x90) { + // nop + COPY_CODES(1); + } else if ((*origBytes & 0xf8) == 0xb8) { + // MOV r32, imm32 + COPY_CODES(5); + } else if (*origBytes == 0x33) { + // xor r32, r/m32 + COPY_CODES(2); + } else if (*origBytes == 0xf6) { + // test r/m8, imm8 (used by ntdll on Windows 10 x64) + // (no flags are affected by near jmp since there is no task switch, + // so it is ok for a jmp to be written immediately after a test) + BYTE subOpcode = 0; + int nModRmSibBytes = CountModRmSib(origBytes + 1, &subOpcode); + if (nModRmSibBytes < 0 || subOpcode != 0) { + // Unsupported + MOZ_ASSERT_UNREACHABLE("Unrecognized opcode sequence"); + return; + } + COPY_CODES(2 + nModRmSibBytes); + } else if (*origBytes == 0x85) { + // test r/m32, r32 + int nModRmSibBytes = CountModRmSib(origBytes + 1); + if (nModRmSibBytes < 0) { + MOZ_ASSERT_UNREACHABLE("Unrecognized opcode sequence"); + return; + } + COPY_CODES(1 + nModRmSibBytes); + } else if (*origBytes == 0xd1 && + (origBytes[1] & kMaskMod) == kModReg) { + // bit shifts/rotates : (SA|SH|RO|RC)(R|L) r32 + // (e.g. 0xd1 0xe0 is SAL, 0xd1 0xc8 is ROR) + COPY_CODES(2); + } else if (*origBytes == 0xc3) { + // ret + COPY_CODES(1); + } else if (*origBytes == 0xcc) { + // int 3 + COPY_CODES(1); + } else if (*origBytes == 0xe8 || + *origBytes == 0xe9) { + // CALL (0xe8) or JMP (0xe9) 32bit offset + foundJmp = *origBytes == 0xe9; + ++origBytes; + + if (!GenerateJump(tramp, origBytes.ReadDisp32AsAbsolute(), + foundJmp ? JumpType::Jmp : JumpType::Call)) { + return; + } + } else if (*origBytes == 0x74 || // je rel8 (0x74) + *origBytes == 0x75) { // jne rel8 (0x75) + uint8_t offset = origBytes[1]; + auto jumpType = JumpType::Je; + if (*origBytes == 0x75) { + jumpType = JumpType::Jne; + } + + origBytes += 2; + + if (!GenerateJump(tramp, origBytes.OffsetToAbsolute(offset), jumpType)) { + return; + } + } else if (*origBytes == 0xff) { + if ((origBytes[1] & (kMaskMod|kMaskReg)) == 0xf0) { + // push r64 + COPY_CODES(2); + } else if (origBytes[1] == 0x25) { + // jmp absolute indirect m32 + foundJmp = true; + + origBytes += 2; + + uintptr_t jmpDest = origBytes.ChasePointerFromDisp(); + + if (!GenerateJump(tramp, jmpDest, JumpType::Jmp)) { + return; + } + } else if ((origBytes[1] & (kMaskMod|kMaskReg)) == BuildModRmByte(kModReg, 2, 0)) { + // CALL reg (ff nn) + COPY_CODES(2); + } else { + MOZ_ASSERT_UNREACHABLE("Unrecognized opcode sequence"); + return; + } + } else if (*origBytes == 0x83 && + (origBytes[1] & 0xf8) == 0x60) { + // and [r+d], imm8 + COPY_CODES(5); + } else if (*origBytes == 0xc6) { + // mov [r+d], imm8 + int len = CountModRmSib(origBytes + 1); + if (len < 0) { + // RIP-relative not yet supported + MOZ_ASSERT_UNREACHABLE("Unrecognized opcode sequence"); + return; + } + COPY_CODES(len + 1); + } else { + MOZ_ASSERT_UNREACHABLE("Unrecognized opcode sequence"); + return; + } + } +#else +#error "Unknown processor type" +#endif + + if (origBytes.GetOffset() > 100) { + //printf ("Too big!"); + return; + } + +#if defined(_M_IX86) + if (pJmp32 >= 0) { + // Jump directly to the original target of the jump instead of jumping to the + // original function. + // Adjust jump target displacement to jump location in the trampoline. + tramp.AdjustDisp32AtOffset(pJmp32 + 1, origBytes.GetBaseAddress()); + } else { + tramp.WriteByte(0xe9); // jmp + tramp.WriteDisp32(origBytes.GetAddress()); + } +#elif defined(_M_X64) + // If the we found a Jmp, we don't need to add another instruction. However, + // if we found a _conditional_ jump or a CALL (or no control operations + // at all) then we still need to run the rest of aOriginalFunction. + if (!foundJmp) { + if (!GenerateJump(tramp, origBytes.GetAddress(), JumpType::Jmp)) { + return; + } + } +#endif + + // The trampoline is now complete. + *aOutTramp = tramp.EndExecutableCode(); + if (!(*aOutTramp)) { + return; + } + + WritableTargetFunction target(origBytes.Promote()); + if (!target) { + return; + } + +#if defined(_M_IX86) + // now modify the original bytes + target.WriteByte(0xe9); //jmp + target.WriteDisp32(aDest); // hook displacement +#elif defined(_M_X64) + // mov r11, address + target.WriteByte(0x49); + target.WriteByte(0xbb); + target.WritePointer(aDest); + + // jmp r11 + target.WriteByte(0x41); + target.WriteByte(0xff); + target.WriteByte(0xe3); +#endif + + target.Commit(); + } +}; + +} // namespace interceptor +} // namespace mozilla + +#endif // mozilla_interceptor_PatcherDetour_h + diff --git a/mozglue/misc/interceptor/PatcherNopSpace.h b/mozglue/misc/interceptor/PatcherNopSpace.h new file mode 100644 index 000000000000..496dcdd5ea35 --- /dev/null +++ b/mozglue/misc/interceptor/PatcherNopSpace.h @@ -0,0 +1,217 @@ +/* -*- 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_interceptor_PatcherNopSpace_h +#define mozilla_interceptor_PatcherNopSpace_h + +#if defined(_M_IX86) + +#include "mozilla/interceptor/PatcherBase.h" + +namespace mozilla { +namespace interceptor { + +template +class WindowsDllNopSpacePatcher final : public WindowsDllPatcherBase +{ + // For remembering the addresses of functions we've patched. + mozilla::Vector mPatchedFns; + +public: + template + explicit WindowsDllNopSpacePatcher(Args... aArgs) + : WindowsDllPatcherBase(mozilla::Forward(aArgs)...) + {} + + ~WindowsDllNopSpacePatcher() + { + Clear(); + } + + WindowsDllNopSpacePatcher(const WindowsDllNopSpacePatcher&) = delete; + WindowsDllNopSpacePatcher(WindowsDllNopSpacePatcher&&) = delete; + WindowsDllNopSpacePatcher& operator=(const WindowsDllNopSpacePatcher&) = delete; + WindowsDllNopSpacePatcher& operator=(WindowsDllNopSpacePatcher&&) = delete; + + void Clear() + { + // Restore the mov edi, edi to the beginning of each function we patched. + + for (auto&& ptr : mPatchedFns) { + WritableTargetFunction fn(mVMPolicy, + reinterpret_cast(ptr), + sizeof(uint16_t)); + if (!fn) { + continue; + } + + // mov edi, edi + fn.WriteShort(0xff8b); + fn.Commit(); + } + + mPatchedFns.clear(); + } + + /** + * NVIDIA Optimus drivers utilize Microsoft Detours 2.x to patch functions + * in our address space. There is a bug in Detours 2.x that causes it to + * patch at the wrong address when attempting to detour code that is already + * NOP space patched. This function is an effort to detect the presence of + * this NVIDIA code in our address space and disable NOP space patching if it + * is. We also check AppInit_DLLs since this is the mechanism that the Optimus + * drivers use to inject into our process. + */ + static bool IsCompatible() + { + // These DLLs are known to have bad interactions with this style of patching + const wchar_t* kIncompatibleDLLs[] = { + L"detoured.dll", + L"_etoured.dll", + L"nvd3d9wrap.dll", + L"nvdxgiwrap.dll" + }; + // See if the infringing DLLs are already loaded + for (unsigned int i = 0; i < mozilla::ArrayLength(kIncompatibleDLLs); ++i) { + if (GetModuleHandleW(kIncompatibleDLLs[i])) { + return false; + } + } + if (GetModuleHandleW(L"user32.dll")) { + // user32 is loaded but the infringing DLLs are not, assume we're safe to + // proceed. + return true; + } + // If user32 has not loaded yet, check AppInit_DLLs to ensure that Optimus + // won't be loaded once user32 is initialized. + HKEY hkey = NULL; + if (!RegOpenKeyExW(HKEY_LOCAL_MACHINE, + L"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Windows", + 0, KEY_QUERY_VALUE, &hkey)) { + nsAutoRegKey key(hkey); + DWORD numBytes = 0; + const wchar_t kAppInitDLLs[] = L"AppInit_DLLs"; + // Query for required buffer size + LONG status = RegQueryValueExW(hkey, kAppInitDLLs, nullptr, + nullptr, nullptr, &numBytes); + mozilla::UniquePtr data; + if (!status) { + // Allocate the buffer and query for the actual data + data = mozilla::MakeUnique((numBytes + 1) / sizeof(wchar_t)); + status = RegQueryValueExW(hkey, kAppInitDLLs, nullptr, + nullptr, (LPBYTE)data.get(), &numBytes); + } + if (!status) { + // For each token, split up the filename components and then check the + // name of the file. + const wchar_t kDelimiters[] = L", "; + wchar_t* tokenContext = nullptr; + wchar_t* token = wcstok_s(data.get(), kDelimiters, &tokenContext); + while (token) { + wchar_t fname[_MAX_FNAME] = {0}; + if (!_wsplitpath_s(token, nullptr, 0, nullptr, 0, + fname, mozilla::ArrayLength(fname), + nullptr, 0)) { + // nvinit.dll is responsible for bootstrapping the DLL injection, so + // that is the library that we check for here + const wchar_t kNvInitName[] = L"nvinit"; + if (!_wcsnicmp(fname, kNvInitName, + mozilla::ArrayLength(kNvInitName))) { + return false; + } + } + token = wcstok_s(nullptr, kDelimiters, &tokenContext); + } + } + } + return true; + } + + bool AddHook(FARPROC aTargetFn, intptr_t aHookDest, void** aOrigFunc) + { + if (!IsCompatible()) { +#if defined(MOZILLA_INTERNAL_API) + NS_WARNING("NOP space patching is unavailable for compatibility reasons"); +#endif + return false; + } + + MOZ_ASSERT(aTargetFn); + if (!aTargetFn) { + return false; + } + + ReadOnlyTargetFunction readOnlyTargetFn( + ResolveRedirectedAddress(aTargetFn)); + + if (!WriteHook(readOnlyTargetFn, aHookDest, aOrigFunc)) { + return false; + } + + mPatchedFns.append(reinterpret_cast(readOnlyTargetFn.GetBaseAddress())); + return true; + } + + bool WriteHook(const ReadOnlyTargetFunction& aFn, + intptr_t aHookDest, void** aOrigFunc) + { + // Ensure we can read and write starting at fn - 5 (for the long jmp we're + // going to write) and ending at fn + 2 (for the short jmp up to the long + // jmp). These bytes may span two pages with different protection. + WritableTargetFunction writableFn(aFn.Promote(7, -5)); + if (!writableFn) { + return false; + } + + // Check that the 5 bytes before the function are NOP's or INT 3's, + const uint8_t nopOrBp[] = { 0x90, 0xCC }; + if (!writableFn.VerifyValuesAreOneOf(nopOrBp)) { + return false; + } + + // ... and that the first 2 bytes of the function are mov(edi, edi). + // There are two ways to encode the same thing: + // + // 0x89 0xff == mov r/m, r + // 0x8b 0xff == mov r, r/m + // + // where "r" is register and "r/m" is register or memory. + // Windows seems to use 0x8B 0xFF. We include 0x89 0xFF out of paranoia. + + // (These look backwards because little-endian) + const uint16_t possibleEncodings[] = { 0xFF8B, 0xFF89 }; + if (!writableFn.VerifyValuesAreOneOf(possibleEncodings, 5)) { + return false; + } + + // Write a long jump into the space above the function. + writableFn.WriteByte(0xe9); // jmp + if (!writableFn) { + return false; + } + + writableFn.WriteDisp32(aHookDest); // target + if (!writableFn) { + return false; + } + + // Set aOrigFunc here, because after this point, aHookDest might be called, + // and aHookDest might use the aOrigFunc pointer. + *aOrigFunc = reinterpret_cast(writableFn.GetCurrentAddress() + + sizeof(uint16_t)); + + // Short jump up into our long jump. + writableFn.WriteShort(0xF9EB); // jmp $-5 + return writableFn.Commit(); + } +}; + +} // namespace interceptor +} // namespace mozilla + +#endif // defined(_M_IX86) + +#endif // mozilla_interceptor_PatcherNopSpace_h diff --git a/mozglue/misc/interceptor/TargetFunction.h b/mozglue/misc/interceptor/TargetFunction.h new file mode 100644 index 000000000000..a3391fbb93e5 --- /dev/null +++ b/mozglue/misc/interceptor/TargetFunction.h @@ -0,0 +1,552 @@ +/* -*- 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_interceptor_TargetFunction_h +#define mozilla_interceptor_TargetFunction_h + +#include "mozilla/Assertions.h" +#include "mozilla/Attributes.h" +#include "mozilla/Maybe.h" +#include "mozilla/Types.h" + +#include + +namespace mozilla { +namespace interceptor { + +template +class MOZ_STACK_CLASS WritableTargetFunction final +{ +public: + /** + * Used to initialize an invalid WritableTargetFunction, thus signalling an error. + */ + explicit WritableTargetFunction(const MMPolicy& aMMPolicy) + : mMMPolicy(aMMPolicy) + , mFunc(0) + , mNumBytes(0) + , mOffset(0) + , mStartWriteOffset(0) + , mPrevProt(0) + , mAccumulatedStatus(false) + { + } + + WritableTargetFunction(const MMPolicy& aMMPolicy, uintptr_t aFunc, + size_t aNumBytes) + : mMMPolicy(aMMPolicy) + , mFunc(aFunc) + , mNumBytes(aNumBytes) + , mOffset(0) + , mStartWriteOffset(0) + , mPrevProt(0) + , mAccumulatedStatus(true) + { + aMMPolicy.Protect(reinterpret_cast(aFunc), aNumBytes, + PAGE_EXECUTE_READWRITE, &mPrevProt); + } + + WritableTargetFunction(WritableTargetFunction&& aOther) + : mMMPolicy(aOther.mMMPolicy) + , mFunc(aOther.mFunc) + , mNumBytes(aOther.mNumBytes) + , mOffset(aOther.mOffset) + , mStartWriteOffset(aOther.mStartWriteOffset) + , mPrevProt(aOther.mPrevProt) + , mLocalBytes(Move(aOther.mLocalBytes)) + , mAccumulatedStatus(aOther.mAccumulatedStatus) + { + aOther.mPrevProt = 0; + aOther.mAccumulatedStatus = false; + } + + ~WritableTargetFunction() + { + if (!mPrevProt) { + return; + } + + MOZ_ASSERT(mLocalBytes.empty(), "Did you forget to call Commit?"); + + DebugOnly ok = mMMPolicy.Protect(reinterpret_cast(mFunc), + mNumBytes, mPrevProt, &mPrevProt); + MOZ_ASSERT(ok); + } + + WritableTargetFunction(const WritableTargetFunction&) = delete; + WritableTargetFunction& operator=(const WritableTargetFunction&) = delete; + WritableTargetFunction& operator=(WritableTargetFunction&&) = delete; + + /** + * @return true if data was successfully committed. + */ + bool Commit() + { + if (!(*this)) { + return false; + } + + if (mLocalBytes.empty()) { + // Nothing to commit, treat like success + return true; + } + + bool ok = mMMPolicy.Write(reinterpret_cast(mFunc + mStartWriteOffset), + mLocalBytes.begin(), mLocalBytes.length()); + if (!ok) { + return false; + } + + mMMPolicy.FlushInstructionCache(); + mLocalBytes.clear(); + return true; + } + + explicit operator bool() const + { + return mPrevProt && mAccumulatedStatus; + } + + void WriteByte(const uint8_t& aValue) + { + if (!mLocalBytes.append(aValue)) { + mAccumulatedStatus = false; + return; + } + + mOffset += sizeof(uint8_t); + } + + Maybe ReadByte() + { + // Reading is only permitted prior to any writing + MOZ_ASSERT(mOffset == mStartWriteOffset); + if (mOffset > mStartWriteOffset) { + mAccumulatedStatus = false; + return Nothing(); + } + + uint8_t value; + if (!mMMPolicy.Read(&value, reinterpret_cast(mFunc + mOffset), + sizeof(uint8_t))) { + mAccumulatedStatus = false; + return Nothing(); + } + + mOffset += sizeof(uint8_t); + mStartWriteOffset += sizeof(uint8_t); + return Some(value); + } + + void WriteShort(const uint16_t& aValue) + { + if (!mLocalBytes.append(reinterpret_cast(&aValue), + sizeof(uint16_t))) { + mAccumulatedStatus = false; + return; + } + + mOffset += sizeof(uint16_t); + } + + void WriteDisp32(const uintptr_t aAbsTarget) + { + intptr_t diff = static_cast(aAbsTarget) - + static_cast(mFunc + mOffset + sizeof(int32_t)); + + CheckedInt checkedDisp(diff); + MOZ_ASSERT(checkedDisp.isValid()); + if (!checkedDisp.isValid()) { + mAccumulatedStatus = false; + return; + } + + int32_t disp = checkedDisp.value(); + if (!mLocalBytes.append(reinterpret_cast(&disp), sizeof(int32_t))) { + mAccumulatedStatus = false; + return; + } + + mOffset += sizeof(int32_t); + } + + void WritePointer(const uintptr_t aAbsTarget) + { + if (!mLocalBytes.append(reinterpret_cast(&aAbsTarget), + sizeof(uintptr_t))) { + mAccumulatedStatus = false; + return; + } + + mOffset += sizeof(uintptr_t); + } + + /** + * @param aValues N-sized array of type T that specifies the set of values + * that are permissible in the first M bytes of the target + * function at aOffset. + * @return true if M values of type T in the function are members of the + * set specified by aValues. + */ + template + bool VerifyValuesAreOneOf(const T (&aValues)[N], const uint8_t aOffset = 0) + { + T buf[M]; + if (!mMMPolicy.Read(buf, reinterpret_cast(mFunc + mOffset + aOffset), + M * sizeof(T))) { + return false; + } + + for (auto&& fnValue : buf) { + bool match = false; + for (auto&& testValue : aValues) { + match |= (fnValue == testValue); + } + + if (!match) { + return false; + } + } + + return true; + } + + uintptr_t GetCurrentAddress() const + { + return mFunc + mOffset; + } + +private: + const MMPolicy& mMMPolicy; + const uintptr_t mFunc; + const size_t mNumBytes; + uint32_t mOffset; + uint32_t mStartWriteOffset; + uint32_t mPrevProt; +#if defined(_M_IX86) + static const size_t kInlineStorage = 16; +#elif defined(_M_X64) + static const size_t kInlineStorage = 32; +#endif + Vector mLocalBytes; + bool mAccumulatedStatus; +}; + +template +class ReadOnlyTargetBytes; + +template <> +class ReadOnlyTargetBytes +{ +public: + ReadOnlyTargetBytes(const MMPolicyInProcess& aMMPolicy, const void* aBase) + : mMMPolicy(aMMPolicy) + , mBase(reinterpret_cast(aBase)) + { + } + + ReadOnlyTargetBytes(ReadOnlyTargetBytes&& aOther) + : mMMPolicy(aOther.mMMPolicy) + , mBase(aOther.mBase) + { + } + + ReadOnlyTargetBytes(const ReadOnlyTargetBytes& aOther, + const uint32_t aOffsetFromOther = 0) + : mMMPolicy(aOther.mMMPolicy) + , mBase(aOther.mBase + aOffsetFromOther) + { + } + + void EnsureLimit(uint32_t aDesiredLimit) + { + // In the out-proc case we use this function to read the target function's + // bytes in the other process into a local buffer. We don't need that for + // the in-process case because we already have direct access to our target + // function's bytes. + } + + bool IsValidAtOffset(const int8_t aOffset) const + { + if (!aOffset) { + return true; + } + + uintptr_t base = reinterpret_cast(mBase); + uintptr_t adjusted = base + aOffset; + uint32_t pageSize = mMMPolicy.GetPageSize(); + + // If |adjusted| is within the same page as |mBase|, we're still valid + if ((base / pageSize) == (adjusted / pageSize)) { + return true; + } + + // Otherwise, let's query |adjusted| + return mMMPolicy.IsPageAccessible(reinterpret_cast(adjusted)); + } + + const uint8_t* Get() const + { + return mBase; + } + + const MMPolicyInProcess& GetMMPolicy() const + { + return mMMPolicy; + } + + ReadOnlyTargetBytes& operator=(const ReadOnlyTargetBytes&) = delete; + ReadOnlyTargetBytes& operator=(ReadOnlyTargetBytes&&) = delete; + +private: + const MMPolicyInProcess& mMMPolicy; + uint8_t const * const mBase; +}; + +template +class MOZ_STACK_CLASS ReadOnlyTargetFunction final +{ + template + class TargetBytesPtr + { + public: + typedef TargetBytesPtr Type; + + static Type Make(const TargetMMPolicy& aMMPolicy, const void* aFunc) + { + return Move(TargetBytesPtr(aMMPolicy, aFunc)); + } + + static Type CopyFromOffset(const TargetBytesPtr& aOther, + const uint32_t aOffsetFromOther) + { + return Move(TargetBytesPtr(aOther, aOffsetFromOther)); + } + + ReadOnlyTargetBytes* operator->() + { + return &mTargetBytes; + } + + TargetBytesPtr(TargetBytesPtr&& aOther) + : mTargetBytes(Move(aOther.mTargetBytes)) + { + } + + TargetBytesPtr(const TargetBytesPtr& aOther) + : mTargetBytes(aOther.mTargetBytes) + { + } + + TargetBytesPtr& operator=(const TargetBytesPtr&) = delete; + TargetBytesPtr& operator=(TargetBytesPtr&&) = delete; + + private: + TargetBytesPtr(const TargetMMPolicy& aMMPolicy, const void* aFunc) + : mTargetBytes(aMMPolicy, aFunc) + { + } + + TargetBytesPtr(const TargetBytesPtr& aOther, + const uint32_t aOffsetFromOther) + : mTargetBytes(aOther.mTargetBytes, aOffsetFromOther) + { + } + + ReadOnlyTargetBytes mTargetBytes; + }; + +public: + ReadOnlyTargetFunction(const MMPolicy& aMMPolicy, const void* aFunc) + : mTargetBytes(TargetBytesPtr::Make(aMMPolicy, aFunc)) + , mOffset(0) + { + } + + ReadOnlyTargetFunction(const MMPolicy& aMMPolicy, uintptr_t aFunc) + : mTargetBytes(TargetBytesPtr::Make(aMMPolicy, + reinterpret_cast(aFunc))) + , mOffset(0) + { + } + + ReadOnlyTargetFunction(ReadOnlyTargetFunction&& aOther) + : mTargetBytes(Move(aOther.mTargetBytes)) + , mOffset(aOther.mOffset) + { + } + + ReadOnlyTargetFunction& operator=(const ReadOnlyTargetFunction&) = delete; + ReadOnlyTargetFunction& operator=(ReadOnlyTargetFunction&&) = delete; + + ~ReadOnlyTargetFunction() = default; + + ReadOnlyTargetFunction operator+(const uint32_t aOffset) const + { + return ReadOnlyTargetFunction(*this, mOffset + aOffset); + } + + uintptr_t GetBaseAddress() const + { + return reinterpret_cast(mTargetBytes->Get()); + } + + uintptr_t GetAddress() const + { + return reinterpret_cast(mTargetBytes->Get() + mOffset); + } + + uintptr_t AsEncodedPtr() const + { + return EncodePtr(const_cast(mTargetBytes->Get() + mOffset)); + } + + static uintptr_t EncodePtr(void* aPtr) + { + return reinterpret_cast(::EncodePointer(aPtr)); + } + + static uintptr_t DecodePtr(uintptr_t aEncodedPtr) + { + return reinterpret_cast( + ::DecodePointer(reinterpret_cast(aEncodedPtr))); + } + + uint8_t const & operator*() const + { + mTargetBytes->EnsureLimit(mOffset); + return *(mTargetBytes->Get() + mOffset); + } + + uint8_t const & operator[](uint32_t aIndex) const + { + mTargetBytes->EnsureLimit(mOffset + aIndex); + return *(mTargetBytes->Get() + mOffset + aIndex); + } + + ReadOnlyTargetFunction& operator++() + { + ++mOffset; + return *this; + } + + ReadOnlyTargetFunction& operator+=(uint32_t aDelta) + { + mOffset += aDelta; + return *this; + } + + uint32_t GetOffset() const + { + return mOffset; + } + + uintptr_t ReadDisp32AsAbsolute() + { + mTargetBytes->EnsureLimit(mOffset + sizeof(int32_t)); + int32_t disp = *reinterpret_cast(mTargetBytes->Get() + mOffset); + uintptr_t result = reinterpret_cast( + mTargetBytes->Get() + mOffset + sizeof(int32_t) + disp); + mOffset += sizeof(int32_t); + return result; + } + + uintptr_t OffsetToAbsolute(const uint8_t aOffset) const + { + return reinterpret_cast(mTargetBytes->Get() + mOffset + aOffset); + } + + /** + * This method promotes the code referenced by this object to be writable. + * + * @param aLen The length of the function's code to make writable. If set + * to zero, this object's current offset is used as the length. + * @param aOffset The result's base address will be offset from this + * object's base address by |aOffset| bytes. This value may be + * negative. + */ + WritableTargetFunction Promote(const uint32_t aLen = 0, + const int8_t aOffset = 0) const + { + const uint32_t effectiveLength = aLen ? aLen : mOffset; + MOZ_RELEASE_ASSERT(effectiveLength, "Cannot Promote a zero-length function"); + + if (!mTargetBytes->IsValidAtOffset(aOffset)) { + return WritableTargetFunction(mTargetBytes->GetMMPolicy()); + } + + WritableTargetFunction result( + mTargetBytes->GetMMPolicy(), + reinterpret_cast(mTargetBytes->Get() + aOffset), + effectiveLength); + + return Move(result); + } + +private: + template + struct ChasePointerHelper + { + template + static T Result(const MMPolicy&, T aValue) + { + return aValue; + } + }; + + template + struct ChasePointerHelper + { + template + static auto Result(const MMPolicy& aPolicy, T* aValue) + { + ReadOnlyTargetFunction ptr(aPolicy, aValue); + return ptr.ChasePointer(); + } + }; + +public: + // Keep chasing pointers until T is not a pointer type anymore + template + auto ChasePointer() + { + mTargetBytes->EnsureLimit(mOffset + sizeof(T)); + const typename RemoveCV::Type result = *reinterpret_cast::Type*>(mTargetBytes->Get() + mOffset); + return ChasePointerHelper::Type>::Result(mTargetBytes->GetMMPolicy(), result); + } + + uintptr_t ChasePointerFromDisp() + { + uintptr_t ptrFromDisp = ReadDisp32AsAbsolute(); + ReadOnlyTargetFunction ptr(mTargetBytes->GetMMPolicy(), + reinterpret_cast(ptrFromDisp)); + return ptr.template ChasePointer(); + } + +private: + ReadOnlyTargetFunction(const ReadOnlyTargetFunction& aOther) + : mTargetBytes(aOther.mTargetBytes) + , mOffset(aOther.mOffset) + { + } + + ReadOnlyTargetFunction(const ReadOnlyTargetFunction& aOther, + const uint32_t aOffsetFromOther) + : mTargetBytes(TargetBytesPtr::CopyFromOffset(aOther.mTargetBytes, + aOffsetFromOther)) + , mOffset(0) + { + } + +private: + mutable typename TargetBytesPtr::Type mTargetBytes; + uint32_t mOffset; +}; + +} // namespace interceptor +} // namespace mozilla + +#endif // mozilla_interceptor_TargetFunction_h diff --git a/mozglue/misc/interceptor/Trampoline.h b/mozglue/misc/interceptor/Trampoline.h new file mode 100644 index 000000000000..7224a31f9b2f --- /dev/null +++ b/mozglue/misc/interceptor/Trampoline.h @@ -0,0 +1,369 @@ +/* -*- 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_interceptor_Trampoline_h +#define mozilla_interceptor_Trampoline_h + +#include "mozilla/Assertions.h" +#include "mozilla/Attributes.h" +#include "mozilla/Maybe.h" +#include "mozilla/Types.h" + +namespace mozilla { +namespace interceptor { + +template +class MOZ_STACK_CLASS Trampoline final +{ +public: + Trampoline(const MMPolicy* aMMPolicy, uint8_t* const aLocalBase, + const uintptr_t aRemoteBase, const uint32_t aChunkSize) + : mMMPolicy(aMMPolicy) + , mPrevLocalProt(0) + , mLocalBase(aLocalBase) + , mRemoteBase(aRemoteBase) + , mOffset(0) + , mExeOffset(0) + , mMaxOffset(aChunkSize) + , mAccumulatedStatus(true) + { + ::VirtualProtect(aLocalBase, aChunkSize, PAGE_EXECUTE_READWRITE, + &mPrevLocalProt); + } + + Trampoline(Trampoline&& aOther) + : mMMPolicy(aOther.mMMPolicy) + , mPrevLocalProt(aOther.mPrevLocalProt) + , mLocalBase(aOther.mLocalBase) + , mRemoteBase(aOther.mRemoteBase) + , mOffset(aOther.mOffset) + , mExeOffset(aOther.mExeOffset) + , mMaxOffset(aOther.mMaxOffset) + , mAccumulatedStatus(aOther.mAccumulatedStatus) + { + aOther.mPrevLocalProt = 0; + aOther.mAccumulatedStatus = false; + } + + MOZ_IMPLICIT Trampoline(decltype(nullptr)) + : mMMPolicy(nullptr) + , mPrevLocalProt(0) + , mLocalBase(nullptr) + , mRemoteBase(0) + , mOffset(0) + , mExeOffset(0) + , mMaxOffset(0) + , mAccumulatedStatus(false) + { + } + + Trampoline(const Trampoline&) = delete; + Trampoline& operator=(const Trampoline&) = delete; + Trampoline&& operator=(Trampoline&&) = delete; + + ~Trampoline() + { + if (!mLocalBase || !mPrevLocalProt) { + return; + } + + ::VirtualProtect(mLocalBase, mMaxOffset, mPrevLocalProt, &mPrevLocalProt); + } + + explicit operator bool() const + { + return mLocalBase && mRemoteBase && mPrevLocalProt && mAccumulatedStatus; + } + + void WriteByte(uint8_t aValue) + { + if (mOffset >= mMaxOffset) { + mAccumulatedStatus = false; + return; + } + + *(mLocalBase + mOffset) = aValue; + ++mOffset; + } + + void WriteInteger(int32_t aValue) + { + if (mOffset + sizeof(int32_t) > mMaxOffset) { + mAccumulatedStatus = false; + return; + } + + *reinterpret_cast(mLocalBase + mOffset) = aValue; + mOffset += sizeof(int32_t); + } + + void WritePointer(uintptr_t aValue) + { + if (mOffset + sizeof(uintptr_t) > mMaxOffset) { + mAccumulatedStatus = false; + return; + } + + *reinterpret_cast(mLocalBase + mOffset) = aValue; + mOffset += sizeof(uintptr_t); + } + + void WriteEncodedPointer(void* aValue) + { + uintptr_t encoded = ReadOnlyTargetFunction::EncodePtr(aValue); + WritePointer(encoded); + } + + Maybe ReadPointer() + { + if (mOffset + sizeof(uintptr_t) > mMaxOffset) { + mAccumulatedStatus = false; + return Nothing(); + } + + auto result = Some(*reinterpret_cast(mLocalBase + mOffset)); + mOffset += sizeof(uintptr_t); + return Move(result); + } + + Maybe ReadEncodedPointer() + { + Maybe encoded(ReadPointer()); + if (!encoded) { + return encoded; + } + + return Some(ReadOnlyTargetFunction::DecodePtr(encoded.value())); + } + + void WriteDisp32(uintptr_t aAbsTarget) + { + if (mOffset + sizeof(int32_t) > mMaxOffset) { + mAccumulatedStatus = false; + return; + } + + // This needs to be computed from the remote location + intptr_t remoteTrampPosition = static_cast(mRemoteBase + mOffset); + + intptr_t diff = static_cast(aAbsTarget) - + (remoteTrampPosition + sizeof(int32_t)); + + CheckedInt checkedDisp(diff); + MOZ_ASSERT(checkedDisp.isValid()); + if (!checkedDisp.isValid()) { + mAccumulatedStatus = false; + return; + } + + int32_t disp = checkedDisp.value(); + *reinterpret_cast(mLocalBase + mOffset) = disp; + mOffset += sizeof(int32_t); + } + +#if defined(_M_IX86) + // 32-bit only + void AdjustDisp32AtOffset(uint32_t aOffset, uintptr_t aAbsTarget) + { + uint32_t effectiveOffset = mExeOffset + aOffset; + + if (effectiveOffset + sizeof(int32_t) > mMaxOffset) { + mAccumulatedStatus = false; + return; + } + + intptr_t diff = static_cast(aAbsTarget) - + static_cast(mRemoteBase + mExeOffset); + *reinterpret_cast(mLocalBase + effectiveOffset) += diff; + } +#endif // defined(_M_IX86) + + void CopyFrom(uintptr_t aOrigBytes, uint32_t aNumBytes) + { + if (!mMMPolicy || mOffset + aNumBytes > mMaxOffset) { + mAccumulatedStatus = false; + return; + } + + if (!mMMPolicy->Read(mLocalBase + mOffset, + reinterpret_cast(aOrigBytes), aNumBytes)) { + mAccumulatedStatus = false; + return; + } + + mOffset += aNumBytes; + } + + void Rewind() + { + mOffset = 0; + } + + uintptr_t GetCurrentRemoteAddress() const + { + return mRemoteBase + mOffset; + } + + void StartExecutableCode() + { + MOZ_ASSERT(!mExeOffset); + mExeOffset = mOffset; + } + + void* EndExecutableCode() const + { + if (!mAccumulatedStatus) { + return nullptr; + } + + // This must always return the start address the executable code + // *in the target process* + return reinterpret_cast(mRemoteBase + mExeOffset); + } + + Trampoline& operator--() + { + MOZ_ASSERT(mOffset); + --mOffset; + return *this; + } + +private: + const MMPolicy* mMMPolicy; + DWORD mPrevLocalProt; + uint8_t* const mLocalBase; + const uintptr_t mRemoteBase; + uint32_t mOffset; + uint32_t mExeOffset; + const uint32_t mMaxOffset; + bool mAccumulatedStatus; +}; + +template +class MOZ_STACK_CLASS TrampolineCollection final +{ +public: + class MOZ_STACK_CLASS TrampolineIterator final + { + public: + Trampoline operator*() + { + uint32_t offset = mCurTramp * mCollection.mTrampSize; + return Trampoline(nullptr, + mCollection.mLocalBase + offset, + mCollection.mRemoteBase + offset, + mCollection.mTrampSize); + } + + TrampolineIterator& operator++() + { + ++mCurTramp; + return *this; + } + + bool operator!=(const TrampolineIterator& aOther) const + { + return mCurTramp != aOther.mCurTramp; + } + + private: + explicit TrampolineIterator( + const TrampolineCollection& aCollection, + const uint32_t aCurTramp = 0) + : mCollection(aCollection) + , mCurTramp(aCurTramp) + { + } + + const TrampolineCollection& mCollection; + uint32_t mCurTramp; + + friend class TrampolineCollection; + }; + + explicit TrampolineCollection(const MMPolicy& aMMPolicy) + : mMMPolicy(aMMPolicy) + , mLocalBase(0) + , mRemoteBase(0) + , mTrampSize(0) + , mNumTramps(0) + , mPrevProt(0) + { + } + + TrampolineCollection(const MMPolicy& aMMPolicy, uint8_t* const aLocalBase, + const uintptr_t aRemoteBase, const uint32_t aTrampSize, + const uint32_t aNumTramps) + : mMMPolicy(aMMPolicy) + , mLocalBase(aLocalBase) + , mRemoteBase(aRemoteBase) + , mTrampSize(aTrampSize) + , mNumTramps(aNumTramps) + , mPrevProt(0) + { + if (!aNumTramps) { + return; + } + + DebugOnly ok = mMMPolicy.Protect(aLocalBase, aNumTramps * aTrampSize, + PAGE_EXECUTE_READWRITE, &mPrevProt); + MOZ_ASSERT(ok); + } + + ~TrampolineCollection() + { + if (!mPrevProt) { + return; + } + + mMMPolicy.Protect(mLocalBase, mNumTramps * mTrampSize, + mPrevProt, &mPrevProt); + } + + TrampolineIterator begin() const + { + if (!mPrevProt) { + return end(); + } + + return TrampolineIterator(*this); + } + + TrampolineIterator end() const + { + return TrampolineIterator(*this, mNumTramps); + } + + TrampolineCollection(const TrampolineCollection&) = delete; + TrampolineCollection& operator=(const TrampolineCollection&) = delete; + TrampolineCollection& operator=(TrampolineCollection&&) = delete; + + TrampolineCollection(TrampolineCollection&& aOther) + : mMMPolicy(aOther.mMMPolicy) + , mLocalBase(aOther.mLocalBase) + , mRemoteBase(aOther.mRemoteBase) + , mTrampSize(aOther.mTrampSize) + , mNumTramps(aOther.mNumTramps) + , mPrevProt(aOther.mPrevProt) + { + aOther.mPrevProt = 0; + } + +private: + const MMPolicy& mMMPolicy; + uint8_t* const mLocalBase; + const uintptr_t mRemoteBase; + const uint32_t mTrampSize; + const uint32_t mNumTramps; + uint32_t mPrevProt; + + friend class TrampolineIterator; +}; + +} // namespace interceptor +} // namespace mozilla + +#endif // mozilla_interceptor_Trampoline_h diff --git a/mozglue/misc/interceptor/VMSharingPolicies.h b/mozglue/misc/interceptor/VMSharingPolicies.h new file mode 100644 index 000000000000..e2368fb877fc --- /dev/null +++ b/mozglue/misc/interceptor/VMSharingPolicies.h @@ -0,0 +1,90 @@ +/* -*- 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_interceptor_VMSharingPolicies_h +#define mozilla_interceptor_VMSharingPolicies_h + +#include "mozilla/Assertions.h" +#include "mozilla/Types.h" + +namespace mozilla { +namespace interceptor { + +template +class VMSharingPolicyUnique : public MMPolicy +{ +public: + template + explicit VMSharingPolicyUnique(Args... aArgs) + : MMPolicy(mozilla::Forward(aArgs)...) + , mNextChunkIndex(0) + { + } + + bool Reserve(uint32_t aCount) + { + MOZ_ASSERT(aCount); + uint32_t bytesReserved = MMPolicy::Reserve(aCount * kChunkSize); + return !!bytesReserved; + } + + Trampoline GetNextTrampoline() + { + uint32_t offset = mNextChunkIndex * kChunkSize; + if (!MaybeCommitNextPage(offset, kChunkSize)) { + return nullptr; + } + + + Trampoline result(this, GetLocalView() + offset, + GetRemoteView() + offset, kChunkSize); + if (!!result) { + ++mNextChunkIndex; + } + + return Move(result); + } + + TrampolineCollection Items() const + { + return TrampolineCollection(*this, GetLocalView(), GetRemoteView(), + kChunkSize, mNextChunkIndex); + } + + void Clear() + { + mNextChunkIndex = 0; + } + + ~VMSharingPolicyUnique() = default; + + VMSharingPolicyUnique(const VMSharingPolicyUnique&) = delete; + VMSharingPolicyUnique& operator=(const VMSharingPolicyUnique&) = delete; + + VMSharingPolicyUnique(VMSharingPolicyUnique&& aOther) + : MMPolicy(Move(aOther)) + , mNextChunkIndex(aOther.mNextChunkIndex) + { + aOther.mNextChunkIndex = 0; + } + + VMSharingPolicyUnique& operator=(VMSharingPolicyUnique&& aOther) + { + static_cast(*this) = Move(aOther); + mNextChunkIndex = aOther.mNextChunkIndex; + aOther.mNextChunkIndex = 0; + return *this; + } + +private: + uint32_t mNextChunkIndex; +}; + +} // namespace interceptor +} // namespace mozilla + +#endif // mozilla_interceptor_VMSharingPolicies_h + diff --git a/mozglue/misc/moz.build b/mozglue/misc/moz.build index 75672ac5364d..36694ce74a3c 100644 --- a/mozglue/misc/moz.build +++ b/mozglue/misc/moz.build @@ -33,6 +33,18 @@ OS_LIBS += CONFIG['REALTIME_LIBS'] DEFINES['IMPL_MFBT'] = True if CONFIG['OS_ARCH'] == 'WINNT': + EXPORTS += [ + 'nsWindowsDllInterceptor.h', + ] + EXPORTS.mozilla.interceptor += [ + 'interceptor/MMPolicies.h', + 'interceptor/PatcherBase.h', + 'interceptor/PatcherDetour.h', + 'interceptor/PatcherNopSpace.h', + 'interceptor/TargetFunction.h', + 'interceptor/Trampoline.h', + 'interceptor/VMSharingPolicies.h', + ] SOURCES += [ 'TimeStamp_windows.cpp', ] diff --git a/mozglue/misc/nsWindowsDllInterceptor.h b/mozglue/misc/nsWindowsDllInterceptor.h new file mode 100644 index 000000000000..a2f8758ae104 --- /dev/null +++ b/mozglue/misc/nsWindowsDllInterceptor.h @@ -0,0 +1,241 @@ +/* -*- 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 "mozilla/Assertions.h" +#include "mozilla/ArrayUtils.h" +#include "mozilla/CheckedInt.h" +#include "mozilla/DebugOnly.h" +#include "mozilla/NotNull.h" +#include "mozilla/TypeTraits.h" +#include "mozilla/Types.h" +#include "mozilla/UniquePtr.h" +#include "mozilla/Vector.h" +#include "nsWindowsHelpers.h" + +#include +#include +#include + +#include "mozilla/interceptor/MMPolicies.h" +#include "mozilla/interceptor/PatcherDetour.h" +#include "mozilla/interceptor/PatcherNopSpace.h" +#include "mozilla/interceptor/VMSharingPolicies.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. + * + */ + +namespace mozilla { +namespace interceptor { + +enum +{ + kDefaultTrampolineSize = 128 +}; + +template > +class WindowsDllInterceptor final +{ + interceptor::WindowsDllDetourPatcher mDetourPatcher; +#if defined(_M_IX86) + interceptor::WindowsDllNopSpacePatcher mNopSpacePatcher; +#endif // defined(_M_IX86) + + HMODULE mModule; + int mNHooks; + +public: + template + explicit WindowsDllInterceptor(Args... aArgs) + : mDetourPatcher(mozilla::Forward(aArgs)...) +#if defined(_M_IX86) + , mNopSpacePatcher(mozilla::Forward(aArgs)...) +#endif // defined(_M_IX86) + , mModule(nullptr) + , mNHooks(0) + { + } + + 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], int aNumHooks = 0) + { + 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, aNumHooks); + } + + void Init(const wchar_t* aModuleName, int aNumHooks = 0) + { + if (mModule) { + return; + } + + mModule = ::LoadLibraryW(aModuleName); + mNHooks = aNumHooks; + } + + void Clear() + { + if (!mModule) { + return; + } + +#if defined(_M_IX86) + mNopSpacePatcher.Clear(); +#endif // defined(_M_IX86) + mDetourPatcher.Clear(); + + // NB: We intentionally leak mModule + } + + /** + * 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) { + return false; + } + + FARPROC proc = ::GetProcAddress(mModule, aName); + if (!proc) { + 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) { + return false; + } + + FARPROC proc = ::GetProcAddress(mModule, aName); + if (!proc) { + return false; + } + + return AddDetour(proc, aHookDest, aOrigFunc); + } + +private: + bool AddDetour(FARPROC aProc, intptr_t aHookDest, void** aOrigFunc) + { + MOZ_ASSERT(mModule && aProc); + + if (!mDetourPatcher.Initialized()) { + mDetourPatcher.Init(mNHooks); + } + + return mDetourPatcher.AddHook(aProc, aHookDest, aOrigFunc); + } +}; + +} // namespace interceptor + +using WindowsDllInterceptor = interceptor::WindowsDllInterceptor<>; + +} // namespace mozilla + +#endif /* NS_WINDOWS_DLL_INTERCEPTOR_H_ */ diff --git a/toolkit/xre/test/win/TestDllInterceptor.cpp b/mozglue/tests/interceptor/TestDllInterceptor.cpp similarity index 97% rename from toolkit/xre/test/win/TestDllInterceptor.cpp rename to mozglue/tests/interceptor/TestDllInterceptor.cpp index 04e6b511c292..6007a8995201 100644 --- a/toolkit/xre/test/win/TestDllInterceptor.cpp +++ b/mozglue/tests/interceptor/TestDllInterceptor.cpp @@ -61,7 +61,8 @@ bool CheckHook(HookTestFunc aHookTestFunc, void* aOrigFunc, return false; } -bool TestHook(HookTestFunc funcTester, const char *dll, const char *func) +template +bool TestHook(HookTestFunc funcTester, const char (&dll)[N], const char *func) { void *orig_func; bool successful = false; @@ -80,7 +81,8 @@ bool TestHook(HookTestFunc funcTester, const char *dll, const char *func) } } -bool TestDetour(const char *dll, const char *func) +template +bool TestDetour(const char (&dll)[N], const char *func) { void *orig_func; bool successful = false; @@ -99,7 +101,8 @@ bool TestDetour(const char *dll, const char *func) } } -bool MaybeTestHook(const bool cond, HookTestFunc funcTester, const char* dll, const char* func) +template +bool MaybeTestHook(const bool cond, HookTestFunc funcTester, const char (&dll)[N], const char* func) { if (!cond) { printf("TEST-SKIPPED | WindowsDllInterceptor | Skipped hook test for %s from %s\n", func, dll); @@ -595,6 +598,9 @@ bool TestFreeCredentialsHandle(void* aFunc) int main() { + LARGE_INTEGER start; + QueryPerformanceCounter(&start); + // We disable this part of the test because the code coverage instrumentation // injects code in rotatePayload in a way that WindowsDllInterceptor doesn't // understand. @@ -733,6 +739,19 @@ int main() TestDetour("kernel32.dll", "BaseThreadInitThunk") && TestDetour("ntdll.dll", "LdrLoadDll")) { printf("TEST-PASS | WindowsDllInterceptor | all checks passed\n"); + + LARGE_INTEGER end, freq; + QueryPerformanceCounter(&end); + + QueryPerformanceFrequency(&freq); + + LARGE_INTEGER result; + result.QuadPart = end.QuadPart - start.QuadPart; + result.QuadPart *= 1000000; + result.QuadPart /= freq.QuadPart; + + printf("Elapsed time: %lld microseconds\n", result.QuadPart); + return 0; } diff --git a/mozglue/tests/interceptor/moz.build b/mozglue/tests/interceptor/moz.build new file mode 100644 index 000000000000..a48736e8a33b --- /dev/null +++ b/mozglue/tests/interceptor/moz.build @@ -0,0 +1,21 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +CppUnitTests([ + 'TestDllInterceptor', +]) + +DEFINES['NS_NO_XPCOM'] = True + +DisableStlWrapping() + +OS_LIBS += [ + 'ole32', +] + +USE_LIBS += [ + 'mfbt', +] diff --git a/mozglue/tests/moz.build b/mozglue/tests/moz.build index 20f4d28fb25e..d0e060181411 100644 --- a/mozglue/tests/moz.build +++ b/mozglue/tests/moz.build @@ -13,3 +13,8 @@ GeckoCppUnitTests([ CppUnitTests([ 'TestPrintf', ]) + +if CONFIG['OS_ARCH'] == 'WINNT': + TEST_DIRS += [ + 'interceptor', + ] diff --git a/toolkit/xre/test/win/moz.build b/toolkit/xre/test/win/moz.build index 552449639542..9a46eb3b1147 100644 --- a/toolkit/xre/test/win/moz.build +++ b/toolkit/xre/test/win/moz.build @@ -8,10 +8,6 @@ SimplePrograms([ 'TestXREMakeCommandLineWin', ]) -CppUnitTests([ - 'TestDllInterceptor', -]) - DEFINES['NS_NO_XPCOM'] = True LOCAL_INCLUDES += [ @@ -24,7 +20,6 @@ USE_STATIC_LIBS = True OS_LIBS += [ 'comctl32', - 'ole32', 'shell32', 'ws2_32', ] diff --git a/xpcom/build/PoisonIOInterposerWin.cpp b/xpcom/build/PoisonIOInterposerWin.cpp index 2c386246f230..6d8bc0a01233 100644 --- a/xpcom/build/PoisonIOInterposerWin.cpp +++ b/xpcom/build/PoisonIOInterposerWin.cpp @@ -485,7 +485,7 @@ ClearPoisonIOInterposer() if (sIOPoisoned) { // Destroy the DLL interceptor sIOPoisoned = false; - sNtDllInterceptor = WindowsDllInterceptor(); + sNtDllInterceptor.Clear(); } } diff --git a/xpcom/build/moz.build b/xpcom/build/moz.build index 34cfe0725e15..fda03c82461a 100755 --- a/xpcom/build/moz.build +++ b/xpcom/build/moz.build @@ -26,8 +26,9 @@ EXPORTS.mozilla += [ ] if CONFIG['OS_ARCH'] == 'WINNT': - EXPORTS += ['nsWindowsDllInterceptor.h'] - EXPORTS.mozilla += ['perfprobe.h'] + EXPORTS.mozilla += [ + 'perfprobe.h', + ] SOURCES += ['perfprobe.cpp'] if CONFIG['CC_TYPE'] != 'gcc': SOURCES += [ diff --git a/xpcom/build/nsWindowsDllInterceptor.h b/xpcom/build/nsWindowsDllInterceptor.h deleted file mode 100644 index 503187312254..000000000000 --- a/xpcom/build/nsWindowsDllInterceptor.h +++ /dev/null @@ -1,1495 +0,0 @@ -/* -*- 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 "mozilla/Assertions.h" -#include "mozilla/ArrayUtils.h" -#include "mozilla/UniquePtr.h" -#include "nsWindowsHelpers.h" - -#include -#include -#include - -/* - * 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. - * - */ - -#include - -#define COPY_CODES(NBYTES) do { \ - memcpy(&tramp[nTrampBytes], &origBytes[nOrigBytes], NBYTES); \ - nOrigBytes += NBYTES; \ - nTrampBytes += NBYTES; \ -} while (0) - -namespace mozilla { -namespace internal { - -class AutoVirtualProtect -{ -public: - AutoVirtualProtect(void* aFunc, size_t aSize, DWORD aProtect) - : mFunc(aFunc), mSize(aSize), mNewProtect(aProtect), mOldProtect(0), - mSuccess(false) - {} - - ~AutoVirtualProtect() - { - if (mSuccess) { - VirtualProtectEx(GetCurrentProcess(), mFunc, mSize, mOldProtect, - &mOldProtect); - } - } - - bool Protect() - { - mSuccess = !!VirtualProtectEx(GetCurrentProcess(), mFunc, mSize, - mNewProtect, &mOldProtect); - if (!mSuccess) { - // printf("VirtualProtectEx failed! %d\n", GetLastError()); - } - return mSuccess; - } - -private: - void* const mFunc; - size_t const mSize; - DWORD const mNewProtect; - DWORD mOldProtect; - bool mSuccess; -}; - -class WindowsDllNopSpacePatcher -{ - typedef uint8_t* byteptr_t; - HMODULE mModule; - - // Dumb array for remembering the addresses of functions we've patched. - // (This should be nsTArray, but non-XPCOM code uses this class.) - static const size_t maxPatchedFns = 16; - byteptr_t mPatchedFns[maxPatchedFns]; - size_t mPatchedFnsLen; - -public: - WindowsDllNopSpacePatcher() - : mModule(0) - , mPatchedFnsLen(0) - {} - -#if defined(_M_IX86) - ~WindowsDllNopSpacePatcher() - { - // Restore the mov edi, edi to the beginning of each function we patched. - - for (size_t i = 0; i < mPatchedFnsLen; i++) { - byteptr_t fn = mPatchedFns[i]; - - // Ensure we can write to the code. - AutoVirtualProtect protect(fn, 2, PAGE_EXECUTE_READWRITE); - if (!protect.Protect()) { - continue; - } - - // mov edi, edi - *((uint16_t*)fn) = 0xff8b; - - // I don't think this is actually necessary, but it can't hurt. - FlushInstructionCache(GetCurrentProcess(), - /* ignored */ nullptr, - /* ignored */ 0); - } - } - - void Init(const char* aModuleName) - { - if (!IsCompatible()) { -#if defined(MOZILLA_INTERNAL_API) - NS_WARNING("NOP space patching is unavailable for compatibility reasons"); -#endif - return; - } - - mModule = LoadLibraryExA(aModuleName, nullptr, 0); - if (!mModule) { - //printf("LoadLibraryEx for '%s' failed\n", aModuleName); - return; - } - } - - /** - * NVIDIA Optimus drivers utilize Microsoft Detours 2.x to patch functions - * in our address space. There is a bug in Detours 2.x that causes it to - * patch at the wrong address when attempting to detour code that is already - * NOP space patched. This function is an effort to detect the presence of - * this NVIDIA code in our address space and disable NOP space patching if it - * is. We also check AppInit_DLLs since this is the mechanism that the Optimus - * drivers use to inject into our process. - */ - static bool IsCompatible() - { - // These DLLs are known to have bad interactions with this style of patching - const wchar_t* kIncompatibleDLLs[] = { - L"detoured.dll", - L"_etoured.dll", - L"nvd3d9wrap.dll", - L"nvdxgiwrap.dll" - }; - // See if the infringing DLLs are already loaded - for (unsigned int i = 0; i < mozilla::ArrayLength(kIncompatibleDLLs); ++i) { - if (GetModuleHandleW(kIncompatibleDLLs[i])) { - return false; - } - } - if (GetModuleHandleW(L"user32.dll")) { - // user32 is loaded but the infringing DLLs are not, assume we're safe to - // proceed. - return true; - } - // If user32 has not loaded yet, check AppInit_DLLs to ensure that Optimus - // won't be loaded once user32 is initialized. - HKEY hkey = NULL; - if (!RegOpenKeyExW(HKEY_LOCAL_MACHINE, - L"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Windows", - 0, KEY_QUERY_VALUE, &hkey)) { - nsAutoRegKey key(hkey); - DWORD numBytes = 0; - const wchar_t kAppInitDLLs[] = L"AppInit_DLLs"; - // Query for required buffer size - LONG status = RegQueryValueExW(hkey, kAppInitDLLs, nullptr, - nullptr, nullptr, &numBytes); - mozilla::UniquePtr data; - if (!status) { - // Allocate the buffer and query for the actual data - data = mozilla::MakeUnique(numBytes / sizeof(wchar_t)); - status = RegQueryValueExW(hkey, kAppInitDLLs, nullptr, - nullptr, (LPBYTE)data.get(), &numBytes); - } - if (!status) { - // For each token, split up the filename components and then check the - // name of the file. - const wchar_t kDelimiters[] = L", "; - wchar_t* tokenContext = nullptr; - wchar_t* token = wcstok_s(data.get(), kDelimiters, &tokenContext); - while (token) { - wchar_t fname[_MAX_FNAME] = {0}; - if (!_wsplitpath_s(token, nullptr, 0, nullptr, 0, - fname, mozilla::ArrayLength(fname), - nullptr, 0)) { - // nvinit.dll is responsible for bootstrapping the DLL injection, so - // that is the library that we check for here - const wchar_t kNvInitName[] = L"nvinit"; - if (!_wcsnicmp(fname, kNvInitName, - mozilla::ArrayLength(kNvInitName))) { - return false; - } - } - token = wcstok_s(nullptr, kDelimiters, &tokenContext); - } - } - } - return true; - } - - bool AddHook(const char* aName, intptr_t aHookDest, void** aOrigFunc) - { - if (!mModule) { - return false; - } - - if (!IsCompatible()) { -#if defined(MOZILLA_INTERNAL_API) - NS_WARNING("NOP space patching is unavailable for compatibility reasons"); -#endif - return false; - } - - MOZ_RELEASE_ASSERT(mPatchedFnsLen < maxPatchedFns, "No room for the hook"); - - byteptr_t fn = reinterpret_cast(GetProcAddress(mModule, aName)); - if (!fn) { - //printf ("GetProcAddress failed\n"); - return false; - } - - fn = ResolveRedirectedAddress(fn); - - // Ensure we can read and write starting at fn - 5 (for the long jmp we're - // going to write) and ending at fn + 2 (for the short jmp up to the long - // jmp). These bytes may span two pages with different protection. - AutoVirtualProtect protectBefore(fn - 5, 5, PAGE_EXECUTE_READWRITE); - AutoVirtualProtect protectAfter(fn, 2, PAGE_EXECUTE_READWRITE); - if (!protectBefore.Protect() || !protectAfter.Protect()) { - return false; - } - - bool rv = WriteHook(fn, aHookDest, aOrigFunc); - - if (rv) { - mPatchedFns[mPatchedFnsLen] = fn; - mPatchedFnsLen++; - } - - return rv; - } - - bool WriteHook(byteptr_t aFn, intptr_t aHookDest, void** aOrigFunc) - { - // Check that the 5 bytes before aFn are NOP's or INT 3's, - // and that the 2 bytes after aFn are mov(edi, edi). - // - // It's safe to read aFn[-5] because we set it to PAGE_EXECUTE_READWRITE - // before calling WriteHook. - - for (int i = -5; i <= -1; i++) { - if (aFn[i] != 0x90 && aFn[i] != 0xcc) { // nop or int 3 - return false; - } - } - - // mov edi, edi. Yes, there are two ways to encode the same thing: - // - // 0x89ff == mov r/m, r - // 0x8bff == mov r, r/m - // - // where "r" is register and "r/m" is register or memory. Windows seems to - // use 8bff; I include 89ff out of paranoia. - if ((aFn[0] != 0x8b && aFn[0] != 0x89) || aFn[1] != 0xff) { - return false; - } - - // Write a long jump into the space above the function. - aFn[-5] = 0xe9; // jmp - *((intptr_t*)(aFn - 4)) = aHookDest - (uintptr_t)(aFn); // target displacement - - // Set aOrigFunc here, because after this point, aHookDest might be called, - // and aHookDest might use the aOrigFunc pointer. - *aOrigFunc = aFn + 2; - - // Short jump up into our long jump. - *((uint16_t*)(aFn)) = 0xf9eb; // jmp $-5 - - // I think this routine is safe without this, but it can't hurt. - FlushInstructionCache(GetCurrentProcess(), - /* ignored */ nullptr, - /* ignored */ 0); - - return true; - } - -private: - static byteptr_t ResolveRedirectedAddress(const byteptr_t aOriginalFunction) - { - // If function entry is jmp rel8 stub to the internal implementation, we - // resolve redirected address from the jump target. - if (aOriginalFunction[0] == 0xeb) { - int8_t offset = (int8_t)(aOriginalFunction[1]); - if (offset <= 0) { - // Bail out for negative offset: probably already patched by some - // third-party code. - return aOriginalFunction; - } - - for (int8_t i = 0; i < offset; i++) { - if (aOriginalFunction[2 + i] != 0x90) { - // Bail out on insufficient nop space. - return aOriginalFunction; - } - } - - return aOriginalFunction + 2 + offset; - } - - // If function entry is jmp [disp32] such as used by kernel32, - // we resolve redirected address from import table. - if (aOriginalFunction[0] == 0xff && aOriginalFunction[1] == 0x25) { - return (byteptr_t)(**((uint32_t**) (aOriginalFunction + 2))); - } - - return aOriginalFunction; - } -#else - void Init(const char* aModuleName) - { - // Not implemented except on x86-32. - } - - bool AddHook(const char* aName, intptr_t aHookDest, void** aOrigFunc) - { - // Not implemented except on x86-32. - return false; - } -#endif -}; - -class WindowsDllDetourPatcher -{ - typedef unsigned char* byteptr_t; -public: - WindowsDllDetourPatcher() - : mModule(0), mHookPage(0), mMaxHooks(0), mCurHooks(0) - { - } - - ~WindowsDllDetourPatcher() - { - int i; - byteptr_t p; - for (i = 0, p = mHookPage; i < mCurHooks; i++, p += kHookSize) { -#if defined(_M_IX86) - size_t nBytes = 1 + sizeof(intptr_t); -#elif defined(_M_X64) - size_t nBytes = 2 + sizeof(intptr_t); -#else -#error "Unknown processor type" -#endif - byteptr_t origBytes = (byteptr_t)DecodePointer(*((byteptr_t*)p)); - - // ensure we can modify the original code - AutoVirtualProtect protect(origBytes, nBytes, PAGE_EXECUTE_READWRITE); - if (!protect.Protect()) { - continue; - } - - // Remove the hook by making the original function jump directly - // in the trampoline. - intptr_t dest = (intptr_t)(p + sizeof(void*)); -#if defined(_M_IX86) - // Ensure the JMP from CreateTrampoline is where we expect it to be. - if (origBytes[0] != 0xE9) - continue; - *((intptr_t*)(origBytes + 1)) = - dest - (intptr_t)(origBytes + 5); // target displacement -#elif defined(_M_X64) - // Ensure the MOV R11 from CreateTrampoline is where we expect it to be. - if (origBytes[0] != 0x49 || origBytes[1] != 0xBB) - continue; - *((intptr_t*)(origBytes + 2)) = dest; -#else -#error "Unknown processor type" -#endif - } - } - - void Init(const char* aModuleName, int aNumHooks = 0) - { - if (mModule) { - return; - } - - mModule = LoadLibraryExA(aModuleName, nullptr, 0); - if (!mModule) { - //printf("LoadLibraryEx for '%s' failed\n", aModuleName); - return; - } - - int hooksPerPage = 4096 / kHookSize; - if (aNumHooks == 0) { - aNumHooks = hooksPerPage; - } - - mMaxHooks = aNumHooks + (hooksPerPage % aNumHooks); - - mHookPage = (byteptr_t)VirtualAllocEx(GetCurrentProcess(), nullptr, - mMaxHooks * kHookSize, - MEM_COMMIT | MEM_RESERVE, - PAGE_EXECUTE_READ); - if (!mHookPage) { - mModule = 0; - return; - } - } - - bool Initialized() { return !!mModule; } - - bool AddHook(const char* aName, intptr_t aHookDest, void** aOrigFunc) - { - if (!mModule) { - return false; - } - - void* pAddr = (void*)GetProcAddress(mModule, aName); - if (!pAddr) { - //printf ("GetProcAddress failed\n"); - return false; - } - - pAddr = ResolveRedirectedAddress((byteptr_t)pAddr); - - CreateTrampoline(pAddr, aHookDest, aOrigFunc); - if (!*aOrigFunc) { - //printf ("CreateTrampoline failed\n"); - return false; - } - - return true; - } - -protected: - const static int kPageSize = 4096; - const static int kHookSize = 128; - - HMODULE mModule; - byteptr_t mHookPage; - int mMaxHooks; - int mCurHooks; - - // rex bits - static const BYTE kMaskHighNibble = 0xF0; - static const BYTE kRexOpcode = 0x40; - static const BYTE kMaskRexW = 0x08; - static const BYTE kMaskRexR = 0x04; - static const BYTE kMaskRexX = 0x02; - static const BYTE kMaskRexB = 0x01; - - // mod r/m bits - static const BYTE kRegFieldShift = 3; - static const BYTE kMaskMod = 0xC0; - static const BYTE kMaskReg = 0x38; - static const BYTE kMaskRm = 0x07; - static const BYTE kRmNeedSib = 0x04; - static const BYTE kModReg = 0xC0; - static const BYTE kModDisp32 = 0x80; - static const BYTE kModDisp8 = 0x40; - static const BYTE kModNoRegDisp = 0x00; - static const BYTE kRmNoRegDispDisp32 = 0x05; - - // sib bits - static const BYTE kMaskSibScale = 0xC0; - static const BYTE kMaskSibIndex = 0x38; - static const BYTE kMaskSibBase = 0x07; - static const BYTE kSibBaseEbp = 0x05; - - // Register bit IDs. - static const BYTE kRegAx = 0x0; - static const BYTE kRegCx = 0x1; - static const BYTE kRegDx = 0x2; - static const BYTE kRegBx = 0x3; - static const BYTE kRegSp = 0x4; - static const BYTE kRegBp = 0x5; - static const BYTE kRegSi = 0x6; - static const BYTE kRegDi = 0x7; - - // Special ModR/M codes. These indicate operands that cannot be simply - // memcpy-ed. - // Operand is a 64-bit RIP-relative address. - static const int kModOperand64 = -2; - // Operand is not yet handled by our trampoline. - static const int kModUnknown = -1; - - /** - * Returns the number of bytes taken by the ModR/M byte, SIB (if present) - * and the instruction's operand. In special cases, the special MODRM codes - * above are returned. - * aModRm points to the ModR/M byte of the instruction. - * On return, aSubOpcode (if present) is filled with the subopcode/register - * code found in the ModR/M byte. - */ - int CountModRmSib(const BYTE *aModRm, BYTE* aSubOpcode = nullptr) - { - if (!aModRm) { - MOZ_ASSERT(aModRm, "Missing ModRM byte"); - return kModUnknown; - } - int numBytes = 1; // Start with 1 for mod r/m byte itself - switch (*aModRm & kMaskMod) { - case kModReg: - return numBytes; - case kModDisp8: - numBytes += 1; - break; - case kModDisp32: - numBytes += 4; - break; - case kModNoRegDisp: - if ((*aModRm & kMaskRm) == kRmNoRegDispDisp32) { -#if defined(_M_X64) - if (aSubOpcode) { - *aSubOpcode = (*aModRm & kMaskReg) >> kRegFieldShift; - } - return kModOperand64; -#else - // On IA-32, all ModR/M instruction modes address memory relative to 0 - numBytes += 4; -#endif - } else if (((*aModRm & kMaskRm) == kRmNeedSib && - (*(aModRm + 1) & kMaskSibBase) == kSibBaseEbp)) { - numBytes += 4; - } - break; - default: - // This should not be reachable - MOZ_ASSERT_UNREACHABLE("Impossible value for modr/m byte mod bits"); - return kModUnknown; - } - if ((*aModRm & kMaskRm) == kRmNeedSib) { - // SIB byte - numBytes += 1; - } - if (aSubOpcode) { - *aSubOpcode = (*aModRm & kMaskReg) >> kRegFieldShift; - } - return numBytes; - } - -#if defined(_M_X64) - // To patch for JMP and JE - - enum JumpType { - Je, - Jne, - Jmp, - Call - }; - - struct JumpPatch { - JumpPatch() - : mHookOffset(0), mJumpAddress(0), mType(JumpType::Jmp) - { - } - - JumpPatch(size_t aOffset, intptr_t aAddress, JumpType aType = JumpType::Jmp) - : mHookOffset(aOffset), mJumpAddress(aAddress), mType(aType) - { - } - - size_t GenerateJump(uint8_t* aCode) - { - size_t offset = mHookOffset; - if (mType == JumpType::Je) { - // JNE RIP+14 - aCode[offset] = 0x75; - aCode[offset + 1] = 14; - offset += 2; - } else if (mType == JumpType::Jne) { - // JE RIP+14 - aCode[offset] = 0x74; - aCode[offset + 1] = 14; - offset += 2; - } - - // Near call/jmp, absolute indirect, address given in r/m32 - if (mType == JumpType::Call) { - // CALL [RIP+0] - aCode[offset] = 0xff; - aCode[offset + 1] = 0x15; - // The offset to jump destination -- ie it is placed 2 bytes after the offset. - *reinterpret_cast(aCode + offset + 2) = 2; - aCode[offset + 2 + 4] = 0xeb; // JMP +8 (jump over mJumpAddress) - aCode[offset + 2 + 4 + 1] = 8; - *reinterpret_cast(aCode + offset + 2 + 4 + 2) = mJumpAddress; - return offset + 2 + 4 + 2 + 8; - } else { - // JMP [RIP+0] - aCode[offset] = 0xff; - aCode[offset + 1] = 0x25; - // The offset to jump destination is 0 - *reinterpret_cast(aCode + offset + 2) = 0; - *reinterpret_cast(aCode + offset + 2 + 4) = mJumpAddress; - return offset + 2 + 4 + 8; - } - } - - size_t mHookOffset; - intptr_t mJumpAddress; - JumpType mType; - }; - -#endif - - enum ePrefixGroupBits - { - eNoPrefixes = 0, - ePrefixGroup1 = (1 << 0), - ePrefixGroup2 = (1 << 1), - ePrefixGroup3 = (1 << 2), - ePrefixGroup4 = (1 << 3) - }; - - int CountPrefixBytes(byteptr_t aBytes, const int aBytesIndex, - unsigned char* aOutGroupBits) - { - unsigned char& groupBits = *aOutGroupBits; - groupBits = eNoPrefixes; - int index = aBytesIndex; - while (true) { - switch (aBytes[index]) { - // Group 1 - case 0xF0: // LOCK - case 0xF2: // REPNZ - case 0xF3: // REP / REPZ - if (groupBits & ePrefixGroup1) { - return -1; - } - groupBits |= ePrefixGroup1; - ++index; - break; - - // Group 2 - case 0x2E: // CS override / branch not taken - case 0x36: // SS override - case 0x3E: // DS override / branch taken - case 0x64: // FS override - case 0x65: // GS override - if (groupBits & ePrefixGroup2) { - return -1; - } - groupBits |= ePrefixGroup2; - ++index; - break; - - // Group 3 - case 0x66: // operand size override - if (groupBits & ePrefixGroup3) { - return -1; - } - groupBits |= ePrefixGroup3; - ++index; - break; - - // Group 4 - case 0x67: // Address size override - if (groupBits & ePrefixGroup4) { - return -1; - } - groupBits |= ePrefixGroup4; - ++index; - break; - - default: - return index - aBytesIndex; - } - } - } - - // Return a ModR/M byte made from the 2 Mod bits, the register used for the - // reg bits and the register used for the R/M bits. - BYTE BuildModRmByte(BYTE aModBits, BYTE aReg, BYTE aRm) - { - MOZ_ASSERT((aRm & kMaskRm) == aRm); - MOZ_ASSERT((aModBits & kMaskMod) == aModBits); - MOZ_ASSERT(((aReg << kRegFieldShift) & kMaskReg) == (aReg << kRegFieldShift)); - return aModBits | (aReg << kRegFieldShift) | aRm; - } - - void CreateTrampoline(void* aOrigFunction, intptr_t aDest, void** aOutTramp) - { - *aOutTramp = nullptr; - - AutoVirtualProtect protectHookPage(mHookPage, mMaxHooks * kHookSize, - PAGE_EXECUTE_READWRITE); - if (!protectHookPage.Protect()) { - return; - } - - byteptr_t tramp = FindTrampolineSpace(); - if (!tramp) { - return; - } - - // We keep the address of the original function in the first bytes of - // the trampoline buffer - *((void**)tramp) = EncodePointer(aOrigFunction); - tramp += sizeof(void*); - - byteptr_t origBytes = (byteptr_t)aOrigFunction; - - // # of bytes of the original function that we can overwrite. - int nOrigBytes = 0; - -#if defined(_M_IX86) - int pJmp32 = -1; - while (nOrigBytes < 5) { - // Understand some simple instructions that might be found in a - // prologue; we might need to extend this as necessary. - // - // Note! If we ever need to understand jump instructions, we'll - // need to rewrite the displacement argument. - unsigned char prefixGroups; - int numPrefixBytes = CountPrefixBytes(origBytes, nOrigBytes, &prefixGroups); - if (numPrefixBytes < 0 || (prefixGroups & (ePrefixGroup3 | ePrefixGroup4))) { - // Either the prefix sequence was bad, or there are prefixes that - // we don't currently support (groups 3 and 4) - MOZ_ASSERT_UNREACHABLE("Unrecognized opcode sequence"); - return; - } - nOrigBytes += numPrefixBytes; - if (origBytes[nOrigBytes] >= 0x88 && - origBytes[nOrigBytes] <= 0x8B) { - // various MOVs - ++nOrigBytes; - int len = CountModRmSib(origBytes + nOrigBytes); - if (len < 0) { - MOZ_ASSERT_UNREACHABLE("Unrecognized MOV opcode sequence"); - return; - } - nOrigBytes += len; - } else if (origBytes[nOrigBytes] == 0x0f && - (origBytes[nOrigBytes + 1] == 0x10 || - origBytes[nOrigBytes + 1] == 0x11)) { - // SSE: movups xmm, xmm/m128 - // movups xmm/m128, xmm - nOrigBytes += 2; - int len = CountModRmSib(origBytes + nOrigBytes); - if (len < 0) { - MOZ_ASSERT_UNREACHABLE("Unrecognized MOV opcode sequence"); - return; - } - nOrigBytes += len; - } else if (origBytes[nOrigBytes] == 0xA1) { - // MOV eax, [seg:offset] - nOrigBytes += 5; - } else if (origBytes[nOrigBytes] == 0xB8) { - // MOV 0xB8: http://ref.x86asm.net/coder32.html#xB8 - nOrigBytes += 5; - } else if (origBytes[nOrigBytes] == 0x33 && - (origBytes[nOrigBytes+1] & kMaskMod) == kModReg) { - // XOR r32, r32 - nOrigBytes += 2; - } else if ((origBytes[nOrigBytes] & 0xf8) == 0x40) { - // INC r32 - nOrigBytes += 1; - } else if (origBytes[nOrigBytes] == 0x83) { - // ADD|ODR|ADC|SBB|AND|SUB|XOR|CMP r/m, imm8 - unsigned char b = origBytes[nOrigBytes + 1]; - if ((b & 0xc0) == 0xc0) { - // ADD|ODR|ADC|SBB|AND|SUB|XOR|CMP r, imm8 - nOrigBytes += 3; - } else { - // bail - MOZ_ASSERT_UNREACHABLE("Unrecognized bit opcode sequence"); - return; - } - } else if (origBytes[nOrigBytes] == 0x68) { - // PUSH with 4-byte operand - nOrigBytes += 5; - } else if ((origBytes[nOrigBytes] & 0xf0) == 0x50) { - // 1-byte PUSH/POP - nOrigBytes++; - } else if (origBytes[nOrigBytes] == 0x6A) { - // PUSH imm8 - nOrigBytes += 2; - } else if (origBytes[nOrigBytes] == 0xe9) { - pJmp32 = nOrigBytes; - // jmp 32bit offset - nOrigBytes += 5; - } else if (origBytes[nOrigBytes] == 0xff && - origBytes[nOrigBytes + 1] == 0x25) { - // jmp [disp32] - nOrigBytes += 6; - } else if (origBytes[nOrigBytes] == 0xc2) { - // ret imm16. We can't handle this but it happens. We don't ASSERT but we do fail to hook. -#if defined(MOZILLA_INTERNAL_API) - NS_WARNING("Cannot hook method -- RET opcode found"); -#endif - return; - } else { - //printf ("Unknown x86 instruction byte 0x%02x, aborting trampoline\n", origBytes[nOrigBytes]); - MOZ_ASSERT_UNREACHABLE("Unrecognized opcode sequence"); - return; - } - } - - // The trampoline is a copy of the instructions that we just traced, - // followed by a jump that we add below. - memcpy(tramp, aOrigFunction, nOrigBytes); -#elif defined(_M_X64) - // The number of bytes used by the trampoline. - int nTrampBytes = 0; - bool foundJmp = false; - - while (nOrigBytes < 13) { - // If we found JMP 32bit offset, we require that the next bytes must - // be NOP or INT3. There is no reason to copy them. - // TODO: This used to trigger for Je as well. Now that I allow - // instructions after CALL and JE, I don't think I need that. - // The only real value of this condition is that if code follows a JMP - // then its _probably_ the target of a JMP somewhere else and we - // will be overwriting it, which would be tragic. This seems - // highly unlikely. - if (foundJmp) { - if (origBytes[nOrigBytes] == 0x90 || origBytes[nOrigBytes] == 0xcc) { - nOrigBytes++; - continue; - } - MOZ_ASSERT_UNREACHABLE("Opcode sequence includes commands after JMP"); - return; - } - if (origBytes[nOrigBytes] == 0x0f) { - COPY_CODES(1); - if (origBytes[nOrigBytes] == 0x1f) { - // nop (multibyte) - COPY_CODES(1); - if ((origBytes[nOrigBytes] & 0xc0) == 0x40 && - (origBytes[nOrigBytes] & 0x7) == 0x04) { - COPY_CODES(3); - } else { - MOZ_ASSERT_UNREACHABLE("Unrecognized opcode sequence"); - return; - } - } else if (origBytes[nOrigBytes] == 0x05) { - // syscall - COPY_CODES(1); - } else if (origBytes[nOrigBytes] == 0x10 || - origBytes[nOrigBytes] == 0x11) { - // SSE: movups xmm, xmm/m128 - // movups xmm/m128, xmm - COPY_CODES(1); - int nModRmSibBytes = CountModRmSib(&origBytes[nOrigBytes]); - if (nModRmSibBytes < 0) { - MOZ_ASSERT_UNREACHABLE("Unrecognized opcode sequence"); - return; - } else { - COPY_CODES(nModRmSibBytes); - } - } else if (origBytes[nOrigBytes] == 0x84) { - // je rel32 - JumpPatch jump(nTrampBytes - 1, // overwrite the 0x0f we copied above - (intptr_t)(origBytes + nOrigBytes + 5 + - *(reinterpret_cast(origBytes + nOrigBytes + 1))), - JumpType::Je); - nTrampBytes = jump.GenerateJump(tramp); - nOrigBytes += 5; - } else { - MOZ_ASSERT_UNREACHABLE("Unrecognized opcode sequence"); - return; - } - } else if (origBytes[nOrigBytes] == 0x40 || - origBytes[nOrigBytes] == 0x41) { - // Plain REX or REX.B - COPY_CODES(1); - if ((origBytes[nOrigBytes] & 0xf0) == 0x50) { - // push/pop with Rx register - COPY_CODES(1); - } else if (origBytes[nOrigBytes] >= 0xb8 && origBytes[nOrigBytes] <= 0xbf) { - // mov r32, imm32 - COPY_CODES(5); - } else { - MOZ_ASSERT_UNREACHABLE("Unrecognized opcode sequence"); - return; - } - } else if (origBytes[nOrigBytes] == 0x44) { - // REX.R - COPY_CODES(1); - - // TODO: Combine with the "0x89" case below in the REX.W section - if (origBytes[nOrigBytes] == 0x89) { - // mov r/m32, r32 - COPY_CODES(1); - int len = CountModRmSib(origBytes + nOrigBytes); - if (len < 0) { - MOZ_ASSERT_UNREACHABLE("Unrecognized opcode sequence"); - return; - } - COPY_CODES(len); - } else { - MOZ_ASSERT_UNREACHABLE("Unrecognized opcode sequence"); - return; - } - } else if (origBytes[nOrigBytes] == 0x45) { - // REX.R & REX.B - COPY_CODES(1); - - if (origBytes[nOrigBytes] == 0x33) { - // xor r32, r32 - COPY_CODES(2); - } else { - MOZ_ASSERT_UNREACHABLE("Unrecognized opcode sequence"); - return; - } - } else if ((origBytes[nOrigBytes] & 0xfa) == 0x48) { - // REX.W | REX.WR | REX.WRB | REX.WB - COPY_CODES(1); - - if (origBytes[nOrigBytes] == 0x81 && - (origBytes[nOrigBytes + 1] & 0xf8) == 0xe8) { - // sub r, dword - COPY_CODES(6); - } else if (origBytes[nOrigBytes] == 0x83 && - (origBytes[nOrigBytes + 1] & 0xf8) == 0xe8) { - // sub r, byte - COPY_CODES(3); - } else if (origBytes[nOrigBytes] == 0x83 && - (origBytes[nOrigBytes + 1] & (kMaskMod|kMaskReg)) == kModReg) { - // add r, byte - COPY_CODES(3); - } else if (origBytes[nOrigBytes] == 0x83 && - (origBytes[nOrigBytes + 1] & 0xf8) == 0x60) { - // and [r+d], imm8 - COPY_CODES(5); - } else if (origBytes[nOrigBytes] == 0x2b && - (origBytes[nOrigBytes + 1] & kMaskMod) == kModReg) { - // sub r64, r64 - COPY_CODES(2); - } else if (origBytes[nOrigBytes] == 0x85) { - // 85 /r => TEST r/m32, r32 - if ((origBytes[nOrigBytes + 1] & 0xc0) == 0xc0) { - COPY_CODES(2); - } else { - MOZ_ASSERT_UNREACHABLE("Unrecognized opcode sequence"); - return; - } - } else if ((origBytes[nOrigBytes] & 0xfd) == 0x89) { - // MOV r/m64, r64 | MOV r64, r/m64 - BYTE reg; - int len = CountModRmSib(origBytes + nOrigBytes + 1, ®); - if (len < 0) { - MOZ_ASSERT(len == kModOperand64); - if (len != kModOperand64) { - return; - } - nOrigBytes += 2; // skip the MOV and MOD R/M bytes - - // The instruction MOVs 64-bit data from a RIP-relative memory - // address (determined with a 32-bit offset from RIP) into a - // 64-bit register. - int64_t* absAddr = - reinterpret_cast(origBytes + nOrigBytes + 4 + - *reinterpret_cast(origBytes + nOrigBytes)); - nOrigBytes += 4; - - if (reg == kRegAx) { - // Destination is RAX. Encode instruction as MOVABS with a - // 64-bit absolute address as its immediate operand. - tramp[nTrampBytes] = 0xa1; - ++nTrampBytes; - int64_t** trampOperandPtr = reinterpret_cast(tramp + nTrampBytes); - *trampOperandPtr = absAddr; - nTrampBytes += 8; - } else { - // The MOV must be done in two steps. First, we MOVABS the - // absolute 64-bit address into our target register. - // Then, we MOV from that address into the register - // using register-indirect addressing. - tramp[nTrampBytes] = 0xb8 + reg; - ++nTrampBytes; - int64_t** trampOperandPtr = reinterpret_cast(tramp + nTrampBytes); - *trampOperandPtr = absAddr; - nTrampBytes += 8; - tramp[nTrampBytes] = 0x48; - tramp[nTrampBytes+1] = 0x8b; - tramp[nTrampBytes+2] = BuildModRmByte(kModNoRegDisp, reg, reg); - nTrampBytes += 3; - } - } else { - COPY_CODES(len+1); - } - } else if (origBytes[nOrigBytes] == 0xc7) { - // MOV r/m64, imm32 - if (origBytes[nOrigBytes + 1] == 0x44) { - // MOV [r64+disp8], imm32 - // ModR/W + SIB + disp8 + imm32 - COPY_CODES(8); - } else { - MOZ_ASSERT_UNREACHABLE("Unrecognized opcode sequence"); - return; - } - } else if (origBytes[nOrigBytes] == 0xff) { - // JMP /4 - if ((origBytes[nOrigBytes + 1] & 0xc0) == 0x0 && - (origBytes[nOrigBytes + 1] & 0x07) == 0x5) { - // [rip+disp32] - // convert JMP 32bit offset to JMP 64bit direct - JumpPatch jump(nTrampBytes - 1, // overwrite the REX.W/REX.WR we copied above - *reinterpret_cast(origBytes + nOrigBytes + 6 + - *reinterpret_cast(origBytes + nOrigBytes + 2)), - JumpType::Jmp); - nTrampBytes = jump.GenerateJump(tramp); - nOrigBytes += 6; - foundJmp = true; - } else { - // not support yet! - MOZ_ASSERT_UNREACHABLE("Unrecognized opcode sequence"); - return; - } - } else if (origBytes[nOrigBytes] == 0x8d) { - // LEA reg, addr - if ((origBytes[nOrigBytes + 1] & kMaskMod) == 0x0 && - (origBytes[nOrigBytes + 1] & kMaskRm) == 0x5) { - // [rip+disp32] - // convert 32bit offset to 64bit direct and convert instruction - // to a simple 64-bit mov - BYTE reg = (origBytes[nOrigBytes + 1] & kMaskReg) >> kRegFieldShift; - intptr_t absAddr = - reinterpret_cast(origBytes + nOrigBytes + 6 + - *reinterpret_cast(origBytes + nOrigBytes + 2)); - nOrigBytes += 6; - tramp[nTrampBytes] = 0xb8 + reg; // mov - ++nTrampBytes; - intptr_t* trampOperandPtr = reinterpret_cast(tramp + nTrampBytes); - *trampOperandPtr = absAddr; - nTrampBytes += 8; - } else { - // Above we dealt with RIP-relative instructions. Any other - // operand form can simply be copied. - int len = CountModRmSib(origBytes + nOrigBytes + 1); - // We handled the kModOperand64 -- ie RIP-relative -- case above - MOZ_ASSERT(len > 0); - COPY_CODES(len + 1); - } - } else if (origBytes[nOrigBytes] == 0x63 && - (origBytes[nOrigBytes + 1] & kMaskMod) == kModReg) { - // movsxd r64, r32 (move + sign extend) - COPY_CODES(2); - } else { - // not support yet! - MOZ_ASSERT_UNREACHABLE("Unrecognized opcode sequence"); - return; - } - } else if (origBytes[nOrigBytes] == 0x66) { - // operand override prefix - COPY_CODES(1); - // This is the same as the x86 version - if (origBytes[nOrigBytes] >= 0x88 && origBytes[nOrigBytes] <= 0x8B) { - // various MOVs - unsigned char b = origBytes[nOrigBytes + 1]; - if (((b & 0xc0) == 0xc0) || - (((b & 0xc0) == 0x00) && - ((b & 0x07) != 0x04) && ((b & 0x07) != 0x05))) { - // REG=r, R/M=r or REG=r, R/M=[r] - COPY_CODES(2); - } else if ((b & 0xc0) == 0x40) { - if ((b & 0x07) == 0x04) { - // REG=r, R/M=[SIB + disp8] - COPY_CODES(4); - } else { - // REG=r, R/M=[r + disp8] - COPY_CODES(3); - } - } else { - // complex MOV, bail - MOZ_ASSERT_UNREACHABLE("Unrecognized MOV opcode sequence"); - return; - } - } else if (origBytes[nOrigBytes] == 0x44 && - origBytes[nOrigBytes+1] == 0x89) { - // mov word ptr [reg+disp8], reg - COPY_CODES(2); - int len = CountModRmSib(origBytes + nOrigBytes); - if (len < 0) { - // no way to support this yet. - MOZ_ASSERT_UNREACHABLE("Unrecognized opcode sequence"); - return; - } - COPY_CODES(len); - } - } else if ((origBytes[nOrigBytes] & 0xf0) == 0x50) { - // 1-byte push/pop - COPY_CODES(1); - } else if (origBytes[nOrigBytes] == 0x65) { - // GS prefix - // - // The entry of GetKeyState on Windows 10 has the following code. - // 65 48 8b 04 25 30 00 00 00 mov rax,qword ptr gs:[30h] - // (GS prefix + REX + MOV (0x8b) ...) - if (origBytes[nOrigBytes + 1] == 0x48 && - (origBytes[nOrigBytes + 2] >= 0x88 && origBytes[nOrigBytes + 2] <= 0x8b)) { - COPY_CODES(3); - int len = CountModRmSib(origBytes + nOrigBytes); - if (len < 0) { - // no way to support this yet. - MOZ_ASSERT_UNREACHABLE("Unrecognized opcode sequence"); - return; - } - COPY_CODES(len); - } else { - MOZ_ASSERT_UNREACHABLE("Unrecognized opcode sequence"); - return; - } - } else if (origBytes[nOrigBytes] == 0x80 && - origBytes[nOrigBytes + 1] == 0x3d) { - // cmp byte ptr [rip-relative address], imm8 - // We'll compute the absolute address and do the cmp in r11 - - // push r11 (to save the old value) - tramp[nTrampBytes] = 0x49; - ++nTrampBytes; - tramp[nTrampBytes] = 0x53; - ++nTrampBytes; - - byteptr_t absAddr = - reinterpret_cast(origBytes + nOrigBytes + 7 + - *reinterpret_cast(origBytes + nOrigBytes + 2)); - nOrigBytes += 6; - - // mov r11, absolute address - tramp[nTrampBytes] = 0x49; - ++nTrampBytes; - tramp[nTrampBytes] = 0xbb; - ++nTrampBytes; - - *reinterpret_cast(tramp + nTrampBytes) = absAddr; - nTrampBytes += 8; - - // cmp byte ptr [r11],... - tramp[nTrampBytes] = 0x41; - ++nTrampBytes; - tramp[nTrampBytes] = 0x80; - ++nTrampBytes; - tramp[nTrampBytes] = 0x3b; - ++nTrampBytes; - - // ...imm8 - COPY_CODES(1); - - // pop r11 (doesn't affect the flags from the cmp) - tramp[nTrampBytes] = 0x49; - ++nTrampBytes; - tramp[nTrampBytes] = 0x5b; - ++nTrampBytes; - } else if (origBytes[nOrigBytes] == 0x90) { - // nop - COPY_CODES(1); - } else if ((origBytes[nOrigBytes] & 0xf8) == 0xb8) { - // MOV r32, imm32 - COPY_CODES(5); - } else if (origBytes[nOrigBytes] == 0x33) { - // xor r32, r/m32 - COPY_CODES(2); - } else if (origBytes[nOrigBytes] == 0xf6) { - // test r/m8, imm8 (used by ntdll on Windows 10 x64) - // (no flags are affected by near jmp since there is no task switch, - // so it is ok for a jmp to be written immediately after a test) - BYTE subOpcode = 0; - int nModRmSibBytes = CountModRmSib(&origBytes[nOrigBytes + 1], &subOpcode); - if (nModRmSibBytes < 0 || subOpcode != 0) { - // Unsupported - MOZ_ASSERT_UNREACHABLE("Unrecognized opcode sequence"); - return; - } - COPY_CODES(2 + nModRmSibBytes); - } else if (origBytes[nOrigBytes] == 0x85) { - // test r/m32, r32 - int nModRmSibBytes = CountModRmSib(&origBytes[nOrigBytes + 1]); - if (nModRmSibBytes < 0) { - MOZ_ASSERT_UNREACHABLE("Unrecognized opcode sequence"); - return; - } - COPY_CODES(1 + nModRmSibBytes); - } else if (origBytes[nOrigBytes] == 0xd1 && - (origBytes[nOrigBytes+1] & kMaskMod) == kModReg) { - // bit shifts/rotates : (SA|SH|RO|RC)(R|L) r32 - // (e.g. 0xd1 0xe0 is SAL, 0xd1 0xc8 is ROR) - COPY_CODES(2); - } else if (origBytes[nOrigBytes] == 0xc3) { - // ret - COPY_CODES(1); - } else if (origBytes[nOrigBytes] == 0xcc) { - // int 3 - COPY_CODES(1); - } else if (origBytes[nOrigBytes] == 0xe8 || - origBytes[nOrigBytes] == 0xe9) { - // CALL (0xe8) or JMP (0xe9) 32bit offset - foundJmp = origBytes[nOrigBytes] == 0xe9; - JumpPatch jump(nTrampBytes, - (intptr_t)(origBytes + nOrigBytes + 5 + - *(reinterpret_cast(origBytes + nOrigBytes + 1))), - origBytes[nOrigBytes] == 0xe8 ? JumpType::Call : JumpType::Jmp); - nTrampBytes = jump.GenerateJump(tramp); - nOrigBytes += 5; - } else if (origBytes[nOrigBytes] == 0x74 || // je rel8 (0x74) - origBytes[nOrigBytes] == 0x75) { // jne rel8 (0x75) - char offset = origBytes[nOrigBytes + 1]; - auto jumpType = JumpType::Je; - if (origBytes[nOrigBytes] == 0x75) - jumpType = JumpType::Jne; - JumpPatch jump(nTrampBytes, - (intptr_t)(origBytes + nOrigBytes + 2 + offset), jumpType); - nTrampBytes = jump.GenerateJump(tramp); - nOrigBytes += 2; - } else if (origBytes[nOrigBytes] == 0xff) { - if ((origBytes[nOrigBytes + 1] & (kMaskMod|kMaskReg)) == 0xf0) { - // push r64 - COPY_CODES(2); - } else if (origBytes[nOrigBytes + 1] == 0x25) { - // jmp absolute indirect m32 - foundJmp = true; - int32_t offset = *(reinterpret_cast(origBytes + nOrigBytes + 2)); - int64_t* ptrToJmpDest = reinterpret_cast(origBytes + nOrigBytes + 6 + offset); - intptr_t jmpDest = static_cast(*ptrToJmpDest); - JumpPatch jump(nTrampBytes, jmpDest, JumpType::Jmp); - nTrampBytes = jump.GenerateJump(tramp); - nOrigBytes += 6; - } else if ((origBytes[nOrigBytes + 1] & (kMaskMod|kMaskReg)) == BuildModRmByte(kModReg, 2, 0)) { - // CALL reg (ff nn) - COPY_CODES(2); - } else { - MOZ_ASSERT_UNREACHABLE("Unrecognized opcode sequence"); - return; - } - } else if (origBytes[nOrigBytes] == 0x83 && - (origBytes[nOrigBytes + 1] & 0xf8) == 0x60) { - // and [r+d], imm8 - COPY_CODES(5); - } else if (origBytes[nOrigBytes] == 0xc6) { - // mov [r+d], imm8 - int len = CountModRmSib(&origBytes[nOrigBytes + 1]); - if (len < 0) { - // RIP-relative not yet supported - MOZ_ASSERT_UNREACHABLE("Unrecognized opcode sequence"); - return; - } - COPY_CODES(len + 1); - } else { - MOZ_ASSERT_UNREACHABLE("Unrecognized opcode sequence"); - return; - } - } -#else -#error "Unknown processor type" -#endif - - if (nOrigBytes > 100) { - //printf ("Too big!"); - return; - } - - // target address of the final jmp instruction in the trampoline - byteptr_t trampDest = origBytes + nOrigBytes; - -#if defined(_M_IX86) - if (pJmp32 >= 0) { - // Jump directly to the original target of the jump instead of jumping to the - // original function. - // Adjust jump target displacement to jump location in the trampoline. - *((intptr_t*)(tramp + pJmp32 + 1)) += origBytes - tramp; - } else { - tramp[nOrigBytes] = 0xE9; // jmp - *((intptr_t*)(tramp + nOrigBytes + 1)) = - (intptr_t)trampDest - (intptr_t)(tramp + nOrigBytes + 5); // target displacement - } -#elif defined(_M_X64) - // If the we found a Jmp, we don't need to add another instruction. However, - // if we found a _conditional_ jump or a CALL (or no control operations - // at all) then we still need to run the rest of aOriginalFunction. - if (!foundJmp) { - JumpPatch patch(nTrampBytes, reinterpret_cast(trampDest)); - patch.GenerateJump(tramp); - } -#endif - - // The trampoline is now valid. - *aOutTramp = tramp; - - // ensure we can modify the original code - AutoVirtualProtect protect(aOrigFunction, nOrigBytes, PAGE_EXECUTE_READWRITE); - if (!protect.Protect()) { - return; - } - -#if defined(_M_IX86) - // now modify the original bytes - origBytes[0] = 0xE9; // jmp - *((intptr_t*)(origBytes + 1)) = - aDest - (intptr_t)(origBytes + 5); // target displacement -#elif defined(_M_X64) - // mov r11, address - origBytes[0] = 0x49; - origBytes[1] = 0xbb; - - *((intptr_t*)(origBytes + 2)) = aDest; - - // jmp r11 - origBytes[10] = 0x41; - origBytes[11] = 0xff; - origBytes[12] = 0xe3; -#endif - } - - byteptr_t FindTrampolineSpace() - { - if (mCurHooks >= mMaxHooks) { - return 0; - } - - byteptr_t p = mHookPage + mCurHooks * kHookSize; - - mCurHooks++; - - return p; - } - - static void* ResolveRedirectedAddress(const byteptr_t aOriginalFunction) - { - // If function entry is jmp rel8 stub to the internal implementation, we - // resolve redirected address from the jump target. - if (aOriginalFunction[0] == 0xeb) { - int8_t offset = (int8_t)(aOriginalFunction[1]); - if (offset <= 0) { - // Bail out for negative offset: probably already patched by some - // third-party code. - return aOriginalFunction; - } - - for (int8_t i = 0; i < offset; i++) { - if (aOriginalFunction[2 + i] != 0x90) { - // Bail out on insufficient nop space. - return aOriginalFunction; - } - } - - return aOriginalFunction + 2 + offset; - } - -#if defined(_M_IX86) - // If function entry is jmp [disp32] such as used by kernel32, - // we resolve redirected address from import table. - if (aOriginalFunction[0] == 0xff && aOriginalFunction[1] == 0x25) { - return (void*)(**((uint32_t**) (aOriginalFunction + 2))); - } -#elif defined(_M_X64) - if (aOriginalFunction[0] == 0xe9) { - // require for TestDllInterceptor with --disable-optimize - int32_t offset = *((int32_t*)(aOriginalFunction + 1)); - return aOriginalFunction + 5 + offset; - } -#endif - - return aOriginalFunction; - } -}; - -} // namespace internal - -class WindowsDllInterceptor -{ - internal::WindowsDllNopSpacePatcher mNopSpacePatcher; - internal::WindowsDllDetourPatcher mDetourPatcher; - - const char* mModuleName; - int mNHooks; - -public: - explicit WindowsDllInterceptor(const char* aModuleName = nullptr, - int aNumHooks = 0) - : mModuleName(nullptr) - , mNHooks(0) - { - if (aModuleName) { - Init(aModuleName, aNumHooks); - } - } - - void Init(const char* aModuleName, int aNumHooks = 0) - { - if (mModuleName) { - return; - } - - mModuleName = aModuleName; - mNHooks = aNumHooks; - mNopSpacePatcher.Init(aModuleName); - - // Lazily initialize mDetourPatcher, since it allocates memory and we might - // not need it. - } - - /** - * 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 (!mModuleName) { - return false; - } - - if (mNopSpacePatcher.AddHook(aName, aHookDest, aOrigFunc)) { - return true; - } - - return AddDetour(aName, 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 (!mModuleName) { - return false; - } - - if (!mDetourPatcher.Initialized()) { - mDetourPatcher.Init(mModuleName, mNHooks); - } - - return mDetourPatcher.AddHook(aName, aHookDest, aOrigFunc); - } -}; - -} // namespace mozilla - -#endif /* NS_WINDOWS_DLL_INTERCEPTOR_H_ */