├── .gitignore ├── CMakeLists.txt ├── README.md ├── cmake.toml ├── cmkr.cmake ├── src ├── Hooker.cpp ├── Hooker.hpp ├── Inspector.cpp ├── Inspector.hpp ├── Main.cpp ├── StringReferences.cpp ├── StringReferences.hpp ├── VtableManager.cpp └── VtableManager.hpp └── vcpkg.json /.gitignore: -------------------------------------------------------------------------------- 1 | # Prerequisites 2 | *.d 3 | 4 | # Compiled Object files 5 | *.slo 6 | *.lo 7 | *.o 8 | *.obj 9 | 10 | # Precompiled Headers 11 | *.gch 12 | *.pch 13 | 14 | # Compiled Dynamic libraries 15 | *.so 16 | *.dylib 17 | *.dll 18 | 19 | # Fortran module files 20 | *.mod 21 | *.smod 22 | 23 | # Compiled Static libraries 24 | *.lai 25 | *.la 26 | *.a 27 | *.lib 28 | 29 | # Executables 30 | *.exe 31 | *.out 32 | *.app 33 | 34 | build/* 35 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # This file is automatically generated from cmake.toml - DO NOT EDIT 2 | # See https://github.com/build-cpp/cmkr for more information 3 | 4 | cmake_minimum_required(VERSION 3.15) 5 | 6 | if(CMAKE_SOURCE_DIR STREQUAL CMAKE_BINARY_DIR) 7 | message(FATAL_ERROR "In-tree builds are not supported. Run CMake from a separate directory: cmake -B build") 8 | endif() 9 | 10 | set(CMKR_ROOT_PROJECT OFF) 11 | if(CMAKE_CURRENT_SOURCE_DIR STREQUAL CMAKE_SOURCE_DIR) 12 | set(CMKR_ROOT_PROJECT ON) 13 | 14 | # Bootstrap cmkr and automatically regenerate CMakeLists.txt 15 | include(cmkr.cmake OPTIONAL RESULT_VARIABLE CMKR_INCLUDE_RESULT) 16 | if(CMKR_INCLUDE_RESULT) 17 | cmkr() 18 | endif() 19 | 20 | # Enable folder support 21 | set_property(GLOBAL PROPERTY USE_FOLDERS ON) 22 | 23 | # Create a configure-time dependency on cmake.toml to improve IDE support 24 | configure_file(cmake.toml cmake.toml COPYONLY) 25 | endif() 26 | 27 | add_compile_options($<$:/MP>) 28 | set(VCPKG_TARGET_TRIPLET x64-windows-static) 29 | 30 | project(template-project) 31 | 32 | set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} /MP") 33 | set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /MP") 34 | set(ASMJIT_STATIC ON CACHE BOOL "" FORCE) 35 | if ("${CMAKE_BUILD_TYPE}" MATCHES "Release") 36 | set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} /MT") 37 | set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /MT") 38 | 39 | # Statically compile runtime 40 | string(REGEX REPLACE "/MD" "/MT" CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS}") 41 | string(REGEX REPLACE "/MD" "/MT" CMAKE_C_FLAGS "${CMAKE_C_FLAGS}") 42 | string(REGEX REPLACE "/MD" "/MT" CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE}") 43 | string(REGEX REPLACE "/MD" "/MT" CMAKE_C_FLAGS_RELEASE "${CMAKE_C_FLAGS_RELEASE}") 44 | set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded") 45 | message(NOTICE "Building in Release mode") 46 | endif() 47 | 48 | if(CMKR_ROOT_PROJECT AND NOT CMKR_DISABLE_VCPKG) 49 | include(FetchContent) 50 | # Fix warnings about DOWNLOAD_EXTRACT_TIMESTAMP 51 | if(POLICY CMP0135) 52 | cmake_policy(SET CMP0135 NEW) 53 | endif() 54 | message(STATUS "Fetching vcpkg (2023.12.12)...") 55 | FetchContent_Declare(vcpkg URL "https://github.com/microsoft/vcpkg/archive/refs/tags/2023.12.12.tar.gz") 56 | FetchContent_GetProperties(vcpkg) 57 | if(NOT vcpkg_POPULATED) 58 | FetchContent_Populate(vcpkg) 59 | if(CMAKE_HOST_SYSTEM_NAME STREQUAL Darwin AND CMAKE_OSX_ARCHITECTURES STREQUAL "") 60 | set(CMAKE_OSX_ARCHITECTURES ${CMAKE_HOST_SYSTEM_PROCESSOR} CACHE STRING "" FORCE) 61 | endif() 62 | include("${vcpkg_SOURCE_DIR}/scripts/buildsystems/vcpkg.cmake") 63 | endif() 64 | endif() 65 | 66 | # Packages 67 | find_package(imgui REQUIRED) 68 | 69 | find_package(glad REQUIRED) 70 | 71 | find_package(glfw3 REQUIRED) 72 | 73 | include(FetchContent) 74 | 75 | message(STATUS "Fetching spdlog (ad0e89cbfb4d0c1ce4d097e134eb7be67baebb36)...") 76 | FetchContent_Declare(spdlog 77 | GIT_REPOSITORY 78 | "https://github.com/gabime/spdlog" 79 | GIT_TAG 80 | ad0e89cbfb4d0c1ce4d097e134eb7be67baebb36 81 | ) 82 | FetchContent_MakeAvailable(spdlog) 83 | 84 | message(STATUS "Fetching bddisasm (v1.34.10)...") 85 | FetchContent_Declare(bddisasm 86 | GIT_REPOSITORY 87 | "https://github.com/bitdefender/bddisasm" 88 | GIT_TAG 89 | v1.34.10 90 | ) 91 | FetchContent_MakeAvailable(bddisasm) 92 | 93 | message(STATUS "Fetching kananlib (main)...") 94 | FetchContent_Declare(kananlib 95 | GIT_REPOSITORY 96 | "https://github.com/cursey/kananlib.git" 97 | GIT_TAG 98 | main 99 | ) 100 | FetchContent_MakeAvailable(kananlib) 101 | 102 | set(SAFETYHOOK_FETCH_ZYDIS ON) 103 | 104 | message(STATUS "Fetching safetyhook (main)...") 105 | FetchContent_Declare(safetyhook 106 | GIT_REPOSITORY 107 | "https://github.com/cursey/safetyhook" 108 | GIT_TAG 109 | main 110 | ) 111 | FetchContent_MakeAvailable(safetyhook) 112 | 113 | message(STATUS "Fetching json (bc889afb4c5bf1c0d8ee29ef35eaaf4c8bef8a5d)...") 114 | FetchContent_Declare(json 115 | GIT_REPOSITORY 116 | "https://github.com/nlohmann/json" 117 | GIT_TAG 118 | bc889afb4c5bf1c0d8ee29ef35eaaf4c8bef8a5d 119 | ) 120 | FetchContent_MakeAvailable(json) 121 | 122 | set(TRACY_STATIC ON CACHE BOOL "" FORCE) 123 | set(TRACY_ENABLE OFF CACHE BOOL "" FORCE) 124 | 125 | message(STATUS "Fetching tracy (897aec5b062664d2485f4f9a213715d2e527e0ca)...") 126 | FetchContent_Declare(tracy 127 | GIT_REPOSITORY 128 | "https://github.com/wolfpld/tracy" 129 | GIT_TAG 130 | 897aec5b062664d2485f4f9a213715d2e527e0ca 131 | ) 132 | FetchContent_MakeAvailable(tracy) 133 | 134 | # Target: vtablemonitor 135 | set(vtablemonitor_SOURCES 136 | cmake.toml 137 | "src/Hooker.cpp" 138 | "src/Hooker.hpp" 139 | "src/Inspector.cpp" 140 | "src/Inspector.hpp" 141 | "src/Main.cpp" 142 | "src/StringReferences.cpp" 143 | "src/StringReferences.hpp" 144 | "src/VtableManager.cpp" 145 | "src/VtableManager.hpp" 146 | ) 147 | 148 | add_library(vtablemonitor SHARED) 149 | 150 | target_sources(vtablemonitor PRIVATE ${vtablemonitor_SOURCES}) 151 | source_group(TREE ${CMAKE_CURRENT_SOURCE_DIR} FILES ${vtablemonitor_SOURCES}) 152 | 153 | target_compile_features(vtablemonitor PUBLIC 154 | cxx_std_23 155 | ) 156 | 157 | target_compile_options(vtablemonitor PUBLIC 158 | "/GS-" 159 | "/bigobj" 160 | "/EHa" 161 | "/MP" 162 | ) 163 | 164 | target_include_directories(vtablemonitor PUBLIC 165 | "src/" 166 | "include/" 167 | ) 168 | 169 | target_link_libraries(vtablemonitor PUBLIC 170 | kananlib 171 | safetyhook 172 | spdlog 173 | imgui::imgui 174 | glad::glad 175 | glfw 176 | ) 177 | 178 | set_target_properties(vtablemonitor PROPERTIES 179 | OUTPUT_NAME 180 | vtable-monitor 181 | RUNTIME_OUTPUT_DIRECTORY_RELEASE 182 | "${CMAKE_BINARY_DIR}/bin/${CMKR_TARGET}" 183 | RUNTIME_OUTPUT_DIRECTORY_RELWITHDEBINFO 184 | "${CMAKE_BINARY_DIR}/bin/${CMKR_TARGET}" 185 | LIBRARY_OUTPUT_DIRECTORY_RELEASE 186 | "${CMAKE_BINARY_DIR}/lib/${CMKR_TARGET}" 187 | LIBRARY_OUTPUT_DIRECTORY_RELWITHDEBINFO 188 | "${CMAKE_BINARY_DIR}/lib/${CMKR_TARGET}" 189 | ARCHIVE_OUTPUT_DIRECTORY_RELEASE 190 | "${CMAKE_BINARY_DIR}/lib/${CMKR_TARGET}" 191 | ARCHIVE_OUTPUT_DIRECTORY_RELWITHDEBINFO 192 | "${CMAKE_BINARY_DIR}/lib/${CMKR_TARGET}" 193 | ) 194 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vtable-monitor 2 | 3 | Injected DLL. Personal project, unorganized and experimental. 4 | -------------------------------------------------------------------------------- /cmake.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "template-project" 3 | cmake-before=""" 4 | add_compile_options($<$:/MP>) 5 | set(VCPKG_TARGET_TRIPLET x64-windows-static) 6 | """ 7 | 8 | cmake-after = """ 9 | set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} /MP") 10 | set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /MP") 11 | set(ASMJIT_STATIC ON CACHE BOOL "" FORCE) 12 | if ("${CMAKE_BUILD_TYPE}" MATCHES "Release") 13 | set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} /MT") 14 | set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /MT") 15 | 16 | # Statically compile runtime 17 | string(REGEX REPLACE "/MD" "/MT" CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS}") 18 | string(REGEX REPLACE "/MD" "/MT" CMAKE_C_FLAGS "${CMAKE_C_FLAGS}") 19 | string(REGEX REPLACE "/MD" "/MT" CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE}") 20 | string(REGEX REPLACE "/MD" "/MT" CMAKE_C_FLAGS_RELEASE "${CMAKE_C_FLAGS_RELEASE}") 21 | set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded") 22 | message(NOTICE "Building in Release mode") 23 | endif() 24 | """ 25 | 26 | [vcpkg] 27 | version = "2023.12.12" 28 | packages = [ 29 | "imgui[docking-experimental,freetype,glfw-binding,opengl3-binding]", 30 | "glad[gl-api-30]", 31 | "glfw3" 32 | ] 33 | 34 | [find-package] 35 | imgui = {} 36 | glad = {} 37 | glfw3 = {} 38 | 39 | [fetch-content] 40 | spdlog = { git = "https://github.com/gabime/spdlog", tag = "ad0e89cbfb4d0c1ce4d097e134eb7be67baebb36" } 41 | bddisasm = { git = "https://github.com/bitdefender/bddisasm", tag = "v1.34.10" } 42 | kananlib = { git = "https://github.com/cursey/kananlib.git", tag = "main" } 43 | 44 | [fetch-content.safetyhook] 45 | git = "https://github.com/cursey/safetyhook" 46 | tag = "main" 47 | cmake-before=""" 48 | set(SAFETYHOOK_FETCH_ZYDIS ON) 49 | """ 50 | 51 | [fetch-content.json] 52 | git = "https://github.com/nlohmann/json" 53 | tag = "bc889afb4c5bf1c0d8ee29ef35eaaf4c8bef8a5d" 54 | 55 | [fetch-content.tracy] 56 | git = "https://github.com/wolfpld/tracy" 57 | tag = "897aec5b062664d2485f4f9a213715d2e527e0ca" 58 | cmake-before=""" 59 | set(TRACY_STATIC ON CACHE BOOL "" FORCE) 60 | set(TRACY_ENABLE OFF CACHE BOOL "" FORCE) 61 | """ 62 | 63 | [target.vtablemonitor] 64 | type = "shared" 65 | sources = ["src/**.cpp", "src/**.c"] 66 | headers = ["src/**.hpp", "src/**.h"] 67 | include-directories = [ 68 | "src/", 69 | "include/", 70 | ] 71 | compile-options = ["/GS-", "/bigobj", "/EHa", "/MP"] 72 | compile-features = ["cxx_std_23"] 73 | compile-definitions = [] 74 | link-libraries = [ 75 | "kananlib", 76 | "safetyhook", 77 | "spdlog", 78 | "imgui::imgui", 79 | "glad::glad", 80 | "glfw", 81 | ] 82 | [target.vtablemonitor.properties] 83 | OUTPUT_NAME = "vtable-monitor" 84 | RUNTIME_OUTPUT_DIRECTORY_RELEASE = "${CMAKE_BINARY_DIR}/bin/${CMKR_TARGET}" 85 | RUNTIME_OUTPUT_DIRECTORY_RELWITHDEBINFO = "${CMAKE_BINARY_DIR}/bin/${CMKR_TARGET}" 86 | LIBRARY_OUTPUT_DIRECTORY_RELEASE = "${CMAKE_BINARY_DIR}/lib/${CMKR_TARGET}" 87 | LIBRARY_OUTPUT_DIRECTORY_RELWITHDEBINFO = "${CMAKE_BINARY_DIR}/lib/${CMKR_TARGET}" 88 | ARCHIVE_OUTPUT_DIRECTORY_RELEASE = "${CMAKE_BINARY_DIR}/lib/${CMKR_TARGET}" 89 | ARCHIVE_OUTPUT_DIRECTORY_RELWITHDEBINFO = "${CMAKE_BINARY_DIR}/lib/${CMKR_TARGET}" 90 | -------------------------------------------------------------------------------- /cmkr.cmake: -------------------------------------------------------------------------------- 1 | include_guard() 2 | 3 | # Change these defaults to point to your infrastructure if desired 4 | set(CMKR_REPO "https://github.com/build-cpp/cmkr" CACHE STRING "cmkr git repository" FORCE) 5 | set(CMKR_TAG "v0.2.34" CACHE STRING "cmkr git tag (this needs to be available forever)" FORCE) 6 | set(CMKR_COMMIT_HASH "" CACHE STRING "cmkr git commit hash (optional)" FORCE) 7 | 8 | # To bootstrap/generate a cmkr project: cmake -P cmkr.cmake 9 | if(CMAKE_SCRIPT_MODE_FILE) 10 | set(CMAKE_BINARY_DIR "${CMAKE_BINARY_DIR}/build") 11 | set(CMAKE_CURRENT_BINARY_DIR "${CMAKE_BINARY_DIR}") 12 | file(MAKE_DIRECTORY "${CMAKE_BINARY_DIR}") 13 | endif() 14 | 15 | # Set these from the command line to customize for development/debugging purposes 16 | set(CMKR_EXECUTABLE "" CACHE FILEPATH "cmkr executable") 17 | set(CMKR_SKIP_GENERATION OFF CACHE BOOL "skip automatic cmkr generation") 18 | set(CMKR_BUILD_TYPE "Debug" CACHE STRING "cmkr build configuration") 19 | mark_as_advanced(CMKR_REPO CMKR_TAG CMKR_COMMIT_HASH CMKR_EXECUTABLE CMKR_SKIP_GENERATION CMKR_BUILD_TYPE) 20 | 21 | # Disable cmkr if generation is disabled 22 | if(DEFINED ENV{CI} OR CMKR_SKIP_GENERATION OR CMKR_BUILD_SKIP_GENERATION) 23 | message(STATUS "[cmkr] Skipping automatic cmkr generation") 24 | unset(CMKR_BUILD_SKIP_GENERATION CACHE) 25 | macro(cmkr) 26 | endmacro() 27 | return() 28 | endif() 29 | 30 | # Disable cmkr if no cmake.toml file is found 31 | if(NOT CMAKE_SCRIPT_MODE_FILE AND NOT EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/cmake.toml") 32 | message(AUTHOR_WARNING "[cmkr] Not found: ${CMAKE_CURRENT_SOURCE_DIR}/cmake.toml") 33 | macro(cmkr) 34 | endmacro() 35 | return() 36 | endif() 37 | 38 | # Convert a Windows native path to CMake path 39 | if(CMKR_EXECUTABLE MATCHES "\\\\") 40 | string(REPLACE "\\" "/" CMKR_EXECUTABLE_CMAKE "${CMKR_EXECUTABLE}") 41 | set(CMKR_EXECUTABLE "${CMKR_EXECUTABLE_CMAKE}" CACHE FILEPATH "" FORCE) 42 | unset(CMKR_EXECUTABLE_CMAKE) 43 | endif() 44 | 45 | # Helper macro to execute a process (COMMAND_ERROR_IS_FATAL ANY is 3.19 and higher) 46 | function(cmkr_exec) 47 | execute_process(COMMAND ${ARGV} RESULT_VARIABLE CMKR_EXEC_RESULT) 48 | if(NOT CMKR_EXEC_RESULT EQUAL 0) 49 | message(FATAL_ERROR "cmkr_exec(${ARGV}) failed (exit code ${CMKR_EXEC_RESULT})") 50 | endif() 51 | endfunction() 52 | 53 | # Windows-specific hack (CMAKE_EXECUTABLE_PREFIX is not set at the moment) 54 | if(WIN32) 55 | set(CMKR_EXECUTABLE_NAME "cmkr.exe") 56 | else() 57 | set(CMKR_EXECUTABLE_NAME "cmkr") 58 | endif() 59 | 60 | # Use cached cmkr if found 61 | if(DEFINED ENV{CMKR_CACHE}) 62 | set(CMKR_DIRECTORY_PREFIX "$ENV{CMKR_CACHE}") 63 | string(REPLACE "\\" "/" CMKR_DIRECTORY_PREFIX "${CMKR_DIRECTORY_PREFIX}") 64 | if(NOT CMKR_DIRECTORY_PREFIX MATCHES "\\/$") 65 | set(CMKR_DIRECTORY_PREFIX "${CMKR_DIRECTORY_PREFIX}/") 66 | endif() 67 | # Build in release mode for the cache 68 | set(CMKR_BUILD_TYPE "Release") 69 | else() 70 | set(CMKR_DIRECTORY_PREFIX "${CMAKE_CURRENT_BINARY_DIR}/_cmkr_") 71 | endif() 72 | set(CMKR_DIRECTORY "${CMKR_DIRECTORY_PREFIX}${CMKR_TAG}") 73 | set(CMKR_CACHED_EXECUTABLE "${CMKR_DIRECTORY}/bin/${CMKR_EXECUTABLE_NAME}") 74 | 75 | # Helper function to check if a string starts with a prefix 76 | # Cannot use MATCHES, see: https://github.com/build-cpp/cmkr/issues/61 77 | function(cmkr_startswith str prefix result) 78 | string(LENGTH "${prefix}" prefix_length) 79 | string(LENGTH "${str}" str_length) 80 | if(prefix_length LESS_EQUAL str_length) 81 | string(SUBSTRING "${str}" 0 ${prefix_length} str_prefix) 82 | if(prefix STREQUAL str_prefix) 83 | set("${result}" ON PARENT_SCOPE) 84 | return() 85 | endif() 86 | endif() 87 | set("${result}" OFF PARENT_SCOPE) 88 | endfunction() 89 | 90 | # Handle upgrading logic 91 | if(CMKR_EXECUTABLE AND NOT CMKR_CACHED_EXECUTABLE STREQUAL CMKR_EXECUTABLE) 92 | cmkr_startswith("${CMKR_EXECUTABLE}" "${CMAKE_CURRENT_BINARY_DIR}/_cmkr" CMKR_STARTSWITH_BUILD) 93 | cmkr_startswith("${CMKR_EXECUTABLE}" "${CMKR_DIRECTORY_PREFIX}" CMKR_STARTSWITH_CACHE) 94 | if(CMKR_STARTSWITH_BUILD) 95 | if(DEFINED ENV{CMKR_CACHE}) 96 | message(AUTHOR_WARNING "[cmkr] Switching to cached cmkr: '${CMKR_CACHED_EXECUTABLE}'") 97 | if(EXISTS "${CMKR_CACHED_EXECUTABLE}") 98 | set(CMKR_EXECUTABLE "${CMKR_CACHED_EXECUTABLE}" CACHE FILEPATH "Full path to cmkr executable" FORCE) 99 | else() 100 | unset(CMKR_EXECUTABLE CACHE) 101 | endif() 102 | else() 103 | message(AUTHOR_WARNING "[cmkr] Upgrading '${CMKR_EXECUTABLE}' to '${CMKR_CACHED_EXECUTABLE}'") 104 | unset(CMKR_EXECUTABLE CACHE) 105 | endif() 106 | elseif(DEFINED ENV{CMKR_CACHE} AND CMKR_STARTSWITH_CACHE) 107 | message(AUTHOR_WARNING "[cmkr] Upgrading cached '${CMKR_EXECUTABLE}' to '${CMKR_CACHED_EXECUTABLE}'") 108 | unset(CMKR_EXECUTABLE CACHE) 109 | endif() 110 | endif() 111 | 112 | if(CMKR_EXECUTABLE AND EXISTS "${CMKR_EXECUTABLE}") 113 | message(VERBOSE "[cmkr] Found cmkr: '${CMKR_EXECUTABLE}'") 114 | elseif(CMKR_EXECUTABLE AND NOT CMKR_EXECUTABLE STREQUAL CMKR_CACHED_EXECUTABLE) 115 | message(FATAL_ERROR "[cmkr] '${CMKR_EXECUTABLE}' not found") 116 | elseif(NOT CMKR_EXECUTABLE AND EXISTS "${CMKR_CACHED_EXECUTABLE}") 117 | set(CMKR_EXECUTABLE "${CMKR_CACHED_EXECUTABLE}" CACHE FILEPATH "Full path to cmkr executable" FORCE) 118 | message(STATUS "[cmkr] Found cached cmkr: '${CMKR_EXECUTABLE}'") 119 | else() 120 | set(CMKR_EXECUTABLE "${CMKR_CACHED_EXECUTABLE}" CACHE FILEPATH "Full path to cmkr executable" FORCE) 121 | message(VERBOSE "[cmkr] Bootstrapping '${CMKR_EXECUTABLE}'") 122 | 123 | message(STATUS "[cmkr] Fetching cmkr...") 124 | if(EXISTS "${CMKR_DIRECTORY}") 125 | cmkr_exec("${CMAKE_COMMAND}" -E rm -rf "${CMKR_DIRECTORY}") 126 | endif() 127 | find_package(Git QUIET REQUIRED) 128 | cmkr_exec("${GIT_EXECUTABLE}" 129 | clone 130 | --config advice.detachedHead=false 131 | --branch ${CMKR_TAG} 132 | --depth 1 133 | ${CMKR_REPO} 134 | "${CMKR_DIRECTORY}" 135 | ) 136 | if(CMKR_COMMIT_HASH) 137 | execute_process( 138 | COMMAND "${GIT_EXECUTABLE}" checkout -q "${CMKR_COMMIT_HASH}" 139 | RESULT_VARIABLE CMKR_EXEC_RESULT 140 | WORKING_DIRECTORY "${CMKR_DIRECTORY}" 141 | ) 142 | if(NOT CMKR_EXEC_RESULT EQUAL 0) 143 | message(FATAL_ERROR "Tag '${CMKR_TAG}' hash is not '${CMKR_COMMIT_HASH}'") 144 | endif() 145 | endif() 146 | message(STATUS "[cmkr] Building cmkr (using system compiler)...") 147 | cmkr_exec("${CMAKE_COMMAND}" 148 | --no-warn-unused-cli 149 | "${CMKR_DIRECTORY}" 150 | "-B${CMKR_DIRECTORY}/build" 151 | "-DCMAKE_BUILD_TYPE=${CMKR_BUILD_TYPE}" 152 | "-DCMAKE_UNITY_BUILD=ON" 153 | "-DCMAKE_INSTALL_PREFIX=${CMKR_DIRECTORY}" 154 | "-DCMKR_GENERATE_DOCUMENTATION=OFF" 155 | ) 156 | cmkr_exec("${CMAKE_COMMAND}" 157 | --build "${CMKR_DIRECTORY}/build" 158 | --config "${CMKR_BUILD_TYPE}" 159 | --parallel 160 | ) 161 | cmkr_exec("${CMAKE_COMMAND}" 162 | --install "${CMKR_DIRECTORY}/build" 163 | --config "${CMKR_BUILD_TYPE}" 164 | --prefix "${CMKR_DIRECTORY}" 165 | --component cmkr 166 | ) 167 | if(NOT EXISTS ${CMKR_EXECUTABLE}) 168 | message(FATAL_ERROR "[cmkr] Failed to bootstrap '${CMKR_EXECUTABLE}'") 169 | endif() 170 | cmkr_exec("${CMKR_EXECUTABLE}" version) 171 | message(STATUS "[cmkr] Bootstrapped ${CMKR_EXECUTABLE}") 172 | endif() 173 | execute_process(COMMAND "${CMKR_EXECUTABLE}" version 174 | RESULT_VARIABLE CMKR_EXEC_RESULT 175 | ) 176 | if(NOT CMKR_EXEC_RESULT EQUAL 0) 177 | message(FATAL_ERROR "[cmkr] Failed to get version, try clearing the cache and rebuilding") 178 | endif() 179 | 180 | # Use cmkr.cmake as a script 181 | if(CMAKE_SCRIPT_MODE_FILE) 182 | if(NOT EXISTS "${CMAKE_SOURCE_DIR}/cmake.toml") 183 | execute_process(COMMAND "${CMKR_EXECUTABLE}" init 184 | RESULT_VARIABLE CMKR_EXEC_RESULT 185 | ) 186 | if(NOT CMKR_EXEC_RESULT EQUAL 0) 187 | message(FATAL_ERROR "[cmkr] Failed to bootstrap cmkr project. Please report an issue: https://github.com/build-cpp/cmkr/issues/new") 188 | else() 189 | message(STATUS "[cmkr] Modify cmake.toml and then configure using: cmake -B build") 190 | endif() 191 | else() 192 | execute_process(COMMAND "${CMKR_EXECUTABLE}" gen 193 | RESULT_VARIABLE CMKR_EXEC_RESULT 194 | ) 195 | if(NOT CMKR_EXEC_RESULT EQUAL 0) 196 | message(FATAL_ERROR "[cmkr] Failed to generate project.") 197 | else() 198 | message(STATUS "[cmkr] Configure using: cmake -B build") 199 | endif() 200 | endif() 201 | endif() 202 | 203 | # This is the macro that contains black magic 204 | macro(cmkr) 205 | # When this macro is called from the generated file, fake some internal CMake variables 206 | get_source_file_property(CMKR_CURRENT_LIST_FILE "${CMAKE_CURRENT_LIST_FILE}" CMKR_CURRENT_LIST_FILE) 207 | if(CMKR_CURRENT_LIST_FILE) 208 | set(CMAKE_CURRENT_LIST_FILE "${CMKR_CURRENT_LIST_FILE}") 209 | get_filename_component(CMAKE_CURRENT_LIST_DIR "${CMAKE_CURRENT_LIST_FILE}" DIRECTORY) 210 | endif() 211 | 212 | # File-based include guard (include_guard is not documented to work) 213 | get_source_file_property(CMKR_INCLUDE_GUARD "${CMAKE_CURRENT_LIST_FILE}" CMKR_INCLUDE_GUARD) 214 | if(NOT CMKR_INCLUDE_GUARD) 215 | set_source_files_properties("${CMAKE_CURRENT_LIST_FILE}" PROPERTIES CMKR_INCLUDE_GUARD TRUE) 216 | 217 | file(SHA256 "${CMAKE_CURRENT_LIST_FILE}" CMKR_LIST_FILE_SHA256_PRE) 218 | 219 | # Generate CMakeLists.txt 220 | cmkr_exec("${CMKR_EXECUTABLE}" gen 221 | WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}" 222 | ) 223 | 224 | file(SHA256 "${CMAKE_CURRENT_LIST_FILE}" CMKR_LIST_FILE_SHA256_POST) 225 | 226 | # Delete the temporary file if it was left for some reason 227 | set(CMKR_TEMP_FILE "${CMAKE_CURRENT_SOURCE_DIR}/CMakerLists.txt") 228 | if(EXISTS "${CMKR_TEMP_FILE}") 229 | file(REMOVE "${CMKR_TEMP_FILE}") 230 | endif() 231 | 232 | if(NOT CMKR_LIST_FILE_SHA256_PRE STREQUAL CMKR_LIST_FILE_SHA256_POST) 233 | # Copy the now-generated CMakeLists.txt to CMakerLists.txt 234 | # This is done because you cannot include() a file you are currently in 235 | configure_file(CMakeLists.txt "${CMKR_TEMP_FILE}" COPYONLY) 236 | 237 | # Add the macro required for the hack at the start of the cmkr macro 238 | set_source_files_properties("${CMKR_TEMP_FILE}" PROPERTIES 239 | CMKR_CURRENT_LIST_FILE "${CMAKE_CURRENT_LIST_FILE}" 240 | ) 241 | 242 | # 'Execute' the newly-generated CMakeLists.txt 243 | include("${CMKR_TEMP_FILE}") 244 | 245 | # Delete the generated file 246 | file(REMOVE "${CMKR_TEMP_FILE}") 247 | 248 | # Do not execute the rest of the original CMakeLists.txt 249 | return() 250 | endif() 251 | # Resume executing the unmodified CMakeLists.txt 252 | endif() 253 | endmacro() 254 | -------------------------------------------------------------------------------- /src/Hooker.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include 4 | 5 | #include "Hooker.hpp" 6 | 7 | Hooker::Hooker(uintptr_t* vtable) 8 | : m_target(vtable), 9 | m_type_info(utility::rtti::get_type_info(&vtable)) 10 | { 11 | spdlog::info("Hooking vtable at 0x{:x}", (uintptr_t)vtable); 12 | 13 | for_each(vtable, [this](uintptr_t entry, size_t i) { 14 | spdlog::info("Hooking {} at 0x{:x}", i, entry); 15 | 16 | auto& hook = m_hooks.emplace_back(std::make_shared()); 17 | m_hook_map[i] = hook; 18 | 19 | hook->parent = this; 20 | hook->target = entry; 21 | hook->stub_code = create_stub(i, hook.get()); 22 | hook->index = i; 23 | hook->impl = safetyhook::create_mid(entry, (safetyhook::MidHookFn)hook->stub_code.get(), safetyhook::MidHook::Flags::StartDisabled); 24 | }); 25 | 26 | // Enable all the hooks now, is more thread safe. 27 | for (auto& hook : m_hooks) { 28 | if (auto err = hook->impl.enable(); !err.has_value()) { 29 | spdlog::error("Failed to enable hook for index: {}, error: {}", hook->index, (int32_t)err.error().type); 30 | } 31 | } 32 | 33 | spdlog::info("Done hooking vtable at 0x{:x}", (uintptr_t)vtable); 34 | } 35 | 36 | void Hooker::generic_hook(safetyhook::Context& ctx, Hook* hook) { 37 | auto hooker = hook->parent; 38 | // This is a function belonging to another vtable, ignore it. 39 | if (!s_ignore_vtable_mismatch && *(uintptr_t*)ctx.rcx != hooker->get_target()) { 40 | return; 41 | } 42 | 43 | if (++hook->calls == 1) { 44 | spdlog::info("Hook {} called for the first time!", hook->index); 45 | } 46 | 47 | hook->last_return_address = *reinterpret_cast(ctx.rsp); 48 | 49 | const auto now = std::chrono::high_resolution_clock::now(); 50 | const auto last_call = hook->last_call.load(); 51 | 52 | hook->delta = now - last_call; 53 | hook->last_call = now; 54 | 55 | // Callstack capture using RtlVirtualUnwind 56 | CONTEXT context{}; 57 | context.ContextFlags = CONTEXT_FULL; 58 | 59 | // Populate the CONTEXT structure with the current register values 60 | context.Rip = hook->target; // The original ctx.rip is not accurate because its points to a hook stub. 61 | context.Rsp = ctx.rsp; 62 | context.Rbp = ctx.rbp; 63 | context.Rax = ctx.rax; 64 | context.Rbx = ctx.rbx; 65 | context.Rcx = ctx.rcx; 66 | context.Rdx = ctx.rdx; 67 | context.Rsi = ctx.rsi; 68 | context.Rdi = ctx.rdi; 69 | context.R8 = ctx.r8; 70 | context.R9 = ctx.r9; 71 | context.R10 = ctx.r10; 72 | context.R11 = ctx.r11; 73 | context.R12 = ctx.r12; 74 | context.R13 = ctx.r13; 75 | context.R14 = ctx.r14; 76 | context.R15 = ctx.r15; 77 | 78 | std::array callstack{}; 79 | size_t count = 0; 80 | 81 | while (count < callstack.size()) { 82 | callstack[count++] = reinterpret_cast(context.Rip); 83 | const auto module_within = (DWORD64)utility::get_module_within(context.Rip).value_or(nullptr); 84 | 85 | auto runtime_function = utility::find_function_entry(context.Rip); // My custom implementation 86 | if (runtime_function == nullptr) { 87 | if (hook->calls == 1) { 88 | spdlog::warn("Failed to find runtime function for 0x{:x}", context.Rip); 89 | } 90 | 91 | break; 92 | } 93 | 94 | void* handler_data = nullptr; 95 | ULONG64 establisher_frame = 0; 96 | 97 | RtlVirtualUnwind(UNW_FLAG_NHANDLER, module_within, 98 | context.Rip, runtime_function, &context, &handler_data, 99 | &establisher_frame, nullptr); 100 | 101 | // If we reach an invalid RIP, stop the unwinding 102 | if (context.Rip == 0) { 103 | break; 104 | } 105 | } 106 | 107 | // Store the callstack and other sensitive data 108 | { 109 | std::unique_lock _{hook->sensitive_data.mutex}; 110 | hook->sensitive_data.callstack.clear(); 111 | 112 | for (size_t i = 0; i < count; ++i) { 113 | hook->sensitive_data.callstack.push_back(reinterpret_cast(callstack[i])); 114 | } 115 | 116 | hook->sensitive_data.last_context = ctx; 117 | } 118 | } 119 | 120 | void Hooker::for_each(uintptr_t* vtable, ForEachFn fn) { 121 | if (vtable == nullptr) { 122 | return; 123 | } 124 | 125 | size_t result = 0; 126 | 127 | for (size_t i = 0; ; ++i) { 128 | uintptr_t& entry = vtable[i]; 129 | 130 | if (entry == 0 || IsBadReadPtr((void*)entry, sizeof(uintptr_t))) { 131 | break; 132 | } 133 | 134 | // If the code is not even executable, we've hit the end of the vtable. 135 | if (!utility::isGoodCodePtr(entry, sizeof(void*))) { 136 | break; 137 | } 138 | 139 | // If the next pointer is a vtable, we've hit the end of the vtable. 140 | if (utility::rtti::is_vtable((const void*)&vtable[i+1])) { 141 | break; 142 | } 143 | 144 | uint8_t* instructions = (uint8_t*)entry; 145 | 146 | // Ignore ret instructions 147 | if (utility::is_stub_code(instructions)) { 148 | continue; 149 | } 150 | 151 | // If we can't successfully disassemble the first instruction, we've hit the end of the vtable. 152 | // Likely hit a string or some other form of data. 153 | if (!utility::decode_one(instructions, 1000).has_value()) { 154 | break; 155 | } 156 | 157 | fn(entry, i); 158 | } 159 | } 160 | 161 | size_t Hooker::count(uintptr_t* vtable) { 162 | if (vtable == nullptr) { 163 | return 0; 164 | } 165 | 166 | size_t highest_i = 0; 167 | 168 | for_each(vtable, [&highest_i](uintptr_t fn, size_t index) { 169 | highest_i = index; 170 | }); 171 | 172 | return highest_i + 1; 173 | } 174 | 175 | std::unique_ptr Hooker::create_stub(uint32_t vtable_index, void* hook_data) { 176 | std::vector initial_data { 177 | 0x48, 0x8B, 0x15, 0x0E, 0x00, 0x00, 0x00, // mov rdx, [rip + 14] 178 | 0xFF, 0x25, 0x00, 0x00, 0x00, 0x00, // jmp [rip + 0] 179 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ptr to generic_hook 180 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ptr to hook data 181 | }; 182 | 183 | *(uintptr_t*)&initial_data[13] = (uintptr_t)&generic_hook; 184 | *(uintptr_t*)&initial_data[21] = (uintptr_t)hook_data; 185 | 186 | auto new_data = std::make_unique(initial_data.size()); 187 | DWORD old_protect{}; 188 | if (!VirtualProtect(new_data.get(), initial_data.size(), PAGE_EXECUTE_READWRITE, &old_protect)) { 189 | spdlog::error("Failed to set memory protection for stub code at 0x{:x}", (uintptr_t)new_data.get()); 190 | } 191 | 192 | std::copy(initial_data.begin(), initial_data.end(), new_data.get()); 193 | 194 | return new_data; 195 | } -------------------------------------------------------------------------------- /src/Hooker.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | #include 7 | 8 | #include 9 | 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | 16 | class Hooker { // haw haw real funny 17 | public: 18 | static inline bool s_ignore_vtable_mismatch{}; 19 | struct Hook; 20 | 21 | static void generic_hook(safetyhook::Context& ctx, Hook* hook); 22 | 23 | struct Hook { 24 | Hooker* parent{}; 25 | uintptr_t target{}; 26 | safetyhook::MidHook impl{}; 27 | std::unique_ptr stub_code{}; 28 | size_t index{}; 29 | std::atomic calls{}; 30 | std::atomic last_return_address{}; 31 | std::atomic last_call{}; 32 | std::atomic delta{}; 33 | struct SensitiveData { 34 | std::shared_mutex mutex{}; 35 | std::vector callstack{}; 36 | safetyhook::Context last_context{}; 37 | } sensitive_data{}; 38 | std::optional original_byte{}; 39 | 40 | // Returns a copy of the callstack. 41 | std::vector get_callstack() { 42 | std::shared_lock _{sensitive_data.mutex}; 43 | return sensitive_data.callstack; 44 | } 45 | 46 | // Returns a copy of the last context. 47 | safetyhook::Context get_last_context() { 48 | std::shared_lock _{sensitive_data.mutex}; 49 | return sensitive_data.last_context; 50 | } 51 | 52 | void insert_ret() { 53 | if (!original_byte.has_value()) { 54 | original_byte = *reinterpret_cast(target); 55 | } 56 | 57 | DWORD old_protect{}; 58 | if (!VirtualProtect((void*)target, 1, PAGE_EXECUTE_READWRITE, &old_protect)) { 59 | spdlog::error("Failed to set memory protection for ret instruction at 0x{:x}", target); 60 | return; 61 | } 62 | 63 | *reinterpret_cast(target) = 0xC3; 64 | 65 | if (!VirtualProtect((void*)target, 1, old_protect, &old_protect)) { 66 | spdlog::error("Failed to restore memory protection for ret instruction at 0x{:x}", target); 67 | } 68 | 69 | spdlog::info("Inserted ret instruction at index: {} (0x{:x})", index, target); 70 | } 71 | 72 | void restore() { 73 | if (!original_byte.has_value()) { 74 | return; 75 | } 76 | 77 | DWORD old_protect{}; 78 | if (!VirtualProtect((void*)target, 1, PAGE_EXECUTE_READWRITE, &old_protect)) { 79 | spdlog::error("Failed to set memory protection for ret instruction at 0x{:x}", target); 80 | return; 81 | } 82 | 83 | *reinterpret_cast(target) = original_byte.value(); 84 | 85 | if (!VirtualProtect((void*)target, 1, old_protect, &old_protect)) { 86 | spdlog::error("Failed to restore memory protection for ret instruction at 0x{:x}", target); 87 | } 88 | 89 | spdlog::info("Restored original instruction at index: {} (0x{:x})", index, target); 90 | } 91 | }; 92 | 93 | std::shared_ptr find_hook(size_t vtable_index) { 94 | for (const auto& hook : m_hooks) { 95 | if (hook->index == vtable_index) { 96 | return hook; 97 | } 98 | } 99 | 100 | return nullptr; 101 | } 102 | 103 | public: 104 | using ForEachFn = std::function; 105 | static void for_each(uintptr_t* vtable, ForEachFn fn); 106 | static size_t count(uintptr_t* vtable); 107 | 108 | Hooker(uintptr_t* vtable); 109 | 110 | virtual ~Hooker() { 111 | spdlog::info("Unhooking vtable at 0x{:x}", (uintptr_t)m_target); 112 | 113 | for (const auto& hook : m_hooks) { 114 | hook->restore(); 115 | } 116 | } 117 | 118 | auto& get_hooks() const { 119 | return m_hooks; 120 | } 121 | 122 | uintptr_t get_target() const { 123 | return (uintptr_t)m_target; 124 | } 125 | 126 | private: 127 | static std::unique_ptr create_stub(uint32_t vtable_index, void* hook_data); 128 | 129 | uintptr_t* m_target{}; 130 | std::type_info* m_type_info{}; 131 | 132 | std::vector> m_hooks{}; 133 | std::unordered_map> m_hook_map{}; 134 | 135 | safetyhook::InlineHook m_special_hook{}; 136 | }; 137 | 138 | static inline std::unique_ptr g_hooker{}; -------------------------------------------------------------------------------- /src/Inspector.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include "StringReferences.hpp" 5 | #include "VtableManager.hpp" 6 | #include "Inspector.hpp" 7 | 8 | void Inspector::render_window_for_main_target() { 9 | if (ImGui::Begin("Inspector")) { 10 | render_inner(m_main_target); 11 | ImGui::End(); 12 | } 13 | } 14 | 15 | void Inspector::render_window(uintptr_t* vtable) { 16 | if (ImGui::Begin(std::format("Inspector: 0x{:x}", (uintptr_t)vtable).c_str())) { 17 | render_inner(vtable); 18 | ImGui::End(); 19 | } 20 | } 21 | 22 | void Inspector::render_inner(uintptr_t* vtable) { 23 | if (vtable == nullptr) { 24 | ImGui::Text("No vtable selected"); 25 | return; 26 | } 27 | 28 | ImGui::Columns(4, "vtablefunctions", true); 29 | ImGui::Separator(); 30 | ImGui::Text("Index"); 31 | ImGui::NextColumn(); 32 | ImGui::Text("Address"); 33 | ImGui::NextColumn(); 34 | ImGui::Text("ASCII Refs"); 35 | ImGui::NextColumn(); 36 | ImGui::Text("Unicode Refs"); 37 | ImGui::NextColumn(); 38 | ImGui::Separator(); 39 | 40 | auto& string_references = StringReferences::get(); 41 | auto& vtable_manager = VtableManager::get(); 42 | 43 | size_t count = vtable_manager.count(vtable); 44 | 45 | for (size_t i = 0; i < count; ++i) { 46 | ImGui::Text("%zu", i); 47 | ImGui::NextColumn(); 48 | 49 | uintptr_t fn = vtable[i]; 50 | ImGui::Text("0x%llx", fn); 51 | ImGui::NextColumn(); 52 | 53 | auto ascii_strs = string_references.ascii_references(fn); 54 | auto unicode_strs = string_references.unicode_references(fn); 55 | 56 | // ASCII 57 | ImGui::BeginGroup(); 58 | if (!ascii_strs.empty()) { 59 | for (const auto& str : ascii_strs) { 60 | ImGui::Text("%s", str.ascii); 61 | } 62 | } else { 63 | ImGui::Text("No ASCII refs"); 64 | } 65 | ImGui::EndGroup(); 66 | 67 | ImGui::NextColumn(); 68 | 69 | // Unicode 70 | ImGui::BeginGroup(); 71 | if (!unicode_strs.empty()) { 72 | for (const auto& str : unicode_strs) { 73 | ImGui::Text("%s", utility::narrow(str.unicode).c_str()); 74 | } 75 | } else { 76 | ImGui::Text("No Unicode refs"); 77 | } 78 | ImGui::EndGroup(); 79 | 80 | ImGui::NextColumn(); 81 | } 82 | } -------------------------------------------------------------------------------- /src/Inspector.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | class Inspector { 6 | public: 7 | static Inspector& get() { 8 | static std::unique_ptr instance = std::make_unique(); 9 | return *instance; 10 | } 11 | 12 | public: 13 | void render_window_for_main_target(); 14 | void render_window(uintptr_t* vtable); 15 | void render_inner(uintptr_t* vtable); 16 | 17 | void set_main_target(uintptr_t* target) { 18 | m_main_target = target; 19 | } 20 | 21 | uintptr_t* get_main_target() const { 22 | return m_main_target; 23 | } 24 | 25 | public: 26 | uintptr_t* m_main_target{}; 27 | }; -------------------------------------------------------------------------------- /src/Main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #include 6 | #include 7 | #include 8 | 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | 17 | #include 18 | 19 | #define USE_GLFW 20 | #include // Initialize with gladLoadGL() 21 | #include 22 | #include 23 | #include 24 | #include 25 | 26 | #include "StringReferences.hpp" 27 | #include "VtableManager.hpp" 28 | #include "Inspector.hpp" 29 | 30 | #include "Hooker.hpp" 31 | 32 | HMODULE g_hModule = nullptr; 33 | 34 | class ImGuiLogSink : public spdlog::sinks::base_sink { 35 | public: 36 | static inline auto sink = std::make_shared(); 37 | 38 | static std::shared_ptr& get() { 39 | return sink; 40 | } 41 | 42 | auto& get_mutex() { 43 | return mutex_; 44 | } 45 | 46 | auto& get_messages() { 47 | return m_log_messages; 48 | } 49 | 50 | void render_log_window() { 51 | ImGui::SetNextWindowSize(ImVec2(500, 300), ImGuiCond_FirstUseEver); 52 | if (ImGui::Begin("Log Window", nullptr, ImGuiWindowFlags_AlwaysVerticalScrollbar)) { 53 | std::lock_guard lock(this->mutex_); 54 | for (const auto& message : this->m_log_messages) { 55 | ImGui::TextUnformatted(message.c_str()); 56 | } 57 | 58 | // Automatically scroll to the bottom when new messages are added 59 | if (ImGui::GetScrollY() >= ImGui::GetScrollMaxY()) { 60 | ImGui::SetScrollHereY(1.0f); 61 | } 62 | 63 | ImGui::End(); 64 | } 65 | } 66 | 67 | protected: 68 | void sink_it_(const spdlog::details::log_msg& msg) override { 69 | spdlog::memory_buf_t formatted; 70 | base_sink::formatter_->format(msg, formatted); 71 | 72 | auto str = fmt::to_string(formatted); 73 | 74 | if (m_log_messages.size() >= max_messages) { 75 | m_log_messages.pop_front(); // Remove oldest log if we exceed max size. 76 | } 77 | m_log_messages.emplace_back(str); 78 | std::cout << str << std::endl; 79 | } 80 | 81 | void flush_() override { 82 | // No need to implement flushing, as logs will be updated automatically in the deque. 83 | } 84 | 85 | private: 86 | std::deque m_log_messages{}; 87 | static constexpr inline size_t max_messages = 1000; // Keep a limit on how many messages to store. 88 | }; 89 | 90 | std::string selected_module_name{}; 91 | HMODULE selected_module{}; // The selected module 92 | 93 | void copy_to_clipboard(std::string_view text) { 94 | if (OpenClipboard(nullptr)) { 95 | EmptyClipboard(); 96 | HGLOBAL hg = GlobalAlloc(GMEM_MOVEABLE, text.size() + 1); 97 | if (hg) { 98 | memcpy(GlobalLock(hg), text.data(), text.size() + 1); 99 | GlobalUnlock(hg); 100 | SetClipboardData(CF_TEXT, hg); 101 | } 102 | CloseClipboard(); 103 | } 104 | } 105 | 106 | void render_module_vtables() { 107 | auto all_vtables = utility::rtti::find_all_vtables(selected_module); 108 | 109 | if (all_vtables.empty()) { 110 | ImGui::Text("No vtables found in the module!"); 111 | return; 112 | } 113 | 114 | // Optional search bar 115 | static std::array search_buffer{}; 116 | ImGui::InputText("Search", search_buffer.data(), search_buffer.size()); 117 | 118 | const auto search_view = std::string_view{search_buffer.data()}; 119 | const auto should_search = !search_view.empty(); 120 | 121 | // Filter the vtables 122 | if (should_search) { 123 | std::erase_if(all_vtables, [&](const uintptr_t vtable) { 124 | const auto ti = utility::rtti::get_type_info(&vtable); 125 | return ti == nullptr || ti->name() == nullptr || std::string_view{ti->name()}.find(search_view) == std::string_view::npos; 126 | }); 127 | } 128 | 129 | // Create a table for the vtables 130 | // Layout: Name | Num Functions | Address | Hook button 131 | ImGui::Columns(4, "vtables", true); 132 | ImGui::Separator(); 133 | ImGui::Text("Name"); 134 | ImGui::NextColumn(); 135 | ImGui::Text("Count"); 136 | ImGui::NextColumn(); 137 | ImGui::Text("Address"); 138 | ImGui::NextColumn(); 139 | ImGui::Text("Hook"); 140 | ImGui::NextColumn(); 141 | ImGui::Separator(); 142 | 143 | static std::unordered_map> vtable_references{}; 144 | 145 | auto& string_references = StringReferences::get(); 146 | auto& vtable_manager = VtableManager::get(); 147 | 148 | for (const auto vtable : all_vtables) { 149 | ImGui::PushID((void*)vtable); 150 | 151 | utility::ScopeGuard guard { []() { 152 | ImGui::PopID(); 153 | }}; 154 | 155 | const auto ti = utility::rtti::get_type_info(&vtable); 156 | //ImGui::Text("%s", (ti != nullptr && ti->name() != nullptr) ? ti->name() : "Unknown"); 157 | if (ImGui::TreeNode((ti != nullptr && ti->name() != nullptr) ? ti->name() : "Unknown")) { 158 | if (ImGui::Button("Open in Inspector")) { 159 | Inspector::get().set_main_target((uintptr_t*)vtable); 160 | } 161 | 162 | if (ImGui::TreeNode("References")) { 163 | auto it = vtable_references.find(vtable); 164 | 165 | if (it == vtable_references.end()) { 166 | vtable_references[vtable] = utility::scan_displacement_references(selected_module, vtable); 167 | it = vtable_references.find(vtable); 168 | } 169 | 170 | for (const auto ref : it->second) { 171 | ImGui::Selectable(std::format("0x{:x}", ref).c_str()); 172 | 173 | if (ImGui::BeginPopupContextItem()) { 174 | if (ImGui::MenuItem("Copy to clipboard")) { 175 | copy_to_clipboard(std::format("0x{:x}", ref)); 176 | } 177 | 178 | ImGui::EndPopup(); 179 | } 180 | } 181 | 182 | ImGui::TreePop(); 183 | } 184 | 185 | ImGui::TreePop(); 186 | } 187 | ImGui::NextColumn(); 188 | size_t count = vtable_manager.count((uintptr_t*)vtable); 189 | 190 | ImGui::Text("%zu", count); 191 | ImGui::NextColumn(); 192 | ImGui::Text("0x%llx", vtable); 193 | ImGui::NextColumn(); 194 | ImGui::PushID((void*)vtable); 195 | if (ImGui::Button("Hook")) { 196 | g_hooker = std::make_unique((uintptr_t*)vtable); 197 | } 198 | ImGui::PopID(); 199 | ImGui::NextColumn(); 200 | } 201 | } 202 | 203 | bool render_gui() { 204 | ImGuiLogSink::get()->render_log_window(); 205 | 206 | // Create the GUI interface for Hooker 207 | bool open = true; 208 | if (ImGui::Begin("Hook Manager", &open)) { 209 | if (ImGui::Button("Toggle VTable Mismatch Ignore")) { 210 | Hooker::s_ignore_vtable_mismatch = !Hooker::s_ignore_vtable_mismatch; 211 | } 212 | 213 | if (g_hooker != nullptr) { 214 | const auto target = g_hooker->get_target(); 215 | const auto ti_target = utility::rtti::get_type_info(&target); 216 | 217 | if (ti_target != nullptr && ti_target->name() != nullptr) { 218 | ImGui::Text("Target: %s", ti_target->name()); 219 | } else { 220 | ImGui::Text("Target: Unknown"); 221 | } 222 | 223 | const auto& hooks = g_hooker->get_hooks(); 224 | 225 | ImGui::Columns(4, "hooks", true); 226 | ImGui::Separator(); 227 | ImGui::Text("Index"); 228 | ImGui::NextColumn(); 229 | ImGui::Text("Calls"); 230 | ImGui::NextColumn(); 231 | ImGui::Text("Last Retaddr"); 232 | ImGui::NextColumn(); 233 | ImGui::Text("Actions"); 234 | ImGui::NextColumn(); 235 | ImGui::Separator(); 236 | 237 | for (const auto& hook : hooks) { 238 | //ImGui::Text("%zu", hook->index); 239 | if (ImGui::TreeNode(std::format("{}", hook->index).c_str())) { 240 | const auto last_context = hook->get_last_context(); 241 | 242 | #define PRINT_REG(reg) \ 243 | ImGui::PushID(#reg); \ 244 | ImGui::Selectable(std::format(#reg ": 0x{:x}", last_context.reg).c_str()); \ 245 | if (ImGui::BeginPopupContextItem()) { \ 246 | if (ImGui::MenuItem("Copy to clipboard")) { \ 247 | copy_to_clipboard(std::format("0x{:x}", last_context.reg)); \ 248 | } \ 249 | ImGui::EndPopup(); \ 250 | }\ 251 | ImGui::PopID(); 252 | 253 | PRINT_REG(rcx); 254 | PRINT_REG(rdx); 255 | PRINT_REG(r8); 256 | PRINT_REG(r9); 257 | PRINT_REG(r10); 258 | PRINT_REG(r11); 259 | PRINT_REG(r12); 260 | PRINT_REG(r13); 261 | PRINT_REG(r14); 262 | PRINT_REG(r15); 263 | PRINT_REG(rax); 264 | PRINT_REG(rbx); 265 | PRINT_REG(rbp); 266 | PRINT_REG(rdi); 267 | PRINT_REG(rsi); 268 | PRINT_REG(rsp); 269 | PRINT_REG(rip); 270 | PRINT_REG(rflags); 271 | 272 | ImGui::TreePop(); 273 | } 274 | ImGui::NextColumn(); 275 | ImGui::Text("%zu", hook->calls.load()); 276 | ImGui::NextColumn(); 277 | //ImGui::Text("0x%llx", hook->last_return_address.load()); 278 | if (ImGui::TreeNode(std::format("0x{:x}", hook->last_return_address.load()).c_str())) { 279 | const auto callstack = hook->get_callstack(); 280 | 281 | for (const auto addr : callstack) { 282 | const auto module_within = utility::get_module_within(addr); 283 | 284 | if (module_within) { 285 | const auto rel = addr - (uintptr_t)*module_within; 286 | const auto module_path = utility::get_module_path(*module_within); 287 | 288 | if (module_path.has_value()) { 289 | const auto module_name = module_path->substr(module_path->find_last_of('\\') + 1); 290 | ImGui::Text("%s+0x%llx", module_name.c_str(), rel); 291 | } else { 292 | ImGui::Text("0x%llx", addr); 293 | } 294 | } else { 295 | ImGui::Text("0x%llx", addr); 296 | } 297 | } 298 | 299 | ImGui::TreePop(); 300 | } 301 | ImGui::NextColumn(); 302 | ImGui::PushID((void*)hook.get()); 303 | if (ImGui::Button("Insert Ret")) { 304 | hook->insert_ret(); 305 | } 306 | 307 | ImGui::SameLine(); 308 | 309 | if (ImGui::Button("Restore")) { 310 | hook->restore(); 311 | } 312 | 313 | ImGui::PopID(); 314 | ImGui::NextColumn(); 315 | } 316 | } 317 | 318 | ImGui::End(); 319 | } 320 | 321 | if (ImGui::Begin("Module Selection", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) { 322 | auto modules = utility::get_loaded_module_names(); 323 | 324 | if (selected_module_name.empty() && !modules.empty()) { 325 | selected_module_name = utility::narrow(modules[0]); // Usually the first module is the executable. 326 | selected_module = GetModuleHandleA(selected_module_name.c_str()); 327 | } 328 | 329 | std::sort(modules.begin(), modules.end()); 330 | 331 | ImGui::Text("Selected module: %s", selected_module_name.c_str()); 332 | 333 | if (ImGui::BeginCombo("Modules", selected_module_name.c_str())) { 334 | for (const auto& module : modules) { 335 | const auto narrow_module = utility::narrow(module); 336 | bool is_selected = (selected_module_name == narrow_module); 337 | if (ImGui::Selectable(narrow_module.c_str(), is_selected)) { 338 | selected_module_name = narrow_module; 339 | selected_module = GetModuleHandleA(selected_module_name.c_str()); 340 | } 341 | if (is_selected) { 342 | ImGui::SetItemDefaultFocus(); 343 | } 344 | } 345 | ImGui::EndCombo(); 346 | } 347 | 348 | ImGui::End(); 349 | } 350 | 351 | ImGui::SetNextWindowSize(ImVec2(500, 400), ImGuiCond_FirstUseEver); 352 | 353 | if (ImGui::Begin("Module VTables")) { 354 | render_module_vtables(); 355 | 356 | ImGui::End(); 357 | } 358 | 359 | if (ImGui::Begin("Manual Hook")) { 360 | static std::array address_buffer{}; 361 | ImGui::InputText("Address", address_buffer.data(), address_buffer.size()); 362 | 363 | // Attempt to convert hex -> 64 bit unsigned integer 364 | uintptr_t address = 0; 365 | if (sscanf(address_buffer.data(), "%llx", &address) == 1) { 366 | if (ImGui::Button("Hook")) { 367 | g_hooker = std::make_unique((uintptr_t*)address); 368 | } 369 | } else { 370 | ImGui::Text("Invalid address"); 371 | } 372 | 373 | ImGui::End(); 374 | } 375 | 376 | auto& inspector = Inspector::get(); 377 | 378 | inspector.render_window_for_main_target(); 379 | 380 | return !open; 381 | } 382 | 383 | void start_gui() { 384 | AllocConsole(); 385 | freopen("CONOUT$", "w", stdout); 386 | freopen("CONIN$", "r", stdin); 387 | SetConsoleTitle("Debug Console"); 388 | 389 | spdlog::set_default_logger(std::make_shared("imgui_logger", ImGuiLogSink::get())); 390 | 391 | spdlog::set_pattern("[%H:%M:%S] [%^%l%$] %v"); 392 | spdlog::set_level(spdlog::level::info); 393 | 394 | spdlog::info("Hello, World!"); 395 | 396 | // Initialize GLFW 397 | if (!glfwInit()) { 398 | spdlog::error("Failed to initialize GLFW"); 399 | return; 400 | } 401 | 402 | // Create GLFW window 403 | GLFWwindow* window = glfwCreateWindow(1280, 720, "Hook Manager", NULL, NULL); 404 | if (!window) { 405 | spdlog::error("Failed to create window"); 406 | glfwTerminate(); 407 | return; 408 | } 409 | glfwMakeContextCurrent(window); 410 | glfwSwapInterval(1); // Enable vsync 411 | 412 | // Setup ImGui context 413 | IMGUI_CHECKVERSION(); 414 | ImGui::CreateContext(); 415 | ImGuiIO& io = ImGui::GetIO(); (void)io; 416 | 417 | // Initialize OpenGL loader (Glad, etc.) 418 | if (gladLoadGL() == 0) { 419 | spdlog::error("Failed to initialize OpenGL loader"); 420 | return; 421 | } 422 | 423 | // Setup Platform/Renderer bindings 424 | ImGui_ImplGlfw_InitForOpenGL(window, true); 425 | ImGui_ImplOpenGL3_Init("#version 130"); 426 | 427 | auto cleanupguard = utility::ScopeGuard { [&window]() { 428 | ImGui_ImplOpenGL3_Shutdown(); 429 | ImGui_ImplGlfw_Shutdown(); 430 | ImGui::DestroyContext(); 431 | glfwDestroyWindow(window); 432 | glfwTerminate(); 433 | 434 | if (g_hModule != nullptr) { 435 | FreeConsole(); 436 | FreeLibraryAndExitThread(g_hModule, 0); 437 | } 438 | }}; 439 | 440 | 441 | // Main loop 442 | while (!glfwWindowShouldClose(window)) { 443 | glfwPollEvents(); 444 | 445 | // Start ImGui frame 446 | ImGui_ImplOpenGL3_NewFrame(); 447 | ImGui_ImplGlfw_NewFrame(); 448 | ImGui::NewFrame(); 449 | 450 | // Render the GUI 451 | bool wants_exit = render_gui(); 452 | 453 | // Render ImGui 454 | ImGui::Render(); 455 | int display_w, display_h; 456 | glfwGetFramebufferSize(window, &display_w, &display_h); 457 | glViewport(0, 0, display_w, display_h); 458 | glClearColor(0.1f, 0.1f, 0.1f, 1.0f); 459 | glClear(GL_COLOR_BUFFER_BIT); 460 | ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData()); 461 | 462 | glfwSwapBuffers(window); 463 | 464 | if (wants_exit) { 465 | break; 466 | } 467 | } 468 | } 469 | 470 | BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved) { 471 | switch (ul_reason_for_call) { 472 | case DLL_PROCESS_ATTACH: { 473 | g_hModule = hModule; 474 | auto h = CreateThread(0, 0, (LPTHREAD_START_ROUTINE)start_gui, 0, 0, 0); 475 | if (h != nullptr) { 476 | CloseHandle(h); 477 | } 478 | break; 479 | } 480 | case DLL_PROCESS_DETACH: 481 | break; 482 | } 483 | return TRUE; 484 | } 485 | -------------------------------------------------------------------------------- /src/StringReferences.cpp: -------------------------------------------------------------------------------- 1 | #include "StringReferences.hpp" 2 | 3 | void StringReferences::populate_ascii(FunctionStart start, size_t max_size, const utility::StringReferenceOptions& options) { 4 | { 5 | std::shared_lock _{m_mutex}; 6 | if (m_ascii_references.contains(start)) { 7 | return; 8 | } 9 | } 10 | 11 | auto result = utility::collect_ascii_string_references(start, max_size, options); 12 | 13 | std::unique_lock _{m_mutex}; 14 | 15 | if (result.empty()) { 16 | m_ascii_references[start] = {}; 17 | return; 18 | } 19 | 20 | m_ascii_references[start] = std::move(result); 21 | } 22 | 23 | void StringReferences::populate_unicode(FunctionStart start, size_t max_size, const utility::StringReferenceOptions& options) { 24 | { 25 | std::shared_lock _{m_mutex}; 26 | if (m_unicode_references.contains(start)) { 27 | return; 28 | } 29 | } 30 | 31 | auto result = utility::collect_unicode_string_references(start, max_size, options); 32 | 33 | std::unique_lock _{m_mutex}; 34 | if (result.empty()) { 35 | m_unicode_references[start] = {}; 36 | return; 37 | } 38 | 39 | m_unicode_references[start] = std::move(result); 40 | } 41 | 42 | StringReferences::StringReferencesList StringReferences::ascii_references(FunctionStart start) { 43 | { 44 | std::shared_lock _{m_mutex}; 45 | auto it = m_ascii_references.find(start); 46 | 47 | if (it != m_ascii_references.end()) { 48 | return it->second; 49 | } 50 | } 51 | 52 | populate_ascii(start); 53 | return ascii_references(start); 54 | } 55 | 56 | StringReferences::StringReferencesList StringReferences::unicode_references(FunctionStart start) { 57 | { 58 | std::shared_lock _{m_mutex}; 59 | auto it = m_unicode_references.find(start); 60 | 61 | if (it != m_unicode_references.end()) { 62 | return it->second; 63 | } 64 | } 65 | 66 | populate_unicode(start); 67 | return unicode_references(start); 68 | } -------------------------------------------------------------------------------- /src/StringReferences.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #include 9 | 10 | class StringReferences { 11 | public: 12 | static StringReferences& get() { 13 | static std::unique_ptr instance = std::make_unique(); 14 | return *instance; 15 | } 16 | 17 | public: 18 | using FunctionStart = uintptr_t; 19 | using StringReferencesList = std::vector; 20 | 21 | void populate_ascii(FunctionStart start, size_t max_size = 100, const utility::StringReferenceOptions& options = utility::StringReferenceOptions{}.with_min_length(4)); 22 | void populate_unicode(FunctionStart start, size_t max_size = 100, const utility::StringReferenceOptions& options = utility::StringReferenceOptions{}.with_min_length(4)); 23 | 24 | StringReferencesList ascii_references(FunctionStart start); 25 | StringReferencesList unicode_references(FunctionStart start); 26 | 27 | private: 28 | mutable std::shared_mutex m_mutex{}; 29 | std::unordered_map m_ascii_references{}; 30 | std::unordered_map m_unicode_references{}; 31 | }; -------------------------------------------------------------------------------- /src/VtableManager.cpp: -------------------------------------------------------------------------------- 1 | #include "Hooker.hpp" 2 | #include "VtableManager.hpp" 3 | 4 | size_t VtableManager::count(uintptr_t* vtable) { 5 | { 6 | std::shared_lock _{m_mutex}; 7 | auto it = m_vtable_counts.find(vtable); 8 | 9 | if (it != m_vtable_counts.end()) { 10 | return it->second; 11 | } 12 | } 13 | 14 | auto count = Hooker::count((uintptr_t*)vtable); 15 | 16 | { 17 | std::unique_lock _{m_mutex}; 18 | m_vtable_counts[vtable] = count; 19 | } 20 | 21 | return count; 22 | } -------------------------------------------------------------------------------- /src/VtableManager.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | class VtableManager { 8 | public: 9 | static VtableManager& get() { 10 | static std::unique_ptr instance = std::make_unique(); 11 | return *instance; 12 | } 13 | 14 | public: 15 | size_t count(uintptr_t* vtable); 16 | 17 | private: 18 | std::shared_mutex m_mutex{}; 19 | std::unordered_map m_vtable_counts{}; 20 | }; -------------------------------------------------------------------------------- /vcpkg.json: -------------------------------------------------------------------------------- 1 | { 2 | "$cmkr": "This file is automatically generated from cmake.toml - DO NOT EDIT", 3 | "$cmkr-url": "https://github.com/build-cpp/cmkr", 4 | "$schema": "https://raw.githubusercontent.com/microsoft/vcpkg/master/scripts/vcpkg.schema.json", 5 | "dependencies": [ 6 | { 7 | "name": "imgui", 8 | "features": ["docking-experimental","freetype","glfw-binding","opengl3-binding"] 9 | }, 10 | { 11 | "name": "glad", 12 | "features": ["gl-api-30"] 13 | }, 14 | "glfw3" 15 | ], 16 | "description": "", 17 | "name": "template-project", 18 | "version-string": "" 19 | } 20 | --------------------------------------------------------------------------------