mirror of
https://git.recroomarchive.org/Radium/libradhook.git
synced 2026-08-07 09:10:59 +00:00
api changes, check README
This commit is contained in:
@@ -2,7 +2,7 @@ cmake_minimum_required(VERSION 3.21)
|
|||||||
|
|
||||||
project(
|
project(
|
||||||
radhook
|
radhook
|
||||||
VERSION 1.0.0
|
VERSION 1.1.0
|
||||||
DESCRIPTION "cross-platform function hooking library"
|
DESCRIPTION "cross-platform function hooking library"
|
||||||
LANGUAGES CXX
|
LANGUAGES CXX
|
||||||
)
|
)
|
||||||
|
|||||||
74
README.md
74
README.md
@@ -5,13 +5,16 @@ MinHook and Dobby with a flat, C-style, handle-based API.
|
|||||||
|
|
||||||
## Features
|
## Features
|
||||||
- Inline hooking on x86-64 and AArch64
|
- Inline hooking on x86-64 and AArch64
|
||||||
|
- Vtable hooking via direct slot swapping, no trampoline required
|
||||||
- Windows and Android support
|
- Windows and Android support
|
||||||
- Flat C API with opaque handles
|
- Flat C API with opaque handles
|
||||||
- Typed convenience overload of `RadHookCreate` for function pointers
|
- Typed convenience overload of `RadHookCreate` for function pointers
|
||||||
- Trampolines to call the original function from within a detour
|
- Trampolines to call the original function from within a detour
|
||||||
- Enable / disable / destroy individual hooks, or all hooks at once
|
- Enable / disable / destroy individual hooks, or all hooks at once
|
||||||
|
- Deferred enable/disable via a queue, applied atomically in one pass
|
||||||
- Hook enumeration and introspection (target, detour, trampoline, enabled state)
|
- Hook enumeration and introspection (target, detour, trampoline, enabled state)
|
||||||
- Human-readable status strings
|
- Human-readable status strings
|
||||||
|
- Runtime version query for ABI sanity checks
|
||||||
- Builds as a static or shared library via CMake
|
- Builds as a static or shared library via CMake
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
@@ -28,8 +31,9 @@ cmake --build build
|
|||||||
```
|
```
|
||||||
|
|
||||||
Useful options:
|
Useful options:
|
||||||
|
|
||||||
| Option | Default | Description |
|
| Option | Default | Description |
|
||||||
|------------------------|------------------------|--------------------------------------------------------|
|
|------------------------|------------------------|---------------------------------------------------------|
|
||||||
| `BUILD_SHARED_LIBS` | `ON` | Build radhook as a shared library instead of static |
|
| `BUILD_SHARED_LIBS` | `ON` | Build radhook as a shared library instead of static |
|
||||||
| `RADHOOK_BUILD_TESTS` | `ON` when top-level | Build the test suite |
|
| `RADHOOK_BUILD_TESTS` | `ON` when top-level | Build the test suite |
|
||||||
|
|
||||||
@@ -44,6 +48,8 @@ A helper PowerShell script is provided for common build targets:
|
|||||||
```
|
```
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
|
### Inline hooking
|
||||||
```cpp
|
```cpp
|
||||||
#include <radhook/radhook.h>
|
#include <radhook/radhook.h>
|
||||||
|
|
||||||
@@ -56,7 +62,7 @@ int main() {
|
|||||||
// create and install the hook
|
// create and install the hook
|
||||||
RadHookResult result = RadHookCreate(TargetFunction, (void*)DetourFunction, &handle);
|
RadHookResult result = RadHookCreate(TargetFunction, (void*)DetourFunction, &handle);
|
||||||
if (result != RadHookResult::Success) {
|
if (result != RadHookResult::Success) {
|
||||||
// handle error
|
// handle error, RadHookResultToString(result) for a message
|
||||||
}
|
}
|
||||||
|
|
||||||
// call through the trampoline
|
// call through the trampoline
|
||||||
@@ -72,14 +78,59 @@ int main() {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Vtable hooking
|
||||||
|
```cpp
|
||||||
|
struct IFoo {
|
||||||
|
virtual int Value() { return 7; }
|
||||||
|
virtual ~IFoo() = default;
|
||||||
|
};
|
||||||
|
|
||||||
|
int Detour(IFoo* self) { return 777; }
|
||||||
|
|
||||||
|
IFoo obj;
|
||||||
|
void** vtable = *reinterpret_cast<void***>(&obj);
|
||||||
|
|
||||||
|
RadHookHandle handle = nullptr;
|
||||||
|
RadHookVTableCreate(vtable, /*index=*/0, (void*)Detour, &handle);
|
||||||
|
|
||||||
|
// no relocation is needed for vtable hooks
|
||||||
|
using Fn = int(*)(IFoo*);
|
||||||
|
Fn original = RadHookGetOriginalAs<Fn>(handle);
|
||||||
|
original(&obj);
|
||||||
|
|
||||||
|
RadHookDestroy(handle);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Deferred apply
|
||||||
|
Stage several enable/disable transitions and flip them together in one pass,
|
||||||
|
instead of one hook at a time:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
RadHookQueueEnable(handleA);
|
||||||
|
RadHookQueueDisable(handleB);
|
||||||
|
|
||||||
|
// nothing has changed yet
|
||||||
|
Foo();
|
||||||
|
|
||||||
|
// then apply
|
||||||
|
RadHookApplyQueued(); // both transitions land together
|
||||||
|
```
|
||||||
|
|
||||||
|
If a handle is queued more than once before `RadHookApplyQueued` runs, the
|
||||||
|
most recent call wins.
|
||||||
|
|
||||||
## API overview
|
## API overview
|
||||||
|
|
||||||
| Function Name | Description |
|
| Function Name | Description |
|
||||||
|---------------------------------------------------|------------------------------------------------------------|
|
|-----------------------------------------------------|-----------------------------------------------------------------|
|
||||||
| `RadHookCreate` | Create and install a hook, enabled by default |
|
| `RadHookCreate` | Create and install a hook, enabled by default |
|
||||||
|
| `RadHookVTableCreate` | Create and install a hook on a vtable slot |
|
||||||
| `RadHookEnable` | Enable an installed hook |
|
| `RadHookEnable` | Enable an installed hook |
|
||||||
| `RadHookDisable` | Disable an installed hook, restoring original bytes |
|
| `RadHookDisable` | Disable an installed hook, restoring original bytes |
|
||||||
| `RadHookDestroy` | Disable, release, and invalidate a hook |
|
| `RadHookDestroy` | Disable, release, and invalidate a hook |
|
||||||
| `RadHookEnableAll` / `RadHookDisableAll` | Enable or disable every registered hook |
|
| `RadHookEnableAll` / `RadHookDisableAll` | Enable or disable every registered hook |
|
||||||
|
| `RadHookQueueEnable` / `RadHookQueueDisable` | Stage a hook's enable/disable state without applying it |
|
||||||
|
| `RadHookApplyQueued` | Apply every staged enable/disable transition at once |
|
||||||
| `RadHookGetCount` | Number of currently registered hooks |
|
| `RadHookGetCount` | Number of currently registered hooks |
|
||||||
| `RadHookGetHandles` / `RadHookEnumerate` | Enumerate registered hook handles |
|
| `RadHookGetHandles` / `RadHookEnumerate` | Enumerate registered hook handles |
|
||||||
| `RadHookIsValid` | Check whether a handle refers to a registered hook |
|
| `RadHookIsValid` | Check whether a handle refers to a registered hook |
|
||||||
@@ -88,6 +139,23 @@ int main() {
|
|||||||
| `RadHookGetDetour` | Get the detour function pointer |
|
| `RadHookGetDetour` | Get the detour function pointer |
|
||||||
| `RadHookGetOriginal` / `RadHookGetOriginalAs<T>` | Get the trampoline to call the original implementation |
|
| `RadHookGetOriginal` / `RadHookGetOriginalAs<T>` | Get the trampoline to call the original implementation |
|
||||||
| `RadHookResultToString` | Human-readable name for a `RadHookResult` |
|
| `RadHookResultToString` | Human-readable name for a `RadHookResult` |
|
||||||
|
| `RadHookGetVersion` | Query the library's major/minor/patch version |
|
||||||
|
|
||||||
|
All fallible operations return a `RadHookResult`, including granular error
|
||||||
|
codes such as `TrampolineTooFar`, `DisassemblyFailed`, and
|
||||||
|
`MemoryProtectFailed`.
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
Tests are built with CTest and cover creation, enable/disable and lifecycle
|
||||||
|
transitions, trampoline behavior, vtable hooking, queued apply, invalid
|
||||||
|
input handling, failure cases, memory integrity, and stress/churn
|
||||||
|
scenarios.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cmake -S . -B build -DRADHOOK_BUILD_TESTS=ON
|
||||||
|
cmake --build build
|
||||||
|
ctest --test-dir build --output-on-failure
|
||||||
|
```
|
||||||
|
|
||||||
## License
|
## License
|
||||||
Licensed under the GNU Lesser General Public License v3 (LGPLv3). See [LICENSE](LICENSE) for details.
|
Licensed under the GNU Lesser General Public License v3 (LGPLv3). See [LICENSE](LICENSE) for details.
|
||||||
@@ -10,6 +10,24 @@
|
|||||||
# define RADHOOK_API
|
# define RADHOOK_API
|
||||||
#endif // RADHOOK_API
|
#endif // RADHOOK_API
|
||||||
|
|
||||||
|
// versioning
|
||||||
|
#define RADHOOK_VERSION_MAJOR 1
|
||||||
|
#define RADHOOK_VERSION_MINOR 1
|
||||||
|
#define RADHOOK_VERSION_PATCH 0
|
||||||
|
|
||||||
|
#if _MSC_VER
|
||||||
|
# include <sal.h> // for source code annotations
|
||||||
|
#else
|
||||||
|
# define _In_
|
||||||
|
# define _In_opt_
|
||||||
|
# define _Out_
|
||||||
|
# define _Out_opt_
|
||||||
|
# define _Outptr_
|
||||||
|
# define _Ret_z_
|
||||||
|
# define _Success_(expr)
|
||||||
|
# define _Out_writes_to_(size, count)
|
||||||
|
#endif
|
||||||
|
|
||||||
enum class RadHookResult : int {
|
enum class RadHookResult : int {
|
||||||
Success = 0,
|
Success = 0,
|
||||||
AlreadyInstalled,
|
AlreadyInstalled,
|
||||||
@@ -26,6 +44,7 @@ enum class RadHookResult : int {
|
|||||||
Unknown,
|
Unknown,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
_Ret_z_
|
||||||
/**
|
/**
|
||||||
* @brief Creates a human readable name for a status code.
|
* @brief Creates a human readable name for a status code.
|
||||||
*
|
*
|
||||||
@@ -36,11 +55,12 @@ RADHOOK_API
|
|||||||
const char*
|
const char*
|
||||||
RadHookResultToString(
|
RadHookResultToString(
|
||||||
RadHookResult result
|
RadHookResult result
|
||||||
);
|
);
|
||||||
|
|
||||||
struct RadHookOpaque;
|
struct RadHookOpaque;
|
||||||
using RadHookHandle = RadHookOpaque*;
|
using RadHookHandle = RadHookOpaque*;
|
||||||
|
|
||||||
|
_Success_(return == RadHookResult::Success)
|
||||||
/**
|
/**
|
||||||
* @brief Creates and installs a hook.
|
* @brief Creates and installs a hook.
|
||||||
*
|
*
|
||||||
@@ -57,18 +77,19 @@ using RadHookHandle = RadHookOpaque*;
|
|||||||
RADHOOK_API
|
RADHOOK_API
|
||||||
RadHookResult
|
RadHookResult
|
||||||
RadHookCreate(
|
RadHookCreate(
|
||||||
void* target,
|
_In_ void* target,
|
||||||
void* detour,
|
_In_ void* detour,
|
||||||
RadHookHandle* outHandle
|
_Out_ RadHookHandle* outHandle
|
||||||
);
|
);
|
||||||
|
|
||||||
|
_Success_(return == RadHookResult::Success)
|
||||||
/**
|
/**
|
||||||
* @brief Creates and installs a hook from a function pointer.
|
* @brief Creates and installs a hook from a function pointer.
|
||||||
*
|
*
|
||||||
* Convenience overload that accepts a typed function pointer for the target.
|
* Convenience overload that accepts a typed function pointer for the target.
|
||||||
*
|
*
|
||||||
* @tparam TargetFn Target function pointer type.
|
* @tparam TargetFn Target function pointer type.
|
||||||
* @param target Function to hook.
|
* @param target Target function to hook.
|
||||||
* @param detour Replacement function.
|
* @param detour Replacement function.
|
||||||
* @param outHandle Receives the created hook handle on success.
|
* @param outHandle Receives the created hook handle on success.
|
||||||
*
|
*
|
||||||
@@ -79,9 +100,10 @@ requires std::is_function_v<std::remove_pointer_t<TargetFn>>
|
|||||||
RadHookResult
|
RadHookResult
|
||||||
RadHookCreate(
|
RadHookCreate(
|
||||||
TargetFn target,
|
TargetFn target,
|
||||||
void* detour,
|
_In_ void* detour,
|
||||||
RadHookHandle* outHandle
|
_Out_ RadHookHandle* outHandle
|
||||||
) {
|
)
|
||||||
|
{
|
||||||
return RadHookCreate(
|
return RadHookCreate(
|
||||||
reinterpret_cast<void*>(target),
|
reinterpret_cast<void*>(target),
|
||||||
detour,
|
detour,
|
||||||
@@ -102,8 +124,8 @@ RadHookCreate(
|
|||||||
RADHOOK_API
|
RADHOOK_API
|
||||||
RadHookResult
|
RadHookResult
|
||||||
RadHookEnable(
|
RadHookEnable(
|
||||||
RadHookHandle handle
|
_In_ RadHookHandle handle
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Disables an installed hook.
|
* @brief Disables an installed hook.
|
||||||
@@ -118,8 +140,8 @@ RadHookEnable(
|
|||||||
RADHOOK_API
|
RADHOOK_API
|
||||||
RadHookResult
|
RadHookResult
|
||||||
RadHookDisable(
|
RadHookDisable(
|
||||||
RadHookHandle handle
|
_In_ RadHookHandle handle
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Destroys a hook.
|
* @brief Destroys a hook.
|
||||||
@@ -134,8 +156,8 @@ RadHookDisable(
|
|||||||
RADHOOK_API
|
RADHOOK_API
|
||||||
RadHookResult
|
RadHookResult
|
||||||
RadHookDestroy(
|
RadHookDestroy(
|
||||||
RadHookHandle handle
|
_In_ RadHookHandle handle
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Enables every registered hook.
|
* @brief Enables every registered hook.
|
||||||
@@ -170,6 +192,7 @@ RADHOOK_API
|
|||||||
size_t
|
size_t
|
||||||
RadHookGetCount();
|
RadHookGetCount();
|
||||||
|
|
||||||
|
_Out_writes_to_(maxCount, return)
|
||||||
/**
|
/**
|
||||||
* @brief Retrieves registered hook handles.
|
* @brief Retrieves registered hook handles.
|
||||||
*
|
*
|
||||||
@@ -184,9 +207,9 @@ RadHookGetCount();
|
|||||||
RADHOOK_API
|
RADHOOK_API
|
||||||
size_t
|
size_t
|
||||||
RadHookGetHandles(
|
RadHookGetHandles(
|
||||||
RadHookHandle* outHandles,
|
_Out_ RadHookHandle* outHandles,
|
||||||
size_t maxCount
|
_In_ size_t maxCount
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Checks whether a hook handle is valid.
|
* @brief Checks whether a hook handle is valid.
|
||||||
@@ -198,8 +221,8 @@ RadHookGetHandles(
|
|||||||
RADHOOK_API
|
RADHOOK_API
|
||||||
bool
|
bool
|
||||||
RadHookIsValid(
|
RadHookIsValid(
|
||||||
RadHookHandle handle
|
_In_ RadHookHandle handle
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Checks whether a hook is currently enabled.
|
* @brief Checks whether a hook is currently enabled.
|
||||||
@@ -211,8 +234,8 @@ RadHookIsValid(
|
|||||||
RADHOOK_API
|
RADHOOK_API
|
||||||
bool
|
bool
|
||||||
RadHookIsEnabled(
|
RadHookIsEnabled(
|
||||||
RadHookHandle handle
|
_In_ RadHookHandle handle
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Returns the hooked target function.
|
* @brief Returns the hooked target function.
|
||||||
@@ -225,8 +248,8 @@ RadHookIsEnabled(
|
|||||||
RADHOOK_API
|
RADHOOK_API
|
||||||
void*
|
void*
|
||||||
RadHookGetTarget(
|
RadHookGetTarget(
|
||||||
RadHookHandle handle
|
_In_ RadHookHandle handle
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Returns the detour function.
|
* @brief Returns the detour function.
|
||||||
@@ -239,8 +262,8 @@ RadHookGetTarget(
|
|||||||
RADHOOK_API
|
RADHOOK_API
|
||||||
void*
|
void*
|
||||||
RadHookGetDetour(
|
RadHookGetDetour(
|
||||||
RadHookHandle handle
|
_In_ RadHookHandle handle
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Returns the trampoline containing the relocated original code.
|
* @brief Returns the trampoline containing the relocated original code.
|
||||||
@@ -255,8 +278,8 @@ RadHookGetDetour(
|
|||||||
RADHOOK_API
|
RADHOOK_API
|
||||||
void*
|
void*
|
||||||
RadHookGetOriginal(
|
RadHookGetOriginal(
|
||||||
RadHookHandle handle
|
_In_ RadHookHandle handle
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Returns the trampoline cast to a function pointer type.
|
* @brief Returns the trampoline cast to a function pointer type.
|
||||||
@@ -269,12 +292,14 @@ RadHookGetOriginal(
|
|||||||
template <typename FnPtr>
|
template <typename FnPtr>
|
||||||
FnPtr
|
FnPtr
|
||||||
RadHookGetOriginalAs(
|
RadHookGetOriginalAs(
|
||||||
RadHookHandle handle
|
_In_ RadHookHandle handle
|
||||||
) {
|
)
|
||||||
|
{
|
||||||
return reinterpret_cast<FnPtr>(RadHookGetOriginal(handle));
|
return reinterpret_cast<FnPtr>(RadHookGetOriginal(handle));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
_Out_writes_to_(maxCount, return)
|
||||||
|
/**
|
||||||
* @brief Enumerates currently registered hooks.
|
* @brief Enumerates currently registered hooks.
|
||||||
*
|
*
|
||||||
* Copies up to @p maxCount active hook handles into @p out.
|
* Copies up to @p maxCount active hook handles into @p out.
|
||||||
@@ -291,8 +316,99 @@ RadHookGetOriginalAs(
|
|||||||
RADHOOK_API
|
RADHOOK_API
|
||||||
size_t
|
size_t
|
||||||
RadHookEnumerate(
|
RadHookEnumerate(
|
||||||
RadHookHandle* out,
|
_Out_ RadHookHandle* out,
|
||||||
size_t maxCount
|
_In_ size_t maxCount
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Returns the library version.
|
||||||
|
*
|
||||||
|
* Any of the output parameters may be nullptr if that component is not
|
||||||
|
* needed. Useful for verifying ABI compatibility when radhook is loaded
|
||||||
|
* as a shared library independently of the consuming module.
|
||||||
|
*
|
||||||
|
* @param outMajor Receives the major version, or nullptr.
|
||||||
|
* @param outMinor Receives the minor version, or nullptr.
|
||||||
|
* @param outPatch Receives the patch version, or nullptr.
|
||||||
|
*/
|
||||||
|
RADHOOK_API
|
||||||
|
void
|
||||||
|
RadHookGetVersion(
|
||||||
|
_Out_opt_ int* outMajor,
|
||||||
|
_Out_opt_ int* outMinor,
|
||||||
|
_Out_opt_ int* outPatch
|
||||||
|
);
|
||||||
|
|
||||||
|
_Success_(return == RadHookResult::Success)
|
||||||
|
/**
|
||||||
|
* @brief Creates and installs a hook on a vtable slot.
|
||||||
|
*
|
||||||
|
* @param vtable Pointer to the base of the vtable.
|
||||||
|
* @param index Index of the slot to hook.
|
||||||
|
* @param detour Replacement function.
|
||||||
|
* @param outHandle Receives the created hook handle on success. Set to nullptr on failure.
|
||||||
|
*
|
||||||
|
* @return InvalidTarget if @p vtable is nullptr or the slot
|
||||||
|
* at @p index is empty; otherwise the result of the operation.
|
||||||
|
*/
|
||||||
|
RADHOOK_API
|
||||||
|
RadHookResult
|
||||||
|
RadHookVTableCreate(
|
||||||
|
_In_ void** vtable,
|
||||||
|
_In_ size_t index,
|
||||||
|
_In_ void* detour,
|
||||||
|
_Out_ RadHookHandle* outHandle
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Queues a hook to be enabled on the next RadHookApplyQueued call.
|
||||||
|
*
|
||||||
|
* Does not modify the hook itself. If the same handle is queued for both
|
||||||
|
* enable and disable before RadHookApplyQueued runs, the most recent call
|
||||||
|
* wins.
|
||||||
|
*
|
||||||
|
* @param handle Hook handle.
|
||||||
|
*
|
||||||
|
* @return RadHookResult::InvalidHandle if the handle is not registered;
|
||||||
|
* otherwise RadHookResult::Success.
|
||||||
|
*/
|
||||||
|
RADHOOK_API
|
||||||
|
RadHookResult
|
||||||
|
RadHookQueueEnable(
|
||||||
|
_In_ RadHookHandle handle
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Queues a hook to be disabled on the next RadHookApplyQueued call.
|
||||||
|
*
|
||||||
|
* Does not modify the hook itself. If the same handle is queued for both
|
||||||
|
* enable and disable before RadHookApplyQueued runs, the most recent call
|
||||||
|
* wins.
|
||||||
|
*
|
||||||
|
* @param handle Hook handle.
|
||||||
|
*
|
||||||
|
* @return RadHookResult::InvalidHandle if the handle is not registered;
|
||||||
|
* otherwise RadHookResult::Success.
|
||||||
|
*/
|
||||||
|
RADHOOK_API
|
||||||
|
RadHookResult
|
||||||
|
RadHookQueueDisable(
|
||||||
|
_In_ RadHookHandle handle
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Applies all queued enable/disable operations.
|
||||||
|
*
|
||||||
|
* Every hook with a pending queued action has that action applied and its
|
||||||
|
* queue cleared, regardless of individual outcomes. Hooks with no queued
|
||||||
|
* action are left untouched.
|
||||||
|
*
|
||||||
|
* @return The result of the first queued operation that did not succeed
|
||||||
|
* (ignoring RadHookResult::AlreadyEnabled / RadHookResult::AlreadyDisabled),
|
||||||
|
* or RadHookResult::Success if all applied cleanly.
|
||||||
|
*/
|
||||||
|
RADHOOK_API
|
||||||
|
RadHookResult
|
||||||
|
RadHookApplyQueued();
|
||||||
|
|
||||||
#endif // __LIBRADHOOK_API_H__
|
#endif // __LIBRADHOOK_API_H__
|
||||||
125
src/radhook.cpp
125
src/radhook.cpp
@@ -18,6 +18,8 @@
|
|||||||
# include <unistd.h>
|
# include <unistd.h>
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
// stupid macros messing up compilation because it conflicts with
|
||||||
|
// std::min and std::max
|
||||||
#undef min
|
#undef min
|
||||||
#undef max
|
#undef max
|
||||||
|
|
||||||
@@ -40,6 +42,17 @@ const char* RadHookResultToString(RadHookResult result) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum class RadHookKind : int {
|
||||||
|
Inline,
|
||||||
|
VTable,
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class RadHookQueuedAction : int {
|
||||||
|
None,
|
||||||
|
Enable,
|
||||||
|
Disable,
|
||||||
|
};
|
||||||
|
|
||||||
// hook record
|
// hook record
|
||||||
struct RadHookOpaque {
|
struct RadHookOpaque {
|
||||||
void* target = nullptr;
|
void* target = nullptr;
|
||||||
@@ -56,6 +69,12 @@ struct RadHookOpaque {
|
|||||||
size_t stubLength = 0;
|
size_t stubLength = 0;
|
||||||
|
|
||||||
bool enabled = false;
|
bool enabled = false;
|
||||||
|
|
||||||
|
RadHookKind kind = RadHookKind::Inline;
|
||||||
|
void** vtableSlot = nullptr;
|
||||||
|
void* vtableOriginalEntry = nullptr;
|
||||||
|
|
||||||
|
RadHookQueuedAction queuedAction = RadHookQueuedAction::None;
|
||||||
};
|
};
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
@@ -121,8 +140,10 @@ void* AllocExecNear(void* target, size_t size) {
|
|||||||
if (mbi.State == MEM_FREE) {
|
if (mbi.State == MEM_FREE) {
|
||||||
uintptr_t allocBase = (regionBase + pageSize - 1) & ~(pageSize - 1);
|
uintptr_t allocBase = (regionBase + pageSize - 1) & ~(pageSize - 1);
|
||||||
if (allocBase + size <= regionBase + mbi.RegionSize && allocBase + size <= maxAddr) {
|
if (allocBase + size <= regionBase + mbi.RegionSize && allocBase + size <= maxAddr) {
|
||||||
void* p = VirtualAlloc(reinterpret_cast<LPVOID>(allocBase), size,
|
void* p = VirtualAlloc(
|
||||||
MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
|
reinterpret_cast<LPVOID>(allocBase), size,
|
||||||
|
MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE
|
||||||
|
);
|
||||||
if (p) return p;
|
if (p) return p;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -598,6 +619,21 @@ RadHookResult InstallHook(RadHookOpaque* h, void* target, void* detour) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
RadHookResult EnableHookInternal(RadHookOpaque* h) {
|
RadHookResult EnableHookInternal(RadHookOpaque* h) {
|
||||||
|
if (h->kind == RadHookKind::VTable) {
|
||||||
|
if (!h->vtableSlot) return RadHookResult::NotInstalled;
|
||||||
|
if (h->enabled) return RadHookResult::AlreadyEnabled;
|
||||||
|
|
||||||
|
unsigned long oldProtect = 0;
|
||||||
|
if (!MakeWritableExecutable(h->vtableSlot, sizeof(void*), &oldProtect)) {
|
||||||
|
return RadHookResult::MemoryProtectFailed;
|
||||||
|
}
|
||||||
|
*h->vtableSlot = h->detour;
|
||||||
|
RestoreProtection(h->vtableSlot, sizeof(void*), oldProtect);
|
||||||
|
|
||||||
|
h->enabled = true;
|
||||||
|
return RadHookResult::Success;
|
||||||
|
}
|
||||||
|
|
||||||
if (!h->target) return RadHookResult::NotInstalled;
|
if (!h->target) return RadHookResult::NotInstalled;
|
||||||
if (h->enabled) return RadHookResult::AlreadyEnabled;
|
if (h->enabled) return RadHookResult::AlreadyEnabled;
|
||||||
|
|
||||||
@@ -614,6 +650,21 @@ RadHookResult EnableHookInternal(RadHookOpaque* h) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
RadHookResult DisableHookInternal(RadHookOpaque* h) {
|
RadHookResult DisableHookInternal(RadHookOpaque* h) {
|
||||||
|
if (h->kind == RadHookKind::VTable) {
|
||||||
|
if (!h->vtableSlot) return RadHookResult::NotInstalled;
|
||||||
|
if (!h->enabled) return RadHookResult::AlreadyDisabled;
|
||||||
|
|
||||||
|
unsigned long oldProtect = 0;
|
||||||
|
if (!MakeWritableExecutable(h->vtableSlot, sizeof(void*), &oldProtect)) {
|
||||||
|
return RadHookResult::MemoryProtectFailed;
|
||||||
|
}
|
||||||
|
*h->vtableSlot = h->vtableOriginalEntry;
|
||||||
|
RestoreProtection(h->vtableSlot, sizeof(void*), oldProtect);
|
||||||
|
|
||||||
|
h->enabled = false;
|
||||||
|
return RadHookResult::Success;
|
||||||
|
}
|
||||||
|
|
||||||
if (!h->target) return RadHookResult::NotInstalled;
|
if (!h->target) return RadHookResult::NotInstalled;
|
||||||
if (!h->enabled) return RadHookResult::AlreadyDisabled;
|
if (!h->enabled) return RadHookResult::AlreadyDisabled;
|
||||||
|
|
||||||
@@ -759,3 +810,73 @@ size_t RadHookEnumerate(RadHookHandle* out, size_t maxCount) {
|
|||||||
|
|
||||||
return written;
|
return written;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void RadHookGetVersion(int* outMajor, int* outMinor, int* outPatch) {
|
||||||
|
if (outMajor) *outMajor = RADHOOK_VERSION_MAJOR;
|
||||||
|
if (outMinor) *outMinor = RADHOOK_VERSION_MINOR;
|
||||||
|
if (outPatch) *outPatch = RADHOOK_VERSION_PATCH;
|
||||||
|
}
|
||||||
|
|
||||||
|
RadHookResult RadHookVTableCreate(void** vtable, size_t index, void* detour, RadHookHandle* outHandle) {
|
||||||
|
if (outHandle) *outHandle = nullptr;
|
||||||
|
if (!vtable) return RadHookResult::InvalidTarget;
|
||||||
|
if (!detour) return RadHookResult::InvalidDetour;
|
||||||
|
|
||||||
|
void** slot = vtable + index;
|
||||||
|
void* original = *slot;
|
||||||
|
if (!original) return RadHookResult::InvalidTarget;
|
||||||
|
|
||||||
|
auto record = std::make_unique<RadHookOpaque>();
|
||||||
|
record->kind = RadHookKind::VTable;
|
||||||
|
record->vtableSlot = slot;
|
||||||
|
record->vtableOriginalEntry = original;
|
||||||
|
record->target = original;
|
||||||
|
record->detour = detour;
|
||||||
|
record->trampoline = original;
|
||||||
|
|
||||||
|
RadHookResult result = EnableHookInternal(record.get());
|
||||||
|
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 RadHookQueueEnable(RadHookHandle handle) {
|
||||||
|
std::lock_guard<std::mutex> lock(g_mutex);
|
||||||
|
if (!IsRegisteredLocked(handle)) return RadHookResult::InvalidHandle;
|
||||||
|
handle->queuedAction = RadHookQueuedAction::Enable;
|
||||||
|
return RadHookResult::Success;
|
||||||
|
}
|
||||||
|
|
||||||
|
RadHookResult RadHookQueueDisable(RadHookHandle handle) {
|
||||||
|
std::lock_guard<std::mutex> lock(g_mutex);
|
||||||
|
if (!IsRegisteredLocked(handle)) return RadHookResult::InvalidHandle;
|
||||||
|
handle->queuedAction = RadHookQueuedAction::Disable;
|
||||||
|
return RadHookResult::Success;
|
||||||
|
}
|
||||||
|
|
||||||
|
RadHookResult RadHookApplyQueued() {
|
||||||
|
std::lock_guard<std::mutex> lock(g_mutex);
|
||||||
|
RadHookResult last = RadHookResult::Success;
|
||||||
|
for (auto& h : g_hooks) {
|
||||||
|
RadHookQueuedAction action = h->queuedAction;
|
||||||
|
if (action == RadHookQueuedAction::None) continue;
|
||||||
|
h->queuedAction = RadHookQueuedAction::None;
|
||||||
|
|
||||||
|
RadHookResult r = (action == RadHookQueuedAction::Enable)
|
||||||
|
? EnableHookInternal(h.get())
|
||||||
|
: DisableHookInternal(h.get());
|
||||||
|
|
||||||
|
if (r != RadHookResult::Success &&
|
||||||
|
r != RadHookResult::AlreadyEnabled &&
|
||||||
|
r != RadHookResult::AlreadyDisabled) {
|
||||||
|
last = r;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return last;
|
||||||
|
}
|
||||||
68
tests/test_queue.cpp
Normal file
68
tests/test_queue.cpp
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
#include <cassert>
|
||||||
|
#include "radhook/radhook.h"
|
||||||
|
|
||||||
|
static int fnA() { return 1; }
|
||||||
|
static int detourA() { return 100; }
|
||||||
|
|
||||||
|
static int fnB() { return 2; }
|
||||||
|
static int detourB() { return 200; }
|
||||||
|
|
||||||
|
static int fnC() { return 3; }
|
||||||
|
static int detourC() { return 300; }
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
RadHookHandle hA = nullptr;
|
||||||
|
RadHookHandle hB = nullptr;
|
||||||
|
|
||||||
|
assert(RadHookCreate((void*)fnA, (void*)detourA, &hA) == RadHookResult::Success);
|
||||||
|
assert(RadHookCreate((void*)fnB, (void*)detourB, &hB) == RadHookResult::Success);
|
||||||
|
|
||||||
|
// start both from a known, disabled state
|
||||||
|
assert(RadHookDisable(hA) == RadHookResult::Success);
|
||||||
|
assert(RadHookDisable(hB) == RadHookResult::Success);
|
||||||
|
assert(fnA() == 1);
|
||||||
|
assert(fnB() == 2);
|
||||||
|
|
||||||
|
// queueing must not apply immediately
|
||||||
|
assert(RadHookQueueEnable(hA) == RadHookResult::Success);
|
||||||
|
assert(RadHookQueueEnable(hB) == RadHookResult::Success);
|
||||||
|
assert(!RadHookIsEnabled(hA));
|
||||||
|
assert(!RadHookIsEnabled(hB));
|
||||||
|
assert(fnA() == 1);
|
||||||
|
assert(fnB() == 2);
|
||||||
|
|
||||||
|
// apply enables both together
|
||||||
|
assert(RadHookApplyQueued() == RadHookResult::Success);
|
||||||
|
assert(RadHookIsEnabled(hA));
|
||||||
|
assert(RadHookIsEnabled(hB));
|
||||||
|
assert(fnA() == 100);
|
||||||
|
assert(fnB() == 200);
|
||||||
|
|
||||||
|
// last queued action before apply wins
|
||||||
|
assert(RadHookQueueDisable(hA) == RadHookResult::Success);
|
||||||
|
assert(RadHookQueueEnable(hA) == RadHookResult::Success);
|
||||||
|
assert(RadHookApplyQueued() == RadHookResult::Success);
|
||||||
|
assert(RadHookIsEnabled(hA));
|
||||||
|
|
||||||
|
// an apply with nothing queued is a no-op
|
||||||
|
assert(RadHookApplyQueued() == RadHookResult::Success);
|
||||||
|
|
||||||
|
// queueing a redundant transition (already enabled) still applies cleanly
|
||||||
|
assert(RadHookQueueEnable(hA) == RadHookResult::Success);
|
||||||
|
assert(RadHookApplyQueued() == RadHookResult::Success);
|
||||||
|
assert(RadHookIsEnabled(hA));
|
||||||
|
|
||||||
|
// invalid handles
|
||||||
|
assert(RadHookQueueEnable(nullptr) == RadHookResult::InvalidHandle);
|
||||||
|
assert(RadHookQueueDisable(nullptr) == RadHookResult::InvalidHandle);
|
||||||
|
|
||||||
|
// a destroyed hook can no longer be queued
|
||||||
|
RadHookHandle hC = nullptr;
|
||||||
|
assert(RadHookCreate((void*)fnC, (void*)detourC, &hC) == RadHookResult::Success);
|
||||||
|
RadHookDestroy(hC);
|
||||||
|
assert(RadHookQueueEnable(hC) == RadHookResult::InvalidHandle);
|
||||||
|
|
||||||
|
RadHookDestroy(hA);
|
||||||
|
RadHookDestroy(hB);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
20
tests/test_version.cpp
Normal file
20
tests/test_version.cpp
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
#include <cassert>
|
||||||
|
#include "radhook/radhook.h"
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
int major = -1, minor = -1, patch = -1;
|
||||||
|
RadHookGetVersion(&major, &minor, &patch);
|
||||||
|
|
||||||
|
assert(major == RADHOOK_VERSION_MAJOR);
|
||||||
|
assert(minor == RADHOOK_VERSION_MINOR);
|
||||||
|
assert(patch == RADHOOK_VERSION_PATCH);
|
||||||
|
|
||||||
|
// individual out params may be omitted independently
|
||||||
|
int onlyMajor = -1;
|
||||||
|
RadHookGetVersion(&onlyMajor, nullptr, nullptr);
|
||||||
|
assert(onlyMajor == RADHOOK_VERSION_MAJOR);
|
||||||
|
|
||||||
|
// must not crash
|
||||||
|
RadHookGetVersion(nullptr, nullptr, nullptr);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
68
tests/test_vtable.cpp
Normal file
68
tests/test_vtable.cpp
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
#include <cassert>
|
||||||
|
#include "radhook/radhook.h"
|
||||||
|
|
||||||
|
struct IFoo {
|
||||||
|
virtual int Value() { return 7; }
|
||||||
|
virtual ~IFoo() = default;
|
||||||
|
};
|
||||||
|
|
||||||
|
static int DetourValue(IFoo* /*self*/) { return 777; }
|
||||||
|
|
||||||
|
using Fn = int(*)(IFoo*);
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
IFoo obj;
|
||||||
|
void** vtable = *reinterpret_cast<void***>(&obj);
|
||||||
|
|
||||||
|
RadHookHandle h = nullptr;
|
||||||
|
assert(RadHookVTableCreate(vtable, 0, (void*)DetourValue, &h) == RadHookResult::Success);
|
||||||
|
assert(RadHookIsValid(h));
|
||||||
|
assert(RadHookIsEnabled(h));
|
||||||
|
|
||||||
|
// handle is created enabled, so the swap must already be visible
|
||||||
|
assert(obj.Value() == 777);
|
||||||
|
assert(reinterpret_cast<Fn>(vtable[0])(&obj) == 777);
|
||||||
|
|
||||||
|
void* originalEntry = RadHookGetTarget(h);
|
||||||
|
assert(originalEntry != nullptr);
|
||||||
|
assert(RadHookGetDetour(h) == reinterpret_cast<void*>(DetourValue));
|
||||||
|
|
||||||
|
// disable restores the original vtable entry
|
||||||
|
assert(RadHookDisable(h) == RadHookResult::Success);
|
||||||
|
assert(!RadHookIsEnabled(h));
|
||||||
|
assert(obj.Value() == 7);
|
||||||
|
assert(vtable[0] == originalEntry);
|
||||||
|
|
||||||
|
// re-enable re-applies the swap
|
||||||
|
assert(RadHookEnable(h) == RadHookResult::Success);
|
||||||
|
assert(obj.Value() == 777);
|
||||||
|
|
||||||
|
// no relocation is needed for vtable hooks -- the trampoline is the
|
||||||
|
// original entry itself and can be called directly while the hook is live
|
||||||
|
auto original = RadHookGetOriginalAs<Fn>(h);
|
||||||
|
assert(reinterpret_cast<void*>(original) == originalEntry);
|
||||||
|
assert(original(&obj) == 7);
|
||||||
|
|
||||||
|
RadHookDestroy(h);
|
||||||
|
assert(!RadHookIsValid(h));
|
||||||
|
assert(obj.Value() == 7);
|
||||||
|
|
||||||
|
// queueing works uniformly across inline and vtable hook kinds
|
||||||
|
RadHookHandle h2 = nullptr;
|
||||||
|
assert(RadHookVTableCreate(vtable, 0, (void*)DetourValue, &h2) == RadHookResult::Success);
|
||||||
|
assert(RadHookQueueDisable(h2) == RadHookResult::Success);
|
||||||
|
assert(RadHookIsEnabled(h2)); // not applied yet
|
||||||
|
assert(RadHookApplyQueued() == RadHookResult::Success);
|
||||||
|
assert(!RadHookIsEnabled(h2));
|
||||||
|
assert(obj.Value() == 7);
|
||||||
|
RadHookDestroy(h2);
|
||||||
|
|
||||||
|
// invalid inputs
|
||||||
|
RadHookHandle bad = nullptr;
|
||||||
|
assert(RadHookVTableCreate(nullptr, 0, (void*)DetourValue, &bad) == RadHookResult::InvalidTarget);
|
||||||
|
assert(bad == nullptr);
|
||||||
|
assert(RadHookVTableCreate(vtable, 0, nullptr, &bad) == RadHookResult::InvalidDetour);
|
||||||
|
assert(bad == nullptr);
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user