Add initial project files (excluding ignored content)
This commit is contained in:
440
external/safetyhook/src/inline_hook.cpp
vendored
Normal file
440
external/safetyhook/src/inline_hook.cpp
vendored
Normal file
@@ -0,0 +1,440 @@
|
||||
#include <iterator>
|
||||
|
||||
#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<uint32_t>(data - src - sizeof(jmp));
|
||||
store(data, dst);
|
||||
|
||||
return jmp;
|
||||
}
|
||||
|
||||
[[nodiscard]] static std::expected<void, InlineHook::Error> 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<uint8_t>(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<uint32_t>(dst - src - sizeof(jmp));
|
||||
|
||||
return jmp;
|
||||
}
|
||||
|
||||
[[nodiscard]] static std::expected<void, InlineHook::Error> 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<uint8_t>(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, InlineHook::Error> InlineHook::create(void* target, void* destination, Flags flags) {
|
||||
return create(Allocator::global(), target, destination, flags);
|
||||
}
|
||||
|
||||
std::expected<InlineHook, InlineHook::Error> InlineHook::create(
|
||||
const std::shared_ptr<Allocator>& allocator, void* target, void* destination, Flags flags) {
|
||||
InlineHook hook{};
|
||||
|
||||
if (const auto setup_result =
|
||||
hook.setup(allocator, reinterpret_cast<uint8_t*>(target), reinterpret_cast<uint8_t*>(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<void, InlineHook::Error> InlineHook::setup(
|
||||
const std::shared_ptr<Allocator>& 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<void, InlineHook::Error> InlineHook::e9_hook(const std::shared_ptr<Allocator>& allocator) {
|
||||
m_original_bytes.clear();
|
||||
m_trampoline_size = sizeof(TrampolineEpilogueE9);
|
||||
|
||||
std::vector<uint8_t*> 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<int32_t>(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<int32_t>(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<int32_t>(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<int32_t>(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<int32_t>(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<int32_t>(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<ptrdiff_t>(ix.raw.imm[0].value.s);
|
||||
}
|
||||
|
||||
*tramp_ip = 0x0F;
|
||||
*(tramp_ip + 1) = 0x10 + ix.opcode;
|
||||
store(tramp_ip + 2, static_cast<int32_t>(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<ptrdiff_t>(ix.raw.imm[0].value.s);
|
||||
}
|
||||
|
||||
*tramp_ip = 0xE9;
|
||||
store(tramp_ip + 1, static_cast<int32_t>(new_disp));
|
||||
tramp_ip += 5;
|
||||
} else {
|
||||
std::copy_n(ip, ix.length, tramp_ip);
|
||||
tramp_ip += ix.length;
|
||||
}
|
||||
}
|
||||
|
||||
auto trampoline_epilogue = reinterpret_cast<TrampolineEpilogueE9*>(
|
||||
m_trampoline.address() + m_trampoline_size - sizeof(TrampolineEpilogueE9));
|
||||
|
||||
// jmp from trampoline to original.
|
||||
auto src = reinterpret_cast<uint8_t*>(&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<uint8_t*>(&trampoline_epilogue->jmp_to_destination);
|
||||
dst = m_destination;
|
||||
|
||||
#if SAFETYHOOK_ARCH_X86_64
|
||||
auto data = reinterpret_cast<uint8_t*>(&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<void, InlineHook::Error> InlineHook::ff_hook(const std::shared_ptr<Allocator>& 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<TrampolineEpilogueFF*>(m_trampoline.data() + m_trampoline_size - sizeof(TrampolineEpilogueFF));
|
||||
|
||||
// jmp from trampoline to original.
|
||||
auto src = reinterpret_cast<uint8_t*>(&trampoline_epilogue->jmp_to_original);
|
||||
auto dst = m_target + m_original_bytes.size();
|
||||
auto data = reinterpret_cast<uint8_t*>(&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<void, InlineHook::Error> InlineHook::enable() {
|
||||
std::scoped_lock lock{m_mutex};
|
||||
|
||||
if (m_enabled) {
|
||||
return {};
|
||||
}
|
||||
|
||||
std::optional<Error> 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<TrampolineEpilogueE9*>(
|
||||
m_trampoline.address() + m_trampoline_size - sizeof(TrampolineEpilogueE9));
|
||||
|
||||
if (auto result = emit_jmp_e9(m_target,
|
||||
reinterpret_cast<uint8_t*>(&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<void, InlineHook::Error> 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
|
||||
Reference in New Issue
Block a user