Compare commits

4 Commits

Author SHA1 Message Date
87e1262bc5 Add a way to know if a class has a unique vtable. 2025-02-04 18:17:20 -05:00
46f75aaca5 vtable hook. 2025-02-04 14:02:22 -05:00
0378055b4d Add features, RV64 still doesn't work 🤷 2025-01-27 13:19:35 -05:00
fe74109b15 Update 2024-07-29 20:27:18 -04:00
6 changed files with 252 additions and 72 deletions

View File

@@ -1,4 +1,4 @@
cmake_minimum_required(VERSION 3.18)
cmake_minimum_required(VERSION 3.18..3.28)
project(FunctionHook)
set(CMAKE_CXX_STANDARD 20)
@@ -6,7 +6,14 @@ file(GLOB_RECURSE HEADERS "include/*.h")
#TODO more architectures.
if (UNIX AND NOT APPLE)
file(GLOB_RECURSE SOURCES "src/linux64/*.cpp")
if (CMAKE_SYSTEM_PROCESSOR MATCHES "x86_64")
file(GLOB_RECURSE SOURCES "src/linux64/*.cpp" "src/linuxCommon/*.cpp")
endif()
if (CMAKE_SYSTEM_PROCESSOR MATCHES "riscv64")
file(GLOB_RECURSE SOURCES "src/linuxRV64/*.cpp" "src/linuxCommon/*.cpp")
endif()
add_library(FunctionHook SHARED ${SOURCES})
endif()

View File

@@ -1,34 +1,95 @@
#pragma once
#include <cstdint>
#include <type_traits>
#include <vector>
namespace FunctionHooking {
class Detour {
private:
void* source = nullptr; //The function to be hooked.
void* destination = nullptr; //The function we are redirecting execution to.
uint8_t overwritten_bytes[13] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
uint8_t hook_bytes[13] = {0x49, 0xBA, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,0x41, 0xFF, 0xE2};
void createHook(void* source_address, void* destination_address);
public:
void removeHook();
Detour(void* source_address, void* destination_address);
Detour() = default;
class Detour;
class VTable;
}
//TODO This is *technically* not thread safe.
template <typename T, typename... Args>
inline typename std::enable_if<!std::is_void<T>::value, T>::type callOriginal(Args... args) {
removeHook();
const T result = reinterpret_cast<T(*)(Args...)>(source)(args...);
createHook(source, destination);
return result;
}
class FunctionHooking::Detour {
private:
void* source = nullptr;
void* destination = nullptr;
std::vector<uint8_t> overwritten_bytes{};
std::vector<uint8_t> hook_bytes{};
private:
void CreateHook(void* source_address, void* destination_address);
void RemoveHook();
public:
/// @returns True if our hook is present.
/// @note This allows you to know if something has overwritten our hook.
[[nodiscard]] bool Valid();
template <typename T, typename... Args>
inline typename std::enable_if<std::is_void<T>::value, void>::type callOriginal(Args... args) {
removeHook();
reinterpret_cast<void(*)(Args...)>(source)(args...);
createHook(source, destination);
}
};
}
template <typename T, typename... Args>
inline typename std::enable_if<!std::is_void<T>::value, T>::type CallOriginal(Args... args) {
RemoveHook();
const T result = reinterpret_cast<T(*)(Args...)>(source)(args...);
CreateHook(source, destination);
return result;
}
template <typename T, typename... Args>
inline typename std::enable_if<std::is_void<T>::value, void>::type CallOriginal(Args... args) {
RemoveHook();
reinterpret_cast<void(*)(Args...)>(source)(args...);
CreateHook(source, destination);
}
public:
/// @returns True if the function is detour hooked.
/// @param function_address The function to check.
// TODO check for other instructions. There's a handful that are common.
static bool Hooked(void* function_address);
public:
/// Create a new detour hook.
/// @param source_address The function to be hooked.
/// @param destination_address The function we are redirecting execution to.
Detour(void* source_address, void* destination_address);
~Detour();
};
// This is *mostly* cpu inspecific.
// TODO test on Arm64 & Risc64.
class FunctionHooking::VTable {
private:
void* class_instance = nullptr;
void* destination = nullptr;
void* original = nullptr;
int vtable_offset = 0;
private:
void CreateHook(void* class_instance, int source_function_vtable_offset, void* destination);
void RemoveHook();
public:
template <typename T, typename... Args>
T CallOriginal(Args... args) { return reinterpret_cast<T(*)(Args...)>(original)(args...); }
public:
/// @returns True if our hook is present.
/// @note This allows you to know if something has overwritten our hook.
[[nodiscard]] bool Valid();
/// Create a new VTable hook.
/// @param class_instance The pointer to an instance of the class.
/// @param vtable_offset The position of the virtual function in the vtable for the *base class* of our target class instance.
/// @param destination The function that we are redirecting execution to.
/// @note vtable offsets are in the same order they appear in the class definition.
/// @note *every* instance of the class will be hooked. not just the instance you pass in.
/// @note if the class instance provided is destroyed before we remove our hook, We can't remove our hook anymore.
// TODO a constructor where we can pass in the function to be hooked and
// find it in the vtable so you don't have to provide the offset.
// This would only work if we know exactly what the class layout is, Like if we're running directly inside the process.
// But it's still a nice feature.
VTable(void* class_instance, int vtable_offset, void* destination);
~VTable();
public:
/// Checks for "Shadow VTable Hook" which is, some cheats will copy the entire vtable to a new
/// memory region and replace the pointer to the vtable on the target instance to that one.
/// The downfall of this is that it makes that instance of the class unique when it would be impossible otherwise.
/// @param class_instance_to_check The instance of our class we suspect to have been modified.
/// @param class_instance_to_compare_against A class instance of the exact same type created directly before this check.
/// @note Shoutouts to AimTux Fuzion for demonstrating how cheats do this.
// TODO provide some mechanism to detect the traditional VMT hook as-well *more tricky*
static bool Hooked(void* class_instance_to_check, void* class_instance_to_compare_against);
};

