#include #if __has_include("Zydis/Zydis.h") #include "Zydis/Zydis.h" #elif __has_include("Zydis.h") #include "Zydis.h" #else #error "Zydis not found" #endif #include "safetyhook/allocator.hpp" #include "safetyhook/common.hpp" #include "safetyhook/os.hpp" #include "safetyhook/utility.hpp" #include "safetyhook/inline_hook.hpp" namespace safetyhook { #pragma pack(push, 1) struct JmpE9 { uint8_t opcode{0xE9}; uint32_t offset{0}; }; #if SAFETYHOOK_ARCH_X86_64 struct JmpFF { uint8_t opcode0{0xFF}; uint8_t opcode1{0x25}; uint32_t offset{0}; }; struct TrampolineEpilogueE9 { JmpE9 jmp_to_original{}; JmpFF jmp_to_destination{}; uint64_t destination_address{}; }; struct TrampolineEpilogueFF { JmpFF jmp_to_original{}; uint64_t original_address{}; }; #elif SAFETYHOOK_ARCH_X86_32 struct TrampolineEpilogueE9 { JmpE9 jmp_to_original{}; JmpE9 jmp_to_destination{}; }; #endif #pragma pack(pop) #if SAFETYHOOK_ARCH_X86_64 static auto make_jmp_ff(uint8_t* src, uint8_t* dst, uint8_t* data) { JmpFF jmp{}; jmp.offset = static_cast(data - src - sizeof(jmp)); store(data, dst); return jmp; } [[nodiscard]] static std::expected emit_jmp_ff( uint8_t* src, uint8_t* dst, uint8_t* data, size_t size = sizeof(JmpFF)) { if (size < sizeof(JmpFF)) { return std::unexpected{InlineHook::Error::not_enough_space(dst)}; } if (size > sizeof(JmpFF)) { std::fill_n(src, size, static_cast(0x90)); } store(src, make_jmp_ff(src, dst, data)); return {}; } #endif constexpr auto make_jmp_e9(uint8_t* src, uint8_t* dst) { JmpE9 jmp{}; jmp.offset = static_cast(dst - src - sizeof(jmp)); return jmp; } [[nodiscard]] static std::expected emit_jmp_e9( uint8_t* src, uint8_t* dst, size_t size = sizeof(JmpE9)) { if (size < sizeof(JmpE9)) { return std::unexpected{InlineHook::Error::not_enough_space(dst)}; } if (size > sizeof(JmpE9)) { std::fill_n(src, size, static_cast(0x90)); } store(src, make_jmp_e9(src, dst)); return {}; } static bool decode(ZydisDecodedInstruction* ix, uint8_t* ip) { ZydisDecoder decoder{}; ZyanStatus status; #if SAFETYHOOK_ARCH_X86_64 status = ZydisDecoderInit(&decoder, ZYDIS_MACHINE_MODE_LONG_64, ZYDIS_STACK_WIDTH_64); #elif SAFETYHOOK_ARCH_X86_32 status = ZydisDecoderInit(&decoder, ZYDIS_MACHINE_MODE_LEGACY_32, ZYDIS_STACK_WIDTH_32); #endif if (!ZYAN_SUCCESS(status)) { return false; } return ZYAN_SUCCESS(ZydisDecoderDecodeInstruction(&decoder, nullptr, ip, 15, ix)); } std::expected InlineHook::create(void* target, void* destination, Flags flags) { return create(Allocator::global(), target, destination, flags); } std::expected InlineHook::create( const std::shared_ptr& allocator, void* target, void* destination, Flags flags) { InlineHook hook{}; if (const auto setup_result = hook.setup(allocator, reinterpret_cast(target), reinterpret_cast(destination)); !setup_result) { return std::unexpected{setup_result.error()}; } if (!(flags & StartDisabled)) { if (auto enable_result = hook.enable(); !enable_result) { return std::unexpected{enable_result.error()}; } } return hook; } InlineHook::InlineHook(InlineHook&& other) noexcept { *this = std::move(other); } InlineHook& InlineHook::operator=(InlineHook&& other) noexcept { if (this != &other) { destroy(); std::scoped_lock lock{m_mutex, other.m_mutex}; m_target = other.m_target; m_destination = other.m_destination; m_trampoline = std::move(other.m_trampoline); m_trampoline_size = other.m_trampoline_size; m_original_bytes = std::move(other.m_original_bytes); m_enabled = other.m_enabled; m_type = other.m_type; other.m_target = nullptr; other.m_destination = nullptr; other.m_trampoline_size = 0; other.m_enabled = false; other.m_type = Type::Unset; } return *this; } InlineHook::~InlineHook() { destroy(); } void InlineHook::reset() { *this = {}; } std::expected InlineHook::setup( const std::shared_ptr& allocator, uint8_t* target, uint8_t* destination) { m_target = target; m_destination = destination; if (auto e9_result = e9_hook(allocator); !e9_result) { #if SAFETYHOOK_ARCH_X86_64 if (auto ff_result = ff_hook(allocator); !ff_result) { return ff_result; } #elif SAFETYHOOK_ARCH_X86_32 return e9_result; #endif } return {}; } std::expected InlineHook::e9_hook(const std::shared_ptr& allocator) { m_original_bytes.clear(); m_trampoline_size = sizeof(TrampolineEpilogueE9); std::vector desired_addresses{m_target}; ZydisDecodedInstruction ix{}; for (auto ip = m_target; ip < m_target + sizeof(JmpE9); ip += ix.length) { if (!decode(&ix, ip)) { return std::unexpected{Error::failed_to_decode_instruction(ip)}; } m_trampoline_size += ix.length; m_original_bytes.insert(m_original_bytes.end(), ip, ip + ix.length); const auto is_relative = (ix.attributes & ZYDIS_ATTRIB_IS_RELATIVE) != 0; if (is_relative) { if (ix.raw.disp.size == 32) { const auto target_address = ip + ix.length + static_cast(ix.raw.disp.value); desired_addresses.emplace_back(target_address); } else if (ix.raw.imm[0].size == 32) { const auto target_address = ip + ix.length + static_cast(ix.raw.imm[0].value.s); desired_addresses.emplace_back(target_address); } else if (ix.meta.category == ZYDIS_CATEGORY_COND_BR && ix.meta.branch_type == ZYDIS_BRANCH_TYPE_SHORT) { const auto target_address = ip + ix.length + static_cast(ix.raw.imm[0].value.s); desired_addresses.emplace_back(target_address); m_trampoline_size += 4; // near conditional branches are 4 bytes larger. } else if (ix.meta.category == ZYDIS_CATEGORY_UNCOND_BR && ix.meta.branch_type == ZYDIS_BRANCH_TYPE_SHORT) { const auto target_address = ip + ix.length + static_cast(ix.raw.imm[0].value.s); desired_addresses.emplace_back(target_address); m_trampoline_size += 3; // near unconditional branches are 3 bytes larger. } else { return std::unexpected{Error::unsupported_instruction_in_trampoline(ip)}; } } } auto trampoline_allocation = allocator->allocate_near(desired_addresses, m_trampoline_size); if (!trampoline_allocation) { return std::unexpected{Error::bad_allocation(trampoline_allocation.error())}; } m_trampoline = std::move(*trampoline_allocation); for (auto ip = m_target, tramp_ip = m_trampoline.data(); ip < m_target + m_original_bytes.size(); ip += ix.length) { if (!decode(&ix, ip)) { m_trampoline.free(); return std::unexpected{Error::failed_to_decode_instruction(ip)}; } const auto is_relative = (ix.attributes & ZYDIS_ATTRIB_IS_RELATIVE) != 0; if (is_relative && ix.raw.disp.size == 32) { std::copy_n(ip, ix.length, tramp_ip); const auto target_address = ip + ix.length + ix.raw.disp.value; const auto new_disp = target_address - (tramp_ip + ix.length); store(tramp_ip + ix.raw.disp.offset, static_cast(new_disp)); tramp_ip += ix.length; } else if (is_relative && ix.raw.imm[0].size == 32) { std::copy_n(ip, ix.length, tramp_ip); const auto target_address = ip + ix.length + ix.raw.imm[0].value.s; const auto new_disp = target_address - (tramp_ip + ix.length); store(tramp_ip + ix.raw.imm[0].offset, static_cast(new_disp)); tramp_ip += ix.length; } else if (ix.meta.category == ZYDIS_CATEGORY_COND_BR && ix.meta.branch_type == ZYDIS_BRANCH_TYPE_SHORT) { const auto target_address = ip + ix.length + ix.raw.imm[0].value.s; auto new_disp = target_address - (tramp_ip + 6); // Handle the case where the target is now in the trampoline. if (target_address >= m_target && target_address < m_target + m_original_bytes.size()) { new_disp = static_cast(ix.raw.imm[0].value.s); } *tramp_ip = 0x0F; *(tramp_ip + 1) = 0x10 + ix.opcode; store(tramp_ip + 2, static_cast(new_disp)); tramp_ip += 6; } else if (ix.meta.category == ZYDIS_CATEGORY_UNCOND_BR && ix.meta.branch_type == ZYDIS_BRANCH_TYPE_SHORT) { const auto target_address = ip + ix.length + ix.raw.imm[0].value.s; auto new_disp = target_address - (tramp_ip + 5); // Handle the case where the target is now in the trampoline. if (target_address >= m_target && target_address < m_target + m_original_bytes.size()) { new_disp = static_cast(ix.raw.imm[0].value.s); } *tramp_ip = 0xE9; store(tramp_ip + 1, static_cast(new_disp)); tramp_ip += 5; } else { std::copy_n(ip, ix.length, tramp_ip); tramp_ip += ix.length; } } auto trampoline_epilogue = reinterpret_cast( m_trampoline.address() + m_trampoline_size - sizeof(TrampolineEpilogueE9)); // jmp from trampoline to original. auto src = reinterpret_cast(&trampoline_epilogue->jmp_to_original); auto dst = m_target + m_original_bytes.size(); if (auto result = emit_jmp_e9(src, dst); !result) { return std::unexpected{result.error()}; } // jmp from trampoline to destination. src = reinterpret_cast(&trampoline_epilogue->jmp_to_destination); dst = m_destination; #if SAFETYHOOK_ARCH_X86_64 auto data = reinterpret_cast(&trampoline_epilogue->destination_address); if (auto result = emit_jmp_ff(src, dst, data); !result) { return std::unexpected{result.error()}; } #elif SAFETYHOOK_ARCH_X86_32 if (auto result = emit_jmp_e9(src, dst); !result) { return std::unexpected{result.error()}; } #endif m_type = Type::E9; return {}; } #if SAFETYHOOK_ARCH_X86_64 std::expected InlineHook::ff_hook(const std::shared_ptr& allocator) { m_original_bytes.clear(); m_trampoline_size = sizeof(TrampolineEpilogueFF); ZydisDecodedInstruction ix{}; for (auto ip = m_target; ip < m_target + sizeof(JmpFF) + sizeof(uintptr_t); ip += ix.length) { if (!decode(&ix, ip)) { return std::unexpected{Error::failed_to_decode_instruction(ip)}; } // We can't support any instruction that is IP relative here because // ff_hook should only be called if e9_hook failed indicating that // we're likely outside the +- 2GB range. if (ix.attributes & ZYDIS_ATTRIB_IS_RELATIVE) { return std::unexpected{Error::ip_relative_instruction_out_of_range(ip)}; } m_original_bytes.insert(m_original_bytes.end(), ip, ip + ix.length); m_trampoline_size += ix.length; } auto trampoline_allocation = allocator->allocate(m_trampoline_size); if (!trampoline_allocation) { return std::unexpected{Error::bad_allocation(trampoline_allocation.error())}; } m_trampoline = std::move(*trampoline_allocation); std::copy(m_original_bytes.begin(), m_original_bytes.end(), m_trampoline.data()); const auto trampoline_epilogue = reinterpret_cast(m_trampoline.data() + m_trampoline_size - sizeof(TrampolineEpilogueFF)); // jmp from trampoline to original. auto src = reinterpret_cast(&trampoline_epilogue->jmp_to_original); auto dst = m_target + m_original_bytes.size(); auto data = reinterpret_cast(&trampoline_epilogue->original_address); if (auto result = emit_jmp_ff(src, dst, data); !result) { return std::unexpected{result.error()}; } m_type = Type::FF; return {}; } #endif std::expected InlineHook::enable() { std::scoped_lock lock{m_mutex}; if (m_enabled) { return {}; } std::optional error; // jmp from original to trampoline. trap_threads(m_target, m_trampoline.data(), m_original_bytes.size(), [this, &error] { if (m_type == Type::E9) { auto trampoline_epilogue = reinterpret_cast( m_trampoline.address() + m_trampoline_size - sizeof(TrampolineEpilogueE9)); if (auto result = emit_jmp_e9(m_target, reinterpret_cast(&trampoline_epilogue->jmp_to_destination), m_original_bytes.size()); !result) { error = result.error(); } } #if SAFETYHOOK_ARCH_X86_64 if (m_type == Type::FF) { if (auto result = emit_jmp_ff(m_target, m_destination, m_target + sizeof(JmpFF), m_original_bytes.size()); !result) { error = result.error(); } } #endif }); if (error) { return std::unexpected{*error}; } m_enabled = true; return {}; } std::expected InlineHook::disable() { std::scoped_lock lock{m_mutex}; if (!m_enabled) { return {}; } trap_threads(m_trampoline.data(), m_target, m_original_bytes.size(), [this] { std::copy(m_original_bytes.begin(), m_original_bytes.end(), m_target); }); m_enabled = false; return {}; } void InlineHook::destroy() { [[maybe_unused]] auto disable_result = disable(); std::scoped_lock lock{m_mutex}; if (!m_trampoline) { return; } m_trampoline.free(); } } // namespace safetyhook