├── .gitignore ├── CMakeLists.txt ├── CMakeSettings.json ├── README.md ├── tests.cpp └── vfptr_swap.hpp /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files 2 | **/.DS_Store 3 | *.slo 4 | *.lo 5 | *.o 6 | *.obj 7 | 8 | # Precompiled Headers 9 | *.gch 10 | *.pch 11 | 12 | # Compiled Dynamic libraries 13 | *.so 14 | *.dylib 15 | *.dll 16 | 17 | # Fortran module files 18 | *.mod 19 | *.smod 20 | 21 | # Compiled Static libraries 22 | *.lai 23 | *.la 24 | *.a 25 | *.lib 26 | 27 | # Executables 28 | *.exe 29 | *.out 30 | *.app 31 | 32 | **/cmake-build-debug 33 | **/CMakeCache.txt 34 | **/cmake_install.cmake 35 | **/install_manifest.txt 36 | **/CMakeFiles/ 37 | **/CTestTestfile.cmake 38 | **/Makefile 39 | **/*.cbp 40 | **/CMakeScripts 41 | **/compile_commands.json 42 | 43 | include/divisible/* 44 | 45 | 46 | ## Local 47 | 48 | .idea/*.xml 49 | 50 | build/**/* 51 | 52 | include/* 53 | lib/* 54 | bin/* 55 | test/test_runner 56 | out/* 57 | .vs/* -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required (VERSION 3.8) 2 | 3 | set (CMAKE_CXX_STANDARD 20) 4 | 5 | project ("vfptr_swap") 6 | 7 | find_package(Catch2 CONFIG REQUIRED) 8 | add_executable (tests tests.cpp) 9 | target_link_libraries(tests PRIVATE Catch2::Catch2) 10 | 11 | include(CTest) 12 | include(Catch) 13 | catch_discover_tests(tests) 14 | -------------------------------------------------------------------------------- /CMakeSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "configurations": [ 3 | { 4 | "name": "x64-Clang-Debug", 5 | "generator": "Ninja", 6 | "configurationType": "Debug", 7 | "buildRoot": "${projectDir}\\out\\build\\${name}", 8 | "installRoot": "${projectDir}\\out\\install\\${name}", 9 | "cmakeCommandArgs": "", 10 | "buildCommandArgs": "", 11 | "ctestCommandArgs": "", 12 | "inheritEnvironments": [ "clang_cl_x64_x64" ] 13 | }, 14 | { 15 | "name": "x64-Clang-Release", 16 | "generator": "Ninja", 17 | "configurationType": "RelWithDebInfo", 18 | "buildRoot": "${projectDir}\\out\\build\\${name}", 19 | "installRoot": "${projectDir}\\out\\install\\${name}", 20 | "cmakeCommandArgs": "", 21 | "buildCommandArgs": "", 22 | "ctestCommandArgs": "", 23 | "inheritEnvironments": [ "clang_cl_x64_x64" ] 24 | }, 25 | { 26 | "name": "x86-Clang-Debug", 27 | "generator": "Ninja", 28 | "configurationType": "Debug", 29 | "buildRoot": "${projectDir}\\out\\build\\${name}", 30 | "installRoot": "${projectDir}\\out\\install\\${name}", 31 | "cmakeCommandArgs": "", 32 | "buildCommandArgs": "", 33 | "ctestCommandArgs": "", 34 | "inheritEnvironments": [ "clang_cl_x86" ] 35 | }, 36 | { 37 | "name": "x86-Clang-Release", 38 | "generator": "Ninja", 39 | "configurationType": "RelWithDebInfo", 40 | "buildRoot": "${projectDir}\\out\\build\\${name}", 41 | "installRoot": "${projectDir}\\out\\install\\${name}", 42 | "cmakeCommandArgs": "", 43 | "ctestCommandArgs": "", 44 | "inheritEnvironments": [ "clang_cl_x86" ] 45 | }, 46 | { 47 | "name": "x86-Debug", 48 | "generator": "Ninja", 49 | "configurationType": "Debug", 50 | "buildRoot": "${projectDir}\\out\\build\\${name}", 51 | "installRoot": "${projectDir}\\out\\install\\${name}", 52 | "cmakeCommandArgs": "", 53 | "buildCommandArgs": "", 54 | "ctestCommandArgs": "", 55 | "inheritEnvironments": [ "msvc_x86" ] 56 | }, 57 | { 58 | "name": "x86-Release", 59 | "generator": "Ninja", 60 | "configurationType": "RelWithDebInfo", 61 | "buildRoot": "${projectDir}\\out\\build\\${name}", 62 | "installRoot": "${projectDir}\\out\\install\\${name}", 63 | "cmakeCommandArgs": "", 64 | "buildCommandArgs": "", 65 | "ctestCommandArgs": "", 66 | "inheritEnvironments": [ "msvc_x86" ] 67 | }, 68 | { 69 | "name": "x64-Debug", 70 | "generator": "Ninja", 71 | "configurationType": "Debug", 72 | "buildRoot": "${projectDir}\\out\\build\\${name}", 73 | "installRoot": "${projectDir}\\out\\install\\${name}", 74 | "cmakeCommandArgs": "", 75 | "buildCommandArgs": "", 76 | "ctestCommandArgs": "", 77 | "inheritEnvironments": [ "msvc_x64_x64" ] 78 | }, 79 | { 80 | "name": "x64-Release", 81 | "generator": "Ninja", 82 | "configurationType": "RelWithDebInfo", 83 | "buildRoot": "${projectDir}\\out\\build\\${name}", 84 | "installRoot": "${projectDir}\\out\\install\\${name}", 85 | "cmakeCommandArgs": "", 86 | "buildCommandArgs": "", 87 | "ctestCommandArgs": "", 88 | "inheritEnvironments": [ "msvc_x64_x64" ] 89 | } 90 | ] 91 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Single-header C++ VMT hooking (vfptr swap) 2 | 3 | * Supports RAII. 4 | * Unit tested with Catch2. 5 | * Tested on x86/x64, MSVC and Clang/LLVM. 6 | * VMT size calculation. 7 | * Copies the RTTI object locator to ensure `dynamic_cast` won't break. 8 | 9 | Usage: 10 | 11 | ```cpp 12 | // Assumption: c_example looks like this 13 | // 14 | // class c_example 15 | // { 16 | // public: 17 | // virtual ~c_example() = 0; 18 | // virtual void print() = 0; 19 | // }; 20 | 21 | uintptr_t example_object = *reinterpret_cast(0x0CA7F00D); 22 | vfptr_swap_t swapper(example_object); 23 | 24 | class c_hooked_example 25 | { 26 | public: 27 | void print(const char* text) 28 | { 29 | printf("c_example::print intercepted. og text: %s\n", text); 30 | Sleep(500); 31 | 32 | // *swapper returns the original object, although we could substitute it with example_object in this case 33 | swapper.original(1)(*swapper, text); 34 | } 35 | }; 36 | 37 | void init() 38 | { 39 | swapper[1] = vmt::cfunc(&c_hooked_example::print); 40 | } 41 | ``` 42 | -------------------------------------------------------------------------------- /tests.cpp: -------------------------------------------------------------------------------- 1 | #include "vfptr_swap.hpp" 2 | 3 | #define CATCH_CONFIG_MAIN 4 | #include "catch2/catch.hpp" 5 | 6 | class parent_t 7 | { 8 | private: 9 | int padding[0x10]; 10 | 11 | public: 12 | virtual ~parent_t() = default; 13 | 14 | virtual auto generate_random_number(int low_bound = 0, int high_bound = 100) -> int 15 | { 16 | return (std::rand() % (high_bound - low_bound + 1)) + low_bound; 17 | } 18 | 19 | virtual auto is_padding_size_correct() const -> bool 20 | { 21 | return sizeof(padding) / sizeof(padding[0]) == 0x10; 22 | } 23 | 24 | virtual auto always_returns_true() const -> bool 25 | { 26 | return true; 27 | } 28 | }; 29 | 30 | class child_t : public parent_t 31 | { 32 | public: 33 | virtual int generate_random_number(int low_bound = 0, int high_bound = 100) 34 | { 35 | throw std::logic_error("unimplemented"); 36 | } 37 | 38 | // pad an extra function to test ::size() 39 | virtual void empty() {} 40 | }; 41 | 42 | vfptr_swap_t* g_swapper = nullptr; 43 | 44 | class hooked_child_t 45 | { 46 | public: 47 | auto get_rng(int a1, int a2) -> int 48 | { 49 | return 1337; 50 | } 51 | 52 | auto ret_false_classmember() -> bool 53 | { 54 | printf("ret_false_classmember | orig returned %d\n", g_swapper->original(2)(**g_swapper)); 55 | 56 | return false; 57 | } 58 | }; 59 | 60 | bool __fastcall ret_false_static(uintptr_t ecx, uintptr_t) 61 | { 62 | return false; 63 | } 64 | 65 | TEST_CASE() 66 | { 67 | child_t child; 68 | auto _child = &child; // if we call functions as `child.func()` then the compiler won't generate a virtual call but rather call the class member function, passing (ecx, args...) to it 69 | g_swapper = new vfptr_swap_t(_child); // global ptr so we can use it to call the original through the hooked functions 70 | REQUIRE(g_swapper->size() == 5); 71 | 72 | (*g_swapper)[1] = vmt::cfunc(&hooked_child_t::get_rng); 73 | REQUIRE(_child->generate_random_number() == 1337); 74 | 75 | (*g_swapper)[2] = vmt::cfunc(&hooked_child_t::ret_false_classmember); 76 | REQUIRE(!_child->is_padding_size_correct()); 77 | 78 | delete g_swapper; // vfptr_swap_t dtor is called, no hooks are applied 79 | 80 | // func should throw an "unimplemented" exception when unhooked 81 | bool thrown = false; 82 | try { _child->generate_random_number(); } 83 | catch(const std::logic_error&) { thrown = true; } 84 | 85 | REQUIRE(thrown); 86 | REQUIRE(_child->is_padding_size_correct()); 87 | 88 | // RAII is supported as well 89 | parent_t parent; 90 | auto _parent = &parent; 91 | { 92 | vfptr_swap_t swapper(_parent); 93 | REQUIRE(swapper.size() == 4); 94 | swapper[3] = vmt::cfunc(ret_false_static); 95 | REQUIRE(!_parent->always_returns_true()); 96 | } 97 | 98 | REQUIRE(_parent->always_returns_true()); 99 | } 100 | -------------------------------------------------------------------------------- /vfptr_swap.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | #ifdef _WIN32 8 | #define WIN32_LEAN_AND_MEAN 9 | #include 10 | #endif 11 | 12 | namespace vmt 13 | { 14 | template 15 | auto cfunc(T fn) -> uintptr_t 16 | { 17 | union UT { T fn; uintptr_t ptr; } u; 18 | u.fn = fn; 19 | 20 | return u.ptr; 21 | } 22 | 23 | auto calc_length(uintptr_t* vmt) -> std::size_t 24 | { 25 | auto is_valid_page = [](uintptr_t ptr) -> bool 26 | { 27 | #ifdef _WIN32 28 | MEMORY_BASIC_INFORMATION mbi{}; 29 | ::VirtualQuery(reinterpret_cast(ptr), &mbi, sizeof(mbi)); 30 | 31 | return 32 | (mbi.Protect & (PAGE_EXECUTE | PAGE_EXECUTE_READ | PAGE_EXECUTE_READWRITE | PAGE_EXECUTE_WRITECOPY)) > 0 && // executable 33 | (mbi.Protect & (PAGE_NOACCESS | PAGE_GUARD)) == 0; // no page protections block our access to read/execute 34 | #else 35 | // Not 100% accurate. In POSIX, I have to manually parse /proc/self/maps and I'm not bothering. 36 | // It does not need to be 100% accurate though. We just need to copy *enough* virtual functions! More won't hurt. 37 | return ptr != 0 && *reinterpret_cast(ptr) != nullptr; 38 | #endif 39 | }; 40 | 41 | std::size_t len = 0; 42 | 43 | while(is_valid_page(*vmt)) 44 | { 45 | len++; 46 | vmt++; 47 | } 48 | 49 | return len; 50 | } 51 | } 52 | 53 | template 54 | class vfptr_swap_t 55 | { 56 | private: 57 | T* obj; 58 | uintptr_t* orig_vfptr; 59 | std::unique_ptr vmt; 60 | std::size_t vmt_size; 61 | 62 | public: 63 | // vmt_size being set to 0 means that we're going to count the size of the methods in the VMT. 64 | // My VMT size calculation method is not heavily tested enough. Results might differ for certain VMT sizes due to alignment with 0s/the next VMT. 65 | // If the VMT's size is known, please specify it instead. 66 | vfptr_swap_t(T* _obj, std::size_t _vmt_size = 0) : obj(_obj), vmt_size(_vmt_size) 67 | { 68 | this->orig_vfptr = *reinterpret_cast(obj); 69 | 70 | if(_vmt_size == 0) 71 | { 72 | this->vmt_size = vmt::calc_length(this->orig_vfptr); 73 | } 74 | 75 | // Accounting for RTTI. 76 | this->vmt = std::make_unique(this->vmt_size + 1); 77 | std::copy(this->orig_vfptr - 1, this->orig_vfptr + this->vmt_size, &this->vmt[0]); 78 | *reinterpret_cast(this->obj) = reinterpret_cast(&this->vmt[1]); 79 | } 80 | 81 | // Restores original vfptr. 82 | ~vfptr_swap_t() 83 | { 84 | *reinterpret_cast(this->obj) = reinterpret_cast(this->orig_vfptr); 85 | this->vmt = nullptr; 86 | } 87 | 88 | // Returns the amount of virtual functions in the VMT. 89 | auto size() const -> std::size_t 90 | { 91 | return this->vmt_size; 92 | } 93 | 94 | // Returns a pointer to the beginning of the copied VMT. 95 | auto get_vmt() const -> uintptr_t* 96 | { 97 | return &this->vmt[1]; 98 | } 99 | 100 | // Retrieves the RTTI object locator, if exists. Otherwise, returned data might be garbage. 101 | auto get_rtti() const -> uintptr_t* 102 | { 103 | return &this->get_vmt()[-1]; 104 | } 105 | 106 | // Returns a virtual function by its index, from the copied VMT. 107 | // The returned virtual function will be hooked if it was replaced by the hooker, otherwise the original virtual function will be returned. 108 | auto operator[](std::size_t idx) const -> uintptr_t& 109 | { 110 | return this->get_vmt()[idx]; 111 | } 112 | 113 | // Returns the object that is hooked. 114 | auto operator*() -> T* 115 | { 116 | return obj; 117 | } 118 | 119 | // Retrieves a specified function pointer from the original VMT. 120 | template 121 | auto original(std::size_t idx) const -> Y 122 | { 123 | return reinterpret_cast(this->orig_vfptr[idx]); 124 | } 125 | }; 126 | --------------------------------------------------------------------------------