View File

@@ -3,36 +3,27 @@
using namespace FunctionHooking;
Detour voidDetour;
Detour stringDetour;
VTable* virtual_hook;
void original_void() {
std::cout << "Original void function." << std::endl;
}
class Base {
public:
virtual void test() {};
Base() = default;
};
void hook_void() {
std::cout << "Void hook function." << std::endl;
voidDetour.callOriginal<void>();
}
class Inherited : public Base {
public:
void test() override { std::cout << "test original" << std::endl; }
Inherited() = default;
};
std::string original_string(const std::string& string) {
std::cout << string + " original function." << std::endl;
return string;
}
std::string hook_string(const std::string& string) {
std::cout << string + " hook function." << std::endl;
return stringDetour.callOriginal<std::string>(string);
}
void vtable_hook_function() { std::cout << "vtable hook function." << std::endl; virtual_hook->CallOriginal<void>(); }
int main() {
//Set up hooks.
voidDetour = FunctionHooking::Detour((void*) original_void, (void*) hook_void);
stringDetour = FunctionHooking::Detour((void*) original_string, (void*) hook_string);
original_void();
std::cout << std::endl;
original_string("String");
return 0;
auto* some_class = new Inherited();
virtual_hook = new VTable(some_class, 0, (void*) vtable_hook_function );
some_class->test();
std::cout << virtual_hook->Valid() << std::endl;
delete virtual_hook;
delete some_class;
}

View File

