diff --git a/CMakeLists.txt b/CMakeLists.txt index be2ae3a..ee16991 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -93,7 +93,8 @@ set(safetyhook_SOURCES "src/easy.cpp" "src/inline_hook.cpp" "src/mid_hook.cpp" - "src/thread_freezer.cpp" + "src/os.linux.cpp" + "src/os.windows.cpp" "src/utility.cpp" "src/vmt_hook.cpp" cmake.toml @@ -143,7 +144,6 @@ target_include_directories(safetyhook PUBLIC target_link_libraries(safetyhook PUBLIC Zydis - ntdll ) # Target: docs @@ -305,7 +305,7 @@ if(SAFETYHOOK_BUILD_EXAMPLES) # build-examples endif() # Target: example-dll -if(SAFETYHOOK_BUILD_EXAMPLES) # build-examples +if(SAFETYHOOK_BUILD_EXAMPLES AND WIN32) # windows-only-example set(example-dll_SOURCES "example/dll.cpp" cmake.toml diff --git a/cmake.toml b/cmake.toml index 40d5d17..e5c79fa 100644 --- a/cmake.toml +++ b/cmake.toml @@ -13,6 +13,7 @@ SAFETYHOOK_FETCH_ZYDIS = false build-docs = "SAFETYHOOK_BUILD_DOCS" build-test = "SAFETYHOOK_BUILD_TEST" build-examples = "SAFETYHOOK_BUILD_EXAMPLES" +windows-only-example = "SAFETYHOOK_BUILD_EXAMPLES AND WIN32" amalgamate = "SAFETYHOOK_AMALGAMATE" build-amalgamate-test = "SAFETYHOOK_BUILD_TEST AND SAFETYHOOK_AMALGAMATE" fetch-zydis = "SAFETYHOOK_FETCH_ZYDIS" @@ -54,7 +55,7 @@ sources = ["src/*.cpp"] include-directories = ["include/"] compile-features = ["cxx_std_23"] alias = "safetyhook::safetyhook" -link-libraries = ["Zydis", "ntdll"] +link-libraries = ["Zydis"] msvc.private-compile-options = ["/permissive-", "/W4", "/w14640"] clang.private-compile-options = ["-Wall", "-Wextra", "-Wshadow", "-Wnon-virtual-dtor", "-pedantic"] gcc.private-compile-options = ["-Wall", "-Wextra", "-Wshadow", "-Wnon-virtual-dtor", "-pedantic"] @@ -104,7 +105,7 @@ link-libraries = ["safetyhook::safetyhook"] compile-features = ["cxx_std_23"] [template.example-dll] -condition = "build-examples" +condition = "windows-only-example" type = "shared" link-libraries = ["safetyhook::safetyhook"] compile-features = ["cxx_std_23"] diff --git a/example/midhook.cpp b/example/midhook.cpp index f9b1f78..065ac9b 100644 --- a/example/midhook.cpp +++ b/example/midhook.cpp @@ -10,7 +10,7 @@ #include -__declspec(noinline) int add_42(int a) { +SAFETYHOOK_NOINLINE int add_42(int a) { return a + 42; } @@ -28,6 +28,8 @@ int main() { std::println("unhooked add_42(2) = {}", add_42(2)); // Let's disassemble add_42 and hook its RET. + // NOTE: On Linux we should specify -falign-functions=32 to add some padding to the function otherwise we'll + // end up hooking the next function's prologue. This is pretty hacky in general but it's just an example. ZydisDecoder decoder{}; #if SAFETYHOOK_ARCH_X86_64 diff --git a/example/minimal.cpp b/example/minimal.cpp index 24efe11..83a8c1f 100644 --- a/example/minimal.cpp +++ b/example/minimal.cpp @@ -2,7 +2,7 @@ #include -__declspec(noinline) int add(int x, int y) { +SAFETYHOOK_NOINLINE int add(int x, int y) { return x + y; } diff --git a/example/multiple.cpp b/example/multiple.cpp index 01c2d42..3eef276 100644 --- a/example/multiple.cpp +++ b/example/multiple.cpp @@ -4,7 +4,7 @@ SafetyHookInline hook0, hook1, hook2, hook3; -__declspec(noinline) void say_hi(const std::string& name) { +SAFETYHOOK_NOINLINE void say_hi(const std::string& name) { std::println("hello {}", name); } diff --git a/example/threadsafe.cpp b/example/threadsafe.cpp index 78a20ae..87f70b9 100644 --- a/example/threadsafe.cpp +++ b/example/threadsafe.cpp @@ -5,7 +5,7 @@ SafetyHookInline g_hook{}; -__declspec(noinline) void SayHello(int times) { +SAFETYHOOK_NOINLINE void SayHello(int times) { std::println("Hello #{}", times); } diff --git a/example/vmthook.cpp b/example/vmthook.cpp index d81c40e..ee6608d 100644 --- a/example/vmthook.cpp +++ b/example/vmthook.cpp @@ -28,13 +28,18 @@ int main() { std::println("unhooked target->add_42(1) = {}", target->add_42(1)); g_target_hook = safetyhook::create_vmt(target.get()); + +#if SAFETYHOOK_OS_WINDOWS g_add_42_hook = safetyhook::create_vm(g_target_hook, 1, &Hook::hooked_add_42); +#elif SAFETYHOOK_OS_LINUX + g_add_42_hook = safetyhook::create_vm(g_target_hook, 2, &Hook::hooked_add_42); +#endif - std::println("hooked target->add_42(2) = {}", target->add_42(1)); + std::println("hooked target->add_42(2) = {}", target->add_42(2)); g_target_hook = {}; - std::println("unhooked target->add_42(3) = {}", target->add_42(1)); + std::println("unhooked target->add_42(3) = {}", target->add_42(3)); return 0; } \ No newline at end of file diff --git a/include/safetyhook.hpp b/include/safetyhook.hpp index a85938d..4d549a4 100644 --- a/include/safetyhook.hpp +++ b/include/safetyhook.hpp @@ -3,7 +3,6 @@ #include "safetyhook/easy.hpp" #include "safetyhook/inline_hook.hpp" #include "safetyhook/mid_hook.hpp" -#include "safetyhook/thread_freezer.hpp" #include "safetyhook/vmt_hook.hpp" using SafetyHookContext = safetyhook::Context; diff --git a/include/safetyhook/common.hpp b/include/safetyhook/common.hpp index 82ebe70..808e85f 100644 --- a/include/safetyhook/common.hpp +++ b/include/safetyhook/common.hpp @@ -37,3 +37,38 @@ #error "Unsupported architecture" #endif #endif + +#if defined(WIN32) +#define SAFETYHOOK_OS_WINDOWS 1 +#define SAFETYHOOK_OS_LINUX 0 +#elif defined(__linux__) +#define SAFETYHOOK_OS_WINDOWS 0 +#define SAFETYHOOK_OS_LINUX 1 +#else +#error "Unsupported OS" +#endif + +#if SAFETYHOOK_OS_WINDOWS +#if SAFETYHOOK_COMPILER_MSVC +#define SAFETYHOOK_CCALL __cdecl +#define SAFETYHOOK_STDCALL __stdcall +#define SAFETYHOOK_FASTCALL __fastcall +#define SAFETYHOOK_THISCALL __thiscall +#elif SAFETYHOOK_COMPILER_GCC || SAFETYHOOK_COMPILER_CLANG +#define SAFETYHOOK_CCALL __attribute__((cdecl)) +#define SAFETYHOOK_STDCALL __attribute__((stdcall)) +#define SAFETYHOOK_FASTCALL __attribute__((fastcall)) +#define SAFETYHOOK_THISCALL __attribute__((thiscall)) +#endif +#else +#define SAFETYHOOK_CCALL +#define SAFETYHOOK_STDCALL +#define SAFETYHOOK_FASTCALL +#define SAFETYHOOK_THISCALL +#endif + +#if SAFETYHOOK_COMPILER_MSVC +#define SAFETYHOOK_NOINLINE __declspec(noinline) +#elif SAFETYHOOK_COMPILER_GCC || SAFETYHOOK_COMPILER_CLANG +#define SAFETYHOOK_NOINLINE __attribute__((noinline)) +#endif \ No newline at end of file diff --git a/include/safetyhook/inline_hook.hpp b/include/safetyhook/inline_hook.hpp index 4114615..418e185 100644 --- a/include/safetyhook/inline_hook.hpp +++ b/include/safetyhook/inline_hook.hpp @@ -185,7 +185,7 @@ class InlineHook final { /// @note This function will use the __cdecl calling convention. template RetT ccall(Args... args) { std::scoped_lock lock{m_mutex}; - return m_trampoline ? original()(args...) : RetT(); + return m_trampoline ? original()(args...) : RetT(); } /// @brief Calls the original function. @@ -196,7 +196,7 @@ class InlineHook final { /// @note This function will use the __thiscall calling convention. template RetT thiscall(Args... args) { std::scoped_lock lock{m_mutex}; - return m_trampoline ? original()(args...) : RetT(); + return m_trampoline ? original()(args...) : RetT(); } /// @brief Calls the original function. @@ -207,7 +207,7 @@ class InlineHook final { /// @note This function will use the __stdcall calling convention. template RetT stdcall(Args... args) { std::scoped_lock lock{m_mutex}; - return m_trampoline ? original()(args...) : RetT(); + return m_trampoline ? original()(args...) : RetT(); } /// @brief Calls the original function. @@ -218,7 +218,7 @@ class InlineHook final { /// @note This function will use the __fastcall calling convention. template RetT fastcall(Args... args) { std::scoped_lock lock{m_mutex}; - return m_trampoline ? original()(args...) : RetT(); + return m_trampoline ? original()(args...) : RetT(); } /// @brief Calls the original function. @@ -242,7 +242,7 @@ class InlineHook final { /// @note This function is unsafe because it doesn't lock the mutex. Only use this if you don't care about unhook /// safety or are worried about the performance cost of locking the mutex. template RetT unsafe_ccall(Args... args) { - return original()(args...); + return original()(args...); } /// @brief Calls the original function. @@ -254,7 +254,7 @@ class InlineHook final { /// @note This function is unsafe because it doesn't lock the mutex. Only use this if you don't care about unhook /// safety or are worried about the performance cost of locking the mutex. template RetT unsafe_thiscall(Args... args) { - return original()(args...); + return original()(args...); } /// @brief Calls the original function. @@ -266,7 +266,7 @@ class InlineHook final { /// @note This function is unsafe because it doesn't lock the mutex. Only use this if you don't care about unhook /// safety or are worried about the performance cost of locking the mutex. template RetT unsafe_stdcall(Args... args) { - return original()(args...); + return original()(args...); } /// @brief Calls the original function. @@ -278,7 +278,7 @@ class InlineHook final { /// @note This function is unsafe because it doesn't lock the mutex. Only use this if you don't care about unhook /// safety or are worried about the performance cost of locking the mutex. template RetT unsafe_fastcall(Args... args) { - return original()(args...); + return original()(args...); } private: diff --git a/include/safetyhook/os.hpp b/include/safetyhook/os.hpp new file mode 100644 index 0000000..20255fb --- /dev/null +++ b/include/safetyhook/os.hpp @@ -0,0 +1,82 @@ +// This is the OS abstraction layer. +#pragma once + +#include +#include +#include + +namespace safetyhook { + +enum class OsError { + FAILED_TO_ALLOCATE, + FAILED_TO_PROTECT, + FAILED_TO_QUERY, + FAILED_TO_GET_NEXT_THREAD, + FAILED_TO_GET_THREAD_CONTEXT, + FAILED_TO_SET_THREAD_CONTEXT, + FAILED_TO_FREEZE_THREAD, + FAILED_TO_UNFREEZE_THREAD, + FAILED_TO_GET_THREAD_ID, +}; + +struct VmAccess { + bool read : 1; + bool write : 1; + bool execute : 1; + + constexpr bool operator==(const VmAccess& other) const { + return read == other.read && write == other.write && execute == other.execute; + } +}; + +constexpr VmAccess VM_ACCESS_R{.read = true, .write = false, .execute = false}; +constexpr VmAccess VM_ACCESS_RW{.read = true, .write = true, .execute = false}; +constexpr VmAccess VM_ACCESS_RX{.read = true, .write = false, .execute = true}; +constexpr VmAccess VM_ACCESS_RWX{.read = true, .write = true, .execute = true}; + +struct VmBasicInfo { + uint8_t* address; + size_t size; + VmAccess access; + bool is_free; +}; + +std::expected vm_allocate(uint8_t* address, size_t size, VmAccess access); +void vm_free(uint8_t* address); +std::expected vm_protect(uint8_t* address, size_t size, VmAccess access); +std::expected vm_protect(uint8_t* address, size_t size, uint32_t access); +std::expected vm_query(uint8_t* address); +bool vm_is_readable(uint8_t* address, size_t size); +bool vm_is_writable(uint8_t* address, size_t size); +bool vm_is_executable(uint8_t* address); + +struct SystemInfo { + uint32_t page_size; + uint32_t allocation_granularity; + uint8_t* min_address; + uint8_t* max_address; +}; + +SystemInfo system_info(); + +using ThreadId = uint32_t; +using ThreadHandle = void*; +using ThreadContext = void*; + +/// @brief Executes a function while all other threads are frozen. Also allows for visiting each frozen thread and +/// modifying it's context. +/// @param run_fn The function to run while all other threads are frozen. +/// @param visit_fn The function that will be called for each frozen thread. +/// @note The visit function will be called in the order that the threads were frozen. +/// @note The visit function will be called before the run function. +/// @note Keep the logic inside run_fn and visit_fn as simple as possible to avoid deadlocks. +void execute_while_frozen(const std::function& run_fn, + const std::function& visit_fn = {}); + +/// @brief Will modify the context of a thread's IP to point to a new address if its IP is at the old address. +/// @param ctx The thread context to modify. +/// @param old_ip The old IP address. +/// @param new_ip The new IP address. +void fix_ip(ThreadContext ctx, uint8_t* old_ip, uint8_t* new_ip); + +} // namespace safetyhook \ No newline at end of file diff --git a/include/safetyhook/thread_freezer.hpp b/include/safetyhook/thread_freezer.hpp deleted file mode 100644 index 74ea23d..0000000 --- a/include/safetyhook/thread_freezer.hpp +++ /dev/null @@ -1,29 +0,0 @@ -/// @file safetyhook/thread_freezer.hpp -/// @brief A class for freezing all threads in the process. - -#pragma once - -#include -#include - -namespace safetyhook { -using ThreadId = uint32_t; -using ThreadHandle = void*; -using ThreadContext = void*; - -/// @brief Executes a function while all other threads are frozen. Also allows for visiting each frozen thread and -/// modifying it's context. -/// @param run_fn The function to run while all other threads are frozen. -/// @param visit_fn The function that will be called for each frozen thread. -/// @note The visit function will be called in the order that the threads were frozen. -/// @note The visit function will be called before the run function. -/// @note Keep the logic inside run_fn and visit_fn as simple as possible to avoid deadlocks. -void execute_while_frozen(const std::function& run_fn, - const std::function& visit_fn = {}); - -/// @brief Will modify the context of a thread's IP to point to a new address if its IP is at the old address. -/// @param ctx The thread context to modify. -/// @param old_ip The old IP address. -/// @param new_ip The new IP address. -void fix_ip(ThreadContext ctx, uint8_t* old_ip, uint8_t* new_ip); -} // namespace safetyhook \ No newline at end of file diff --git a/include/safetyhook/utility.hpp b/include/safetyhook/utility.hpp index 933b338..90d710e 100644 --- a/include/safetyhook/utility.hpp +++ b/include/safetyhook/utility.hpp @@ -2,6 +2,7 @@ #include #include +#include #include namespace safetyhook { @@ -35,4 +36,16 @@ class UnprotectMemory { }; [[nodiscard]] std::optional unprotect(uint8_t* address, size_t size); + +template constexpr T align_up(T address, size_t align) { + const auto unaligned_address = (uintptr_t)address; + const auto aligned_address = (unaligned_address + align - 1) & ~(align - 1); + return (T)aligned_address; +} + +template constexpr T align_down(T address, size_t align) { + const auto unaligned_address = (uintptr_t)address; + const auto aligned_address = unaligned_address & ~(align - 1); + return (T)aligned_address; +} } // namespace safetyhook diff --git a/include/safetyhook/vmt_hook.hpp b/include/safetyhook/vmt_hook.hpp index bf624c0..04ed9cb 100644 --- a/include/safetyhook/vmt_hook.hpp +++ b/include/safetyhook/vmt_hook.hpp @@ -8,6 +8,7 @@ #include #include "safetyhook/allocator.hpp" +#include "safetyhook/common.hpp" #include "safetyhook/utility.hpp" namespace safetyhook { @@ -43,7 +44,7 @@ class VmHook final { /// @param args The arguments to pass to the method. /// @return The return value of the method. template RetT ccall(Args... args) { - return original()(args...); + return original()(args...); } /// @brief Calls the original method with the __thiscall calling convention. @@ -52,7 +53,7 @@ class VmHook final { /// @param args The arguments to pass to the method. /// @return The return value of the method. template RetT thiscall(Args... args) { - return original()(args...); + return original()(args...); } /// @brief Calls the original method with the __stdcall calling convention. @@ -61,7 +62,7 @@ class VmHook final { /// @param args The arguments to pass to the method. /// @return The return value of the method. template RetT stdcall(Args... args) { - return original()(args...); + return original()(args...); } /// @brief Calls the original method with the __fastcall calling convention. @@ -70,7 +71,7 @@ class VmHook final { /// @param args The arguments to pass to the method. /// @return The return value of the method. template RetT fastcall(Args... args) { - return original()(args...); + return original()(args...); } private: diff --git a/src/allocator.cpp b/src/allocator.cpp index df2d6ce..eaa3d84 100644 --- a/src/allocator.cpp +++ b/src/allocator.cpp @@ -2,30 +2,12 @@ #include #include -#define NOMINMAX -#if __has_include() -#include -#elif __has_include() -#include -#else -#error "Windows.h not found" -#endif +#include "safetyhook/os.hpp" +#include "safetyhook/utility.hpp" #include "safetyhook/allocator.hpp" namespace safetyhook { -template constexpr T align_up(T address, size_t align) { - const auto unaligned_address = (uintptr_t)address; - const auto aligned_address = (unaligned_address + align - 1) & ~(align - 1); - return (T)aligned_address; -} - -template constexpr T align_down(T address, size_t align) { - const auto unaligned_address = (uintptr_t)address; - const auto aligned_address = unaligned_address & ~(align - 1); - return (T)aligned_address; -} - Allocation::Allocation(Allocation&& other) noexcept { *this = std::move(other); } @@ -127,18 +109,14 @@ std::expected Allocator::internal_allocate_near( } // If we didn't find a free block, we need to allocate a new one. - SYSTEM_INFO si{}; - - GetSystemInfo(&si); - - const auto allocation_size = align_up(size, si.dwAllocationGranularity); - const auto allocation_address = allocate_nearby_memory(desired_addresses, allocation_size, max_distance); + auto allocation_size = align_up(size, system_info().allocation_granularity); + auto allocation_address = allocate_nearby_memory(desired_addresses, allocation_size, max_distance); if (!allocation_address) { return std::unexpected{allocation_address.error()}; } - const auto& allocation = m_memory.emplace_back(new Memory); + auto& allocation = m_memory.emplace_back(new Memory); allocation->address = *allocation_address; allocation->size = allocation_size; @@ -199,9 +177,8 @@ void Allocator::combine_adjacent_freenodes(Memory& memory) { std::expected Allocator::allocate_nearby_memory( const std::vector& desired_addresses, size_t size, size_t max_distance) { if (desired_addresses.empty()) { - if (const auto result = VirtualAlloc(nullptr, size, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE); - result != nullptr) { - return static_cast(result); + if (auto result = vm_allocate(nullptr, size, VM_ACCESS_RWX)) { + return result.value(); } return std::unexpected{Error::BAD_VIRTUAL_ALLOC}; @@ -212,16 +189,17 @@ std::expected Allocator::allocate_nearby_memory( return nullptr; } - return static_cast(VirtualAlloc(p, size, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE)); - }; - - SYSTEM_INFO si{}; + if (auto result = vm_allocate(p, size, VM_ACCESS_RWX)) { + return result.value(); + } - GetSystemInfo(&si); + return nullptr; + }; + auto si = system_info(); auto desired_address = desired_addresses[0]; - auto search_start = reinterpret_cast(std::numeric_limits::min()); - auto search_end = reinterpret_cast(std::numeric_limits::max()); + auto search_start = si.min_address; + auto search_end = si.max_address; if (static_cast(desired_address - search_start) > max_distance) { search_start = desired_address - max_distance; @@ -231,35 +209,42 @@ std::expected Allocator::allocate_nearby_memory( search_end = desired_address + max_distance; } - search_start = std::max(search_start, static_cast(si.lpMinimumApplicationAddress)); - search_end = std::min(search_end, static_cast(si.lpMaximumApplicationAddress)); - desired_address = align_up(desired_address, si.dwAllocationGranularity); - MEMORY_BASIC_INFORMATION mbi{}; + search_start = std::max(search_start, si.min_address); + search_end = std::min(search_end, si.max_address); + desired_address = align_up(desired_address, si.allocation_granularity); + VmBasicInfo mbi{}; // Search backwards from the desired_address. for (auto p = desired_address; p > search_start && in_range(p, desired_addresses, max_distance); - p = align_down(static_cast(mbi.AllocationBase) - 1, si.dwAllocationGranularity)) { - if (VirtualQuery(p, &mbi, sizeof(mbi)) == 0) { + p = align_down(mbi.address - 1, si.allocation_granularity)) { + auto result = vm_query(p); + + if (!result) { break; } - if (mbi.State != MEM_FREE) { + mbi = result.value(); + + if (!mbi.is_free) { continue; } - if (auto allocation_address = attempt_allocation(p); allocation_address != 0) { + if (auto allocation_address = attempt_allocation(p); allocation_address != nullptr) { return allocation_address; } } // Search forwards from the desired_address. - for (auto p = desired_address; p < search_end && in_range(p, desired_addresses, max_distance); - p += mbi.RegionSize) { - if (VirtualQuery(p, &mbi, sizeof(mbi)) == 0) { + for (auto p = desired_address; p < search_end && in_range(p, desired_addresses, max_distance); p += mbi.size) { + auto result = vm_query(p); + + if (!result) { break; } - if (mbi.State != MEM_FREE) { + mbi = result.value(); + + if (!mbi.is_free) { continue; } @@ -279,6 +264,6 @@ bool Allocator::in_range(uint8_t* address, const std::vector& desired_ } Allocator::Memory::~Memory() { - VirtualFree(address, 0, MEM_RELEASE); + vm_free(address); } } // namespace safetyhook \ No newline at end of file diff --git a/src/inline_hook.cpp b/src/inline_hook.cpp index 35de774..76657e8 100644 --- a/src/inline_hook.cpp +++ b/src/inline_hook.cpp @@ -1,13 +1,5 @@ #include -#if __has_include() -#include -#elif __has_include() -#include -#else -#error "Windows.h not found" -#endif - #if __has_include("Zydis/Zydis.h") #include "Zydis/Zydis.h" #elif __has_include("Zydis.h") @@ -18,7 +10,7 @@ #include "safetyhook/allocator.hpp" #include "safetyhook/common.hpp" -#include "safetyhook/thread_freezer.hpp" +#include "safetyhook/os.hpp" #include "safetyhook/utility.hpp" #include "safetyhook/inline_hook.hpp" diff --git a/src/mid_hook.cpp b/src/mid_hook.cpp index a28391d..3284ff4 100644 --- a/src/mid_hook.cpp +++ b/src/mid_hook.cpp @@ -10,6 +10,7 @@ namespace safetyhook { #if SAFETYHOOK_ARCH_X86_64 +#if SAFETYHOOK_OS_WINDOWS constexpr std::array asm_data = {0xFF, 0x35, 0x79, 0x01, 0x00, 0x00, 0x54, 0x54, 0x55, 0x50, 0x53, 0x51, 0x52, 0x56, 0x57, 0x41, 0x50, 0x41, 0x51, 0x41, 0x52, 0x41, 0x53, 0x41, 0x54, 0x41, 0x55, 0x41, 0x56, 0x41, 0x57, 0x9C, 0x48, 0x81, 0xEC, 0x00, 0x01, 0x00, 0x00, 0xF3, 0x44, 0x0F, 0x7F, 0xBC, 0x24, 0xF0, 0x00, 0x00, 0x00, 0xF3, @@ -31,6 +32,29 @@ constexpr std::array asm_data = {0xFF, 0x35, 0x79, 0x01, 0x00, 0x0 0x00, 0x00, 0x48, 0x81, 0xC4, 0x00, 0x01, 0x00, 0x00, 0x9D, 0x41, 0x5F, 0x41, 0x5E, 0x41, 0x5D, 0x41, 0x5C, 0x41, 0x5B, 0x41, 0x5A, 0x41, 0x59, 0x41, 0x58, 0x5F, 0x5E, 0x5A, 0x59, 0x5B, 0x58, 0x5D, 0x48, 0x8D, 0x64, 0x24, 0x08, 0x5C, 0xC3, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; +#elif SAFETYHOOK_OS_LINUX +constexpr std::array asm_data = {0xFF, 0x35, 0x79, 0x01, 0x00, 0x00, 0x54, 0x54, 0x55, 0x50, 0x53, 0x51, + 0x52, 0x56, 0x57, 0x41, 0x50, 0x41, 0x51, 0x41, 0x52, 0x41, 0x53, 0x41, 0x54, 0x41, 0x55, 0x41, 0x56, 0x41, 0x57, + 0x9C, 0x48, 0x81, 0xEC, 0x00, 0x01, 0x00, 0x00, 0xF3, 0x44, 0x0F, 0x7F, 0xBC, 0x24, 0xF0, 0x00, 0x00, 0x00, 0xF3, + 0x44, 0x0F, 0x7F, 0xB4, 0x24, 0xE0, 0x00, 0x00, 0x00, 0xF3, 0x44, 0x0F, 0x7F, 0xAC, 0x24, 0xD0, 0x00, 0x00, 0x00, + 0xF3, 0x44, 0x0F, 0x7F, 0xA4, 0x24, 0xC0, 0x00, 0x00, 0x00, 0xF3, 0x44, 0x0F, 0x7F, 0x9C, 0x24, 0xB0, 0x00, 0x00, + 0x00, 0xF3, 0x44, 0x0F, 0x7F, 0x94, 0x24, 0xA0, 0x00, 0x00, 0x00, 0xF3, 0x44, 0x0F, 0x7F, 0x8C, 0x24, 0x90, 0x00, + 0x00, 0x00, 0xF3, 0x44, 0x0F, 0x7F, 0x84, 0x24, 0x80, 0x00, 0x00, 0x00, 0xF3, 0x0F, 0x7F, 0x7C, 0x24, 0x70, 0xF3, + 0x0F, 0x7F, 0x74, 0x24, 0x60, 0xF3, 0x0F, 0x7F, 0x6C, 0x24, 0x50, 0xF3, 0x0F, 0x7F, 0x64, 0x24, 0x40, 0xF3, 0x0F, + 0x7F, 0x5C, 0x24, 0x30, 0xF3, 0x0F, 0x7F, 0x54, 0x24, 0x20, 0xF3, 0x0F, 0x7F, 0x4C, 0x24, 0x10, 0xF3, 0x0F, 0x7F, + 0x04, 0x24, 0x48, 0x8B, 0xBC, 0x24, 0x80, 0x01, 0x00, 0x00, 0x48, 0x83, 0xC7, 0x10, 0x48, 0x89, 0xBC, 0x24, 0x80, + 0x01, 0x00, 0x00, 0x48, 0x8D, 0x3C, 0x24, 0x48, 0x89, 0xE3, 0x48, 0x83, 0xEC, 0x30, 0x48, 0x83, 0xE4, 0xF0, 0xFF, + 0x15, 0xA8, 0x00, 0x00, 0x00, 0x48, 0x89, 0xDC, 0xF3, 0x0F, 0x6F, 0x04, 0x24, 0xF3, 0x0F, 0x6F, 0x4C, 0x24, 0x10, + 0xF3, 0x0F, 0x6F, 0x54, 0x24, 0x20, 0xF3, 0x0F, 0x6F, 0x5C, 0x24, 0x30, 0xF3, 0x0F, 0x6F, 0x64, 0x24, 0x40, 0xF3, + 0x0F, 0x6F, 0x6C, 0x24, 0x50, 0xF3, 0x0F, 0x6F, 0x74, 0x24, 0x60, 0xF3, 0x0F, 0x6F, 0x7C, 0x24, 0x70, 0xF3, 0x44, + 0x0F, 0x6F, 0x84, 0x24, 0x80, 0x00, 0x00, 0x00, 0xF3, 0x44, 0x0F, 0x6F, 0x8C, 0x24, 0x90, 0x00, 0x00, 0x00, 0xF3, + 0x44, 0x0F, 0x6F, 0x94, 0x24, 0xA0, 0x00, 0x00, 0x00, 0xF3, 0x44, 0x0F, 0x6F, 0x9C, 0x24, 0xB0, 0x00, 0x00, 0x00, + 0xF3, 0x44, 0x0F, 0x6F, 0xA4, 0x24, 0xC0, 0x00, 0x00, 0x00, 0xF3, 0x44, 0x0F, 0x6F, 0xAC, 0x24, 0xD0, 0x00, 0x00, + 0x00, 0xF3, 0x44, 0x0F, 0x6F, 0xB4, 0x24, 0xE0, 0x00, 0x00, 0x00, 0xF3, 0x44, 0x0F, 0x6F, 0xBC, 0x24, 0xF0, 0x00, + 0x00, 0x00, 0x48, 0x81, 0xC4, 0x00, 0x01, 0x00, 0x00, 0x9D, 0x41, 0x5F, 0x41, 0x5E, 0x41, 0x5D, 0x41, 0x5C, 0x41, + 0x5B, 0x41, 0x5A, 0x41, 0x59, 0x41, 0x58, 0x5F, 0x5E, 0x5A, 0x59, 0x5B, 0x58, 0x5D, 0x48, 0x8D, 0x64, 0x24, 0x08, + 0x5C, 0xC3, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; +#endif #elif SAFETYHOOK_ARCH_X86_32 constexpr std::array asm_data = {0xFF, 0x35, 0xA7, 0x00, 0x00, 0x00, 0x54, 0x54, 0x55, 0x50, 0x53, 0x51, 0x52, 0x56, 0x57, 0x9C, 0x81, 0xEC, 0x80, 0x00, 0x00, 0x00, 0xF3, 0x0F, 0x7F, 0x7C, 0x24, 0x70, 0xF3, 0x0F, 0x7F, diff --git a/src/mid_hook.x86.asm b/src/mid_hook.x86_32.asm similarity index 100% rename from src/mid_hook.x86.asm rename to src/mid_hook.x86_32.asm diff --git a/src/mid_hook.x86_64-linux.asm b/src/mid_hook.x86_64-linux.asm new file mode 100644 index 0000000..2f126a7 --- /dev/null +++ b/src/mid_hook.x86_64-linux.asm @@ -0,0 +1,101 @@ +bits 64 + +; save context +push qword [rel trampoline] +push rsp ; push trampoline rsp +push rsp ; push original rsp (this gets fixed later) +push rbp +push rax +push rbx +push rcx +push rdx +push rsi +push rdi +push r8 +push r9 +push r10 +push r11 +push r12 +push r13 +push r14 +push r15 +pushfq +sub rsp, 256 +movdqu [rsp+240], xmm15 +movdqu [rsp+224], xmm14 +movdqu [rsp+208], xmm13 +movdqu [rsp+192], xmm12 +movdqu [rsp+176], xmm11 +movdqu [rsp+160], xmm10 +movdqu [rsp+144], xmm9 +movdqu [rsp+128], xmm8 +movdqu [rsp+112], xmm7 +movdqu [rsp+96], xmm6 +movdqu [rsp+80], xmm5 +movdqu [rsp+64], xmm4 +movdqu [rsp+48], xmm3 +movdqu [rsp+32], xmm2 +movdqu [rsp+16], xmm1 +movdqu [rsp], xmm0 + +; fix stored rsp. +mov rdi, [rsp+384] +add rdi, 16 +mov [rsp+384], rdi + +; set destination parameter +lea rdi, [rsp] + +; align stack, save original +mov rbx, rsp +sub rsp, 48 +and rsp, -16 + +; call destination +call [rel destination] + +; restore stack +mov rsp, rbx + +; restore context +movdqu xmm0, [rsp] +movdqu xmm1, [rsp+16] +movdqu xmm2, [rsp+32] +movdqu xmm3, [rsp+48] +movdqu xmm4, [rsp+64] +movdqu xmm5, [rsp+80] +movdqu xmm6, [rsp+96] +movdqu xmm7, [rsp+112] +movdqu xmm8, [rsp+128] +movdqu xmm9, [rsp+144] +movdqu xmm10, [rsp+160] +movdqu xmm11, [rsp+176] +movdqu xmm12, [rsp+192] +movdqu xmm13, [rsp+208] +movdqu xmm14, [rsp+224] +movdqu xmm15, [rsp+240] +add rsp, 256 +popfq +pop r15 +pop r14 +pop r13 +pop r12 +pop r11 +pop r10 +pop r9 +pop r8 +pop rdi +pop rsi +pop rdx +pop rcx +pop rbx +pop rax +pop rbp +lea rsp, [rsp+8] ; skip original rsp +pop rsp +ret + +destination: +dq 0 +trampoline: +dq 0 \ No newline at end of file diff --git a/src/mid_hook.x86_64.asm b/src/mid_hook.x86_64-windows.asm similarity index 100% rename from src/mid_hook.x86_64.asm rename to src/mid_hook.x86_64-windows.asm diff --git a/src/os.linux.cpp b/src/os.linux.cpp new file mode 100644 index 0000000..17ce09d --- /dev/null +++ b/src/os.linux.cpp @@ -0,0 +1,198 @@ +#include "safetyhook/common.hpp" + +#if SAFETYHOOK_OS_LINUX + +#include + +#include +#include + +#include "safetyhook/utility.hpp" + +#include "safetyhook/os.hpp" + +namespace safetyhook { +std::expected vm_allocate(uint8_t* address, size_t size, VmAccess access) { + int prot = 0; + int flags = MAP_PRIVATE | MAP_ANONYMOUS; + + if (access == VM_ACCESS_R) { + prot = PROT_READ; + } else if (access == VM_ACCESS_RW) { + prot = PROT_READ | PROT_WRITE; + } else if (access == VM_ACCESS_RX) { + prot = PROT_READ | PROT_EXEC; + } else if (access == VM_ACCESS_RWX) { + prot = PROT_READ | PROT_WRITE | PROT_EXEC; + } else { + return std::unexpected{OsError::FAILED_TO_ALLOCATE}; + } + + auto* result = mmap(address, size, prot, flags, -1, 0); + + if (result == MAP_FAILED) { + return std::unexpected{OsError::FAILED_TO_ALLOCATE}; + } + + return static_cast(result); +} + +void vm_free(uint8_t* address) { + munmap(address, 0); +} + +std::expected vm_protect(uint8_t* address, size_t size, VmAccess access) { + int prot = 0; + + if (access == VM_ACCESS_R) { + prot = PROT_READ; + } else if (access == VM_ACCESS_RW) { + prot = PROT_READ | PROT_WRITE; + } else if (access == VM_ACCESS_RX) { + prot = PROT_READ | PROT_EXEC; + } else if (access == VM_ACCESS_RWX) { + prot = PROT_READ | PROT_WRITE | PROT_EXEC; + } else { + return std::unexpected{OsError::FAILED_TO_PROTECT}; + } + + return vm_protect(address, size, prot); +} + +std::expected vm_protect(uint8_t* address, size_t size, uint32_t protect) { + auto mbi = vm_query(address); + + if (!mbi.has_value()) { + return std::unexpected{OsError::FAILED_TO_PROTECT}; + } + + uint32_t old_protect = 0; + + if (mbi->access.read) { + old_protect |= PROT_READ; + } + + if (mbi->access.write) { + old_protect |= PROT_WRITE; + } + + if (mbi->access.execute) { + old_protect |= PROT_EXEC; + } + + auto* addr = align_down(address, static_cast(sysconf(_SC_PAGESIZE))); + + if (mprotect(addr, size, static_cast(protect)) == -1) { + return std::unexpected{OsError::FAILED_TO_PROTECT}; + } + + return old_protect; +} + +std::expected vm_query(uint8_t* address) { + auto* maps = fopen("/proc/self/maps", "r"); + + if (maps == nullptr) { + return std::unexpected{OsError::FAILED_TO_QUERY}; + } + + char line[512]; + unsigned long start; + unsigned long end; + char perms[5]; + unsigned long offset; + int dev_major; + int dev_minor; + unsigned long inode; + char path[256]; + unsigned long last_end = + reinterpret_cast(system_info().min_address); // Track the end address of the last mapping. + auto addr = reinterpret_cast(address); + std::optional info = std::nullopt; + + while (fgets(line, sizeof(line), maps) != nullptr) { + path[0] = '\0'; + + sscanf(line, "%lx-%lx %4s %lx %x:%x %lu %255[^\n]", &start, &end, perms, &offset, &dev_major, &dev_minor, + &inode, path); + + if (last_end < start && addr >= last_end && addr < start) { + info = { + .address = reinterpret_cast(last_end), + .size = start - last_end, + .access = VmAccess{}, + .is_free = true, + }; + + break; + } + + last_end = end; + + if (addr >= start && addr < end) { + info = { + .address = reinterpret_cast(start), + .size = end - start, + .access = VmAccess{}, + .is_free = false, + }; + + if (perms[0] == 'r') { + info->access.read = true; + } + + if (perms[1] == 'w') { + info->access.write = true; + } + + if (perms[2] == 'x') { + info->access.execute = true; + } + + break; + } + } + + fclose(maps); + + if (!info.has_value()) { + return std::unexpected{OsError::FAILED_TO_QUERY}; + } + + return info.value(); +} + +bool vm_is_readable(uint8_t* address, [[maybe_unused]] size_t size) { + return vm_query(address).value_or(VmBasicInfo{}).access.read; +} + +bool vm_is_writable(uint8_t* address, [[maybe_unused]] size_t size) { + return vm_query(address).value_or(VmBasicInfo{}).access.write; +} + +bool vm_is_executable(uint8_t* address) { + return vm_query(address).value_or(VmBasicInfo{}).access.execute; +} + +SystemInfo system_info() { + auto page_size = static_cast(sysconf(_SC_PAGESIZE)); + + return { + .page_size = page_size, + .allocation_granularity = page_size, + .min_address = reinterpret_cast(0x10000), + .max_address = reinterpret_cast(1ull << 47), + }; +} + +void execute_while_frozen(const std::function& run_fn, + [[maybe_unused]] const std::function& visit_fn) { + run_fn(); +} + +void fix_ip([[maybe_unused]] ThreadContext ctx, [[maybe_unused]] uint8_t* old_ip, [[maybe_unused]] uint8_t* new_ip) { +} + +} // namespace safetyhook + +#endif \ No newline at end of file diff --git a/src/os.windows.cpp b/src/os.windows.cpp new file mode 100644 index 0000000..ab8755c --- /dev/null +++ b/src/os.windows.cpp @@ -0,0 +1,281 @@ +#include "safetyhook/common.hpp" + +#if SAFETYHOOK_OS_WINDOWS + +#define NOMINMAX +#if __has_include() +#include +#elif __has_include() +#include +#else +#error "Windows.h not found" +#endif + +#include + +#include "safetyhook/os.hpp" + +#pragma comment(lib, "ntdll") + +extern "C" { +NTSTATUS +NTAPI +NtGetNextThread(HANDLE ProcessHandle, HANDLE ThreadHandle, ACCESS_MASK DesiredAccess, ULONG HandleAttributes, + ULONG Flags, PHANDLE NewThreadHandle); +} + +namespace safetyhook { +std::expected vm_allocate(uint8_t* address, size_t size, VmAccess access) { + DWORD protect = 0; + + if (access == VM_ACCESS_R) { + protect = PAGE_READONLY; + } else if (access == VM_ACCESS_RW) { + protect = PAGE_READWRITE; + } else if (access == VM_ACCESS_RX) { + protect = PAGE_EXECUTE_READ; + } else if (access == VM_ACCESS_RWX) { + protect = PAGE_EXECUTE_READWRITE; + } else { + return std::unexpected{OsError::FAILED_TO_ALLOCATE}; + } + + auto* result = VirtualAlloc(address, size, MEM_COMMIT | MEM_RESERVE, protect); + + if (result == nullptr) { + return std::unexpected{OsError::FAILED_TO_ALLOCATE}; + } + + return static_cast(result); +} + +void vm_free(uint8_t* address) { + VirtualFree(address, 0, MEM_RELEASE); +} + +std::expected vm_protect(uint8_t* address, size_t size, VmAccess access) { + DWORD protect = 0; + + if (access == VM_ACCESS_R) { + protect = PAGE_READONLY; + } else if (access == VM_ACCESS_RW) { + protect = PAGE_READWRITE; + } else if (access == VM_ACCESS_RX) { + protect = PAGE_EXECUTE_READ; + } else if (access == VM_ACCESS_RWX) { + protect = PAGE_EXECUTE_READWRITE; + } else { + return std::unexpected{OsError::FAILED_TO_PROTECT}; + } + + return vm_protect(address, size, protect); +} + +std::expected vm_protect(uint8_t* address, size_t size, uint32_t protect) { + DWORD old_protect = 0; + + if (VirtualProtect(address, size, protect, &old_protect) == FALSE) { + return std::unexpected{OsError::FAILED_TO_PROTECT}; + } + + return old_protect; +} + +std::expected vm_query(uint8_t* address) { + MEMORY_BASIC_INFORMATION mbi{}; + auto result = VirtualQuery(address, &mbi, sizeof(mbi)); + + if (result == 0) { + return std::unexpected{OsError::FAILED_TO_QUERY}; + } + + VmAccess access{ + .read = (mbi.Protect & (PAGE_READONLY | PAGE_READWRITE | PAGE_EXECUTE_READ | PAGE_EXECUTE_READWRITE)) != 0, + .write = (mbi.Protect & (PAGE_READWRITE | PAGE_EXECUTE_READWRITE)) != 0, + .execute = (mbi.Protect & (PAGE_EXECUTE | PAGE_EXECUTE_READ | PAGE_EXECUTE_READWRITE)) != 0, + }; + + return VmBasicInfo{ + .address = static_cast(mbi.AllocationBase), + .size = mbi.RegionSize, + .access = access, + .is_free = mbi.State == MEM_FREE, + }; +} + +bool vm_is_readable(uint8_t* address, size_t size) { + return IsBadReadPtr(address, size) == FALSE; +} + +bool vm_is_writable(uint8_t* address, size_t size) { + return IsBadWritePtr(address, size) == FALSE; +} + +bool vm_is_executable(uint8_t* address) { + LPVOID image_base_ptr; + + if (RtlPcToFileHeader(address, &image_base_ptr) == nullptr) { + return vm_query(address).value_or(VmBasicInfo{}).access.execute; + } + + // Just check if the section is executable. + const auto* image_base = reinterpret_cast(image_base_ptr); + const auto* dos_hdr = reinterpret_cast(image_base); + + if (dos_hdr->e_magic != IMAGE_DOS_SIGNATURE) { + return vm_query(address).value_or(VmBasicInfo{}).access.execute; + } + + const auto* nt_hdr = reinterpret_cast(image_base + dos_hdr->e_lfanew); + + if (nt_hdr->Signature != IMAGE_NT_SIGNATURE) { + return vm_query(address).value_or(VmBasicInfo{}).access.execute; + } + + const auto* section = IMAGE_FIRST_SECTION(nt_hdr); + + for (auto i = 0; i < nt_hdr->FileHeader.NumberOfSections; ++i, ++section) { + if (address >= image_base + section->VirtualAddress && + address < image_base + section->VirtualAddress + section->Misc.VirtualSize) { + return (section->Characteristics & IMAGE_SCN_MEM_EXECUTE) != 0; + } + } + + return vm_query(address).value_or(VmBasicInfo{}).access.execute; +} + +SystemInfo system_info() { + SystemInfo info{}; + + SYSTEM_INFO si{}; + GetSystemInfo(&si); + + info.page_size = si.dwPageSize; + info.allocation_granularity = si.dwAllocationGranularity; + info.min_address = static_cast(si.lpMinimumApplicationAddress); + info.max_address = static_cast(si.lpMaximumApplicationAddress); + + return info; +} + +void execute_while_frozen( + const std::function& run_fn, const std::function& visit_fn) { + // Freeze all threads. + int num_threads_frozen; + auto first_run = true; + + do { + num_threads_frozen = 0; + HANDLE thread{}; + + while (true) { + HANDLE next_thread{}; + const auto status = NtGetNextThread(GetCurrentProcess(), thread, + THREAD_QUERY_LIMITED_INFORMATION | THREAD_SUSPEND_RESUME | THREAD_GET_CONTEXT | THREAD_SET_CONTEXT, 0, + 0, &next_thread); + + if (thread != nullptr) { + CloseHandle(thread); + } + + if (!NT_SUCCESS(status)) { + break; + } + + thread = next_thread; + + const auto thread_id = GetThreadId(thread); + + if (thread_id == 0 || thread_id == GetCurrentThreadId()) { + continue; + } + + const auto suspend_count = SuspendThread(thread); + + if (suspend_count == static_cast(-1)) { + continue; + } + + // Check if the thread was already frozen. Only resume if the thread was already frozen, and it wasn't the + // first run of this freeze loop to account for threads that may have already been frozen for other reasons. + if (suspend_count != 0 && !first_run) { + ResumeThread(thread); + continue; + } + + CONTEXT thread_ctx{}; + + thread_ctx.ContextFlags = CONTEXT_FULL; + + if (GetThreadContext(thread, &thread_ctx) == FALSE) { + continue; + } + + if (visit_fn) { + visit_fn(static_cast(thread_id), static_cast(thread), + static_cast(&thread_ctx)); + } + + ++num_threads_frozen; + } + + first_run = false; + } while (num_threads_frozen != 0); + + // Run the function. + if (run_fn) { + run_fn(); + } + + // Resume all threads. + HANDLE thread{}; + + while (true) { + HANDLE next_thread{}; + const auto status = NtGetNextThread(GetCurrentProcess(), thread, + THREAD_QUERY_LIMITED_INFORMATION | THREAD_SUSPEND_RESUME | THREAD_GET_CONTEXT | THREAD_SET_CONTEXT, 0, 0, + &next_thread); + + if (thread != nullptr) { + CloseHandle(thread); + } + + if (!NT_SUCCESS(status)) { + break; + } + + thread = next_thread; + + const auto thread_id = GetThreadId(thread); + + if (thread_id == 0 || thread_id == GetCurrentThreadId()) { + continue; + } + + ResumeThread(thread); + } +} + +void fix_ip(ThreadContext thread_ctx, uint8_t* old_ip, uint8_t* new_ip) { + auto* ctx = reinterpret_cast(thread_ctx); + +#if SAFETYHOOK_ARCH_X86_64 + auto ip = ctx->Rip; +#elif SAFETYHOOK_ARCH_X86_32 + auto ip = ctx->Eip; +#endif + + if (ip == reinterpret_cast(old_ip)) { + ip = reinterpret_cast(new_ip); + } + +#if SAFETYHOOK_ARCH_X86_64 + ctx->Rip = ip; +#elif SAFETYHOOK_ARCH_X86_32 + ctx->Eip = ip; +#endif +} + +} // namespace safetyhook + +#endif \ No newline at end of file diff --git a/src/thread_freezer.cpp b/src/thread_freezer.cpp deleted file mode 100644 index b9b3ae0..0000000 --- a/src/thread_freezer.cpp +++ /dev/null @@ -1,141 +0,0 @@ -#if __has_include() -#include -#elif __has_include() -#include -#else -#error "Windows.h not found" -#endif -#include - -#include "safetyhook/common.hpp" - -#include "safetyhook/thread_freezer.hpp" - -#pragma comment(lib, "ntdll") - -extern "C" { -NTSTATUS -NTAPI -NtGetNextThread(HANDLE ProcessHandle, HANDLE ThreadHandle, ACCESS_MASK DesiredAccess, ULONG HandleAttributes, - ULONG Flags, PHANDLE NewThreadHandle); -} - -namespace safetyhook { -void execute_while_frozen( - const std::function& run_fn, const std::function& visit_fn) { - // Freeze all threads. - int num_threads_frozen; - auto first_run = true; - - do { - num_threads_frozen = 0; - HANDLE thread{}; - - while (true) { - HANDLE next_thread{}; - const auto status = NtGetNextThread(GetCurrentProcess(), thread, - THREAD_QUERY_LIMITED_INFORMATION | THREAD_SUSPEND_RESUME | THREAD_GET_CONTEXT | THREAD_SET_CONTEXT, 0, - 0, &next_thread); - - if (thread != nullptr) { - CloseHandle(thread); - } - - if (!NT_SUCCESS(status)) { - break; - } - - thread = next_thread; - - const auto thread_id = GetThreadId(thread); - - if (thread_id == 0 || thread_id == GetCurrentThreadId()) { - continue; - } - - const auto suspend_count = SuspendThread(thread); - - if (suspend_count == static_cast(-1)) { - continue; - } - - // Check if the thread was already frozen. Only resume if the thread was already frozen, and it wasn't the - // first run of this freeze loop to account for threads that may have already been frozen for other reasons. - if (suspend_count != 0 && !first_run) { - ResumeThread(thread); - continue; - } - - CONTEXT thread_ctx{}; - - thread_ctx.ContextFlags = CONTEXT_FULL; - - if (GetThreadContext(thread, &thread_ctx) == FALSE) { - continue; - } - - if (visit_fn) { - visit_fn(static_cast(thread_id), static_cast(thread), - static_cast(&thread_ctx)); - } - - ++num_threads_frozen; - } - - first_run = false; - } while (num_threads_frozen != 0); - - // Run the function. - if (run_fn) { - run_fn(); - } - - // Resume all threads. - HANDLE thread{}; - - while (true) { - HANDLE next_thread{}; - const auto status = NtGetNextThread(GetCurrentProcess(), thread, - THREAD_QUERY_LIMITED_INFORMATION | THREAD_SUSPEND_RESUME | THREAD_GET_CONTEXT | THREAD_SET_CONTEXT, 0, 0, - &next_thread); - - if (thread != nullptr) { - CloseHandle(thread); - } - - if (!NT_SUCCESS(status)) { - break; - } - - thread = next_thread; - - const auto thread_id = GetThreadId(thread); - - if (thread_id == 0 || thread_id == GetCurrentThreadId()) { - continue; - } - - ResumeThread(thread); - } -} - -void fix_ip(ThreadContext thread_ctx, uint8_t* old_ip, uint8_t* new_ip) { - auto* ctx = reinterpret_cast(thread_ctx); - -#if SAFETYHOOK_ARCH_X86_64 - auto ip = ctx->Rip; -#elif SAFETYHOOK_ARCH_X86_32 - auto ip = ctx->Eip; -#endif - - if (ip == reinterpret_cast(old_ip)) { - ip = reinterpret_cast(new_ip); - } - -#if SAFETYHOOK_ARCH_X86_64 - ctx->Rip = ip; -#elif SAFETYHOOK_ARCH_X86_32 - ctx->Eip = ip; -#endif -} -} // namespace safetyhook \ No newline at end of file diff --git a/src/utility.cpp b/src/utility.cpp index 1803d24..0404d33 100644 --- a/src/utility.cpp +++ b/src/utility.cpp @@ -1,63 +1,15 @@ -#if __has_include() -#include -#elif __has_include() -#include -#else -#error "Windows.h not found" -#endif +#include "safetyhook/os.hpp" #include "safetyhook/utility.hpp" namespace safetyhook { -bool is_page_executable(uint8_t* address) { - MEMORY_BASIC_INFORMATION mbi; - - if (VirtualQuery(address, &mbi, sizeof(mbi)) == 0) { - return false; - } - - const auto executable_protect = PAGE_EXECUTE | PAGE_EXECUTE_READ | PAGE_EXECUTE_READWRITE | PAGE_EXECUTE_WRITECOPY; - - return (mbi.Protect & executable_protect) != 0; -} - bool is_executable(uint8_t* address) { - LPVOID image_base_ptr; - - if (RtlPcToFileHeader(address, &image_base_ptr) == nullptr) { - return is_page_executable(address); - } - - // Just check if the section is executable. - const auto* image_base = reinterpret_cast(image_base_ptr); - const auto* dos_hdr = reinterpret_cast(image_base); - - if (dos_hdr->e_magic != IMAGE_DOS_SIGNATURE) { - return is_page_executable(address); - } - - const auto* nt_hdr = reinterpret_cast(image_base + dos_hdr->e_lfanew); - - if (nt_hdr->Signature != IMAGE_NT_SIGNATURE) { - return is_page_executable(address); - } - - const auto* section = IMAGE_FIRST_SECTION(nt_hdr); - - for (auto i = 0; i < nt_hdr->FileHeader.NumberOfSections; ++i, ++section) { - if (address >= image_base + section->VirtualAddress && - address < image_base + section->VirtualAddress + section->Misc.VirtualSize) { - return (section->Characteristics & IMAGE_SCN_MEM_EXECUTE) != 0; - } - } - - return is_page_executable(address); + return vm_is_executable(address); } UnprotectMemory::~UnprotectMemory() { if (m_address != nullptr) { - DWORD old_protection; - VirtualProtect(m_address, m_size, m_original_protection, &old_protection); + vm_protect(m_address, m_size, m_original_protection); } } @@ -79,13 +31,13 @@ UnprotectMemory& UnprotectMemory::operator=(UnprotectMemory&& other) noexcept { } std::optional unprotect(uint8_t* address, size_t size) { - DWORD old_protection; + auto old_protection = vm_protect(address, size, VM_ACCESS_RWX); - if (!VirtualProtect(address, size, PAGE_EXECUTE_READWRITE, &old_protection)) { - return {}; + if (!old_protection) { + return std::nullopt; } - return UnprotectMemory{address, size, old_protection}; + return UnprotectMemory{address, size, old_protection.value()}; } } // namespace safetyhook \ No newline at end of file diff --git a/src/vmt_hook.cpp b/src/vmt_hook.cpp index d62d74e..7831703 100644 --- a/src/vmt_hook.cpp +++ b/src/vmt_hook.cpp @@ -1,12 +1,4 @@ -#if __has_include() -#include -#elif __has_include() -#include -#else -#error "Windows.h not found" -#endif - -#include "safetyhook/thread_freezer.hpp" +#include "safetyhook/os.hpp" #include "safetyhook/vmt_hook.hpp" @@ -113,7 +105,7 @@ void VmtHook::remove(void* object) { const auto original_vmt = search->second; execute_while_frozen([&] { - if (IsBadWritePtr(object, sizeof(void*))) { + if (!vm_is_writable(reinterpret_cast(object), sizeof(void*))) { return; } @@ -134,7 +126,7 @@ void VmtHook::reset() { void VmtHook::destroy() { execute_while_frozen([this] { for (const auto [object, original_vmt] : m_objects) { - if (IsBadWritePtr(object, sizeof(void*))) { + if (!vm_is_writable(reinterpret_cast(object), sizeof(void*))) { return; } diff --git a/test/inline_hook.cpp b/test/inline_hook.cpp index d702a19..5a45af2 100644 --- a/test/inline_hook.cpp +++ b/test/inline_hook.cpp @@ -11,7 +11,7 @@ using namespace Xbyak::util; static suite<"inline hook"> inline_hook_tests = [] { "Function hooked multiple times"_test = [] { struct Target { - __declspec(noinline) static std::string fn(std::string name) { return "hello " + name; } + SAFETYHOOK_NOINLINE static std::string fn(std::string name) { return "hello " + name; } }; expect(eq(Target::fn("world"), "hello world"sv)); @@ -85,7 +85,7 @@ static suite<"inline hook"> inline_hook_tests = [] { "Function with multiple args hooked"_test = [] { struct Target { - __declspec(noinline) static int add(int x, int y) { return x + y; } + SAFETYHOOK_NOINLINE static int add(int x, int y) { return x + y; } }; expect(Target::add(2, 3) == 5_i); @@ -109,12 +109,13 @@ static suite<"inline hook"> inline_hook_tests = [] { expect(Target::add(5, 6) == 11_i); }; +#if SAFETYHOOK_OS_WINDOWS "Active function is hooked and unhooked"_test = [] { static int count = 0; static bool is_running = true; struct Target { - __declspec(noinline) static std::string say_hello(int times) { return "Hello #" + std::to_string(times); } + SAFETYHOOK_NOINLINE static std::string say_hello(int times) { return "Hello #" + std::to_string(times); } static void say_hello_infinitely() { while (is_running) { @@ -150,12 +151,13 @@ static suite<"inline hook"> inline_hook_tests = [] { expect(eq(Target::say_hello(0), "Hello #0"sv)); expect(count > 0_i); }; +#endif "Function with short unconditional branch is hooked"_test = [] { static SafetyHookInline hook; struct Hook { - static int __fastcall fn() { return hook.fastcall() + 42; }; + static int SAFETYHOOK_FASTCALL fn() { return hook.fastcall() + 42; }; }; Xbyak::CodeGenerator cg{}; @@ -169,7 +171,7 @@ static suite<"inline hook"> inline_hook_tests = [] { cg.ret(); cg.nop(10, false); - const auto fn = cg.getCode(); + const auto fn = cg.getCode(); expect(fn() == 1_i); @@ -186,7 +188,7 @@ static suite<"inline hook"> inline_hook_tests = [] { static SafetyHookInline hook; struct Hook { - static int __fastcall fn(int x) { return hook.fastcall(x) + 42; }; + static int SAFETYHOOK_FASTCALL fn(int x) { return hook.fastcall(x) + 42; }; }; Xbyak::CodeGenerator cg{}; @@ -199,11 +201,17 @@ static suite<"inline hook"> inline_hook_tests = [] { cg.mov(eax, 1); cg.ret(); cg.nop(10, false); - return cg.getCode(); + return cg.getCode(); }; +#if SAFETYHOOK_OS_WINDOWS + constexpr auto param = ecx; +#elif SAFETYHOOK_OS_LINUX + constexpr auto param = edi; +#endif + "JB"_test = [&] { - cg.cmp(ecx, 8); + cg.cmp(param, 8); cg.jb(label); const auto fn = finalize(); @@ -227,7 +235,7 @@ static suite<"inline hook"> inline_hook_tests = [] { }; "JBE"_test = [&] { - cg.cmp(ecx, 8); + cg.cmp(param, 8); cg.jbe(label); const auto fn = finalize(); @@ -251,7 +259,7 @@ static suite<"inline hook"> inline_hook_tests = [] { }; "JL"_test = [&] { - cg.cmp(ecx, 8); + cg.cmp(param, 8); cg.jl(label); const auto fn = finalize(); @@ -275,7 +283,7 @@ static suite<"inline hook"> inline_hook_tests = [] { }; "JLE"_test = [&] { - cg.cmp(ecx, 8); + cg.cmp(param, 8); cg.jle(label); const auto fn = finalize(); @@ -299,7 +307,7 @@ static suite<"inline hook"> inline_hook_tests = [] { }; "JNB"_test = [&] { - cg.cmp(ecx, 8); + cg.cmp(param, 8); cg.jnb(label); const auto fn = finalize(); @@ -323,7 +331,7 @@ static suite<"inline hook"> inline_hook_tests = [] { }; "JNBE"_test = [&] { - cg.cmp(ecx, 8); + cg.cmp(param, 8); cg.jnbe(label); const auto fn = finalize(); @@ -347,7 +355,7 @@ static suite<"inline hook"> inline_hook_tests = [] { }; "JNL"_test = [&] { - cg.cmp(ecx, 8); + cg.cmp(param, 8); cg.jnl(label); const auto fn = finalize(); @@ -371,7 +379,7 @@ static suite<"inline hook"> inline_hook_tests = [] { }; "JNLE"_test = [&] { - cg.cmp(ecx, 8); + cg.cmp(param, 8); cg.jnle(label); const auto fn = finalize(); @@ -395,7 +403,7 @@ static suite<"inline hook"> inline_hook_tests = [] { }; "JNO"_test = [&] { - cg.cmp(ecx, 8); + cg.cmp(param, 8); cg.jno(label); const auto fn = finalize(); @@ -419,7 +427,7 @@ static suite<"inline hook"> inline_hook_tests = [] { }; "JNP"_test = [&] { - cg.cmp(ecx, 8); + cg.cmp(param, 8); cg.jnp(label); const auto fn = finalize(); @@ -443,7 +451,7 @@ static suite<"inline hook"> inline_hook_tests = [] { }; "JNS"_test = [&] { - cg.cmp(ecx, 8); + cg.cmp(param, 8); cg.jns(label); const auto fn = finalize(); @@ -467,7 +475,7 @@ static suite<"inline hook"> inline_hook_tests = [] { }; "JNZ"_test = [&] { - cg.cmp(ecx, 8); + cg.cmp(param, 8); cg.jnz(label); const auto fn = finalize(); @@ -491,7 +499,7 @@ static suite<"inline hook"> inline_hook_tests = [] { }; "JO"_test = [&] { - cg.cmp(ecx, 8); + cg.cmp(param, 8); cg.jo(label); const auto fn = finalize(); @@ -515,7 +523,7 @@ static suite<"inline hook"> inline_hook_tests = [] { }; "JP"_test = [&] { - cg.cmp(ecx, 8); + cg.cmp(param, 8); cg.jp(label); const auto fn = finalize(); @@ -539,7 +547,7 @@ static suite<"inline hook"> inline_hook_tests = [] { }; "JS"_test = [&] { - cg.cmp(ecx, 8); + cg.cmp(param, 8); cg.js(label); const auto fn = finalize(); @@ -563,7 +571,7 @@ static suite<"inline hook"> inline_hook_tests = [] { }; "JZ"_test = [&] { - cg.cmp(ecx, 8); + cg.cmp(param, 8); cg.jz(label); const auto fn = finalize(); diff --git a/test/inline_hook.x86_64.cpp b/test/inline_hook.x86_64.cpp index 1ce045e..cf00b5b 100644 --- a/test/inline_hook.x86_64.cpp +++ b/test/inline_hook.x86_64.cpp @@ -58,9 +58,15 @@ static suite<"inline hook (x64)"> inline_hook_x64_tests = [] { Xbyak::CodeGenerator cg{5'000'000'000}; // 5 GB Xbyak::Label start{}; +#if SAFETYHOOK_OS_WINDOWS + constexpr auto param = ecx; +#elif SAFETYHOOK_OS_LINUX + constexpr auto param = edi; +#endif + cg.nop(2'500'000'000, false); // 2.5 GB cg.L(start); - cg.mov(dword[rsp + 8], ecx); + cg.mov(dword[rsp + 8], param); cg.mov(eax, dword[rsp + 8]); cg.imul(eax, dword[rsp + 8]); cg.ret(); diff --git a/test/mid_hook.cpp b/test/mid_hook.cpp index d6920ab..8560f3f 100644 --- a/test/mid_hook.cpp +++ b/test/mid_hook.cpp @@ -6,7 +6,7 @@ using namespace boost::ut; static suite<"mid hook"> mid_hook_tests = [] { "Mid hook to change a register"_test = [] { struct Target { - __declspec(noinline) static int __fastcall add_42(int a) { return a + 42; } + SAFETYHOOK_NOINLINE static int SAFETYHOOK_FASTCALL add_42(int a) { return a + 42; } }; expect(Target::add_42(0) == 42_i); @@ -15,10 +15,18 @@ static suite<"mid hook"> mid_hook_tests = [] { struct Hook { static void add_42(SafetyHookContext& ctx) { +#if SAFETYHOOK_OS_WINDOWS #if SAFETYHOOK_ARCH_X86_64 ctx.rcx = 1337 - 42; #elif SAFETYHOOK_ARCH_X86_32 ctx.ecx = 1337 - 42; +#endif +#elif SAFETYHOOK_OS_LINUX +#if SAFETYHOOK_ARCH_X86_64 + ctx.rdi = 1337 - 42; +#elif SAFETYHOOK_ARCH_X86_32 + ctx.edi = 1337 - 42; +#endif #endif } }; @@ -39,7 +47,7 @@ static suite<"mid hook"> mid_hook_tests = [] { #if SAFETYHOOK_ARCH_X86_64 "Mid hook to change an XMM register"_test = [] { struct Target { - __declspec(noinline) static float __fastcall add_42(float a) { return a + 0.42f; } + SAFETYHOOK_NOINLINE static float SAFETYHOOK_FASTCALL add_42(float a) { return a + 0.42f; } }; expect(Target::add_42(0.0f) == 0.42_f); diff --git a/test/vmt_hook.cpp b/test/vmt_hook.cpp index 8bc7d7b..c02e499 100644 --- a/test/vmt_hook.cpp +++ b/test/vmt_hook.cpp @@ -3,6 +3,12 @@ using namespace boost::ut; +#if SAFETYHOOK_OS_WINDOWS +static constexpr auto VMT_OFFSET = 0; +#elif SAFETYHOOK_OS_LINUX +static constexpr auto VMT_OFFSET = 1; +#endif + static suite<"vmt hook"> vmt_hook_tests = [] { "VMT hook an object instance"_test = [] { struct Interface { @@ -11,7 +17,7 @@ static suite<"vmt hook"> vmt_hook_tests = [] { }; struct Target : Interface { - __declspec(noinline) int add_42(int a) override { return a + 42; } + SAFETYHOOK_NOINLINE int add_42(int a) override { return a + 42; } }; std::unique_ptr target = std::make_unique(); @@ -31,7 +37,7 @@ static suite<"vmt hook"> vmt_hook_tests = [] { target_hook = std::move(*vmt_result); - auto vm_result = target_hook.hook_method(1, &Hook::hooked_add_42); + auto vm_result = target_hook.hook_method(1 + VMT_OFFSET, &Hook::hooked_add_42); expect(vm_result.has_value()); @@ -52,8 +58,8 @@ static suite<"vmt hook"> vmt_hook_tests = [] { }; struct Target : Interface { - __declspec(noinline) int add_42(int a) override { return a + 42; } - __declspec(noinline) int add_43(int a) override { return a + 43; } + SAFETYHOOK_NOINLINE int add_42(int a) override { return a + 42; } + SAFETYHOOK_NOINLINE int add_43(int a) override { return a + 43; } }; std::unique_ptr target = std::make_unique(); @@ -76,7 +82,7 @@ static suite<"vmt hook"> vmt_hook_tests = [] { target_hook = std::move(*vmt_result); - auto vm_result = target_hook.hook_method(1, &Hook::hooked_add_42); + auto vm_result = target_hook.hook_method(1 + VMT_OFFSET, &Hook::hooked_add_42); expect(vm_result.has_value()); @@ -84,7 +90,7 @@ static suite<"vmt hook"> vmt_hook_tests = [] { expect(target->add_42(1) == 1380_i); - vm_result = target_hook.hook_method(2, &Hook::hooked_add_43); + vm_result = target_hook.hook_method(2 + VMT_OFFSET, &Hook::hooked_add_43); expect(vm_result.has_value()); @@ -105,13 +111,13 @@ static suite<"vmt hook"> vmt_hook_tests = [] { }; struct Target : Interface { - __declspec(noinline) int add_42(int a) override { return a + 42; } + SAFETYHOOK_NOINLINE int add_42(int a) override { return a + 42; } }; - std::unique_ptr target = std::make_unique(); + auto target = std::make_unique(); expect(target->add_42(0) == 42_i); - expect(neq(dynamic_cast(target.get()), nullptr)); + expect(neq(dynamic_cast(target.get()), nullptr)); static SafetyHookVmt target_hook{}; static SafetyHookVm add_42_hook{}; @@ -126,14 +132,14 @@ static suite<"vmt hook"> vmt_hook_tests = [] { target_hook = std::move(*vmt_result); - auto vm_result = target_hook.hook_method(1, &Hook::hooked_add_42); + auto vm_result = target_hook.hook_method(1 + VMT_OFFSET, &Hook::hooked_add_42); expect(vm_result.has_value()); add_42_hook = std::move(*vm_result); expect(target->add_42(1) == 1380_i); - expect(dynamic_cast(target.get()) != nullptr); + expect(neq(dynamic_cast(target.get()), nullptr)); }; "Can safely destroy VmtHook after object is deleted"_test = [] { @@ -143,7 +149,7 @@ static suite<"vmt hook"> vmt_hook_tests = [] { }; struct Target : Interface { - __declspec(noinline) int add_42(int a) override { return a + 42; } + SAFETYHOOK_NOINLINE int add_42(int a) override { return a + 42; } }; std::unique_ptr target = std::make_unique(); @@ -163,7 +169,7 @@ static suite<"vmt hook"> vmt_hook_tests = [] { target_hook = std::move(*vmt_result); - auto vm_result = target_hook.hook_method(1, &Hook::hooked_add_42); + auto vm_result = target_hook.hook_method(1 + VMT_OFFSET, &Hook::hooked_add_42); expect(vm_result.has_value()); @@ -182,7 +188,7 @@ static suite<"vmt hook"> vmt_hook_tests = [] { }; struct Target : Interface { - __declspec(noinline) int add_42(int a) override { return a + 42; } + SAFETYHOOK_NOINLINE int add_42(int a) override { return a + 42; } }; std::unique_ptr target = std::make_unique(); @@ -205,7 +211,7 @@ static suite<"vmt hook"> vmt_hook_tests = [] { target_hook = std::move(*vmt_result); - auto vm_result = target_hook.hook_method(1, &Hook::hooked_add_42); + auto vm_result = target_hook.hook_method(1 + VMT_OFFSET, &Hook::hooked_add_42); expect(vm_result.has_value()); @@ -235,7 +241,7 @@ static suite<"vmt hook"> vmt_hook_tests = [] { }; struct Target : Interface { - __declspec(noinline) int add_42(int a) override { return a + 42; } + SAFETYHOOK_NOINLINE int add_42(int a) override { return a + 42; } }; std::unique_ptr target = std::make_unique(); @@ -258,7 +264,7 @@ static suite<"vmt hook"> vmt_hook_tests = [] { target_hook = std::move(*vmt_result); - auto vm_result = target_hook.hook_method(1, &Hook::hooked_add_42); + auto vm_result = target_hook.hook_method(1 + VMT_OFFSET, &Hook::hooked_add_42); expect(vm_result.has_value()); @@ -309,7 +315,7 @@ static suite<"vmt hook"> vmt_hook_tests = [] { }; struct Target : Interface { - __declspec(noinline) int add_42(int a) override { return a + 42; } + SAFETYHOOK_NOINLINE int add_42(int a) override { return a + 42; } }; std::unique_ptr target = std::make_unique(); @@ -324,7 +330,7 @@ static suite<"vmt hook"> vmt_hook_tests = [] { }; target_hook = safetyhook::create_vmt(target.get()); - add_42_hook = safetyhook::create_vm(target_hook, 1, &Hook::hooked_add_42); + add_42_hook = safetyhook::create_vm(target_hook, 1 + VMT_OFFSET, &Hook::hooked_add_42); expect(target->add_42(1) == 1380_i);