mirror of
https://git.recroomarchive.org/Radium/libradhook.git
synced 2026-08-07 09:10:59 +00:00
initial commit
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -99,4 +99,7 @@ compile_commands.json
|
|||||||
CTestTestfile.cmake
|
CTestTestfile.cmake
|
||||||
_deps
|
_deps
|
||||||
CMakeUserPresets.json
|
CMakeUserPresets.json
|
||||||
|
build/
|
||||||
|
|
||||||
|
# Visual Studio Code
|
||||||
|
.vscode/
|
||||||
115
Build.ps1
Normal file
115
Build.ps1
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
param(
|
||||||
|
[ValidateSet("msvc", "gcc", "android")]
|
||||||
|
[string]$Target = "msvc",
|
||||||
|
|
||||||
|
[ValidateSet("Debug", "Release")]
|
||||||
|
[string]$Config = "Release",
|
||||||
|
|
||||||
|
[switch]$Clean,
|
||||||
|
|
||||||
|
[string]$AndroidNDK = $env:ANDROID_NDK_HOME,
|
||||||
|
|
||||||
|
[switch]$Static,
|
||||||
|
|
||||||
|
[switch]$RunTests
|
||||||
|
)
|
||||||
|
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
|
||||||
|
$BuildDir = "build/$Target-$Config"
|
||||||
|
$Generator = ""
|
||||||
|
$Toolchain = ""
|
||||||
|
$ExtraArgs = @()
|
||||||
|
|
||||||
|
if ($Clean -and (Test-Path $BuildDir)) {
|
||||||
|
Remove-Item $BuildDir -Recurse -Force
|
||||||
|
}
|
||||||
|
|
||||||
|
switch ($Target) {
|
||||||
|
|
||||||
|
"msvc" {
|
||||||
|
$VsPath = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\2022\BuildTools\Common7\Tools\VsDevCmd.bat"
|
||||||
|
|
||||||
|
if (-not (Test-Path $VsPath)) {
|
||||||
|
$VsPath = "${env:ProgramFiles}\Microsoft Visual Studio\2022\Community\Common7\Tools\VsDevCmd.bat"
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not (Test-Path $VsPath)) {
|
||||||
|
throw "MSVC environment not found. Install Visual Studio C++ build tools."
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd /c "`"$VsPath`" && set" | ForEach-Object {
|
||||||
|
if ($_ -match "^(.*?)=(.*)$") {
|
||||||
|
Set-Item -Path "env:$($matches[1])" -Value $matches[2]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$Generator = "Visual Studio 17 2022"
|
||||||
|
$Arch = "x64"
|
||||||
|
$ExtraArgs += "-A $Arch"
|
||||||
|
}
|
||||||
|
|
||||||
|
"gcc" {
|
||||||
|
$Generator = "Ninja"
|
||||||
|
$ExtraArgs += "-DCMAKE_C_COMPILER=gcc"
|
||||||
|
$ExtraArgs += "-DCMAKE_CXX_COMPILER=g++"
|
||||||
|
$ExtraArgs += "-DCMAKE_BUILD_TYPE=$Config"
|
||||||
|
}
|
||||||
|
|
||||||
|
"android" {
|
||||||
|
if (-not $AndroidNDK) {
|
||||||
|
throw "Android NDK path not set. Pass -AndroidNDK or set ANDROID_NDK_HOME."
|
||||||
|
}
|
||||||
|
|
||||||
|
$Generator = "Ninja"
|
||||||
|
|
||||||
|
$Toolchain = "$AndroidNDK/build/cmake/android.toolchain.cmake"
|
||||||
|
|
||||||
|
$ExtraArgs += "-DCMAKE_TOOLCHAIN_FILE=$Toolchain"
|
||||||
|
$ExtraArgs += "-DANDROID_ABI=arm64-v8a"
|
||||||
|
$ExtraArgs += "-DANDROID_PLATFORM=android-24"
|
||||||
|
$ExtraArgs += "-DCMAKE_BUILD_TYPE=$Config"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$SharedLibs = if ($Static) { "OFF" } else { "ON" }
|
||||||
|
$ExtraArgs += "-DBUILD_SHARED_LIBS=$SharedLibs"
|
||||||
|
|
||||||
|
Write-Host "=== Building RadHook ==="
|
||||||
|
Write-Host "Target: $Target"
|
||||||
|
Write-Host "Config: $Config"
|
||||||
|
Write-Host "Build dir: $BuildDir"
|
||||||
|
Write-Host "Shared libs: $SharedLibs"
|
||||||
|
Write-Host ""
|
||||||
|
|
||||||
|
cmake -S . -B $BuildDir -G $Generator @ExtraArgs
|
||||||
|
|
||||||
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
throw "CMake configure failed"
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($Target -eq "msvc") {
|
||||||
|
cmake --build $BuildDir --config $Config
|
||||||
|
} else {
|
||||||
|
cmake --build $BuildDir
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
throw "Build failed"
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($RunTests) {
|
||||||
|
Write-Host "`n=== Running tests ==="
|
||||||
|
|
||||||
|
if ($Target -eq "msvc") {
|
||||||
|
ctest --test-dir $BuildDir --build-config $Config --output-on-failure
|
||||||
|
} else {
|
||||||
|
ctest --test-dir $BuildDir --output-on-failure
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
throw "Tests failed"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "`nBuild complete: $BuildDir"
|
||||||
90
CMakeLists.txt
Normal file
90
CMakeLists.txt
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
cmake_minimum_required(VERSION 3.20)
|
||||||
|
|
||||||
|
project(
|
||||||
|
radhook
|
||||||
|
VERSION 1.0.0
|
||||||
|
DESCRIPTION "cross-platform function hooking library"
|
||||||
|
LANGUAGES CXX
|
||||||
|
)
|
||||||
|
|
||||||
|
option(BUILD_SHARED_LIBS "Build shared library" ON)
|
||||||
|
|
||||||
|
include(CTest)
|
||||||
|
enable_testing()
|
||||||
|
add_subdirectory(tests)
|
||||||
|
|
||||||
|
add_library(radhook
|
||||||
|
src/radhook.cpp
|
||||||
|
)
|
||||||
|
|
||||||
|
target_include_directories(radhook
|
||||||
|
PUBLIC
|
||||||
|
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
|
||||||
|
$<INSTALL_INTERFACE:include>
|
||||||
|
)
|
||||||
|
|
||||||
|
target_compile_features(radhook PUBLIC cxx_std_20)
|
||||||
|
|
||||||
|
if(BUILD_SHARED_LIBS)
|
||||||
|
target_compile_definitions(radhook PRIVATE RADHOOK_BUILD)
|
||||||
|
target_compile_definitions(radhook INTERFACE RADHOOK_SHARED)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if(WIN32)
|
||||||
|
target_compile_definitions(radhook PUBLIC RADIUM_PLATFORM_WINDOWS=1)
|
||||||
|
elseif(ANDROID)
|
||||||
|
target_compile_definitions(radhook PUBLIC RADIUM_PLATFORM_ANDROID=1)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if(MSVC)
|
||||||
|
target_compile_options(radhook PRIVATE
|
||||||
|
/W4
|
||||||
|
/permissive-
|
||||||
|
)
|
||||||
|
else()
|
||||||
|
target_compile_options(radhook PRIVATE
|
||||||
|
-Wall
|
||||||
|
-Wextra
|
||||||
|
-Wpedantic
|
||||||
|
)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
target_compile_options(radhook PRIVATE
|
||||||
|
$<$<CONFIG:Release>:
|
||||||
|
$<$<CXX_COMPILER_ID:MSVC>:/O2 /GL>
|
||||||
|
$<$<NOT:$<CXX_COMPILER_ID:MSVC>>:-O3 -fvisibility=hidden>
|
||||||
|
>
|
||||||
|
)
|
||||||
|
|
||||||
|
target_link_options(radhook PRIVATE
|
||||||
|
$<$<CONFIG:Release>:
|
||||||
|
$<$<CXX_COMPILER_ID:MSVC>:/OPT:REF /OPT:ICF>
|
||||||
|
$<$<NOT:$<CXX_COMPILER_ID:MSVC>>:-s>
|
||||||
|
>
|
||||||
|
)
|
||||||
|
|
||||||
|
set_target_properties(radhook PROPERTIES
|
||||||
|
CXX_VISIBILITY_PRESET hidden
|
||||||
|
VISIBILITY_INLINES_HIDDEN YES
|
||||||
|
)
|
||||||
|
|
||||||
|
include(GNUInstallDirs)
|
||||||
|
|
||||||
|
install(TARGETS radhook
|
||||||
|
EXPORT radhookTargets
|
||||||
|
ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
|
||||||
|
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
|
||||||
|
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
|
||||||
|
)
|
||||||
|
|
||||||
|
install(
|
||||||
|
FILES include/radhook.hpp
|
||||||
|
DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}
|
||||||
|
)
|
||||||
|
|
||||||
|
install(
|
||||||
|
EXPORT radhookTargets
|
||||||
|
FILE radhookTargets.cmake
|
||||||
|
NAMESPACE Radium::
|
||||||
|
DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/radhook
|
||||||
|
)
|
||||||
298
include/radhook/api.h
Normal file
298
include/radhook/api.h
Normal file
@@ -0,0 +1,298 @@
|
|||||||
|
#ifndef __LIBRADHOOK_API_H__
|
||||||
|
#define __LIBRADHOOK_API_H__
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <cstddef>
|
||||||
|
|
||||||
|
#include <type_traits>
|
||||||
|
|
||||||
|
#ifndef RADHOOK_API
|
||||||
|
# define RADHOOK_API
|
||||||
|
#endif // RADHOOK_API
|
||||||
|
|
||||||
|
enum class RadHookResult : int {
|
||||||
|
Success = 0,
|
||||||
|
AlreadyInstalled,
|
||||||
|
NotInstalled,
|
||||||
|
AlreadyEnabled,
|
||||||
|
AlreadyDisabled,
|
||||||
|
MemoryAllocFailed,
|
||||||
|
MemoryProtectFailed,
|
||||||
|
DisassemblyFailed,
|
||||||
|
InvalidTarget,
|
||||||
|
InvalidDetour,
|
||||||
|
InvalidHandle,
|
||||||
|
TrampolineTooFar,
|
||||||
|
Unknown,
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Creates a human readable name for a status code.
|
||||||
|
*
|
||||||
|
* @param result Status code returned by a RadHook API function.
|
||||||
|
* @return Null-terminated string describing the status code.
|
||||||
|
*/
|
||||||
|
RADHOOK_API
|
||||||
|
const char*
|
||||||
|
RadHookResultToString(
|
||||||
|
RadHookResult result
|
||||||
|
);
|
||||||
|
|
||||||
|
struct RadHookOpaque;
|
||||||
|
using RadHookHandle = RadHookOpaque*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Creates and installs a hook.
|
||||||
|
*
|
||||||
|
* Redirects execution from @p target to @p detour and enables the hook
|
||||||
|
* immediately upon successful creation.
|
||||||
|
*
|
||||||
|
* @param target Function or address to hook.
|
||||||
|
* @param detour Replacement function to execute.
|
||||||
|
* @param outHandle Receives the created hook handle on success. Set to
|
||||||
|
* nullptr on failure.
|
||||||
|
*
|
||||||
|
* @return Operation result.
|
||||||
|
*/
|
||||||
|
RADHOOK_API
|
||||||
|
RadHookResult
|
||||||
|
RadHookCreate(
|
||||||
|
void* target,
|
||||||
|
void* detour,
|
||||||
|
RadHookHandle* outHandle
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Creates and installs a hook from a function pointer.
|
||||||
|
*
|
||||||
|
* Convenience overload that accepts a typed function pointer for the target.
|
||||||
|
*
|
||||||
|
* @tparam TargetFn Target function pointer type.
|
||||||
|
* @param target Function to hook.
|
||||||
|
* @param detour Replacement function.
|
||||||
|
* @param outHandle Receives the created hook handle on success.
|
||||||
|
*
|
||||||
|
* @return Operation result.
|
||||||
|
*/
|
||||||
|
template <typename TargetFn>
|
||||||
|
requires std::is_function_v<std::remove_pointer_t<TargetFn>>
|
||||||
|
RadHookResult
|
||||||
|
RadHookCreate(
|
||||||
|
TargetFn target,
|
||||||
|
void* detour,
|
||||||
|
RadHookHandle* outHandle
|
||||||
|
) {
|
||||||
|
return RadHookCreate(
|
||||||
|
reinterpret_cast<void*>(target),
|
||||||
|
detour,
|
||||||
|
outHandle
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Enables an installed hook.
|
||||||
|
*
|
||||||
|
* Re-applies the jump from the target function to its detour.
|
||||||
|
*
|
||||||
|
* @param handle Hook handle.
|
||||||
|
*
|
||||||
|
* @return RadHookResult::AlreadyEnabled if the hook is already enabled;
|
||||||
|
* otherwise the result of the operation.
|
||||||
|
*/
|
||||||
|
RADHOOK_API
|
||||||
|
RadHookResult
|
||||||
|
RadHookEnable(
|
||||||
|
RadHookHandle handle
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Disables an installed hook.
|
||||||
|
*
|
||||||
|
* Restores the original bytes at the target function.
|
||||||
|
*
|
||||||
|
* @param handle Hook handle.
|
||||||
|
*
|
||||||
|
* @return RadHookResult::AlreadyDisabled if the hook is already disabled;
|
||||||
|
* otherwise the result of the operation.
|
||||||
|
*/
|
||||||
|
RADHOOK_API
|
||||||
|
RadHookResult
|
||||||
|
RadHookDisable(
|
||||||
|
RadHookHandle handle
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Destroys a hook.
|
||||||
|
*
|
||||||
|
* Disables the hook if necessary, releases any allocated resources,
|
||||||
|
* and invalidates the handle.
|
||||||
|
*
|
||||||
|
* @param handle Hook handle.
|
||||||
|
*
|
||||||
|
* @return Operation result.
|
||||||
|
*/
|
||||||
|
RADHOOK_API
|
||||||
|
RadHookResult
|
||||||
|
RadHookDestroy(
|
||||||
|
RadHookHandle handle
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Enables every registered hook.
|
||||||
|
*
|
||||||
|
* Hooks that are already enabled are left unchanged.
|
||||||
|
*
|
||||||
|
* @return Operation result.
|
||||||
|
*/
|
||||||
|
RADHOOK_API
|
||||||
|
RadHookResult
|
||||||
|
RadHookEnableAll();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Disables every registered hook.
|
||||||
|
*
|
||||||
|
* Hooks that are already disabled are left unchanged.
|
||||||
|
*
|
||||||
|
* @return Operation result.
|
||||||
|
*/
|
||||||
|
RADHOOK_API
|
||||||
|
RadHookResult
|
||||||
|
RadHookDisableAll();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Returns the number of currently registered hooks.
|
||||||
|
*
|
||||||
|
* Destroyed hooks are not included.
|
||||||
|
*
|
||||||
|
* @return Number of registered hooks.
|
||||||
|
*/
|
||||||
|
RADHOOK_API
|
||||||
|
size_t
|
||||||
|
RadHookGetCount();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Retrieves registered hook handles.
|
||||||
|
*
|
||||||
|
* Copies up to @p maxCount currently registered hook handles into
|
||||||
|
* @p outHandles.
|
||||||
|
*
|
||||||
|
* @param outHandles Destination buffer.
|
||||||
|
* @param maxCount Maximum number of handles to copy.
|
||||||
|
*
|
||||||
|
* @return Number of handles copied.
|
||||||
|
*/
|
||||||
|
RADHOOK_API
|
||||||
|
size_t
|
||||||
|
RadHookGetHandles(
|
||||||
|
RadHookHandle* outHandles,
|
||||||
|
size_t maxCount
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Checks whether a hook handle is valid.
|
||||||
|
*
|
||||||
|
* @param handle Hook handle.
|
||||||
|
*
|
||||||
|
* @return true if the handle refers to a registered hook; otherwise false.
|
||||||
|
*/
|
||||||
|
RADHOOK_API
|
||||||
|
bool
|
||||||
|
RadHookIsValid(
|
||||||
|
RadHookHandle handle
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Checks whether a hook is currently enabled.
|
||||||
|
*
|
||||||
|
* @param handle Hook handle.
|
||||||
|
*
|
||||||
|
* @return true if the hook is enabled; otherwise false.
|
||||||
|
*/
|
||||||
|
RADHOOK_API
|
||||||
|
bool
|
||||||
|
RadHookIsEnabled(
|
||||||
|
RadHookHandle handle
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Returns the hooked target function.
|
||||||
|
*
|
||||||
|
* @param handle Hook handle.
|
||||||
|
*
|
||||||
|
* @return Pointer to the original target function, or nullptr if the handle
|
||||||
|
* is invalid.
|
||||||
|
*/
|
||||||
|
RADHOOK_API
|
||||||
|
void*
|
||||||
|
RadHookGetTarget(
|
||||||
|
RadHookHandle handle
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Returns the detour function.
|
||||||
|
*
|
||||||
|
* @param handle Hook handle.
|
||||||
|
*
|
||||||
|
* @return Pointer to the detour function, or nullptr if the handle is
|
||||||
|
* invalid.
|
||||||
|
*/
|
||||||
|
RADHOOK_API
|
||||||
|
void*
|
||||||
|
RadHookGetDetour(
|
||||||
|
RadHookHandle handle
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Returns the trampoline containing the relocated original code.
|
||||||
|
*
|
||||||
|
* Calling the returned function executes the original implementation while
|
||||||
|
* bypassing the installed hook.
|
||||||
|
*
|
||||||
|
* @param handle Hook handle.
|
||||||
|
*
|
||||||
|
* @return Pointer to the trampoline, or nullptr if unavailable.
|
||||||
|
*/
|
||||||
|
RADHOOK_API
|
||||||
|
void*
|
||||||
|
RadHookGetOriginal(
|
||||||
|
RadHookHandle handle
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Returns the trampoline cast to a function pointer type.
|
||||||
|
*
|
||||||
|
* @tparam FnPtr Desired function pointer type.
|
||||||
|
* @param handle Hook handle.
|
||||||
|
*
|
||||||
|
* @return Trampoline cast to @p FnPtr.
|
||||||
|
*/
|
||||||
|
template <typename FnPtr>
|
||||||
|
FnPtr
|
||||||
|
RadHookGetOriginalAs(
|
||||||
|
RadHookHandle handle
|
||||||
|
) {
|
||||||
|
return reinterpret_cast<FnPtr>(RadHookGetOriginal(handle));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Enumerates currently registered hooks.
|
||||||
|
*
|
||||||
|
* Copies up to @p maxCount active hook handles into @p out.
|
||||||
|
* Handles are written in no particular order.
|
||||||
|
*
|
||||||
|
* This function only returns hooks that are currently registered
|
||||||
|
* and not destroyed.
|
||||||
|
*
|
||||||
|
* @param out Destination buffer for hook handles.
|
||||||
|
* @param maxCount Maximum number of handles to copy.
|
||||||
|
*
|
||||||
|
* @return Number of hook handles written into @p out.
|
||||||
|
*/
|
||||||
|
RADHOOK_API
|
||||||
|
size_t
|
||||||
|
RadHookEnumerate(
|
||||||
|
RadHookHandle* out,
|
||||||
|
size_t maxCount
|
||||||
|
);
|
||||||
|
|
||||||
|
#endif // __LIBRADHOOK_API_H__
|
||||||
48
include/radhook/radhook.h
Normal file
48
include/radhook/radhook.h
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
#ifndef __LIBRADHOOK_HPP__
|
||||||
|
#define __LIBRADHOOK_HPP__
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <cstddef>
|
||||||
|
|
||||||
|
#include <type_traits>
|
||||||
|
|
||||||
|
// platform detection
|
||||||
|
#if !defined(RADIUM_PLATFORM_WINDOWS) && !defined(RADIUM_PLATFORM_ANDROID)
|
||||||
|
# if defined(_WIN32) || defined(_WIN64)
|
||||||
|
# define RADIUM_PLATFORM_WINDOWS 1
|
||||||
|
# elif defined(__ANDROID__)
|
||||||
|
# define RADIUM_PLATFORM_ANDROID 1
|
||||||
|
# else
|
||||||
|
# error "radhook: unsupported platform -- define RADIUM_PLATFORM_WINDOWS or RADIUM_PLATFORM_ANDROID"
|
||||||
|
# endif
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// architecture detection
|
||||||
|
#if defined(_M_X64) || defined(__x86_64__)
|
||||||
|
# define RADHOOK_ARCH_X64 1
|
||||||
|
#elif defined(_M_ARM64) || defined(__aarch64__)
|
||||||
|
# define RADHOOK_ARCH_ARM64 1
|
||||||
|
#else
|
||||||
|
# error "radhook: unsupported architecture -- need x86-64 or aarch64"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// api macros
|
||||||
|
#if defined(_WIN32)
|
||||||
|
# if defined(RADHOOK_BUILD)
|
||||||
|
# define RADHOOK_API __declspec(dllexport)
|
||||||
|
# elif defined(RADHOOK_SHARED)
|
||||||
|
# define RADHOOK_API __declspec(dllimport)
|
||||||
|
# else
|
||||||
|
# define RADHOOK_API
|
||||||
|
# endif
|
||||||
|
#else
|
||||||
|
# if defined(__GNUC__) || defined(__clang__)
|
||||||
|
# define RADHOOK_API __attribute__((visibility("default")))
|
||||||
|
# else
|
||||||
|
# define RADHOOK_API
|
||||||
|
# endif
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#include "api.h"
|
||||||
|
|
||||||
|
#endif // __LIBRADHOOK_HPP__
|
||||||
751
src/radhook.cpp
Normal file
751
src/radhook.cpp
Normal file
@@ -0,0 +1,751 @@
|
|||||||
|
#include "radhook/radhook.h"
|
||||||
|
|
||||||
|
#include <cstring>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <cstdio>
|
||||||
|
#include <vector>
|
||||||
|
#include <memory>
|
||||||
|
#include <mutex>
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
|
#if defined(RADIUM_PLATFORM_WINDOWS)
|
||||||
|
# ifndef WIN32_LEAN_AND_MEAN
|
||||||
|
# define WIN32_LEAN_AND_MEAN
|
||||||
|
# endif
|
||||||
|
# include <windows.h>
|
||||||
|
#elif defined(RADIUM_PLATFORM_ANDROID)
|
||||||
|
# include <sys/mman.h>
|
||||||
|
# include <unistd.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#undef min
|
||||||
|
#undef max
|
||||||
|
|
||||||
|
// result strings
|
||||||
|
const char* RadHookResultToString(RadHookResult result) {
|
||||||
|
switch (result) {
|
||||||
|
case RadHookResult::Success: return "Success";
|
||||||
|
case RadHookResult::AlreadyInstalled: return "AlreadyInstalled";
|
||||||
|
case RadHookResult::NotInstalled: return "NotInstalled";
|
||||||
|
case RadHookResult::AlreadyEnabled: return "AlreadyEnabled";
|
||||||
|
case RadHookResult::AlreadyDisabled: return "AlreadyDisabled";
|
||||||
|
case RadHookResult::MemoryAllocFailed: return "MemoryAllocFailed";
|
||||||
|
case RadHookResult::MemoryProtectFailed: return "MemoryProtectFailed";
|
||||||
|
case RadHookResult::DisassemblyFailed: return "DisassemblyFailed";
|
||||||
|
case RadHookResult::InvalidTarget: return "InvalidTarget";
|
||||||
|
case RadHookResult::InvalidDetour: return "InvalidDetour";
|
||||||
|
case RadHookResult::InvalidHandle: return "InvalidHandle";
|
||||||
|
case RadHookResult::TrampolineTooFar: return "TrampolineTooFar";
|
||||||
|
default: return "Unknown";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// hook record
|
||||||
|
struct RadHookOpaque {
|
||||||
|
void* target = nullptr;
|
||||||
|
void* detour = nullptr;
|
||||||
|
void* trampoline = nullptr;
|
||||||
|
|
||||||
|
void* trampolineAlloc = nullptr;
|
||||||
|
size_t trampolineAllocSize = 0;
|
||||||
|
|
||||||
|
unsigned char originalBytes[32] = {};
|
||||||
|
size_t originalLength = 0;
|
||||||
|
|
||||||
|
unsigned char stubBytes[32] = {};
|
||||||
|
size_t stubLength = 0;
|
||||||
|
|
||||||
|
bool enabled = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
std::vector<std::unique_ptr<RadHookOpaque>> g_hooks;
|
||||||
|
std::mutex g_mutex;
|
||||||
|
|
||||||
|
bool IsRegisteredLocked(RadHookHandle handle) {
|
||||||
|
if (!handle) return false;
|
||||||
|
for (auto& h : g_hooks) {
|
||||||
|
if (h.get() == handle) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// platform memory backend
|
||||||
|
#if defined(RADIUM_PLATFORM_WINDOWS)
|
||||||
|
|
||||||
|
void* AllocExec(size_t size) {
|
||||||
|
return VirtualAlloc(nullptr, size, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
|
||||||
|
}
|
||||||
|
|
||||||
|
void FreeExec(void* p, size_t /*size*/) {
|
||||||
|
if (p) VirtualFree(p, 0, MEM_RELEASE);
|
||||||
|
}
|
||||||
|
|
||||||
|
void* AllocExecNear(void* target, size_t size) {
|
||||||
|
SYSTEM_INFO si;
|
||||||
|
GetSystemInfo(&si);
|
||||||
|
const uintptr_t pageSize = si.dwPageSize;
|
||||||
|
const uintptr_t targetAddr = reinterpret_cast<uintptr_t>(target);
|
||||||
|
const uintptr_t margin = 0x7FFF0000ULL; // stay safely under INT32_MAX
|
||||||
|
|
||||||
|
uintptr_t minAddr = reinterpret_cast<uintptr_t>(si.lpMinimumApplicationAddress);
|
||||||
|
uintptr_t maxAddr = reinterpret_cast<uintptr_t>(si.lpMaximumApplicationAddress);
|
||||||
|
if (targetAddr > margin && targetAddr - margin > minAddr) minAddr = targetAddr - margin;
|
||||||
|
if (targetAddr + margin < maxAddr) maxAddr = targetAddr + margin;
|
||||||
|
|
||||||
|
uintptr_t alignedTarget = targetAddr & ~(pageSize - 1);
|
||||||
|
|
||||||
|
// search backward from target
|
||||||
|
for (uintptr_t addr = alignedTarget; addr >= minAddr && addr != 0; ) {
|
||||||
|
MEMORY_BASIC_INFORMATION mbi;
|
||||||
|
if (VirtualQuery(reinterpret_cast<LPCVOID>(addr), &mbi, sizeof(mbi)) == 0) break;
|
||||||
|
uintptr_t regionBase = reinterpret_cast<uintptr_t>(mbi.BaseAddress);
|
||||||
|
if (mbi.State == MEM_FREE) {
|
||||||
|
uintptr_t allocBase = (regionBase + pageSize - 1) & ~(pageSize - 1);
|
||||||
|
if (allocBase >= minAddr && allocBase + size <= regionBase + mbi.RegionSize) {
|
||||||
|
void* p = VirtualAlloc(reinterpret_cast<LPVOID>(allocBase), size,
|
||||||
|
MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
|
||||||
|
if (p) return p;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (regionBase <= minAddr || regionBase < pageSize) break;
|
||||||
|
addr = regionBase - pageSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
// search forward from target
|
||||||
|
for (uintptr_t addr = alignedTarget; addr <= maxAddr; ) {
|
||||||
|
MEMORY_BASIC_INFORMATION mbi;
|
||||||
|
if (VirtualQuery(reinterpret_cast<LPCVOID>(addr), &mbi, sizeof(mbi)) == 0) break;
|
||||||
|
uintptr_t regionBase = reinterpret_cast<uintptr_t>(mbi.BaseAddress);
|
||||||
|
if (mbi.State == MEM_FREE) {
|
||||||
|
uintptr_t allocBase = (regionBase + pageSize - 1) & ~(pageSize - 1);
|
||||||
|
if (allocBase + size <= regionBase + mbi.RegionSize && allocBase + size <= maxAddr) {
|
||||||
|
void* p = VirtualAlloc(reinterpret_cast<LPVOID>(allocBase), size,
|
||||||
|
MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
|
||||||
|
if (p) return p;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (mbi.RegionSize == 0) break;
|
||||||
|
addr = regionBase + mbi.RegionSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool MakeWritableExecutable(void* addr, size_t size, unsigned long* oldProtect) {
|
||||||
|
DWORD prev = 0;
|
||||||
|
if (!VirtualProtect(addr, size, PAGE_EXECUTE_READWRITE, &prev)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (oldProtect) *oldProtect = prev;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void RestoreProtection(void* addr, size_t size, unsigned long oldProtect) {
|
||||||
|
DWORD tmp;
|
||||||
|
VirtualProtect(addr, size, static_cast<DWORD>(oldProtect), &tmp);
|
||||||
|
}
|
||||||
|
|
||||||
|
void FlushICache(void* addr, size_t size) {
|
||||||
|
FlushInstructionCache(GetCurrentProcess(), addr, size);
|
||||||
|
}
|
||||||
|
|
||||||
|
#elif defined(RADIUM_PLATFORM_ANDROID)
|
||||||
|
|
||||||
|
size_t PageSize() {
|
||||||
|
static size_t sz = static_cast<size_t>(sysconf(_SC_PAGESIZE));
|
||||||
|
return sz;
|
||||||
|
}
|
||||||
|
|
||||||
|
void* AllocExec(size_t size) {
|
||||||
|
size_t pageSize = PageSize();
|
||||||
|
size_t allocSize = (size + pageSize - 1) & ~(pageSize - 1);
|
||||||
|
void* p = mmap(nullptr, allocSize, PROT_READ | PROT_WRITE | PROT_EXEC,
|
||||||
|
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
|
||||||
|
return (p == MAP_FAILED) ? nullptr : p;
|
||||||
|
}
|
||||||
|
|
||||||
|
void FreeExec(void* p, size_t size) {
|
||||||
|
if (!p) return;
|
||||||
|
size_t pageSize = PageSize();
|
||||||
|
size_t allocSize = (size + pageSize - 1) & ~(pageSize - 1);
|
||||||
|
munmap(p, allocSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifndef MAP_FIXED_NOREPLACE
|
||||||
|
# define MAP_FIXED_NOREPLACE 0x100000 // linux >= 4.17 defined manually for older headers
|
||||||
|
#endif
|
||||||
|
|
||||||
|
void* AllocExecNear(void* target, size_t size) {
|
||||||
|
size_t pageSize = PageSize();
|
||||||
|
uintptr_t targetAddr = reinterpret_cast<uintptr_t>(target);
|
||||||
|
const uintptr_t margin = 0x7FFF0000ULL;
|
||||||
|
uintptr_t rangeLo = (targetAddr > margin) ? targetAddr - margin : pageSize;
|
||||||
|
uintptr_t rangeHi = targetAddr + margin;
|
||||||
|
|
||||||
|
FILE* f = fopen("/proc/self/maps", "r");
|
||||||
|
if (!f) return nullptr;
|
||||||
|
|
||||||
|
struct Region { uintptr_t start, end; };
|
||||||
|
std::vector<Region> regions;
|
||||||
|
char line[512];
|
||||||
|
while (fgets(line, sizeof(line), f)) {
|
||||||
|
unsigned long long s = 0, e = 0;
|
||||||
|
if (sscanf(line, "%llx-%llx", &s, &e) == 2) {
|
||||||
|
regions.push_back({ static_cast<uintptr_t>(s), static_cast<uintptr_t>(e) });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fclose(f);
|
||||||
|
std::sort(regions.begin(), regions.end(),
|
||||||
|
[](const Region& a, const Region& b) { return a.start < b.start; });
|
||||||
|
|
||||||
|
std::vector<Region> gaps;
|
||||||
|
uintptr_t prevEnd = rangeLo;
|
||||||
|
for (auto& r : regions) {
|
||||||
|
if (r.start >= rangeHi) break;
|
||||||
|
uintptr_t gapStart = prevEnd;
|
||||||
|
uintptr_t gapEnd = std::min(r.start, rangeHi);
|
||||||
|
if (gapEnd > gapStart) gaps.push_back({ gapStart, gapEnd });
|
||||||
|
if (r.end > prevEnd) prevEnd = r.end;
|
||||||
|
}
|
||||||
|
if (prevEnd < rangeHi) gaps.push_back({ prevEnd, rangeHi });
|
||||||
|
|
||||||
|
size_t pageAlignedSize = (size + pageSize - 1) & ~(pageSize - 1);
|
||||||
|
|
||||||
|
void* best = nullptr;
|
||||||
|
uintptr_t bestDist = static_cast<uintptr_t>(-1);
|
||||||
|
for (auto& g : gaps) {
|
||||||
|
uintptr_t alignedStart = (g.start + pageSize - 1) & ~(pageSize - 1);
|
||||||
|
uintptr_t alignedEnd = g.end & ~(pageSize - 1);
|
||||||
|
if (alignedEnd < alignedStart + pageAlignedSize) continue;
|
||||||
|
|
||||||
|
uintptr_t lastValidStart = alignedEnd - pageAlignedSize; // already page-aligned
|
||||||
|
uintptr_t candidate;
|
||||||
|
if (targetAddr < alignedStart) candidate = alignedStart;
|
||||||
|
else if (targetAddr > lastValidStart) candidate = lastValidStart;
|
||||||
|
else candidate = targetAddr & ~(pageSize - 1);
|
||||||
|
|
||||||
|
// clamp
|
||||||
|
if (candidate > lastValidStart) candidate = lastValidStart;
|
||||||
|
if (candidate < alignedStart) candidate = alignedStart;
|
||||||
|
|
||||||
|
uintptr_t dist = (candidate > targetAddr) ? (candidate - targetAddr) : (targetAddr - candidate);
|
||||||
|
if (dist < bestDist) {
|
||||||
|
bestDist = dist;
|
||||||
|
best = reinterpret_cast<void*>(candidate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!best) return nullptr;
|
||||||
|
|
||||||
|
void* p = mmap(best, pageAlignedSize, PROT_READ | PROT_WRITE | PROT_EXEC,
|
||||||
|
MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED_NOREPLACE, -1, 0);
|
||||||
|
if (p == MAP_FAILED) return nullptr;
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool MakeWritableExecutable(void* addr, size_t size, unsigned long* oldProtect) {
|
||||||
|
if (oldProtect) *oldProtect = 0;
|
||||||
|
size_t pageSize = PageSize();
|
||||||
|
uintptr_t start = reinterpret_cast<uintptr_t>(addr) & ~(pageSize - 1);
|
||||||
|
uintptr_t end = (reinterpret_cast<uintptr_t>(addr) + size + pageSize - 1) & ~(pageSize - 1);
|
||||||
|
return mprotect(reinterpret_cast<void*>(start), end - start,
|
||||||
|
PROT_READ | PROT_WRITE | PROT_EXEC) == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void RestoreProtection(void* /*addr*/, size_t /*size*/, unsigned long /*oldProtect*/) {
|
||||||
|
// no-op
|
||||||
|
}
|
||||||
|
|
||||||
|
void FlushICache(void* addr, size_t size) {
|
||||||
|
__builtin___clear_cache(reinterpret_cast<char*>(addr),
|
||||||
|
reinterpret_cast<char*>(addr) + size);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// x86-64 disassembler
|
||||||
|
#if defined(RADHOOK_ARCH_X64)
|
||||||
|
|
||||||
|
struct X64Insn {
|
||||||
|
size_t length = 0;
|
||||||
|
bool ripRelative = false;
|
||||||
|
size_t dispOffset = 0;
|
||||||
|
bool relativeControlFlow = false;
|
||||||
|
bool valid = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
bool IsLegacyPrefix(uint8_t b) {
|
||||||
|
switch (b) {
|
||||||
|
case 0x66: case 0x67: case 0xF0: case 0xF2: case 0xF3:
|
||||||
|
case 0x2E: case 0x36: case 0x3E: case 0x26: case 0x64: case 0x65:
|
||||||
|
return true;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
X64Insn DecodeX64(const uint8_t* p, size_t avail) {
|
||||||
|
X64Insn insn;
|
||||||
|
size_t i = 0;
|
||||||
|
bool opSize16 = false;
|
||||||
|
bool rexW = false;
|
||||||
|
|
||||||
|
while (i < avail && IsLegacyPrefix(p[i])) {
|
||||||
|
if (p[i] == 0x66) opSize16 = true;
|
||||||
|
++i;
|
||||||
|
}
|
||||||
|
if (i < avail && (p[i] & 0xF0) == 0x40) {
|
||||||
|
rexW = (p[i] & 0x08) != 0;
|
||||||
|
++i;
|
||||||
|
}
|
||||||
|
if (i >= avail) return insn;
|
||||||
|
|
||||||
|
uint8_t opcode = p[i++];
|
||||||
|
bool twoByte = false;
|
||||||
|
if (opcode == 0x0F) {
|
||||||
|
if (i >= avail) return insn;
|
||||||
|
twoByte = true;
|
||||||
|
opcode = p[i++];
|
||||||
|
}
|
||||||
|
|
||||||
|
bool hasModRM = false;
|
||||||
|
int immSize = 0;
|
||||||
|
bool relBranch = false;
|
||||||
|
|
||||||
|
if (!twoByte) {
|
||||||
|
uint8_t lo = opcode & 0x0F;
|
||||||
|
|
||||||
|
if (opcode <= 0x3D && (opcode & 0xC0) == 0x00 && lo <= 0x05 &&
|
||||||
|
(opcode & 0x07) <= 0x05) {
|
||||||
|
if (lo == 0x04) { immSize = 1; }
|
||||||
|
else if (lo == 0x05) { immSize = opSize16 ? 2 : 4; }
|
||||||
|
else if (lo <= 0x03) { hasModRM = true; }
|
||||||
|
else { return insn; }
|
||||||
|
} else if (opcode >= 0x50 && opcode <= 0x5F) { /* push/pop r64, no operand bytes */ }
|
||||||
|
else if (opcode == 0x68) { immSize = opSize16 ? 2 : 4; }
|
||||||
|
else if (opcode == 0x6A) { immSize = 1; }
|
||||||
|
else if (opcode == 0x69) { hasModRM = true; immSize = opSize16 ? 2 : 4; }
|
||||||
|
else if (opcode == 0x6B) { hasModRM = true; immSize = 1; }
|
||||||
|
else if (opcode >= 0x70 && opcode <= 0x7F) { immSize = 1; relBranch = true; }
|
||||||
|
else if (opcode == 0x80) { hasModRM = true; immSize = 1; }
|
||||||
|
else if (opcode == 0x81) { hasModRM = true; immSize = opSize16 ? 2 : 4; }
|
||||||
|
else if (opcode == 0x83) { hasModRM = true; immSize = 1; }
|
||||||
|
else if (opcode >= 0x84 && opcode <= 0x8F) { hasModRM = true; }
|
||||||
|
else if (opcode >= 0x90 && opcode <= 0x97) { /* xchg/nop, no operand bytes */ }
|
||||||
|
else if (opcode == 0x98 || opcode == 0x99) { /* cbw/cwd family */ }
|
||||||
|
else if (opcode == 0xA8) { immSize = 1; }
|
||||||
|
else if (opcode == 0xA9) { immSize = opSize16 ? 2 : 4; }
|
||||||
|
else if (opcode >= 0xB0 && opcode <= 0xB7) { immSize = 1; }
|
||||||
|
else if (opcode >= 0xB8 && opcode <= 0xBF) { immSize = rexW ? 8 : (opSize16 ? 2 : 4); }
|
||||||
|
else if (opcode == 0xC0 || opcode == 0xC1) { hasModRM = true; immSize = 1; }
|
||||||
|
else if (opcode == 0xC2) { immSize = 2; }
|
||||||
|
else if (opcode == 0xC3) { /* ret */ }
|
||||||
|
else if (opcode == 0xC6) { hasModRM = true; immSize = 1; }
|
||||||
|
else if (opcode == 0xC7) { hasModRM = true; immSize = opSize16 ? 2 : 4; }
|
||||||
|
else if (opcode == 0xC9) { /* leave */ }
|
||||||
|
else if (opcode == 0xCC) { /* int3 */ }
|
||||||
|
else if (opcode == 0xCD) { immSize = 1; }
|
||||||
|
else if (opcode == 0xD0 || opcode == 0xD1 || opcode == 0xD2 || opcode == 0xD3) { hasModRM = true; }
|
||||||
|
else if (opcode == 0xE8) { immSize = 4; relBranch = true; }
|
||||||
|
else if (opcode == 0xE9) { immSize = 4; relBranch = true; }
|
||||||
|
else if (opcode == 0xEB) { immSize = 1; relBranch = true; }
|
||||||
|
else if (opcode == 0xF6) { hasModRM = true; immSize = 1; }
|
||||||
|
else if (opcode == 0xF7) { hasModRM = true; immSize = opSize16 ? 2 : 4; }
|
||||||
|
else if (opcode == 0xFE || opcode == 0xFF) { hasModRM = true; }
|
||||||
|
else {
|
||||||
|
return insn; // unrecognized opcode
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (opcode >= 0x80 && opcode <= 0x8F) { immSize = 4; relBranch = true; }
|
||||||
|
else if (opcode == 0x1E) { immSize = 1; }
|
||||||
|
else if (opcode == 0x1F) { hasModRM = true; }
|
||||||
|
else if (opcode == 0x05 || opcode == 0x31 || opcode == 0x34 || opcode == 0x35 || opcode == 0xA2) { /* syscall/rdtsc/cpuid etc */ }
|
||||||
|
else if (opcode >= 0x40 && opcode <= 0x4F) { hasModRM = true; } // CMOVcc
|
||||||
|
else if (opcode == 0xAF) { hasModRM = true; } // imul
|
||||||
|
else if (opcode == 0xB6 || opcode == 0xB7 || opcode == 0xBE || opcode == 0xBF) { hasModRM = true; } // movzx/movsx
|
||||||
|
else if (opcode >= 0x10 && opcode <= 0x17) { hasModRM = true; } // movups/movaps family
|
||||||
|
else if (opcode >= 0x28 && opcode <= 0x2F) { hasModRM = true; }
|
||||||
|
else if (opcode >= 0x54 && opcode <= 0x5F) { hasModRM = true; }
|
||||||
|
else if (opcode == 0x6E || opcode == 0x6F || opcode == 0x7E || opcode == 0x7F || opcode == 0xD6) { hasModRM = true; }
|
||||||
|
else {
|
||||||
|
return insn;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasModRM) {
|
||||||
|
if (i >= avail) return insn;
|
||||||
|
uint8_t modrm = p[i++];
|
||||||
|
uint8_t mod = (modrm >> 6) & 0x3;
|
||||||
|
uint8_t rm = modrm & 0x7;
|
||||||
|
|
||||||
|
if (mod != 3 && rm == 4) {
|
||||||
|
if (i >= avail) return insn;
|
||||||
|
uint8_t sib = p[i++];
|
||||||
|
uint8_t base = sib & 0x7;
|
||||||
|
if (mod == 0 && base == 5) {
|
||||||
|
i += 4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mod == 0 && rm == 5) {
|
||||||
|
insn.ripRelative = true;
|
||||||
|
insn.dispOffset = i;
|
||||||
|
i += 4;
|
||||||
|
} else if (mod == 1) {
|
||||||
|
i += 1;
|
||||||
|
} else if (mod == 2) {
|
||||||
|
i += 4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
i += static_cast<size_t>(immSize);
|
||||||
|
if (i > avail) return insn;
|
||||||
|
|
||||||
|
insn.length = i;
|
||||||
|
insn.relativeControlFlow = relBranch;
|
||||||
|
insn.valid = true;
|
||||||
|
return insn;
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr size_t kX64StubLen = 14; // FF25 00000000 + imm64
|
||||||
|
constexpr size_t kX64MaxStolen = 32;
|
||||||
|
constexpr size_t kX64MaxRipFixups = 8;
|
||||||
|
|
||||||
|
bool BuildStolenBytesX64(const uint8_t* target, size_t minLen,
|
||||||
|
uint8_t* outBytes, size_t& outLen,
|
||||||
|
size_t* outRipOffsets, size_t& outRipCount) {
|
||||||
|
size_t len = 0;
|
||||||
|
outRipCount = 0;
|
||||||
|
while (len < minLen) {
|
||||||
|
if (len >= kX64MaxStolen) return false;
|
||||||
|
X64Insn insn = DecodeX64(target + len, kX64MaxStolen - len);
|
||||||
|
if (!insn.valid) return false;
|
||||||
|
if (insn.relativeControlFlow) return false;
|
||||||
|
if (insn.ripRelative) {
|
||||||
|
if (outRipCount >= kX64MaxRipFixups) return false;
|
||||||
|
outRipOffsets[outRipCount++] = len + insn.dispOffset;
|
||||||
|
}
|
||||||
|
len += insn.length;
|
||||||
|
}
|
||||||
|
std::memcpy(outBytes, target, len);
|
||||||
|
outLen = len;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif // RADHOOK_ARCH_X64
|
||||||
|
|
||||||
|
// aarch64 prologue check
|
||||||
|
#if defined(RADHOOK_ARCH_ARM64)
|
||||||
|
|
||||||
|
constexpr size_t kArm64StubLen = 16;
|
||||||
|
|
||||||
|
bool IsArm64PcRelative(uint32_t instr) {
|
||||||
|
if ((instr & 0x9F000000) == 0x10000000) return true; // ADR
|
||||||
|
if ((instr & 0x9F000000) == 0x90000000) return true; // ADRP
|
||||||
|
if ((instr & 0xFC000000) == 0x14000000) return true; // B
|
||||||
|
if ((instr & 0xFC000000) == 0x94000000) return true; // BL
|
||||||
|
if ((instr & 0xFF000010) == 0x54000000) return true; // B.cond
|
||||||
|
if ((instr & 0x7E000000) == 0x34000000) return true; // CBZ/CBNZ
|
||||||
|
if ((instr & 0x7E000000) == 0x36000000) return true; // TBZ/TBNZ
|
||||||
|
if ((instr & 0xBF000000) == 0x18000000) return true; // LDR
|
||||||
|
if ((instr & 0x3B000000) == 0x18000000) return true; // LDR/LDRSW
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void WriteArm64Stub(void* at, uintptr_t targetAddr) {
|
||||||
|
uint32_t* words = reinterpret_cast<uint32_t*>(at);
|
||||||
|
words[0] = 0x58000051; // LDR X17, #8
|
||||||
|
words[1] = 0xD61F0220; // BR X17
|
||||||
|
std::memcpy(&words[2], &targetAddr, 8);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif // RADHOOK_ARCH_ARM64
|
||||||
|
|
||||||
|
} // anonymous namespace
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
RadHookResult InstallHook(RadHookOpaque* h, void* target, void* detour) {
|
||||||
|
if (!target) return RadHookResult::InvalidTarget;
|
||||||
|
if (!detour) return RadHookResult::InvalidDetour;
|
||||||
|
|
||||||
|
#if defined(RADHOOK_ARCH_X64)
|
||||||
|
uint8_t stolen[kX64MaxStolen];
|
||||||
|
size_t stolenLen = 0;
|
||||||
|
size_t ripOffsets[kX64MaxRipFixups];
|
||||||
|
size_t ripCount = 0;
|
||||||
|
|
||||||
|
if (!BuildStolenBytesX64(reinterpret_cast<const uint8_t*>(target), kX64StubLen,
|
||||||
|
stolen, stolenLen, ripOffsets, ripCount)) {
|
||||||
|
return RadHookResult::DisassemblyFailed;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t allocSize = stolenLen + kX64StubLen;
|
||||||
|
void* trampolineAlloc = AllocExecNear(target, allocSize);
|
||||||
|
if (!trampolineAlloc) trampolineAlloc = AllocExec(allocSize); // best-effort fallback
|
||||||
|
if (!trampolineAlloc) return RadHookResult::MemoryAllocFailed;
|
||||||
|
|
||||||
|
std::memcpy(trampolineAlloc, stolen, stolenLen);
|
||||||
|
|
||||||
|
for (size_t k = 0; k < ripCount; ++k) {
|
||||||
|
size_t off = ripOffsets[k];
|
||||||
|
int32_t originalDisp = 0;
|
||||||
|
std::memcpy(&originalDisp, reinterpret_cast<const uint8_t*>(target) + off, 4);
|
||||||
|
uintptr_t absTarget = reinterpret_cast<uintptr_t>(target) + off + 4 + static_cast<intptr_t>(originalDisp);
|
||||||
|
intptr_t newDisp = static_cast<intptr_t>(absTarget) -
|
||||||
|
(reinterpret_cast<intptr_t>(trampolineAlloc) + off + 4);
|
||||||
|
if (newDisp < INT32_MIN || newDisp > INT32_MAX) {
|
||||||
|
FreeExec(trampolineAlloc, allocSize);
|
||||||
|
return RadHookResult::TrampolineTooFar;
|
||||||
|
}
|
||||||
|
int32_t newDisp32 = static_cast<int32_t>(newDisp);
|
||||||
|
std::memcpy(reinterpret_cast<uint8_t*>(trampolineAlloc) + off, &newDisp32, 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t* jumpBack = reinterpret_cast<uint8_t*>(trampolineAlloc) + stolenLen;
|
||||||
|
uintptr_t backAddr = reinterpret_cast<uintptr_t>(target) + stolenLen;
|
||||||
|
jumpBack[0] = 0xFF; jumpBack[1] = 0x25;
|
||||||
|
jumpBack[2] = 0; jumpBack[3] = 0; jumpBack[4] = 0; jumpBack[5] = 0;
|
||||||
|
std::memcpy(jumpBack + 6, &backAddr, 8);
|
||||||
|
|
||||||
|
FlushICache(trampolineAlloc, allocSize);
|
||||||
|
|
||||||
|
unsigned long oldProtect = 0;
|
||||||
|
if (!MakeWritableExecutable(target, stolenLen, &oldProtect)) {
|
||||||
|
FreeExec(trampolineAlloc, allocSize);
|
||||||
|
return RadHookResult::MemoryProtectFailed;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::memcpy(h->originalBytes, target, stolenLen);
|
||||||
|
h->originalLength = stolenLen;
|
||||||
|
|
||||||
|
uint8_t hookStub[kX64StubLen];
|
||||||
|
hookStub[0] = 0xFF; hookStub[1] = 0x25;
|
||||||
|
hookStub[2] = 0; hookStub[3] = 0; hookStub[4] = 0; hookStub[5] = 0;
|
||||||
|
uintptr_t detourAddr = reinterpret_cast<uintptr_t>(detour);
|
||||||
|
std::memcpy(hookStub + 6, &detourAddr, 8);
|
||||||
|
std::memcpy(h->stubBytes, hookStub, kX64StubLen);
|
||||||
|
h->stubLength = kX64StubLen;
|
||||||
|
|
||||||
|
std::memcpy(target, hookStub, kX64StubLen);
|
||||||
|
RestoreProtection(target, stolenLen, oldProtect);
|
||||||
|
FlushICache(target, stolenLen);
|
||||||
|
|
||||||
|
h->target = target;
|
||||||
|
h->detour = detour;
|
||||||
|
h->trampoline = trampolineAlloc;
|
||||||
|
h->trampolineAlloc = trampolineAlloc;
|
||||||
|
h->trampolineAllocSize = allocSize;
|
||||||
|
h->enabled = true;
|
||||||
|
return RadHookResult::Success;
|
||||||
|
|
||||||
|
#elif defined(RADHOOK_ARCH_ARM64)
|
||||||
|
const uint32_t* src = reinterpret_cast<const uint32_t*>(target);
|
||||||
|
for (size_t k = 0; k < kArm64StubLen / 4; ++k) {
|
||||||
|
if (IsArm64PcRelative(src[k])) {
|
||||||
|
return RadHookResult::DisassemblyFailed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t allocSize = kArm64StubLen + kArm64StubLen;
|
||||||
|
void* trampolineAlloc = AllocExec(allocSize);
|
||||||
|
if (!trampolineAlloc) return RadHookResult::MemoryAllocFailed;
|
||||||
|
|
||||||
|
std::memcpy(trampolineAlloc, target, kArm64StubLen);
|
||||||
|
uintptr_t backAddr = reinterpret_cast<uintptr_t>(target) + kArm64StubLen;
|
||||||
|
WriteArm64Stub(reinterpret_cast<uint8_t*>(trampolineAlloc) + kArm64StubLen, backAddr);
|
||||||
|
|
||||||
|
FlushICache(trampolineAlloc, allocSize);
|
||||||
|
|
||||||
|
unsigned long oldProtect = 0;
|
||||||
|
if (!MakeWritableExecutable(target, kArm64StubLen, &oldProtect)) {
|
||||||
|
FreeExec(trampolineAlloc, allocSize);
|
||||||
|
return RadHookResult::MemoryProtectFailed;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::memcpy(h->originalBytes, target, kArm64StubLen);
|
||||||
|
h->originalLength = kArm64StubLen;
|
||||||
|
|
||||||
|
uint8_t hookStub[kArm64StubLen];
|
||||||
|
uintptr_t detourAddr = reinterpret_cast<uintptr_t>(detour);
|
||||||
|
WriteArm64Stub(hookStub, detourAddr);
|
||||||
|
std::memcpy(h->stubBytes, hookStub, kArm64StubLen);
|
||||||
|
h->stubLength = kArm64StubLen;
|
||||||
|
|
||||||
|
std::memcpy(target, hookStub, kArm64StubLen);
|
||||||
|
RestoreProtection(target, kArm64StubLen, oldProtect);
|
||||||
|
FlushICache(target, kArm64StubLen);
|
||||||
|
|
||||||
|
h->target = target;
|
||||||
|
h->detour = detour;
|
||||||
|
h->trampoline = trampolineAlloc;
|
||||||
|
h->trampolineAlloc = trampolineAlloc;
|
||||||
|
h->trampolineAllocSize = allocSize;
|
||||||
|
h->enabled = true;
|
||||||
|
return RadHookResult::Success;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
RadHookResult EnableHookInternal(RadHookOpaque* h) {
|
||||||
|
if (!h->target) return RadHookResult::NotInstalled;
|
||||||
|
if (h->enabled) return RadHookResult::AlreadyEnabled;
|
||||||
|
|
||||||
|
unsigned long oldProtect = 0;
|
||||||
|
if (!MakeWritableExecutable(h->target, h->originalLength, &oldProtect)) {
|
||||||
|
return RadHookResult::MemoryProtectFailed;
|
||||||
|
}
|
||||||
|
std::memcpy(h->target, h->stubBytes, h->stubLength);
|
||||||
|
RestoreProtection(h->target, h->originalLength, oldProtect);
|
||||||
|
FlushICache(h->target, h->originalLength);
|
||||||
|
|
||||||
|
h->enabled = true;
|
||||||
|
return RadHookResult::Success;
|
||||||
|
}
|
||||||
|
|
||||||
|
RadHookResult DisableHookInternal(RadHookOpaque* h) {
|
||||||
|
if (!h->target) return RadHookResult::NotInstalled;
|
||||||
|
if (!h->enabled) return RadHookResult::AlreadyDisabled;
|
||||||
|
|
||||||
|
unsigned long oldProtect = 0;
|
||||||
|
if (!MakeWritableExecutable(h->target, h->originalLength, &oldProtect)) {
|
||||||
|
return RadHookResult::MemoryProtectFailed;
|
||||||
|
}
|
||||||
|
std::memcpy(h->target, h->originalBytes, h->originalLength);
|
||||||
|
RestoreProtection(h->target, h->originalLength, oldProtect);
|
||||||
|
FlushICache(h->target, h->originalLength);
|
||||||
|
|
||||||
|
h->enabled = false;
|
||||||
|
return RadHookResult::Success;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // anonymous namespace
|
||||||
|
|
||||||
|
RadHookResult RadHookCreate(void* target, void* detour, RadHookHandle* outHandle) {
|
||||||
|
if (outHandle) *outHandle = nullptr;
|
||||||
|
if (!target) return RadHookResult::InvalidTarget;
|
||||||
|
if (!detour) return RadHookResult::InvalidDetour;
|
||||||
|
|
||||||
|
auto record = std::make_unique<RadHookOpaque>();
|
||||||
|
RadHookResult result = InstallHook(record.get(), target, detour);
|
||||||
|
if (result != RadHookResult::Success) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::lock_guard<std::mutex> lock(g_mutex);
|
||||||
|
RadHookOpaque* raw = record.get();
|
||||||
|
g_hooks.push_back(std::move(record));
|
||||||
|
if (outHandle) *outHandle = raw;
|
||||||
|
return RadHookResult::Success;
|
||||||
|
}
|
||||||
|
|
||||||
|
RadHookResult RadHookEnable(RadHookHandle handle) {
|
||||||
|
std::lock_guard<std::mutex> lock(g_mutex);
|
||||||
|
if (!IsRegisteredLocked(handle)) return RadHookResult::InvalidHandle;
|
||||||
|
return EnableHookInternal(handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
RadHookResult RadHookDisable(RadHookHandle handle) {
|
||||||
|
std::lock_guard<std::mutex> lock(g_mutex);
|
||||||
|
if (!IsRegisteredLocked(handle)) return RadHookResult::InvalidHandle;
|
||||||
|
return DisableHookInternal(handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
RadHookResult RadHookDestroy(RadHookHandle handle) {
|
||||||
|
std::lock_guard<std::mutex> lock(g_mutex);
|
||||||
|
if (!IsRegisteredLocked(handle)) return RadHookResult::InvalidHandle;
|
||||||
|
|
||||||
|
RadHookResult disableResult = RadHookResult::Success;
|
||||||
|
if (handle->enabled) {
|
||||||
|
disableResult = DisableHookInternal(handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (handle->trampolineAlloc) {
|
||||||
|
FreeExec(handle->trampolineAlloc, handle->trampolineAllocSize);
|
||||||
|
handle->trampolineAlloc = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
g_hooks.erase(
|
||||||
|
std::remove_if(g_hooks.begin(), g_hooks.end(),
|
||||||
|
[handle](const std::unique_ptr<RadHookOpaque>& h) { return h.get() == handle; }),
|
||||||
|
g_hooks.end());
|
||||||
|
|
||||||
|
return disableResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
RadHookResult RadHookEnableAll() {
|
||||||
|
std::lock_guard<std::mutex> lock(g_mutex);
|
||||||
|
RadHookResult last = RadHookResult::Success;
|
||||||
|
for (auto& h : g_hooks) {
|
||||||
|
RadHookResult r = EnableHookInternal(h.get());
|
||||||
|
if (r != RadHookResult::Success && r != RadHookResult::AlreadyEnabled) {
|
||||||
|
last = r;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return last;
|
||||||
|
}
|
||||||
|
|
||||||
|
RadHookResult RadHookDisableAll() {
|
||||||
|
std::lock_guard<std::mutex> lock(g_mutex);
|
||||||
|
RadHookResult last = RadHookResult::Success;
|
||||||
|
for (auto& h : g_hooks) {
|
||||||
|
RadHookResult r = DisableHookInternal(h.get());
|
||||||
|
if (r != RadHookResult::Success && r != RadHookResult::AlreadyDisabled) {
|
||||||
|
last = r;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return last;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t RadHookGetCount() {
|
||||||
|
std::lock_guard<std::mutex> lock(g_mutex);
|
||||||
|
return g_hooks.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t RadHookGetHandles(RadHookHandle* outHandles, size_t maxCount) {
|
||||||
|
std::lock_guard<std::mutex> lock(g_mutex);
|
||||||
|
size_t n = std::min(maxCount, g_hooks.size());
|
||||||
|
for (size_t i = 0; i < n; ++i) {
|
||||||
|
outHandles[i] = g_hooks[i].get();
|
||||||
|
}
|
||||||
|
return g_hooks.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool RadHookIsValid(RadHookHandle handle) {
|
||||||
|
std::lock_guard<std::mutex> lock(g_mutex);
|
||||||
|
return IsRegisteredLocked(handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool RadHookIsEnabled(RadHookHandle handle) {
|
||||||
|
std::lock_guard<std::mutex> lock(g_mutex);
|
||||||
|
if (!IsRegisteredLocked(handle)) return false;
|
||||||
|
return handle->enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
void* RadHookGetTarget(RadHookHandle handle) {
|
||||||
|
std::lock_guard<std::mutex> lock(g_mutex);
|
||||||
|
if (!IsRegisteredLocked(handle)) return nullptr;
|
||||||
|
return handle->target;
|
||||||
|
}
|
||||||
|
|
||||||
|
void* RadHookGetDetour(RadHookHandle handle) {
|
||||||
|
std::lock_guard<std::mutex> lock(g_mutex);
|
||||||
|
if (!IsRegisteredLocked(handle)) return nullptr;
|
||||||
|
return handle->detour;
|
||||||
|
}
|
||||||
|
|
||||||
|
void* RadHookGetOriginal(RadHookHandle handle) {
|
||||||
|
std::lock_guard<std::mutex> lock(g_mutex);
|
||||||
|
if (!IsRegisteredLocked(handle)) return nullptr;
|
||||||
|
return handle->trampoline;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t RadHookEnumerate(RadHookHandle* out, size_t maxCount) {
|
||||||
|
size_t written = 0;
|
||||||
|
for (auto const& h : g_hooks) {
|
||||||
|
if (written >= maxCount) break;
|
||||||
|
out[written++] = h.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
return written;
|
||||||
|
}
|
||||||
16
tests/CMakeLists.txt
Normal file
16
tests/CMakeLists.txt
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
file(GLOB TEST_FILES CONFIGURE_DEPENDS "*.cpp")
|
||||||
|
|
||||||
|
foreach(TEST_FILE ${TEST_FILES})
|
||||||
|
get_filename_component(TEST_NAME ${TEST_FILE} NAME_WE)
|
||||||
|
|
||||||
|
add_executable(${TEST_NAME} ${TEST_FILE})
|
||||||
|
target_link_libraries(${TEST_NAME} PRIVATE radhook)
|
||||||
|
|
||||||
|
add_test(NAME ${TEST_NAME} COMMAND ${TEST_NAME})
|
||||||
|
|
||||||
|
add_custom_command(TARGET ${TEST_NAME} POST_BUILD
|
||||||
|
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||||
|
$<TARGET_FILE:radhook>
|
||||||
|
$<TARGET_FILE_DIR:${TEST_NAME}>
|
||||||
|
)
|
||||||
|
endforeach()
|
||||||
18
tests/test_basic.cpp
Normal file
18
tests/test_basic.cpp
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
#include <cassert>
|
||||||
|
#include "radhook/radhook.h"
|
||||||
|
|
||||||
|
static int fn() { return 42; }
|
||||||
|
static int detour() { return 1337; }
|
||||||
|
|
||||||
|
using Fn = int(*)();
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
RadHookHandle h = nullptr;
|
||||||
|
|
||||||
|
auto r = RadHookCreate((void*)fn, (void*)detour, &h);
|
||||||
|
assert(r == RadHookResult::Success);
|
||||||
|
assert(RadHookIsValid(h));
|
||||||
|
|
||||||
|
RadHookDestroy(h);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
24
tests/test_enable_disable.cpp
Normal file
24
tests/test_enable_disable.cpp
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
#include <cassert>
|
||||||
|
#include "radhook/radhook.h"
|
||||||
|
|
||||||
|
static int fn() { return 42; }
|
||||||
|
static int detour() { return 1337; }
|
||||||
|
|
||||||
|
using Fn = int(*)();
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
RadHookHandle h = nullptr;
|
||||||
|
|
||||||
|
RadHookCreate((void*)fn, (void*)detour, &h);
|
||||||
|
|
||||||
|
assert(RadHookIsEnabled(h));
|
||||||
|
|
||||||
|
RadHookDisable(h);
|
||||||
|
assert(!RadHookIsEnabled(h));
|
||||||
|
|
||||||
|
RadHookEnable(h);
|
||||||
|
assert(RadHookIsEnabled(h));
|
||||||
|
|
||||||
|
RadHookDestroy(h);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
30
tests/test_execution_flow.cpp
Normal file
30
tests/test_execution_flow.cpp
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
#include <cassert>
|
||||||
|
#include "radhook/radhook.h"
|
||||||
|
|
||||||
|
static int fn() { return 42; }
|
||||||
|
static int detour() { return 1337; }
|
||||||
|
|
||||||
|
using Fn = int(*)();
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
RadHookHandle h = nullptr;
|
||||||
|
|
||||||
|
RadHookCreate((void*)fn, (void*)detour, &h);
|
||||||
|
|
||||||
|
// must trigger detour
|
||||||
|
assert(fn() == 1337);
|
||||||
|
|
||||||
|
// disable restores original
|
||||||
|
RadHookDisable(h);
|
||||||
|
assert(fn() == 42);
|
||||||
|
|
||||||
|
// re-enable restores hook
|
||||||
|
RadHookEnable(h);
|
||||||
|
assert(fn() == 1337);
|
||||||
|
|
||||||
|
// trampoline correctness
|
||||||
|
auto orig = RadHookGetOriginalAs<Fn>(h);
|
||||||
|
assert(orig() == 42);
|
||||||
|
|
||||||
|
RadHookDestroy(h);
|
||||||
|
}
|
||||||
19
tests/test_failure_cases.cpp
Normal file
19
tests/test_failure_cases.cpp
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
#include <cassert>
|
||||||
|
#include "radhook/radhook.h"
|
||||||
|
|
||||||
|
static int fn() { return 42; }
|
||||||
|
static int detour() { return 1337; }
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
RadHookHandle h = nullptr;
|
||||||
|
|
||||||
|
// invalid detour
|
||||||
|
auto r1 = RadHookCreate((void*)fn, nullptr, &h);
|
||||||
|
assert(r1 != RadHookResult::Success);
|
||||||
|
|
||||||
|
// invalid target
|
||||||
|
auto r2 = RadHookCreate(nullptr, (void*)detour, &h);
|
||||||
|
assert(r2 != RadHookResult::Success);
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
19
tests/test_invalid_inputs.cpp
Normal file
19
tests/test_invalid_inputs.cpp
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
#include <cassert>
|
||||||
|
#include "radhook/radhook.h"
|
||||||
|
|
||||||
|
static int fn() { return 42; }
|
||||||
|
static int detour() { return 1337; }
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
RadHookHandle h = nullptr;
|
||||||
|
|
||||||
|
// invalid detour
|
||||||
|
assert(RadHookCreate((void*)fn, nullptr, &h) != RadHookResult::Success);
|
||||||
|
|
||||||
|
// invalid target
|
||||||
|
assert(RadHookCreate(nullptr, (void*)detour, &h) != RadHookResult::Success);
|
||||||
|
|
||||||
|
// invalid handle operations
|
||||||
|
assert(RadHookEnable(nullptr) == RadHookResult::InvalidHandle || RadHookEnable(nullptr) != RadHookResult::Success);
|
||||||
|
assert(RadHookDisable(nullptr) == RadHookResult::InvalidHandle || RadHookDisable(nullptr) != RadHookResult::Success);
|
||||||
|
}
|
||||||
23
tests/test_lifecycle.cpp
Normal file
23
tests/test_lifecycle.cpp
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
#include <cassert>
|
||||||
|
#include "radhook/radhook.h"
|
||||||
|
|
||||||
|
static int fn() { return 42; }
|
||||||
|
static int detour() { return 1337; }
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
RadHookHandle h = nullptr;
|
||||||
|
|
||||||
|
// destroy before create safety
|
||||||
|
assert(RadHookDestroy(nullptr) != RadHookResult::Success);
|
||||||
|
|
||||||
|
RadHookCreate((void*)fn, (void*)detour, &h);
|
||||||
|
|
||||||
|
RadHookDestroy(h);
|
||||||
|
|
||||||
|
// use after destroy safety
|
||||||
|
assert(RadHookEnable(h) != RadHookResult::Success);
|
||||||
|
assert(RadHookDisable(h) != RadHookResult::Success);
|
||||||
|
assert(RadHookIsValid(h) == false || true); // depends on implementation
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
21
tests/test_memory_integrity.cpp
Normal file
21
tests/test_memory_integrity.cpp
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
#include <cassert>
|
||||||
|
#include "radhook/radhook.h"
|
||||||
|
|
||||||
|
static int fn() { return 42; }
|
||||||
|
static int detour() { return 1337; }
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
RadHookHandle h = nullptr;
|
||||||
|
|
||||||
|
RadHookCreate((void*)fn, (void*)detour, &h);
|
||||||
|
|
||||||
|
for (int i = 0; i < 10000; i++) {
|
||||||
|
RadHookEnable(h);
|
||||||
|
assert(fn() == 1337);
|
||||||
|
|
||||||
|
RadHookDisable(h);
|
||||||
|
assert(fn() == 42);
|
||||||
|
}
|
||||||
|
|
||||||
|
RadHookDestroy(h);
|
||||||
|
}
|
||||||
56
tests/test_stability.cpp
Normal file
56
tests/test_stability.cpp
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
#include <cassert>
|
||||||
|
#include <vector>
|
||||||
|
#include "radhook/radhook.h"
|
||||||
|
|
||||||
|
static int fn() { return 42; }
|
||||||
|
static int detour() { return 1337; }
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
RadHookHandle handles[200];
|
||||||
|
size_t count = 0;
|
||||||
|
|
||||||
|
// create many hooks
|
||||||
|
for (int i = 0; i < 200; i++) {
|
||||||
|
RadHookHandle h = nullptr;
|
||||||
|
auto r = RadHookCreate(
|
||||||
|
(void*)fn,
|
||||||
|
(void*)detour,
|
||||||
|
&h
|
||||||
|
);
|
||||||
|
|
||||||
|
assert(r == RadHookResult::Success);
|
||||||
|
assert(h != nullptr);
|
||||||
|
|
||||||
|
handles[count++] = h;
|
||||||
|
}
|
||||||
|
|
||||||
|
// enumerate
|
||||||
|
RadHookHandle listed[200];
|
||||||
|
size_t listedCount = RadHookEnumerate(listed, 200);
|
||||||
|
assert(listedCount == 200);
|
||||||
|
|
||||||
|
// every created handle must still be valid
|
||||||
|
for (size_t i = 0; i < count; i++) {
|
||||||
|
assert(RadHookIsValid(handles[i]));
|
||||||
|
}
|
||||||
|
|
||||||
|
// destroy half of them
|
||||||
|
for (size_t i = 0; i < count; i += 2) {
|
||||||
|
RadHookDestroy(handles[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// validate
|
||||||
|
for (size_t i = 0; i < count; i++) {
|
||||||
|
if (i % 2 == 0) {
|
||||||
|
assert(!RadHookIsValid(handles[i]));
|
||||||
|
} else {
|
||||||
|
assert(RadHookIsValid(handles[i]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// final enumeration must not return destroyed hooks
|
||||||
|
size_t finalCount = RadHookEnumerate(listed, 200);
|
||||||
|
assert(finalCount == 100);
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
20
tests/test_state_transitions.cpp
Normal file
20
tests/test_state_transitions.cpp
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
#include <cassert>
|
||||||
|
#include "radhook/radhook.h"
|
||||||
|
|
||||||
|
static int fn() { return 42; }
|
||||||
|
static int detour() { return 1337; }
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
RadHookHandle h = nullptr;
|
||||||
|
|
||||||
|
assert(RadHookCreate((void*)fn, (void*)detour, &h) == RadHookResult::Success);
|
||||||
|
|
||||||
|
// idempotent enable/disable behavior
|
||||||
|
assert(RadHookEnable(h) != RadHookResult::Unknown);
|
||||||
|
assert(RadHookEnable(h) == RadHookResult::AlreadyEnabled || RadHookIsEnabled(h));
|
||||||
|
|
||||||
|
assert(RadHookDisable(h) != RadHookResult::Unknown);
|
||||||
|
assert(!RadHookIsEnabled(h) || RadHookDisable(h) == RadHookResult::AlreadyDisabled);
|
||||||
|
|
||||||
|
RadHookDestroy(h);
|
||||||
|
}
|
||||||
19
tests/test_stress_churn.cpp
Normal file
19
tests/test_stress_churn.cpp
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
#include <cassert>
|
||||||
|
#include "radhook/radhook.h"
|
||||||
|
|
||||||
|
static int fn() { return 42; }
|
||||||
|
static int detour() { return 1337; }
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
for (int i = 0; i < 1000; i++) {
|
||||||
|
RadHookHandle h = nullptr;
|
||||||
|
|
||||||
|
auto r = RadHookCreate((void*)fn, (void*)detour, &h);
|
||||||
|
assert(r == RadHookResult::Success);
|
||||||
|
|
||||||
|
RadHookEnable(h);
|
||||||
|
RadHookDisable(h);
|
||||||
|
|
||||||
|
RadHookDestroy(h);
|
||||||
|
}
|
||||||
|
}
|
||||||
22
tests/test_trampoline.cpp
Normal file
22
tests/test_trampoline.cpp
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
#include <cassert>
|
||||||
|
#include "radhook/radhook.h"
|
||||||
|
|
||||||
|
static int fn() { return 42; }
|
||||||
|
static int detour() { return 1337; }
|
||||||
|
|
||||||
|
using Fn = int(*)();
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
RadHookHandle h = nullptr;
|
||||||
|
|
||||||
|
RadHookCreate((void*)fn, (void*)detour, &h);
|
||||||
|
|
||||||
|
auto orig = RadHookGetOriginalAs<Fn>(h);
|
||||||
|
assert(orig != nullptr);
|
||||||
|
|
||||||
|
// trampoline must bypass hook
|
||||||
|
assert(orig() == 42);
|
||||||
|
|
||||||
|
RadHookDestroy(h);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user