diff options
Diffstat (limited to 'libs/usvfs/src/thooklib')
| -rw-r--r-- | libs/usvfs/src/thooklib/CMakeLists.txt | 14 | ||||
| -rw-r--r-- | libs/usvfs/src/thooklib/asmjit_sane.h | 29 | ||||
| -rw-r--r-- | libs/usvfs/src/thooklib/hooklib.cpp | 740 | ||||
| -rw-r--r-- | libs/usvfs/src/thooklib/hooklib.h | 125 | ||||
| -rw-r--r-- | libs/usvfs/src/thooklib/pch.cpp | 1 | ||||
| -rw-r--r-- | libs/usvfs/src/thooklib/ttrampolinepool.cpp | 601 | ||||
| -rw-r--r-- | libs/usvfs/src/thooklib/ttrampolinepool.h | 222 | ||||
| -rw-r--r-- | libs/usvfs/src/thooklib/udis86wrapper.cpp | 102 | ||||
| -rw-r--r-- | libs/usvfs/src/thooklib/udis86wrapper.h | 62 | ||||
| -rw-r--r-- | libs/usvfs/src/thooklib/utility.cpp | 86 | ||||
| -rw-r--r-- | libs/usvfs/src/thooklib/utility.h | 35 |
11 files changed, 2017 insertions, 0 deletions
diff --git a/libs/usvfs/src/thooklib/CMakeLists.txt b/libs/usvfs/src/thooklib/CMakeLists.txt new file mode 100644 index 0000000..48426db --- /dev/null +++ b/libs/usvfs/src/thooklib/CMakeLists.txt @@ -0,0 +1,14 @@ +cmake_minimum_required(VERSION 3.16) + +find_package(asmjit CONFIG REQUIRED) +find_library(LIBUDIS86_LIBRARY libudis86) +find_package(spdlog CONFIG REQUIRED) + +file(GLOB sources CONFIGURE_DEPENDS "*.cpp" "*.h") +source_group("" FILES ${sources}) + +add_library(thooklib STATIC ${sources}) +target_link_libraries(thooklib PRIVATE shared asmjit::asmjit ${LIBUDIS86_LIBRARY} spdlog::spdlog_header_only) +target_include_directories(thooklib PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) +set_target_properties(thooklib PROPERTIES FOLDER injection) +target_precompile_headers(shared PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/../shared/pch.h) diff --git a/libs/usvfs/src/thooklib/asmjit_sane.h b/libs/usvfs/src/thooklib/asmjit_sane.h new file mode 100644 index 0000000..365622a --- /dev/null +++ b/libs/usvfs/src/thooklib/asmjit_sane.h @@ -0,0 +1,29 @@ +/* +Userspace Virtual Filesystem + +Copyright (C) 2015 Sebastian Herbord. All rights reserved. + +This file is part of usvfs. + +usvfs is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +usvfs is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with usvfs. If not, see <http://www.gnu.org/licenses/>. +*/ +#pragma once + +#pragma warning(push) +#pragma warning(disable : 4201) +#pragma warning(disable : 4244) +#pragma warning(disable : 4245) +#include <asmjit/asmjit.h> +#include <asmjit/x86.h> +#pragma warning(pop) diff --git a/libs/usvfs/src/thooklib/hooklib.cpp b/libs/usvfs/src/thooklib/hooklib.cpp new file mode 100644 index 0000000..9dbbafa --- /dev/null +++ b/libs/usvfs/src/thooklib/hooklib.cpp @@ -0,0 +1,740 @@ +/* +Userspace Virtual Filesystem + +Copyright (C) 2015 Sebastian Herbord. All rights reserved. + +This file is part of usvfs. + +usvfs is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +usvfs is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with usvfs. If not, see <http://www.gnu.org/licenses/>. +*/ +#include "udis86wrapper.h" +#include <boost/format.hpp> +#include <boost/predef.h> +#include <map> +#pragma warning(push, 3) +#include <asmjit/asmjit.h> +#pragma warning(pop) +#include "hooklib.h" +#include "ttrampolinepool.h" +#include "utility.h" +#include <addrtools.h> +#include <formatters.h> +#include <shmlogger.h> +#include <winapi.h> + +#if BOOST_ARCH_X86_64 +#pragma message("64bit build") +#define JUMP_SIZE 13 +#elif BOOST_ARCH_X86_32 +#define JUMP_SIZE 5 +#else +#error "unsupported architecture" +#endif + +using namespace asmjit; +// from here on out I'll only test for 64 or "other" + +using namespace HookLib; +using namespace asmjit; + +using namespace usvfs; + +struct THookInfo +{ + LPVOID originalFunction; + LPVOID replacementFunction; + LPVOID detour; // detour to call the original function after hook was installed. + LPVOID trampoline; // code fragment that decides whether the replacement function or + // detour is executed (preventing endless loops) + std::vector<uint8_t> + preamble; // part of the detour that needs to be re-inserted into the original + // function to return it to vanilla state + bool stub; // if this is true, the trampoline calls the "replacement"-function that + // before the original function, not instead of it + enum + { + TYPE_HOTPATCH, // official hot-patch variant as used on 32-bit windows + TYPE_WIN64PATCH, // custom patch variant used on 64-bit windows + TYPE_CHAINPATCH, // the hook is part of the hook chain (and not the first) + TYPE_OVERWRITE, // full jump overwrite used if none of the above work + TYPE_RIPINDIRECT // the function already started on a rip-relative jump so we only + // modified that variable + } type; +}; + +UDis86Wrapper& disasm() +{ + static UDis86Wrapper instance; + return instance; +} + +void PauseOtherThreads() +{ + // TODO: implement me! +} + +void ResumePausedThreads() +{ + // TODO: implement me! should resume only the threads paused by PauseOtherThreads +} + +// not using the disassembler because this is simpler +LPBYTE ShortJumpTarget(LPBYTE address) +{ + int8_t off = *(address + 1); + return address + 2 + off; +} + +size_t GetJumpSize(LPBYTE, LPVOID) +{ + // TODO: it would be neater to use asmjit to generate this jump and ask asmjit for + // the size of this jump but with asmjit I can only generate absolute jumps, which + // take too much space. + + // Since trampoline buffers is always allocated within 32-bit + // address range of jump, we can say with confidence that a 5-byte jump is possible + return 5; +} + +void WriteLongJump(LPBYTE jumpAddr, LPVOID destination) +{ + // TODO: not using asmjit here because I couldn't figure out how to generate + // a working, space-optimized, relative jump to outside the generated code and + // we do want to optimize this jump +#if BOOST_ARCH_X86_64 + intptr_t dist = reinterpret_cast<intptr_t>(destination) - + (reinterpret_cast<intptr_t>(jumpAddr) + 5); + int32_t distShort = static_cast<int32_t>(dist); +#else + int32_t distShort = reinterpret_cast<intptr_t>(destination) - + (reinterpret_cast<intptr_t>(jumpAddr) + 5); +#endif + *jumpAddr = 0xE9; + *reinterpret_cast<int32_t*>(jumpAddr + 1) = distShort; +} + +void WriteSingleJump(THookInfo& hookInfo, HookError* error) +{ + DWORD oldprotect, ignore; + // Set the target function to copy on write, so we don't modify code for other + // processes + if (!VirtualProtect(hookInfo.originalFunction, JUMP_SIZE, PAGE_EXECUTE_WRITECOPY, + &oldprotect)) { + throw std::runtime_error("failed to change virtual protection"); + } + + WriteLongJump(reinterpret_cast<LPBYTE>(hookInfo.originalFunction), + hookInfo.trampoline); + + // restore old memory protection + if (!VirtualProtect(hookInfo.originalFunction, JUMP_SIZE, oldprotect, &ignore)) { + throw std::runtime_error("failed to change virtual protection"); + } + + if (error != nullptr) { + *error = ERR_NONE; + } +} + +void WriteShortJump(LPBYTE jumpAddr, const signed char offset) +{ + *jumpAddr = 0xEB; + *(jumpAddr + 1) = offset; +} + +void WriteIndirectJump(THookInfo& hookInfo, size_t jumpSize, HookError* error) +{ + DWORD oldProtect = 0; + LPBYTE jumpAddr = reinterpret_cast<LPBYTE>(hookInfo.originalFunction) - jumpSize; + // allow write to jump address + the short jump inside the function + if (!VirtualProtect(jumpAddr, jumpSize + 2, PAGE_EXECUTE_WRITECOPY, &oldProtect)) { + throw std::runtime_error("failed to change virtual protection"); + } + + // insert the long jump first, then the short jump to the long jump, thus + // activating the reroute + WriteLongJump(jumpAddr, hookInfo.trampoline); + + PauseOtherThreads(); + WriteShortJump(jumpAddr + jumpSize, -(static_cast<int8_t>(jumpSize) + 2)); + ResumePausedThreads(); + + // restore access protection + if (!VirtualProtect(jumpAddr, jumpSize + 2, oldProtect, &oldProtect)) { + throw std::runtime_error("failed to change virtual protection"); + } + if (error != nullptr) { + *error = ERR_NONE; + } +} + +/// implements function hooking using the mechanism intended for hot patching +/// Explanation: the visual studio compiler offers an option to prepare functions +/// for hot patching. In this case the compiler leaves room for one far jump before +/// the actual function and a 2-byte nop inside the function. +/// to hook such a function we write a jump to our replacement function to the +/// space before the function and short jump to that jump to where the 2-byte nop +/// was. +/// On 32-bit windows, MS seems to have compiled all relevant functions in the +/// windows API for hot patching starting with Windows XP SP3 +/// On 64-bit windows a lot of functions have the space for a jump before the function +/// but they don't have the 2-byte nop so this function doesn't work +/// \param hookInfo info about the hook to be installed +/// \return true on success, false on error +BOOL HookHotPatch(THookInfo& hookInfo, HookError* error) +{ + LPVOID original = reinterpret_cast<LPVOID>(hookInfo.originalFunction); + + if (hookInfo.stub) { + hookInfo.trampoline = TrampolinePool::instance().storeStub( + hookInfo.replacementFunction, + reinterpret_cast<LPVOID>(hookInfo.originalFunction), + shared::AddrAdd(original, 2)); + } else { + hookInfo.trampoline = TrampolinePool::instance().storeTrampoline( + hookInfo.replacementFunction, + reinterpret_cast<LPVOID>(hookInfo.originalFunction), + shared::AddrAdd(original, 2)); + } + + WriteIndirectJump(hookInfo, JUMP_SIZE, error); + hookInfo.type = THookInfo::TYPE_HOTPATCH; + // in this case we don't need a separate detour, we simply jump past the 2-byte nop + hookInfo.detour = reinterpret_cast<LPBYTE>(hookInfo.originalFunction) + 2; + + return TRUE; +} + +uintptr_t followJumps(THookInfo& hookInfo) +{ + LPBYTE original = reinterpret_cast<LPBYTE>(hookInfo.originalFunction); + LPBYTE shortTarget = ShortJumpTarget(original); + + // disassemble the long jump + disasm().setInputBuffer(shortTarget, JUMP_SIZE); + + ud_disassemble(disasm()); + if (ud_insn_mnemonic(disasm()) != UD_Ijmp) { + // this shouldn't happen, we only call this if the jump was discovered before + throw std::runtime_error("failed to find jump in patch"); + } + + uint64_t res = ud_insn_off(disasm()) + ud_insn_len(disasm()); + res += disasm().jumpOffset(); + + return static_cast<uintptr_t>(res); +} + +/// +/// \brief hook a call that is implemented as a short-jump to a long jump using a +/// rip-relative address variable +/// \param hookInfo info about the hook to be installed +/// \param error if the return code is false and this is not null, the referred-to +/// variable is set to an error code +/// \return true on success, false on error +/// +BOOL HookRIPIndirection(THookInfo& hookInfo, HookError* error) +{ + uintptr_t res = followJumps(hookInfo); + + const ud_operand_t* op = ud_insn_opr(disasm(), 0); + + if (op->base != UD_R_RIP) { + throw std::runtime_error("expected rip-relative addressing"); + } + + auto chainNext = disasm().jumpTarget(); + if (hookInfo.stub) { + hookInfo.trampoline = TrampolinePool::instance().storeStub( + hookInfo.replacementFunction, + reinterpret_cast<LPVOID>(hookInfo.originalFunction), + reinterpret_cast<LPVOID>(chainNext)); + } else { + hookInfo.trampoline = TrampolinePool::instance().storeTrampoline( + hookInfo.replacementFunction, + reinterpret_cast<LPVOID>(hookInfo.originalFunction), + reinterpret_cast<LPVOID>(chainNext)); + } + + DWORD oldProtect = 0; + if (!VirtualProtect(reinterpret_cast<LPVOID>(res), 2, PAGE_EXECUTE_WRITECOPY, + &oldProtect)) { + throw std::runtime_error("failed to change virtual protection"); + } + + *reinterpret_cast<uintptr_t*>(res) = reinterpret_cast<uintptr_t>(hookInfo.trampoline); + + if (!VirtualProtect(reinterpret_cast<LPVOID>(res), 2, oldProtect, &oldProtect)) { + throw std::runtime_error("failed to change virtual protection"); + } + + hookInfo.type = THookInfo::TYPE_RIPINDIRECT; + hookInfo.detour = reinterpret_cast<LPVOID>(chainNext); + + if (error != nullptr) { + *error = ERR_NONE; + } + + return TRUE; +} + +BOOL HookChainHook(THookInfo& hookInfo, LPBYTE jumpPos, HookError* error) +{ + // disassemble the long jump + disasm().setInputBuffer(jumpPos, JUMP_SIZE); + + ud_disassemble(disasm()); + if (ud_insn_mnemonic(disasm()) != UD_Ijmp) { + // this shouldn't happen, we only call this if the jump was discovered before + throw std::runtime_error("failed to find jump in patch"); + } + + auto chainTarget = disasm().jumpTarget(); + + size_t size = ud_insn_len(disasm()); + + // save the original code for the preamble so we can restore it later + hookInfo.preamble.resize(size); + memcpy(&hookInfo.preamble[0], jumpPos, size); + + spdlog::get("usvfs")->info("existing hook to {0:x} in {1}", chainTarget, + shared::string_cast<std::string>( + winapi::ex::wide::getSectionName((void*)chainTarget))); + + if (hookInfo.stub) { + hookInfo.trampoline = TrampolinePool::instance().storeStub( + hookInfo.replacementFunction, + reinterpret_cast<LPVOID>(hookInfo.originalFunction), + reinterpret_cast<LPVOID>(chainTarget)); + } else { + hookInfo.trampoline = TrampolinePool::instance().storeTrampoline( + hookInfo.replacementFunction, + reinterpret_cast<LPVOID>(hookInfo.originalFunction), + reinterpret_cast<LPVOID>(chainTarget)); + } + + DWORD oldProtect = 0; + if (!VirtualProtect(jumpPos, size, PAGE_EXECUTE_WRITECOPY, &oldProtect)) { + throw std::runtime_error("failed to change virtual protection"); + } + + WriteLongJump(jumpPos, hookInfo.trampoline); + + if (!VirtualProtect(jumpPos, size, oldProtect, &oldProtect)) { + throw std::runtime_error("failed to change virtual protection"); + } + + hookInfo.type = THookInfo::TYPE_CHAINPATCH; + hookInfo.detour = reinterpret_cast<LPVOID>(chainTarget); + + if (error != nullptr) { + *error = ERR_NONE; + } + + return TRUE; +} + +/// +/// implements hooking by chaining to an existing hook +/// \param hookInfo info about the hook to be installed +/// \param error if the return code is false and this is not null, +/// the referred-to variable is set to an error code +/// \return true on success, false on error +/// +BOOL HookChainHook(THookInfo& hookInfo, HookError* error) +{ + return HookChainHook(hookInfo, reinterpret_cast<LPBYTE>(hookInfo.originalFunction), + error); +} + +/// +/// implements hooking by chaining to an existing hot patch +/// \param hookInfo info about the hook to be installed +/// \param error if the return code is false and this is not null, +/// the referred-to variable is set to an error code +/// \return true on success, false on error +/// +BOOL HookChainPatch(THookInfo& hookInfo, HookError* error) +{ + LPBYTE original = reinterpret_cast<LPBYTE>(hookInfo.originalFunction); + LPBYTE shortTarget = ShortJumpTarget(original); + + return HookChainHook(hookInfo, shortTarget, error); +} + +/// implements function hooking by overwriting the first n bytes of the function +/// with a jump to the replacement function. Since this is destructive to the original +/// function code the first n bytes of the function need to be copied somewhere else +/// and that code needs to be called via a detour. This is a lot more complex than +/// the hotpatch mechanism. +/// \param hookInfo info about the hook to be installed +/// \param error if the return code is false and this is not null, the referred-to +/// variable is set to an error code +/// \return true on success, false on error +BOOL HookDisasm(THookInfo& hookInfo, HookError* error) +{ + LPBYTE address = reinterpret_cast<LPBYTE>(hookInfo.originalFunction); + ud_set_input_buffer(disasm(), address, 40); + + size_t jumpSize = GetJumpSize( + static_cast<LPBYTE>(hookInfo.originalFunction), + TrampolinePool::instance().currentBufferAddress(hookInfo.originalFunction)); + + // test if we have room for a jump before the function + bool jumpspace = true; + for (size_t i = 0; i < jumpSize; ++i) { + if (*(address - i - 1) != 0x90) { + jumpspace = false; + break; + } + } + + size_t minSize = jumpspace ? 2 : jumpSize; + + // iterate over all instructions that overlap with the jump instructions + // we want to write. + // TODO right now, this does not test if the function is smaller than the jump. + size_t size = 0; + while (size < minSize) { + if (ud_disassemble(disasm()) == 0) { + throw std::runtime_error("premature end of file in disassembly"); + } + + if ((size == 0) && (ud_insn_mnemonic(disasm()) == UD_Ijmp)) { + if (error != nullptr) { + *error = ERR_JUMP; + } + return FALSE; + } + + size += ud_insn_len(disasm()); + + // ret instruction or int3 smells like function end + if ((ud_insn_mnemonic(disasm()) == UD_Iret) || + (ud_insn_mnemonic(disasm()) == UD_Iint3)) { + if (error != nullptr) { + *error = ERR_FUNCEND; + } + return FALSE; + } + + // check the operands for relative addressing (not supported) + for (int i = 0; i < 3; ++i) { + const ud_operand* op = ud_insn_opr(disasm(), i); + if (op) { + if ( + // rip-relative are not handled, usually relative jumps + op->base == UD_R_RIP + + // rsp-relative call are not handled (there are valid rsp-relative + // instructions, e.g. sub) + || (ud_insn_mnemonic(disasm()) == UD_Icall && op->base == UD_R_RSP)) { + if (error != nullptr) { + *error = ERR_RIP; + } + return FALSE; + } + } + } + } + + // save the original code for the preamble so we can restore it later + hookInfo.preamble.resize(size); + memcpy(&hookInfo.preamble[0], hookInfo.originalFunction, size); + + size_t rerouteOffset = 0; + if (hookInfo.stub) { + hookInfo.trampoline = TrampolinePool::instance().storeStub( + hookInfo.replacementFunction, hookInfo.originalFunction, size, &rerouteOffset); + } else { + hookInfo.trampoline = TrampolinePool::instance().storeTrampoline( + hookInfo.replacementFunction, hookInfo.originalFunction, size, &rerouteOffset); + } + + if (jumpspace) { + WriteIndirectJump(hookInfo, jumpSize, error); + hookInfo.type = THookInfo::TYPE_WIN64PATCH; + } else { + WriteSingleJump(hookInfo, error); + hookInfo.type = THookInfo::TYPE_OVERWRITE; + } + hookInfo.detour = reinterpret_cast<LPBYTE>(hookInfo.trampoline) + rerouteOffset; + + return TRUE; +} + +enum EPreamble +{ + PRE_PATCHFREE, + PRE_PATCHUSED, + PRE_RIPINDIRECT, + PRE_FOREIGNHOOK, + PRE_UNKNOWN +}; + +EPreamble DeterminePreamble(LPBYTE address) +{ + ud_set_input_buffer(disasm(), address, JUMP_SIZE); + ud_disassemble(disasm()); + + if ((ud_insn_mnemonic(disasm()) == UD_Imov) && + (ud_insn_opr(disasm(), 0) == ud_insn_opr(disasm(), 1)) && + (ud_insn_opr(disasm(), 0)->type == UD_OP_REG)) { + // mov edi, edi + return PRE_PATCHFREE; + } else if ((ud_insn_mnemonic(disasm()) == UD_Ijmp) && (ud_insn_len(disasm()) == 2)) { + // determine target of the short jump + LPBYTE shortTarget = ShortJumpTarget(address); + + // test if that short jump leads to a long jump + ud_set_input_buffer(disasm(), shortTarget, JUMP_SIZE); + ud_disassemble(disasm()); + if (ud_insn_mnemonic(disasm()) == UD_Ijmp) { + const ud_operand* op = ud_insn_opr(disasm(), 0); + if (op->base == UD_R_RIP) { + return PRE_RIPINDIRECT; + } else { + return PRE_PATCHUSED; + } + } else { + return PRE_UNKNOWN; + } + } else if (ud_insn_mnemonic(disasm()) == UD_Ijmp) { + return PRE_FOREIGNHOOK; + } else { + return PRE_UNKNOWN; + } +} + +static std::map<HOOKHANDLE, THookInfo> s_Hooks; + +static HOOKHANDLE GenerateHandle() +{ + static ULONG NextHandle = 1; + return NextHandle++; +} + +HOOKHANDLE applyHook(THookInfo info, HookError* error) +{ + // apply the correct hook function depending on how the function start looks + EPreamble preamble = DeterminePreamble((LPBYTE)info.originalFunction); + + BOOL success = FALSE; + switch (preamble) { + case PRE_PATCHUSED: { + success = HookChainPatch(info, error); + } break; + case PRE_PATCHFREE: { + success = HookHotPatch(info, error); + } break; + case PRE_RIPINDIRECT: { + success = HookRIPIndirection(info, error); + } break; + case PRE_FOREIGNHOOK: { + success = HookChainHook(info, error); + } break; + default: { + success = HookDisasm(info, error); + } break; + } + + if (success == TRUE) { + HOOKHANDLE handle = GenerateHandle(); + s_Hooks[handle] = info; + return handle; + } else { + return INVALID_HOOK; + } +} + +HOOKHANDLE HookLib::InstallStub(LPVOID functionAddress, LPVOID stubAddress, + HookError* error) +{ + if (functionAddress == nullptr) { + if (error != nullptr) + *error = ERR_INVALIDPARAMETERS; + return INVALID_HOOK; + } + + THookInfo info; + info.originalFunction = functionAddress; + info.replacementFunction = stubAddress; + info.stub = true; + info.detour = nullptr; + info.trampoline = nullptr; + info.type = THookInfo::TYPE_OVERWRITE; + + return applyHook(info, error); +} + +HOOKHANDLE HookLib::InstallStub(HMODULE module, LPCSTR functionName, LPVOID stubAddress, + HookError* error) +{ + LPVOID funcAddr = MyGetProcAddress(module, functionName); + return InstallStub(funcAddr, stubAddress, error); +} + +HOOKHANDLE HookLib::InstallHook(LPVOID functionAddress, LPVOID hookAddress, + HookError* error) +{ + if (functionAddress == nullptr) { + if (error != nullptr) + *error = ERR_INVALIDPARAMETERS; + return INVALID_HOOK; + } + THookInfo info; + info.originalFunction = functionAddress; + info.replacementFunction = hookAddress; + info.stub = false; + info.detour = nullptr; + info.trampoline = nullptr; + info.type = THookInfo::TYPE_OVERWRITE; + + return applyHook(info, error); +} + +HOOKHANDLE HookLib::InstallHook(HMODULE module, LPCSTR functionName, LPVOID hookAddress, + HookError* error) +{ + LPVOID funcAddr = MyGetProcAddress(module, functionName); + return InstallHook(funcAddr, hookAddress, error); +} + +void HookLib::RemoveHook(HOOKHANDLE handle) +{ + auto iter = s_Hooks.find(handle); + if (iter != s_Hooks.end()) { + THookInfo info = iter->second; + PauseOtherThreads(); + LPBYTE address = reinterpret_cast<LPBYTE>(info.originalFunction); + if (info.type == THookInfo::TYPE_HOTPATCH) { + // return the short jump to 2-byte nop + // TODO: This doesn't take into account if we chain-loaded another hook + + DWORD oldProtect = 0; + if (!VirtualProtect(address, 2, PAGE_EXECUTE_WRITECOPY, &oldProtect)) { + throw shared::windows_error("failed to gain write access to remove hook"); + } + *address = 0x8b; + *(address + 1) = 0xff; + VirtualProtect(address, 2, oldProtect, &oldProtect); + } else if ((info.type == THookInfo::TYPE_OVERWRITE) || + (info.type == THookInfo::TYPE_WIN64PATCH)) { + DWORD oldProtect = 0; + if (!VirtualProtect(address, info.preamble.size(), PAGE_EXECUTE_WRITECOPY, + &oldProtect)) { + throw shared::windows_error("failed to gain write access to remove hook"); + } + // TODO: remove hook by restoring the original function. This only works if we + // have the exact code available somewhere + memcpy(address, &info.preamble[0], info.preamble.size()); + VirtualProtect(address, info.preamble.size(), oldProtect, &oldProtect); + } else if (info.type == THookInfo::TYPE_CHAINPATCH) { + // we could attempt to restore the original function preamble but I'm not + // sure we can reliably write the jump with same (or lower) size. + // Instead overwrite our own trampoline + disasm().setInputBuffer(static_cast<uint8_t*>(info.originalFunction), JUMP_SIZE); + ud_disassemble(disasm()); + if (ud_insn_mnemonic(disasm()) != UD_Ijmp) { + // this shouldn't happen, we only call this if the jump was discovered before + throw std::runtime_error("failed to find jump in patch"); + } + + LPBYTE jumpPos = static_cast<LPBYTE>(info.originalFunction); + if (ud_insn_len(disasm()) == 2) { + jumpPos = reinterpret_cast<LPBYTE>(disasm().jumpTarget()); + } + + DWORD oldProtect = 0; + if (!VirtualProtect(jumpPos, info.preamble.size(), PAGE_EXECUTE_WRITECOPY, + &oldProtect)) { + throw shared::windows_error("failed to gain write access to remove hook"); + } + memcpy(jumpPos, info.preamble.data(), info.preamble.size()); + VirtualProtect(jumpPos, info.preamble.size(), oldProtect, &oldProtect); + } else if (info.type == THookInfo::TYPE_RIPINDIRECT) { + uintptr_t res = followJumps(info); + DWORD oldProtect = 0; + if (!VirtualProtect(reinterpret_cast<LPVOID>(res), JUMP_SIZE, + PAGE_EXECUTE_WRITECOPY, &oldProtect)) { + throw shared::windows_error("failed to gain write access to remove hook"); + } + *reinterpret_cast<uintptr_t*>(res) = + reinterpret_cast<uintptr_t>(info.originalFunction); + VirtualProtect(reinterpret_cast<LPVOID>(res), JUMP_SIZE, oldProtect, &oldProtect); + } else { + spdlog::get("usvfs")->critical("can't remove hook, unknown hook type!"); + } + + s_Hooks.erase(iter); + ResumePausedThreads(); + } else { + spdlog::get("usvfs")->info("handle unknown: {0:x}", handle); + } +} + +const char* HookLib::GetErrorString(HookError err) +{ + switch (err) { + case ERR_NONE: + return "No Error"; + case ERR_INVALIDPARAMETERS: + return "Invalid parameters"; + case ERR_FUNCEND: + return "Function too short"; + case ERR_JUMP: + return "Function starts on a jump"; + case ERR_RIP: + return "RIP-relative addressing can't be relocated."; + case ERR_RELJUMP: + return "Relative Jump can't be relocated."; + default: + return "Unkown error code"; + } +} + +const char* HookLib::GetHookType(HOOKHANDLE handle) +{ + auto iter = s_Hooks.find(handle); + if (iter != s_Hooks.end()) { + THookInfo info = iter->second; + switch (info.type) { + case THookInfo::TYPE_HOTPATCH: + return "hot patch"; + case THookInfo::TYPE_WIN64PATCH: + return "64-bit hot patch"; + case THookInfo::TYPE_CHAINPATCH: + return "chained patch"; + case THookInfo::TYPE_OVERWRITE: + return "overwrite"; + case THookInfo::TYPE_RIPINDIRECT: + return "rip indirection modified"; + default: { + spdlog::get("usvfs")->error("invalid hook type {0}", info.type); + return "invalid hook type"; + } + } + } + return "invalid handle"; +} + +LPVOID HookLib::GetDetour(HOOKHANDLE handle) +{ + auto iter = s_Hooks.find(handle); + if (iter != s_Hooks.end()) { + THookInfo info = iter->second; + return info.detour; + } + return nullptr; +} diff --git a/libs/usvfs/src/thooklib/hooklib.h b/libs/usvfs/src/thooklib/hooklib.h new file mode 100644 index 0000000..a549c03 --- /dev/null +++ b/libs/usvfs/src/thooklib/hooklib.h @@ -0,0 +1,125 @@ +/* +Userspace Virtual Filesystem + +Copyright (C) 2015 Sebastian Herbord. All rights reserved. + +This file is part of usvfs. + +usvfs is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +usvfs is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with usvfs. If not, see <http://www.gnu.org/licenses/>. +*/ +#pragma once + +#include <map> +#include <windows_sane.h> + +namespace HookLib +{ + +enum HookError +{ + ERR_NONE, + ERR_INVALIDPARAMETERS, // parameters are invalid + ERR_FUNCEND, // function is too short to be hooked + ERR_JUMP, // function consists only of an unconditional jump. Maybe it has already + // been hooked? + ERR_RIP, // segment of the function to be overwritten contains a instruction-relative + // operation + ERR_RELJUMP // segment of the function to be overwritten contains a relative jump we + // can't relocated +}; + +typedef ULONG HOOKHANDLE; +static const HOOKHANDLE INVALID_HOOK = (HOOKHANDLE)-1; + +/// +/// \brief install a stub (function to be called before the target function) +/// \param functionAddress address of the function to stub +/// \param stubAddress address of the stub function. This function has to have the +/// signature of void foobar(LPVOID address). +/// address receives the address of the function. +/// \param error (optional) if set, the referenced variable will receive an error code +/// describing the problem (if any) +/// \return a handle to reference the hook in later operations or INVALID_HOOK on error +/// +HOOKHANDLE InstallStub(LPVOID functionAddress, LPVOID stubAddress, + HookError* error = nullptr); + +/// +/// \brief install a stub (function to be called before the target function) +/// \param module the module containing the function to hook +/// \param functionName name of the function to stub (as exported by the library) +/// \param stubAddress address of the stub function. This function has to have the +/// signature of void foobar(LPVOID address). +/// address receives the address of the function. +/// \param error (optional) if set, the referenced variable will receive an error code +/// describing the problem (if any) +/// \return a handle to reference the hook in later operations or INVALID_HOOK on error +/// +HOOKHANDLE InstallStub(HMODULE module, LPCSTR functionName, LPVOID stubAddress, + HookError* error = nullptr); + +/// +/// \brief install a hook (function replacing the existing functionality of the +/// function) +/// \param functionAddress address of the function to hook +/// \param hookAddress address of the replacement function. This function has to have +/// the exact same signature as the replaced function +/// \param error (optional) if set, the referenced variable will receive an error code +/// describing the problem (if any) +/// \return a handle to reference the hook in later operations or INVALID_HOOK on error +/// +HOOKHANDLE InstallHook(LPVOID functionAddress, LPVOID hookAddress, + HookError* error = nullptr); + +/// +/// \brief install a hook (function replacing the existing functionality of the +/// function) +/// \param functionName name of the function to hook (as exported by the library) +/// \param hookAddress address of the replacement function. This function has to have +/// the exact same signature as the replaced function +/// \param error (optional) if set, the referenced variable will receive an error code +/// describing the problem (if any) +/// \return a handle to reference the hook in later operations or INVALID_HOOK on error +/// +HOOKHANDLE InstallHook(HMODULE module, LPCSTR functionName, LPVOID hookAddress, + HookError* error = nullptr); + +/// +/// \brief remove a hook +/// \param handle handle returned in InstallStub or InstallHook +/// +void RemoveHook(HOOKHANDLE handle); + +/// +/// \brief determine the type of a hook +/// \param handle the handle to look up +/// \return a string describing the used hooking mechanism +/// +const char* GetHookType(HOOKHANDLE handle); + +/// +/// \brief retrieve the address that can be used to directly call a detour +/// \param handle handle for the hook +/// \return function address +/// +LPVOID GetDetour(HOOKHANDLE handle); + +/// +/// \brief resolve an error code to a descriptive string +/// \param err the error code to resolve +/// \return the error string +/// +const char* GetErrorString(HookError err); + +} // namespace HookLib diff --git a/libs/usvfs/src/thooklib/pch.cpp b/libs/usvfs/src/thooklib/pch.cpp new file mode 100644 index 0000000..1d9f38c --- /dev/null +++ b/libs/usvfs/src/thooklib/pch.cpp @@ -0,0 +1 @@ +#include "pch.h" diff --git a/libs/usvfs/src/thooklib/ttrampolinepool.cpp b/libs/usvfs/src/thooklib/ttrampolinepool.cpp new file mode 100644 index 0000000..3b6a51f --- /dev/null +++ b/libs/usvfs/src/thooklib/ttrampolinepool.cpp @@ -0,0 +1,601 @@ +/* +Userspace Virtual Filesystem + +Copyright (C) 2015 Sebastian Herbord. All rights reserved. + +This file is part of usvfs. + +usvfs is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +usvfs is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with usvfs. If not, see <http://www.gnu.org/licenses/>. +*/ +#include "ttrampolinepool.h" +#include <addrtools.h> +#include <shmlogger.h> +// #include <boost/thread/lock_guard.hpp> +#include "udis86wrapper.h" + +using namespace asmjit; +#if BOOST_ARCH_X86_64 +using namespace x86; +#elif BOOST_ARCH_X86_32 +using namespace asmjit::x86; +#endif + +using namespace usvfs::shared; + +namespace HookLib +{ + +TrampolinePool* TrampolinePool::s_Instance = nullptr; + +TrampolinePool::TrampolinePool() : m_MaxTrampolineSize(sizeof(LPVOID)) +{ + m_BarrierAddr = &TrampolinePool::barrier; + m_ReleaseAddr = &TrampolinePool::release; + + SYSTEM_INFO sysInfo; + ::ZeroMemory(&sysInfo, sizeof(SYSTEM_INFO)); + GetSystemInfo(&sysInfo); + m_BufferSize = sysInfo.dwPageSize; + + // if search range = ffffff then addressmask = ffffffffff000000 + // => all jumps between xxxxxxxxxx000000 and xxxxxxxxxxffffff will use the same buffer + // for trampolines which is guaranteed to be in that range + // TODO it should be valid to use 2 ^ 32 as the search range to increase our chances + // of finding a memory block we can reserve but then there is a problem with + // converting negative jump distances to 32 bit I didn't understand. Everything + // up to 2 ^ 31 seems to be fine though + m_SearchRange = static_cast<size_t>(pow(2, 30)) - 1; + m_AddressMask = std::numeric_limits<uint64_t>::max() - m_SearchRange; +} + +// static +void TrampolinePool::initialize() +{ + if (!s_Instance) + s_Instance = new TrampolinePool(); +} + +void TrampolinePool::setBlock(bool block) +{ + m_FullBlock = block; + if (m_ThreadGuards.get() == nullptr) { + m_ThreadGuards.reset(new TThreadMap()); + } +} + +#if BOOST_ARCH_X86_64 +// push all registers (except rax) and flags to the stack +static void pushAll(X86Assembler& assembler) +{ + assembler.pushf(); + assembler.push(rcx); + assembler.push(rdx); + assembler.push(rbx); + assembler.push(rbp); + assembler.push(rsi); + assembler.push(rdi); + assembler.push(r8); + assembler.push(r9); + assembler.push(r10); + assembler.push(r11); + assembler.push(r12); + assembler.push(r13); + assembler.push(r14); + assembler.push(r15); +} + +// pop all registers (except rax) and flags from stack +static void popAll(X86Assembler& assembler) +{ + assembler.pop(r15); + assembler.pop(r14); + assembler.pop(r13); + assembler.pop(r12); + assembler.pop(r11); + assembler.pop(r10); + assembler.pop(r9); + assembler.pop(r8); + assembler.pop(rdi); + assembler.pop(rsi); + assembler.pop(rbp); + assembler.pop(rbx); + assembler.pop(rdx); + assembler.pop(rcx); + assembler.popf(); +} +#endif // BOOST_ARCH_X86_64 + +void TrampolinePool::addBarrier(LPVOID rerouteAddr, LPVOID original, + X86Assembler& assembler) +{ + Label skipLabel = assembler.newLabel(); + +#if BOOST_ARCH_X86_64 + pushAll(assembler); + assembler.mov(rcx, + imm(reinterpret_cast<int64_t>( + original))); // set call parameter for call to barrier function + assembler.mov(rax, imm((intptr_t)(void*)barrier)); + assembler.sub(rsp, 32); + assembler.call(rax); + assembler.add(rsp, 32); + popAll(assembler); + // test barrier + assembler.cmp(rax, 0); // test if the barrier is locked + assembler.jz(skipLabel); // skip if barrier was locked + + // call replacement function + // for this call no registers are saved. the called function is a compiled function so + // it should correctly save non-volatile registers, and the caller can't expect the + // volatile ones to remain valid + assembler.pop(r10); + assembler.mov(dword_ptr(rax), r10); // store that return address to the variable + // supplied by the barrier function + assembler.mov(rax, imm((intptr_t)(LPVOID)rerouteAddr)); + assembler.call(rax); + assembler.push(rax); // save away result + + // open the barrier again + pushAll(assembler); + assembler.mov(rcx, imm(reinterpret_cast<int64_t>(original))); + assembler.mov(rax, imm((intptr_t)(void*)release)); + assembler.sub(rsp, 32); + assembler.call(rax); + assembler.add(rsp, 32); + popAll(assembler); + assembler.pop(r10); // get the result from the replacement function to a register + assembler.push(rax); // push the original return address back on the stack + assembler.mov(rax, r10); // move result of actual call to rax + assembler.ret(); // return, using the original return address +#else // BOOST_ARCH_X86_64 + assembler.push(imm(void_ptr_cast<int32_t>( + original))); // push original function, as parameter to barrier + assembler.mov(ecx, (Ptr) static_cast<void*>(TrampolinePool::barrier)); + assembler.call(ecx); // call barrier function + assembler.cmp(eax, 0); + assembler.jz(skipLabel); // if barrier is locked, jump to end of function + + // case a: we got through the barrier + assembler.pop(ecx); // pop the return address into ecx + assembler.mov(dword_ptr(eax), ecx); // store that return address in the variable + // supplied by the barrier function + + assembler.mov(eax, (Ptr) static_cast<void*>(rerouteAddr)); + assembler.call(eax); // call replacement function (pointer was stored in front of the + // trampoline) (this function gets the parameters that were on + // the stack already and cleans + // them up itself (stdcall convention)) + assembler.push(eax); // save away result + assembler.push(imm(void_ptr_cast<int32_t>(original))); // open the barrier again + assembler.mov(eax, (Ptr) static_cast<void*>(TrampolinePool::release)); + assembler.call(eax); + assembler.pop(ecx); // pop the result from the actual call to ecx + assembler.push(eax); // push the original return address (returned by + // TTrampolinePool::release) back on the stack + assembler.mov(eax, ecx); // move result of actual call to eax + assembler.ret(); // return, using the original return address +#endif // BOOST_ARCH_X86_64 + + assembler.bind(skipLabel); +} + +LPVOID TrampolinePool::roundAddress(LPVOID address) const +{ + return reinterpret_cast<LPVOID>(reinterpret_cast<intptr_t>(address) & m_AddressMask); +} + +TrampolinePool::BufferList& TrampolinePool::getBufferList(LPVOID address) +{ + LPVOID rounded = roundAddress(address); + auto iter = m_Buffers.find(rounded); + if (iter == m_Buffers.end()) { + BufferList newBufList = {0, std::vector<LPVOID>()}; + m_Buffers[rounded] = newBufList; + iter = allocateBuffer(address); + } + return iter->second; +} + +LPVOID TrampolinePool::storeStub(LPVOID reroute, LPVOID original, LPVOID returnAddress) +{ + BufferList& bufferList = getBufferList(original); + // first test to increase likelyhood we don't have to reallocate later + if (bufferList.offset + m_MaxTrampolineSize > m_BufferSize) { + allocateBuffer(original); + } + + LPVOID spot = AddrAdd(*bufferList.buffers.rbegin(), bufferList.offset); + + // ??? write address of reroute to trampoline and move past the address + *reinterpret_cast<LPVOID*>(spot) = reroute; + // coverity[suspicious_sizeof] + spot = AddrAdd(spot, sizeof(LPVOID)); + bufferList.offset += sizeof(LPVOID); + + JitRuntime runtime; +#if BOOST_ARCH_X86_64 + X86Assembler assembler(&runtime); +#else + X86Assembler assembler(&runtime); +#endif + addCallToStub(assembler, original, reroute); + addAbsoluteJump(assembler, reinterpret_cast<uint64_t>(returnAddress)); + + size_t codeSize = assembler.getCodeSize(); + + m_MaxTrampolineSize = + std::max(m_MaxTrampolineSize, static_cast<int>(codeSize + sizeof(LPVOID))); + + // final test to see if we can store the trampoline in the buffer + if ((bufferList.offset + codeSize) > m_BufferSize) { + // can't place function in buffer, allocate another and try again + allocateBuffer(original); + // we could relocate the code and the data but this is simpler + return storeStub(reroute, original, returnAddress); + } + + // adjust relative jumps for move to buffer + codeSize = assembler.relocCode(spot); + + uint8_t* code = assembler.getBuffer(); + memcpy(spot, code, codeSize); + + bufferList.offset += codeSize; + + return spot; +} + +LPVOID TrampolinePool::storeTrampoline(LPVOID reroute, LPVOID original, + LPVOID returnAddress) +{ + BufferList& bufferList = getBufferList(original); + // first test to increase likelyhood we don't have to reallocate later + if (bufferList.offset + m_MaxTrampolineSize > m_BufferSize) { + allocateBuffer(original); + } + + LPVOID spot = AddrAdd(*bufferList.buffers.rbegin(), bufferList.offset); + + *reinterpret_cast<LPVOID*>(spot) = reroute; + // coverity[suspicious_sizeof] + spot = AddrAdd(spot, sizeof(LPVOID)); + bufferList.offset += sizeof(LPVOID); + + JitRuntime runtime; + X86Assembler assembler(&runtime); + addBarrier(reroute, original, assembler); +#if BOOST_ARCH_X86_64 + assembler.mov(rax, imm((intptr_t)(void*)(returnAddress))); + assembler.jmp(rax); +#else + assembler.mov(eax, imm((intptr_t)(void*)(returnAddress))); + assembler.jmp(eax); +#endif + size_t codeSize = assembler.getCodeSize(); + + m_MaxTrampolineSize = + std::max(m_MaxTrampolineSize, static_cast<int>(codeSize + sizeof(LPVOID))); + + // final test to see if we can store the trampoline in the buffer + if ((bufferList.offset + codeSize) > m_BufferSize) { + // can't place function in buffer, allocate another and try again + allocateBuffer(original); + // we could relocate the code and the data but this is simpler + return storeTrampoline(reroute, original, returnAddress); + } + + // adjust relative jumps for move to buffer + codeSize = assembler.relocCode(spot); + + // copy code to buffer + uint8_t* code = assembler.getBuffer(); + memcpy(spot, code, codeSize); + + bufferList.offset += codeSize; + return spot; +} + +#if BOOST_ARCH_X86_64 +void TrampolinePool::copyCode(X86Assembler& assembler, LPVOID source, size_t numBytes) +{ + static UDis86Wrapper disasm; + + disasm.setInputBuffer(static_cast<const uint8_t*>(source), numBytes); + + size_t offset = 0; + + while (ud_disassemble(disasm) != 0) { + // rewrite relative jumps, blind copy everything else + + offset += ud_insn_len(disasm); + + // WARNING: doesn't support conditional jumps + if ((ud_insn_mnemonic(disasm) == UD_Ijmp) && + (ud_insn_opr(disasm, 0)->type == UD_OP_JIMM)) { + uintptr_t dest = disasm.jumpTarget(); + assembler.mov(rax, imm(static_cast<uint64_t>(dest))); + assembler.jmp(rax); + } else { + assembler.embed(ud_insn_ptr(&disasm.obj()), ud_insn_len(&disasm.obj())); + // assembler.data(); + } + } +} +#endif + +void TrampolinePool::addCallToStub(X86Assembler& assembler, LPVOID original, + LPVOID reroute) +{ +#if BOOST_ARCH_X86_64 + pushAll(assembler); + assembler.mov(rcx, imm(reinterpret_cast<int64_t>(original))); + assembler.mov(rax, imm((intptr_t)(LPVOID)reroute)); + assembler.sub(rsp, 32); + assembler.call(rax); + assembler.add(rsp, 32); + popAll(assembler); +#else // BOOST_ARCH_X86_64 + assembler.push(reinterpret_cast<int64_t>(original)); + assembler.mov(ecx, imm((intptr_t)(LPVOID)reroute)); + assembler.call(ecx); + assembler.pop(ecx); // remove argument from stack +#endif // BOOST_ARCH_X86_64 +} + +void TrampolinePool::addAbsoluteJump(X86Assembler& assembler, uint64_t destination) +{ +#if BOOST_ARCH_X86_64 + assembler.push(rax); + assembler.push(rax); + assembler.mov(rax, imm(destination)); + assembler.mov(ptr(rsp, 8), rax); + assembler.pop(rax); + assembler.ret(); +#else // BOOST_ARCH_X86_64 + assembler.push(imm(destination)); + assembler.ret(); +#endif // BOOST_ARCH_X86_64 +} + +LPVOID TrampolinePool::storeStub(LPVOID reroute, LPVOID original, size_t preambleSize, + size_t* rerouteOffset) +{ + BufferList& bufferList = getBufferList(original); + // first test to increase likelyhood we don't have to reallocate later + if (bufferList.offset + m_MaxTrampolineSize > m_BufferSize) { + allocateBuffer(original); + } + + LPVOID spot = AddrAdd(*bufferList.buffers.rbegin(), bufferList.offset); + + *reinterpret_cast<LPVOID*>(spot) = reroute; + // coverity[suspicious_sizeof] + spot = AddrAdd(spot, sizeof(LPVOID)); + bufferList.offset += sizeof(LPVOID); + + JitRuntime runtime; + X86Assembler assembler(&runtime); + addCallToStub(assembler, original, reroute); +#if BOOST_ARCH_X86_64 + // insert backup code + *rerouteOffset = assembler.getCodeSize(); + copyCode(assembler, original, preambleSize); +#else // BOOST_ARCH_X86_64 + assembler.embed(original, preambleSize); +#endif // BOOST_ARCH_X86_64 + addAbsoluteJump(assembler, reinterpret_cast<uint64_t>(original) + preambleSize); + + // adjust relative jumps for move to buffer + size_t codeSize = assembler.getCodeSize(); + + m_MaxTrampolineSize = + std::max(m_MaxTrampolineSize, static_cast<int>(codeSize + sizeof(LPVOID))); + + // final test to see if we can store the trampoline in the buffer + if ((bufferList.offset + codeSize) > m_BufferSize) { + // can't place function in buffer, allocate another and try again + allocateBuffer(original); + // we could relocate the code and the data but this is simpler + return storeStub(reroute, original, preambleSize, rerouteOffset); + } + + // copy code to buffer + codeSize = assembler.relocCode(spot); + + bufferList.offset += preambleSize + codeSize; + return spot; +} + +LPVOID TrampolinePool::storeTrampoline(LPVOID reroute, LPVOID original, + size_t preambleSize, size_t* rerouteOffset) +{ + BufferList& bufferList = getBufferList(original); + // first test to increase likelyhood we don't have to reallocate later + if (bufferList.offset + m_MaxTrampolineSize > m_BufferSize) { + allocateBuffer(original); + } + + LPVOID spot = AddrAdd(*bufferList.buffers.rbegin(), bufferList.offset); + + *reinterpret_cast<LPVOID*>(spot) = reroute; + // coverity[suspicious_sizeof] + spot = AddrAdd(spot, sizeof(LPVOID)); + bufferList.offset += sizeof(LPVOID); + + JitRuntime runtime; + X86Assembler assembler(&runtime); + addBarrier(reroute, original, assembler); + // insert backup code + *rerouteOffset = assembler.getCodeSize(); + assembler.embed(original, static_cast<uint32_t>(preambleSize)); + addAbsoluteJump(assembler, reinterpret_cast<uint64_t>(original) + preambleSize); + + // adjust relative jumps for move to buffer + size_t codeSize = assembler.getCodeSize(); + + m_MaxTrampolineSize = + std::max(m_MaxTrampolineSize, static_cast<int>(codeSize + sizeof(LPVOID))); + + // TODO this does not take into account that the code size may technically change + // after relocation in which case the following test may determine the code fits into + // the buffer when it really doesnt't. asmjit doesn't seem to provide a way to adjust + // jumps without actually moving the code though + + // final test to see if we can store the trampoline in the buffer + if ((bufferList.offset + codeSize) > m_BufferSize) { + // can't place function in buffer, allocate another and try again + allocateBuffer(original); + // we could relocate the code and the data but this is simpler + return storeTrampoline(reroute, original, preambleSize, rerouteOffset); + } + + // copy code to buffer + codeSize = static_cast<size_t>(assembler.relocCode(spot)); + + bufferList.offset += preambleSize + codeSize; + + return spot; +} + +LPVOID TrampolinePool::currentBufferAddress(LPVOID addressNear) +{ + LPVOID rounded = roundAddress(addressNear); + auto lookupAddress = m_Buffers.find(rounded); + + if (lookupAddress == m_Buffers.end()) { + lookupAddress = m_Buffers.insert(std::make_pair(rounded, BufferList())).first; + } + if (lookupAddress->second.buffers.size() == 0) { + allocateBuffer(addressNear); + } + + LPVOID res = *(lookupAddress->second.buffers.rbegin()); + return res; +} + +void TrampolinePool::forceUnlockBarrier() +{ + if (m_ThreadGuards.get() != nullptr) { + for (auto funcId : *m_ThreadGuards) { + (*m_ThreadGuards)[funcId.first] = nullptr; + } + } // else no barriers to unlock +} + +TrampolinePool::BufferMap::iterator TrampolinePool::allocateBuffer(LPVOID addressNear) +{ + // allocate a buffer that we can write to and that is executable + SYSTEM_INFO sysInfo; + ::ZeroMemory(&sysInfo, sizeof(SYSTEM_INFO)); + GetSystemInfo(&sysInfo); + + LPVOID rounded = roundAddress(addressNear); + auto iter = m_Buffers.find(rounded); + uintptr_t lowerEnd = reinterpret_cast<uintptr_t>(rounded); + if (iter->second.buffers.size() > 0) { + // start searching were we last found a buffer + lowerEnd = reinterpret_cast<uintptr_t>(*iter->second.buffers.rbegin()) + + sysInfo.dwPageSize; + } + + uintptr_t start = + std::max(std::max(lowerEnd, MIN_ALLOC_ADDR), + reinterpret_cast<uintptr_t>(sysInfo.lpMinimumApplicationAddress)); + uintptr_t upperEnd = reinterpret_cast<uintptr_t>(rounded) + m_SearchRange; + uintptr_t end = std::min( + upperEnd, reinterpret_cast<uintptr_t>(sysInfo.lpMaximumApplicationAddress)); + + LPVOID buffer = nullptr; + for (uintptr_t cur = start; cur < end; cur += sysInfo.dwPageSize) { + buffer = VirtualAlloc(reinterpret_cast<LPVOID>(cur), m_BufferSize, + MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE); + if (buffer != nullptr) { + break; + } + } + if (buffer == nullptr) { + throw std::runtime_error("failed to allocate buffer in range"); + } + + // the caller must have looked up the bufferlist in order to determine that a + // buffer has to be allocated + assert(iter != m_Buffers.end()); + + iter->second.offset = 0; + iter->second.buffers.push_back(buffer); + spdlog::get("usvfs")->debug( + "allocated trampoline buffer for jumps between {0:p} and {1:x} at {2:p}" + "(size {3})", + rounded, (reinterpret_cast<uintptr_t>(rounded) + m_SearchRange), buffer, + m_BufferSize); + return iter; +} + +LPVOID TrampolinePool::barrier(LPVOID function) +{ + DWORD err = GetLastError(); + LPVOID res = instance().barrierInt(function); + SetLastError(err); + return res; +} + +LPVOID TrampolinePool::release(LPVOID function) +{ + DWORD err = GetLastError(); + LPVOID res = instance().releaseInt(function); + SetLastError(err); + return res; +} + +LPVOID TrampolinePool::barrierInt(LPVOID func) +{ + if (m_FullBlock) { + return nullptr; + } + + if (m_ThreadGuards.get() == nullptr) { + m_ThreadGuards.reset(new TThreadMap()); + } + + auto iter = m_ThreadGuards->find(func); + if ((iter == m_ThreadGuards->end()) || (iter->second == nullptr)) { + (*m_ThreadGuards)[func] = reinterpret_cast<LPVOID>(1); + return &(*m_ThreadGuards)[func]; + } else { + return nullptr; + } +} + +LPVOID TrampolinePool::releaseInt(LPVOID func) +{ + DWORD lastError = GetLastError(); + if (m_ThreadGuards.get() == nullptr) { + m_ThreadGuards.reset(new TThreadMap()); + } + + auto iter = m_ThreadGuards->find(func); + if (iter == m_ThreadGuards->end()) { + spdlog::get("hooks")->error("failed to release barrier for func {}", func); + ::SetLastError(lastError); + return nullptr; + } + + LPVOID res = (*m_ThreadGuards)[func]; + (*m_ThreadGuards)[func] = nullptr; + + ::SetLastError(lastError); + return res; +} + +} // namespace HookLib diff --git a/libs/usvfs/src/thooklib/ttrampolinepool.h b/libs/usvfs/src/thooklib/ttrampolinepool.h new file mode 100644 index 0000000..5b44c51 --- /dev/null +++ b/libs/usvfs/src/thooklib/ttrampolinepool.h @@ -0,0 +1,222 @@ +/* +Userspace Virtual Filesystem + +Copyright (C) 2015 Sebastian Herbord. All rights reserved. + +This file is part of usvfs. + +usvfs is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +usvfs is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with usvfs. If not, see <http://www.gnu.org/licenses/>. +*/ +#pragma once + +#include "asmjit_sane.h" +#include <logging.h> +#include <map> +#include <vector> +#include <windows_sane.h> + +#ifdef _MSC_VER +#pragma warning(disable : 4714) +#include <boost/config/compiler/visualc.hpp> +#else +#include <boost/config/compiler/gcc.hpp> +#endif +#include <boost/predef.h> +#include <boost/thread.hpp> +#include <mutex> + +// #include <boost/thread/mutex.hpp> + +namespace HookLib +{ + +/// +/// trampolines are runtime-generated mini-functions that are used to call the +/// original code of a function +/// +class TrampolinePool +{ +public: + /// Call initialize before you use the TrampolinePool + static void initialize(); + + static TrampolinePool& instance() + { + // This is a very very sensitive place so we want to keep this function as simple as + // possible and having it inlined probably cann't hurt + return *s_Instance; + } + + void setBlock(bool block); + + /// + /// store a stub without moving code from the original function. This is used in cases + /// where the hook can be placed without overwriting logic (i.e. hot-patchable + /// functions and when chaining hooks) + /// \param reroute the stub function to call before the regular function (on x86 this + /// needs to be cdecl calling convention!) + /// \param original the original function + /// \param returnAddress address under which the original functionality can be + /// reached. + /// for the first hook this should be (original + 2), otherwise + /// the address of the next hook in the chain + /// \return address of the created trampoline function + /// + LPVOID storeStub(LPVOID reroute, LPVOID original, LPVOID returnAddress); + + /// + /// store a stub, moving part of the original function to the trampoline + /// \param reroute the stub function to call before the regular function (on x86 this + /// needs to be cdecl calling convention!) + /// \param original the original function + /// \param preambleSize number of bytes from the original function to backup. Needs to + /// correspond to complete instructions + /// \param rerouteOffset offset in bytes from the created trampoline to the preamble + /// that leads back to the original code + /// \return address of the created trampoline function + /// + LPVOID storeStub(LPVOID reroute, LPVOID original, size_t preambleSize, + size_t* rerouteOffset); + + /// + /// store a trampoline for hot-patchable functions, where the original function + /// is unharmed. + /// \param reroute the reroute function + /// \param original original function + /// \param returnAddress address under which the original functionality can be + /// reached. + /// \return address of the trampoline function + /// + LPVOID storeTrampoline(LPVOID reroute, LPVOID original, LPVOID returnAddress); + + /// + /// store a trampoline, copying a part of the original function to the trampoline. + /// This is used for the case where the hooking mechanism needs to overwrite part of + /// the function + /// \param reroute the reroute function + /// \param original original function + /// \param preambleSize number of bytes from the original function to backup. Needs to + /// correspond to complete instructions + /// \param rerouteOffset offset in bytes from the created trampoline to the preamble + /// that leads us back to the original code + /// \return address of the trampoline function + /// + LPVOID storeTrampoline(LPVOID reroute, LPVOID original, size_t preambleSize, + size_t* rerouteOffset); + + /// + /// \param addressNear used to find a trampoline buffer near the jump instruction + /// \return retrieve address of current trampoline buffer + /// + LPVOID currentBufferAddress(LPVOID addressNear); + + /// + /// \brief forces the barrier(s) for the current thread to be released + /// + void forceUnlockBarrier(); + +private: + struct BufferList + { + size_t offset; + std::vector<LPVOID> buffers; + }; + + typedef std::map<LPVOID, BufferList> BufferMap; + static const intptr_t ADDRESS_MASK = + 0xFFFFFFFFFF000000LL; // mask to "round" addresses to consolidate near + // trampolines + +private: + TrampolinePool(); + + TrampolinePool& operator=(const TrampolinePool& reference); // not implemented + + /** + * @brief allocates a buffer with read, write and execute rights near the + * specified adress. The purpose is that we want to be able to jump from + * adressNear to generated code with a 5-byte jump, even on x64 systems. + * @param addressNear the reference adress + * @note the resulting buffer is stored in the m_Buffers map + */ + BufferMap::iterator allocateBuffer(LPVOID addressNear); + + void addBarrier(LPVOID rerouteAddr, LPVOID original, asmjit::X86Assembler& assembler); + +#if BOOST_ARCH_X86_64 + void copyCode(asmjit::X86Assembler& assembler, LPVOID source, size_t numBytes); +#endif // BOOST_ARCH_X86_64 + + BufferList& getBufferList(LPVOID address); + + LPVOID roundAddress(LPVOID address) const; + +public: + static LPVOID __stdcall barrier(LPVOID function); + static LPVOID __stdcall release(LPVOID function); + + LPVOID barrierInt(LPVOID function); + LPVOID releaseInt(LPVOID function); + + void addCallToStub(asmjit::X86Assembler& assembler, LPVOID original, LPVOID reroute); + +private: + /// + /// \brief add a jump to an address outside the custom generated asm code (without + /// modifying registers) + /// \param assembler the assembler generator to write to + /// \param destination destination address + /// \note this currently generates a lot code on x64, may be overly complicated + /// + void addAbsoluteJump(asmjit::X86Assembler& assembler, uint64_t destination); + + DWORD determinePageSize(); + +private: +#if BOOST_ARCH_X86_64 + static const int SIZE_OF_JUMP = 13; +#elif BOOST_ARCH_X86_32 + static const int SIZE_OF_JUMP = 5; +#endif + + // the hook lib tries to allocate buffer close to the hooked functions, + // for 32-bits application, this often falls on 0x40000000, which is the + // address at which DLLs can be loaded on Windows + // + // some old-style DRM (e.g. Dragon Age 2) check that address and prevent + // the game from running if it's not 0x40000000, so we give them a bit of + // spaces before allocating our buffers + // + static constexpr uintptr_t MIN_ALLOC_ADDR = 0x40000000 + 0x200000; + + static TrampolinePool* s_Instance; + + bool m_FullBlock{false}; + + BufferMap m_Buffers; + + typedef std::map<void*, void*> TThreadMap; + boost::thread_specific_ptr<TThreadMap> m_ThreadGuards; + + LPVOID m_BarrierAddr; + LPVOID m_ReleaseAddr; + + DWORD m_BufferSize = {1024}; + size_t m_SearchRange; + uint64_t m_AddressMask; + + int m_MaxTrampolineSize; +}; + +} // namespace HookLib diff --git a/libs/usvfs/src/thooklib/udis86wrapper.cpp b/libs/usvfs/src/thooklib/udis86wrapper.cpp new file mode 100644 index 0000000..1ac7332 --- /dev/null +++ b/libs/usvfs/src/thooklib/udis86wrapper.cpp @@ -0,0 +1,102 @@ +/* +Userspace Virtual Filesystem + +Copyright (C) 2015 Sebastian Herbord. All rights reserved. + +This file is part of usvfs. + +usvfs is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +usvfs is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with usvfs. If not, see <http://www.gnu.org/licenses/>. +*/ +#include "udis86wrapper.h" +#include "pch.h" +#include <boost/predef.h> +#include <shmlogger.h> +#include <stdexcept> + +namespace HookLib +{ + +UDis86Wrapper::UDis86Wrapper() +{ + ud_init(&m_Obj); + ud_set_syntax(&m_Obj, UD_SYN_INTEL); +#if BOOST_ARCH_X86_64 + ud_set_mode(&m_Obj, 64); +#else + ud_set_mode(&m_Obj, 32); +#endif +} + +void UDis86Wrapper::setInputBuffer(const uint8_t* buffer, size_t size) +{ + m_Buffer = buffer; + ud_set_input_buffer(&m_Obj, buffer, size); + ud_set_pc(&m_Obj, reinterpret_cast<uint64_t>(m_Buffer)); +} + +ud_t& UDis86Wrapper::obj() +{ + return m_Obj; +} + +bool UDis86Wrapper::isRelativeJump() +{ + ud_mnemonic_code code = ud_insn_mnemonic(&m_Obj); + // all conditional jumps and loops are relative, as are unconditional jumps with an + // offset operand + return (code == UD_Ijo) || (code == UD_Ijno) || (code == UD_Ijb) || + (code == UD_Ijae) || (code == UD_Ijz) || (code == UD_Ijnz) || + (code == UD_Ijbe) || (code == UD_Ija) || (code == UD_Ijs) || + (code == UD_Ijns) || (code == UD_Ijp) || (code == UD_Ijnp) || + (code == UD_Ijl) || (code == UD_Ijge) || (code == UD_Ijle) || + (code == UD_Ijg) || (code == UD_Ijcxz) || (code == UD_Ijecxz) || + (code == UD_Ijrcxz) || (code == UD_Iloop) || (code == UD_Iloope) || + (code == UD_Iloopne) || + ((code == UD_Icall) && (ud_insn_opr(&m_Obj, 0)->type == UD_OP_JIMM)) || + ((code == UD_Ijmp) && (ud_insn_opr(&m_Obj, 0)->type == UD_OP_JIMM)); +} + +intptr_t UDis86Wrapper::jumpOffset() +{ + const ud_operand_t* op = ud_insn_opr(&m_Obj, 0); + switch (op->size) { + case 8: + return static_cast<intptr_t>(op->lval.sbyte); + case 16: + return static_cast<intptr_t>(op->lval.sword); + case 32: + return static_cast<intptr_t>(op->lval.sdword); + case 64: + return static_cast<intptr_t>(op->lval.sqword); + default: + throw std::runtime_error("unsupported jump size"); + } +} + +uint64_t UDis86Wrapper::jumpTarget() +{ + // TODO: assert we're actually on a jump + + uint64_t res = ud_insn_off(&m_Obj) + ud_insn_len(&m_Obj); + + res += jumpOffset(); + + if (ud_insn_opr(&m_Obj, 0)->base == UD_R_RIP) { + res = *reinterpret_cast<uintptr_t*>(res); + } + + return res; +} + +} // namespace HookLib diff --git a/libs/usvfs/src/thooklib/udis86wrapper.h b/libs/usvfs/src/thooklib/udis86wrapper.h new file mode 100644 index 0000000..48d8a42 --- /dev/null +++ b/libs/usvfs/src/thooklib/udis86wrapper.h @@ -0,0 +1,62 @@ +/* +Userspace Virtual Filesystem + +Copyright (C) 2015 Sebastian Herbord. All rights reserved. + +This file is part of usvfs. + +usvfs is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +usvfs is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with usvfs. If not, see <http://www.gnu.org/licenses/>. +*/ +#pragma once + +#include <udis86.h> +#undef inline // libudis86/types.h defines inline to __inline which is no longer legal + // since vs2012 + +namespace HookLib +{ + +class UDis86Wrapper +{ + +public: + UDis86Wrapper(); + + void setInputBuffer(const uint8_t* buffer, size_t size); + + ud_t& obj(); + + operator ud_t*() { return &m_Obj; } + + bool isRelativeJump(); + + intptr_t jumpOffset(); + + /// + /// determines the absolute jump target at the current instruction, taking into + /// account relative instructions of all sizes and RIP-relative addressing. + /// \return absolute address of the jump at the current disassembler instruction + /// \note this works correctly ONLY if the input buffer has been set with + /// setInputBuffer or + /// if ud_set_pc has been called + /// + uint64_t jumpTarget(); + +private: +private: + ud_t m_Obj; + const uint8_t* m_Buffer{nullptr}; +}; + +} // namespace HookLib diff --git a/libs/usvfs/src/thooklib/utility.cpp b/libs/usvfs/src/thooklib/utility.cpp new file mode 100644 index 0000000..124141f --- /dev/null +++ b/libs/usvfs/src/thooklib/utility.cpp @@ -0,0 +1,86 @@ +/* +Userspace Virtual Filesystem + +Copyright (C) 2015 Sebastian Herbord. All rights reserved. + +This file is part of usvfs. + +usvfs is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +usvfs is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with usvfs. If not, see <http://www.gnu.org/licenses/>. +*/ +#include "utility.h" +#include "exceptionex.h" +#include <cstdlib> + +namespace HookLib +{ + +FARPROC MyGetProcAddress(HMODULE module, LPCSTR functionName) +{ + // determine position of the exports of the module + PIMAGE_DOS_HEADER dosHeader = (PIMAGE_DOS_HEADER)module; + if (dosHeader->e_magic != IMAGE_DOS_SIGNATURE) { + return nullptr; + } + + PIMAGE_NT_HEADERS ntHeaders = + (PIMAGE_NT_HEADERS)(((LPBYTE)dosHeader) + dosHeader->e_lfanew); + if (ntHeaders->Signature != IMAGE_NT_SIGNATURE) { + return nullptr; + } + + PIMAGE_OPTIONAL_HEADER optionalHeader = &ntHeaders->OptionalHeader; + if (optionalHeader->NumberOfRvaAndSizes <= IMAGE_DIRECTORY_ENTRY_EXPORT) { + return nullptr; + } + PIMAGE_DATA_DIRECTORY dataDirectory = + &optionalHeader->DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT]; + PIMAGE_EXPORT_DIRECTORY exportDirectory = + (PIMAGE_EXPORT_DIRECTORY)((LPBYTE)dosHeader + dataDirectory->VirtualAddress); + + ULONG* addressOfNames = (ULONG*)((LPBYTE)module + exportDirectory->AddressOfNames); + ULONG* funcAddr = (ULONG*)((LPBYTE)module + exportDirectory->AddressOfFunctions); + + // search exports for the specified name + for (DWORD i = 0; i < exportDirectory->NumberOfNames; ++i) { + char* curFunctionName = (char*)((LPBYTE)module + addressOfNames[i]); + USHORT* nameOrdinals = + (USHORT*)((LPBYTE)module + exportDirectory->AddressOfNameOrdinals); + if (strcmp(functionName, curFunctionName) == 0) { + if (funcAddr[nameOrdinals[i]] >= dataDirectory->VirtualAddress && + funcAddr[nameOrdinals[i]] < + dataDirectory->VirtualAddress + dataDirectory->Size) { + char* forwardLibName = _strdup((LPSTR)module + funcAddr[nameOrdinals[i]]); + ON_BLOCK_EXIT([forwardLibName]() { + free(forwardLibName); + }); + char* forwardFunctionName = strchr(forwardLibName, '.'); + *forwardFunctionName = 0; + ++forwardFunctionName; + + HMODULE forwardLib = LoadLibraryA(forwardLibName); + FARPROC forward = nullptr; + if (forwardLib != nullptr) { + forward = MyGetProcAddress(forwardLib, forwardFunctionName); + FreeLibrary(forwardLib); + } + + return forward; + } + return (FARPROC)((LPBYTE)module + funcAddr[nameOrdinals[i]]); + } + } + return nullptr; +} + +} // namespace HookLib diff --git a/libs/usvfs/src/thooklib/utility.h b/libs/usvfs/src/thooklib/utility.h new file mode 100644 index 0000000..e0be9f7 --- /dev/null +++ b/libs/usvfs/src/thooklib/utility.h @@ -0,0 +1,35 @@ +/* +Userspace Virtual Filesystem + +Copyright (C) 2015 Sebastian Herbord. All rights reserved. + +This file is part of usvfs. + +usvfs is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +usvfs is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with usvfs. If not, see <http://www.gnu.org/licenses/>. +*/ +#pragma once + +#include <windows_sane.h> + +namespace HookLib +{ + +/// \brief reimplementation of GetProcAddress to circumvent foreign hooks of +/// GetProcAddress like AcLayer +/// \param module handle to the module that contains the function or variable +/// \param functionName function to retrieve the address of +/// \return address of the exported function +FARPROC MyGetProcAddress(HMODULE module, LPCSTR functionName); + +} // namespace HookLib |