@@ -2,13 +2,19 @@
#include <sys/mman.h>
#include <csignal>
#include <cstring>
#include <array>
void FunctionHooking::Detour::createHook(void* source_address, void* destination_address) {
source = source_address;
destination = destination_address;
using namespace FunctionHooking;
constexpr std::array<uint8_t, 13> empty_hook = { 0x49, 0xBA, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x41, 0xFF, 0xE2 };
void Detour::CreateHook(void* source_address, void* destination_address) {
hook_bytes.resize(empty_hook.size());
memcpy(hook_bytes.data(), empty_hook.data(), hook_bytes.size());
overwritten_bytes.resize(hook_bytes.size());
//Save the bytes to be overwritten from the function prelude.
memcpy(&overwritten_bytes, source_address, 13);
memcpy(overwritten_bytes.data(), source_address, hook_bytes.size());
//Put the destination address into the jump.
memcpy(&hook_bytes[2], &destination_address, sizeof(void*));
@@ -17,24 +23,59 @@ void FunctionHooking::Detour::createHook(void* source_address, void* destination
mprotect((void*)((uintptr_t)source_address & ~(sysconf(_SC_PAGE_SIZE)-1)), sysconf(_SC_PAGE_SIZE), PROT_READ|PROT_WRITE|PROT_EXEC);
//Write the jmp to the beginning of the function prelude.
memcpy(source_address, hook_bytes, sizeof(hook_bytes));
memcpy(source_address, hook_bytes.data(), hook_bytes.size());
//Make the memory page non-writable again.
mprotect((void*)((uintptr_t)source_address & ~(sysconf(_SC_PAGE_SIZE)-1)), sysconf(_SC_PAGE_SIZE), PROT_READ|PROT_EXEC);
}
void FunctionHooking::Detour::removeHook() {
bool Detour::Hooked(void* function_address) {
std::vector<uint8_t> current_data(empty_hook.size());
memcpy(current_data.data(), function_address, current_data.size());
//Make the target memory page writable.
mprotect((void*)((uintptr_t)source & ~(sysconf(_SC_PAGE_SIZE)-1)), sysconf(_SC_PAGE_SIZE), PROT_READ|PROT_WRITE|PROT_EXEC);
//Replace our hook with the bytes that were originally there.
memcpy(source, &overwritten_bytes, 13);
//Make the memory page non-writable again.
mprotect((void*)((uintptr_t)source & ~(sysconf(_SC_PAGE_SIZE)-1)), sysconf(_SC_PAGE_SIZE), PROT_READ|PROT_EXEC);
if (current_data[0] == empty_hook[0] && current_data[1] == empty_hook[1] && current_data[10] == empty_hook[10]
&& current_data[11] == empty_hook[11] && current_data[12] == empty_hook[12])
return true;
return false;
}
FunctionHooking::Detour::Detour(void *source_address, void *destination_address) {
createHook(source_address, destination_address);
void VTable::CreateHook(void* class_inst, int offset, void* dest) {
class_instance = class_inst;
destination = dest;
vtable_offset = offset;
intptr_t vtable = *((intptr_t*) class_inst);
intptr_t entry = vtable + sizeof(intptr_t) * offset;
original = (intptr_t*) *((intptr_t*) entry);
mprotect((void*) ((uintptr_t) entry & ~(sysconf(_SC_PAGE_SIZE)-1)), sysconf(_SC_PAGE_SIZE), PROT_READ| PROT_WRITE| PROT_EXEC);
*((intptr_t *) entry) = (intptr_t) dest;
mprotect((void*) ((uintptr_t) entry & ~(sysconf(_SC_PAGE_SIZE)-1)), sysconf(_SC_PAGE_SIZE), PROT_READ | PROT_EXEC);
}
void VTable::RemoveHook() {
intptr_t vtable = *((intptr_t*) class_instance);
intptr_t entry = vtable + sizeof(intptr_t) * vtable_offset;
mprotect((void*) ((uintptr_t) entry & ~(sysconf(_SC_PAGE_SIZE)-1)), sysconf(_SC_PAGE_SIZE), PROT_READ| PROT_WRITE| PROT_EXEC);
*((intptr_t *) entry) = (intptr_t) original;
mprotect((void*) ((uintptr_t) entry & ~(sysconf(_SC_PAGE_SIZE)-1)), sysconf(_SC_PAGE_SIZE), PROT_READ | PROT_EXEC);
}
VTable::VTable(void* class_instance, int vtable_offset, void* destination) {
CreateHook(class_instance, vtable_offset, destination);
}
VTable::~VTable() {
if (Valid())
RemoveHook();
}
bool VTable::Valid() {
intptr_t vtable = *((intptr_t*) class_instance);
intptr_t entry = vtable + sizeof(intptr_t) * vtable_offset;
return *((intptr_t*) entry) == (intptr_t) destination;
}
bool VTable::Hooked(void* class_instance_to_check, void* class_instance_to_compare_against) {
return *reinterpret_cast<void**>(class_instance_to_check) != *reinterpret_cast<void**>(class_instance_to_compare_against);
}

39
src/linuxCommon/Hook.cpp Normal file
View File

@@ -0,0 +1,39 @@
#include <FunctionHook/Hook.h>
#include <sys/mman.h>
#include <csignal>
#include <cstring>
using namespace FunctionHooking;
void Detour::RemoveHook() {
//Make the target memory page writable.
mprotect((void*)((uintptr_t)source & ~(sysconf(_SC_PAGE_SIZE)-1)), sysconf(_SC_PAGE_SIZE), PROT_READ|PROT_WRITE|PROT_EXEC);
//Replace our hook with the bytes that were originally there.
memcpy(source, overwritten_bytes.data(), overwritten_bytes.size());
//Make the memory page non-writable again.
mprotect((void*)((uintptr_t)source & ~(sysconf(_SC_PAGE_SIZE)-1)), sysconf(_SC_PAGE_SIZE), PROT_READ|PROT_EXEC);
}
Detour::Detour(void* source_address, void* destination_address) : source(source_address), destination(destination_address) {
CreateHook(source, destination);
}
bool FunctionHooking::Detour::Valid() {
if (source == nullptr || destination == nullptr)
return false;
if (hook_bytes.empty() || overwritten_bytes.empty())
return false;
std::vector<uint8_t> current_data(hook_bytes.size());
memcpy(current_data.data(), source, current_data.size());
return current_data == hook_bytes;
}
Detour::~Detour() {
if (Valid())
RemoveHook();
}

41
src/linuxRV64/Hook.cpp Normal file
View File

@@ -0,0 +1,41 @@
#include <FunctionHook/Hook.h>
#include <sys/mman.h>
#include <csignal>
#include <cstring>
constexpr uint32_t JAL_OPCODE = 0b1101111;
constexpr int JAL_OFFSET_SHIFT = 12;
constexpr int JAL_MASK = 0xfff;
void FunctionHooking::Detour::CreateHook(void* source_address, void* destination_address) {
intptr_t target_addr = reinterpret_cast<intptr_t>(source_address);
intptr_t hook_addr = reinterpret_cast<intptr_t>(destination_address);
int32_t offset = (hook_addr - target_addr) >> 1;
// Encode the offset for the JAL instruction
uint32_t instruction = (JAL_OPCODE & JAL_MASK) |
((offset & 0x7fe) << 20) | // bits 1 to 10 of offset
((offset & 0x800) << 9) | // bit 11 of offset
((offset & 0xff000) << 9); // bits 12 to 19 of offset
hook_bytes.resize(sizeof(instruction));
std::memcpy(hook_bytes.data(), &instruction, sizeof(instruction));
overwritten_bytes.resize(sizeof(instruction));
std::memcpy(overwritten_bytes.data(), source_address, sizeof(instruction));
// Determine the page size and page containing the target function
long page_size = sysconf(_SC_PAGESIZE);
void* page_start = reinterpret_cast<void*>(target_addr & ~(page_size - 1));
// Change memory protection to allow writing
if (mprotect(page_start, page_size, PROT_READ | PROT_WRITE | PROT_EXEC) == -1)
return;
// Write the instruction to the target function
std::memcpy(source_address, &instruction, sizeof(instruction));
// Restore original memory protection
if (mprotect(page_start, page_size, PROT_READ | PROT_EXEC) == -1)
return;
}