├── .github ├── ISSUE_TEMPLATE │ ├── config.yml │ └── bug_report.yml ├── FUNDING.yml ├── Patreon-Button.png └── workflows │ └── create_release.yaml ├── src ├── stdafx.h ├── helper.hpp └── dllmain.cpp ├── .gitmodules ├── release_body.md ├── .gitignore ├── xmake.lua ├── DragonTweak.ini ├── LICENSE ├── external └── safetyhook │ ├── LICENSE │ ├── safetyhook.hpp │ └── safetyhook.cpp └── README.md /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | patreon: Wintermance 2 | ko_fi: lyall 3 | github: Lyall -------------------------------------------------------------------------------- /.github/Patreon-Button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lyall/DragonTweak/HEAD/.github/Patreon-Button.png -------------------------------------------------------------------------------- /src/stdafx.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #define WIN32_LEAN_AND_MEAN 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "external/inipp"] 2 | path = external/inipp 3 | url = https://github.com/mcmtroffaes/inipp 4 | [submodule "external/spdlog"] 5 | path = external/spdlog 6 | url = https://github.com/gabime/spdlog 7 | -------------------------------------------------------------------------------- /release_body.md: -------------------------------------------------------------------------------- 1 | ## Changes 2 | - 3 | 4 | ## Installation 5 | - Download the file marked `` from the "Assets" section below. 6 | - Extract the contents of the release zip in to the the game folder. 7 | 8 | ### Steam Deck/Linux Additional Instructions 9 | 🚩**You do not need to do this if you are using Windows!** 10 | - Open up the game properties in Steam and add `WINEDLLOVERRIDES="dinput8=n,b" %command%` to the launch options. -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .xmake/* 2 | build/* 3 | 4 | **/.vscode/ 5 | !.vscode/settings.json 6 | !.vscode/tasks.json 7 | !.vscode/launch.json 8 | !.vscode/extensions.json 9 | !.vscode/*.code-snippets 10 | 11 | # Local History for Visual Studio Code 12 | .history/ 13 | 14 | # Built Visual Studio Code Extensions 15 | *.vsix 16 | 17 | # Prerequisites 18 | *.d 19 | 20 | # Compiled Object files 21 | *.slo 22 | *.lo 23 | *.o 24 | *.obj 25 | 26 | # Precompiled Headers 27 | *.gch 28 | *.pch 29 | 30 | # Compiled Dynamic libraries 31 | *.so 32 | *.dylib 33 | *.dll 34 | 35 | # Fortran module files 36 | *.mod 37 | *.smod 38 | 39 | # Compiled Static libraries 40 | *.lai 41 | *.la 42 | *.a 43 | *.lib 44 | 45 | # Executables 46 | *.exe 47 | *.out 48 | *.app -------------------------------------------------------------------------------- /xmake.lua: -------------------------------------------------------------------------------- 1 | set_project("DragonTweak") 2 | add_rules("mode.debug", "mode.release") 3 | set_languages("cxxlatest", "clatest") 4 | set_optimize("faster") 5 | 6 | target("DragonTweak") 7 | set_kind("shared") 8 | add_files("src/**.cpp", "external/safetyhook/safetyhook.cpp", "external/safetyhook/Zydis.c") 9 | add_syslinks("user32") 10 | add_includedirs("external/spdlog/include", "external/inipp", "external/safetyhook") 11 | set_prefixname("") 12 | set_extension(".asi") 13 | 14 | -- Set platform specific toolchain 15 | if is_plat("windows") then 16 | set_toolchains("msvc") 17 | add_cxflags("/utf-8") 18 | if is_mode("release") then 19 | add_cxflags("/MT") 20 | elseif is_mode("debug") then 21 | add_cxflags("/MTd") 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /DragonTweak.ini: -------------------------------------------------------------------------------- 1 | ;;;;;;;;;; General ;;;;;;;;;; 2 | 3 | [Intro Skip] 4 | ; Set to true to skip intro logos. 5 | Enabled = true 6 | 7 | ;;;;;;;;;; Ultrawide/Narrower ;;;;;;;;;; 8 | 9 | [Disable Pillarboxing] 10 | ; Set to true to disable pillarboxing/letterboxing in cutscenes/dialog only. 11 | CutscenesOnly = true 12 | ; Set to true to disable pillarboxing/letterboxing everywhere. 13 | AllScenes = false 14 | 15 | ;;;;;;;;; Graphics ;;;;;;;;;; 16 | 17 | [Shadow Quality] 18 | ; Adjust "Resolution" to set the "High" shadow setting's resolution. 19 | ; Valid range: 64 to 8192. 20 | Resolution = 2048 21 | ; Set to true to increase the draw distance for shadows. 22 | DrawDistance = false 23 | 24 | [Adjust LOD] 25 | ; Set to true to disable LOD switching for objects/foliage. 26 | ; This can have a hit to performance but will reduce object pop-in throughout the game. 27 | Enabled = false -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Lyall 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /external/safetyhook/LICENSE: -------------------------------------------------------------------------------- 1 | Boost Software License - Version 1.0 - August 17th, 2003 2 | 3 | Permission is hereby granted, free of charge, to any person or organization 4 | obtaining a copy of the software and accompanying documentation covered by 5 | this license (the "Software") to use, reproduce, display, distribute, 6 | execute, and transmit the Software, and to prepare derivative works of the 7 | Software, and to permit third-parties to whom the Software is furnished to 8 | do so, all subject to the following: 9 | 10 | The copyright notices in the Software and this entire statement, including 11 | the above license grant, this restriction and the following disclaimer, 12 | must be included in all copies of the Software, in whole or in part, and 13 | all derivative works of the Software, unless such copies or derivative 14 | works are solely in the form of machine-executable object code generated by 15 | a source language processor. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT 20 | SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE 21 | FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, 22 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: Bug Report 2 | description: Report an issue with DragonTweak 3 | title: "[Bug]: " 4 | labels: ["bug"] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: "## Please fill out the details below." 9 | 10 | - type: input 11 | id: version 12 | attributes: 13 | label: "Version Number" 14 | description: "Which version of DragonTweak are you using? (Check the release name or `DragonTweak.log`)" 15 | placeholder: "e.g., v0.0.1" 16 | validations: 17 | required: true 18 | 19 | - type: textarea 20 | id: description 21 | attributes: 22 | label: "Describe the Issue" 23 | description: "What happened? Provide a clear and concise description of the bug." 24 | placeholder: "Example: The game crashes when entering a specific area..." 25 | validations: 26 | required: true 27 | 28 | - type: textarea 29 | id: steps-to-reproduce 30 | attributes: 31 | label: "Steps to Reproduce" 32 | description: "How can the issue be reproduced? Provide step-by-step instructions." 33 | placeholder: | 34 | 1. Launch the game 35 | 2. Load a specific save 36 | 3. Trigger the issue 37 | 4. Observe the bug 38 | validations: 39 | required: true 40 | 41 | - type: textarea 42 | id: log-file 43 | attributes: 44 | label: "Log File" 45 | description: "Please upload `DragonTweak.log` from the game folder." 46 | validations: 47 | required: true 48 | -------------------------------------------------------------------------------- /.github/workflows/create_release.yaml: -------------------------------------------------------------------------------- 1 | name: create-release 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | version: 7 | description: 'Release version number' 8 | required: true 9 | 10 | defaults: 11 | run: 12 | shell: pwsh 13 | 14 | jobs: 15 | build: 16 | runs-on: windows-latest 17 | permissions: 18 | contents: write 19 | 20 | steps: 21 | - uses: actions/checkout@v4 22 | with: 23 | submodules: 'true' 24 | 25 | - uses: xmake-io/github-action-setup-xmake@v1 26 | with: 27 | xmake-version: latest 28 | build-cache: true 29 | 30 | - name: Build with Xmake 31 | run: | 32 | xmake f -m release 33 | xmake build -v 34 | 35 | - uses: robinraju/release-downloader@v1.11 36 | with: 37 | repository: "ThirteenAG/Ultimate-ASI-Loader" 38 | latest: true 39 | fileName: "Ultimate-ASI-Loader_x64.zip" 40 | 41 | - name: Prepare Ultimate ASI Loader 42 | run: | 43 | unzip Ultimate-ASI-Loader_x64.zip -d ./ 44 | C:\msys64\usr\bin\wget.exe -O ./UltimateASILoader_LICENSE.md https://raw.githubusercontent.com/ThirteenAG/Ultimate-ASI-Loader/master/license 45 | 46 | - name: Create Directory Structure 47 | run: | 48 | mkdir ./zip/runtime/media 49 | 50 | - name: Prepare Release Files 51 | run: | 52 | cp build/windows/x64/release/${{ github.event.repository.name }}.asi ./zip/runtime/media 53 | cp ${{ github.event.repository.name }}.ini ./zip/runtime/media 54 | cp dinput8.dll ./zip/runtime/media/dinput8.dll 55 | cp UltimateASILoader_LICENSE.md ./zip/runtime/media 56 | New-Item -Path "./zip/EXTRACT_TO_GAME_FOLDER" -ItemType File 57 | 58 | - name: Create Release Zip 59 | run: | 60 | cd ./zip; Compress-Archive -Path ./* -DestinationPath ../${{ github.event.repository.name }}_${{ github.event.inputs.version }}.zip 61 | 62 | - name: Update release_body.md 63 | run: | 64 | $releaseBody = "release_body.md" 65 | $zipName = "${{ github.event.repository.name }}_${{ github.event.inputs.version }}.zip" 66 | (Get-Content $releaseBody -Raw) -replace '', $zipName | Set-Content $releaseBody 67 | 68 | - uses: ncipollo/release-action@v1 69 | with: 70 | artifacts: "${{ github.event.repository.name }}_${{ github.event.inputs.version }}.zip" 71 | token: ${{ secrets.GITHUB_TOKEN }} 72 | tag: ${{ github.event.inputs.version }} 73 | name: "${{ github.event.inputs.version }}" 74 | draft: true 75 | bodyFile: "release_body.md" 76 | generateReleaseNotes: false 77 | artifactErrorsFailBuild: true 78 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## ⚠️ This repository has been migrated 2 | Active development has moved to [Codeberg](https://codeberg.org/Lyall/DragonTweak). 3 | This repo is archived and will no longer receive updates. 4 | 5 | # DragonTweak 6 | [![Patreon-Button](https://github.com/Lyall/DragonTweak/blob/main/.github/Patreon-Button.png?raw=true)](https://www.patreon.com/Wintermance) 7 | [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/W7W01UAI9)
8 | [![Github All Releases](https://img.shields.io/github/downloads/Lyall/DragonTweak/total.svg)](https://github.com/Lyall/DragonTweak/releases) 9 | 10 | **DragonTweak** is an ASI plugin for Dragon Engine games that can: 11 | - Disable pillarboxing/letterboxing. 12 | - Adjust shadow quality. 13 | - Adjust level of detail (LOD). 14 | - Skip intro logos. 15 | 16 | ### Current Features 17 | | Game | Disable Pillarboxing/Letterboxing | Adjust Shadow Quality | Adjust LOD | Skip Intro Logos | 18 | |---------------------------------------------------|-----------------------------------|--------------------------|------------|------------------| 19 | | Like a Dragon: Pirate Yakuza in Hawaii | ✔️ | ✔️ | ✔️ | ✔️ | 20 | | Like a Dragon: Infinite Wealth | ✔️ | ✔️ | ✔️ | ✔️ | 21 | | Like a Dragon Gaiden: The Man Who Erased His Name | ✔️ | ✔️ | ✔️ | ✔️ | 22 | | Lost Judgment | ✔️ | ✔️ | ✔️ | ✔️ | 23 | | Yakuza: Like A Dragon | ✔️ | ✔️ | ❌ | ✔️ | 24 | | Judgment | ✔️ | ✔️ | ❌ | ✔️ | 25 | | Yakuza Kiwami 2 | ✔️ | ✔️ | ❌ | ✔️ | 26 | | Yakuza 6: The Song of Life | ✔️ | ✔️ | ❌ | ✔️ | 27 | 28 | ## Installation 29 | - Download the latest release from [here](https://github.com/Lyall/DragonTweak/releases). 30 | - Extract the contents of the release zip in to the the game folder. 31 | 32 | ### Steam Deck/Linux Additional Instructions 33 | 🚩**You do not need to do this if you are using Windows!** 34 | - Open the game properties in Steam and add `WINEDLLOVERRIDES="dinput8=n,b" %command%` to the launch options. 35 | 36 | ## Configuration 37 | - Open **`DragonTweak.ini`** to adjust settings. 38 | 39 | ## Troubleshooting 40 | - If the ASI loader name **`dinput8.dll`** is incompatible with your configuration, please refer to this [list of possible DLL proxies](https://github.com/ThirteenAG/Ultimate-ASI-Loader#description). 41 | - **Yakuza 6 & Kiwami 2** use a different folder structure so remember to make sure that **`DragonTweak.asi`**, **`DragonTweak.ini`** and **`dinput8.dll`** are in the same folder as **`Yakuza6.exe`** or **`YakuzaKiwami2.exe`**. 42 | 43 | ## Credits 44 | [Ultimate ASI Loader](https://github.com/ThirteenAG/Ultimate-ASI-Loader) for ASI loading.
45 | [inipp](https://github.com/mcmtroffaes/inipp) for ini reading.
46 | [spdlog](https://github.com/gabime/spdlog) for logging.
47 | [safetyhook](https://github.com/cursey/safetyhook) for hooking. 48 | -------------------------------------------------------------------------------- /src/helper.hpp: -------------------------------------------------------------------------------- 1 | #include "stdafx.h" 2 | 3 | namespace Memory 4 | { 5 | template 6 | void Write(std::uint8_t* writeAddress, T value) 7 | { 8 | DWORD oldProtect; 9 | VirtualProtect((LPVOID)(writeAddress), sizeof(T), PAGE_EXECUTE_WRITECOPY, &oldProtect); 10 | *(reinterpret_cast(writeAddress)) = value; 11 | VirtualProtect((LPVOID)(writeAddress), sizeof(T), oldProtect, &oldProtect); 12 | } 13 | 14 | void PatchBytes(std::uint8_t* address, const char* pattern, unsigned int numBytes) 15 | { 16 | DWORD oldProtect; 17 | VirtualProtect((LPVOID)address, numBytes, PAGE_EXECUTE_READWRITE, &oldProtect); 18 | memcpy((LPVOID)address, pattern, numBytes); 19 | VirtualProtect((LPVOID)address, numBytes, oldProtect, &oldProtect); 20 | } 21 | 22 | std::vector pattern_to_byte(const char* pattern) 23 | { 24 | auto bytes = std::vector{}; 25 | auto start = const_cast(pattern); 26 | auto end = const_cast(pattern) + strlen(pattern); 27 | 28 | for (auto current = start; current < end; ++current) { 29 | if (*current == '?') { 30 | ++current; 31 | if (*current == '?') 32 | ++current; 33 | bytes.push_back(-1); 34 | } 35 | else { 36 | bytes.push_back(strtoul(current, ¤t, 16)); 37 | } 38 | } 39 | return bytes; 40 | } 41 | 42 | std::uint8_t* PatternScan(void* module, const char* signature) 43 | { 44 | auto dosHeader = (PIMAGE_DOS_HEADER)module; 45 | auto ntHeaders = (PIMAGE_NT_HEADERS)((std::uint8_t*)module + dosHeader->e_lfanew); 46 | 47 | auto sizeOfImage = ntHeaders->OptionalHeader.SizeOfImage; 48 | auto patternBytes = pattern_to_byte(signature); 49 | auto scanBytes = reinterpret_cast(module); 50 | 51 | auto s = patternBytes.size(); 52 | auto d = patternBytes.data(); 53 | 54 | for (auto i = 0ul; i < sizeOfImage - s; ++i) { 55 | bool found = true; 56 | for (auto j = 0ul; j < s; ++j) { 57 | if (scanBytes[i + j] != d[j] && d[j] != -1) { 58 | found = false; 59 | break; 60 | } 61 | } 62 | if (found) { 63 | return &scanBytes[i]; 64 | } 65 | } 66 | 67 | return nullptr; 68 | } 69 | 70 | std::uint8_t* MultiPatternScan(void* module, const std::vector& signatures) 71 | { 72 | for (const auto& signature : signatures) 73 | { 74 | std::uint8_t* result = PatternScan(module, signature); 75 | if (result) 76 | return result; 77 | } 78 | return nullptr; 79 | } 80 | 81 | std::vector PatternScanAll(void* module, const char* signature) 82 | { 83 | auto dosHeader = (PIMAGE_DOS_HEADER)module; 84 | auto ntHeaders = (PIMAGE_NT_HEADERS)((std::uint8_t*)module + dosHeader->e_lfanew); 85 | 86 | auto sizeOfImage = ntHeaders->OptionalHeader.SizeOfImage; 87 | auto patternBytes = pattern_to_byte(signature); 88 | auto scanBytes = reinterpret_cast(module); 89 | 90 | auto s = patternBytes.size(); 91 | auto d = patternBytes.data(); 92 | 93 | std::vector results; 94 | 95 | for (auto i = 0ul; i < sizeOfImage - s; ++i) { 96 | bool found = true; 97 | for (auto j = 0ul; j < s; ++j) { 98 | if (scanBytes[i + j] != d[j] && d[j] != -1) { 99 | found = false; 100 | break; 101 | } 102 | } 103 | if (found) { 104 | results.push_back(&scanBytes[i]); 105 | } 106 | } 107 | 108 | return results; 109 | } 110 | 111 | std::vector MultiPatternScanAll(void* module, const std::vector& signatures) 112 | { 113 | std::vector results; 114 | 115 | for (const auto& signature : signatures) 116 | { 117 | auto matches = PatternScanAll(module, signature); 118 | results.insert(results.end(), matches.begin(), matches.end()); 119 | } 120 | 121 | return results; 122 | } 123 | 124 | std::uint32_t ModuleTimestamp(void* module) 125 | { 126 | auto dosHeader = (PIMAGE_DOS_HEADER)module; 127 | auto ntHeaders = (PIMAGE_NT_HEADERS)((std::uint8_t*)module + dosHeader->e_lfanew); 128 | return ntHeaders->FileHeader.TimeDateStamp; 129 | } 130 | 131 | std::uint8_t* GetAbsolute(std::uint8_t* address) noexcept 132 | { 133 | if (address == nullptr) 134 | return nullptr; 135 | 136 | std::int32_t offset = *reinterpret_cast(address); 137 | std::uint8_t* absoluteAddress = address + 4 + offset; 138 | 139 | return absoluteAddress; 140 | } 141 | 142 | BOOL HookIAT(HMODULE callerModule, char const* targetModule, const void* targetFunction, void* detourFunction) 143 | { 144 | auto* base = (uint8_t*)callerModule; 145 | const auto* dos_header = (IMAGE_DOS_HEADER*)base; 146 | const auto nt_headers = (IMAGE_NT_HEADERS*)(base + dos_header->e_lfanew); 147 | const auto* imports = (IMAGE_IMPORT_DESCRIPTOR*)(base + nt_headers->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress); 148 | 149 | for (int i = 0; imports[i].Characteristics; i++) 150 | { 151 | const char* name = (const char*)(base + imports[i].Name); 152 | if (lstrcmpiA(name, targetModule) != 0) 153 | continue; 154 | 155 | void** thunk = (void**)(base + imports[i].FirstThunk); 156 | 157 | for (; *thunk; thunk++) 158 | { 159 | const void* import = *thunk; 160 | 161 | if (import != targetFunction) 162 | continue; 163 | 164 | DWORD oldState; 165 | if (!VirtualProtect(thunk, sizeof(void*), PAGE_READWRITE, &oldState)) 166 | return FALSE; 167 | 168 | *thunk = detourFunction; 169 | 170 | VirtualProtect(thunk, sizeof(void*), oldState, &oldState); 171 | 172 | return TRUE; 173 | } 174 | } 175 | return FALSE; 176 | } 177 | } 178 | 179 | namespace Util 180 | { 181 | std::string wstring_to_string(const std::wstring& wstr) 182 | { 183 | if (wstr.empty()) return {}; 184 | std::string str(wstr.size() * 2, '\0'); 185 | size_t converted = 0; 186 | wcstombs_s(&converted, &str[0], str.size() + 1, wstr.c_str(), str.size()); 187 | str.resize(converted - 1); 188 | return str; 189 | } 190 | 191 | std::string wstring_to_string(const wchar_t* wstr) 192 | { 193 | return wstr ? wstring_to_string(std::wstring(wstr)) : std::string{}; 194 | } 195 | 196 | bool string_cmp_caseless(const std::string& str1, const std::string& str2) 197 | { 198 | if (str1.size() != str2.size()) { 199 | return false; 200 | } 201 | return std::equal(str1.begin(), str1.end(), str2.begin(), 202 | [](char a, char b) { 203 | return std::tolower(a) == std::tolower(b); 204 | }); 205 | } 206 | 207 | bool file_exists(const WCHAR* fileName) 208 | { 209 | DWORD dwAttrib = GetFileAttributesW(fileName); 210 | return (dwAttrib != INVALID_FILE_ATTRIBUTES && !(dwAttrib & FILE_ATTRIBUTE_DIRECTORY)); 211 | } 212 | } -------------------------------------------------------------------------------- /src/dllmain.cpp: -------------------------------------------------------------------------------- 1 | #include "stdafx.h" 2 | #include "helper.hpp" 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #define spdlog_confparse(var) spdlog::info("Config Parse: {}: {}", #var, var) 10 | 11 | HMODULE exeModule = GetModuleHandle(NULL); 12 | HMODULE thisModule; 13 | 14 | // Fix details 15 | std::string sFixName = "DragonTweak"; 16 | std::string sFixVersion = "0.0.4"; 17 | std::filesystem::path sFixPath; 18 | 19 | // Ini 20 | inipp::Ini ini; 21 | std::string sConfigFile = sFixName + ".ini"; 22 | 23 | // Logger 24 | std::shared_ptr logger; 25 | std::string sLogFile = sFixName + ".log"; 26 | std::filesystem::path sExePath; 27 | std::string sExeName; 28 | 29 | // Ini variables 30 | bool bIntroSkip; 31 | int iShadowResolution; 32 | bool bShadowDrawDistance; 33 | bool bAdjustLOD; 34 | bool bDisableBarsCutscene; 35 | bool bDisableBarsGlobal; 36 | 37 | // Variables 38 | int iCurrentResX; 39 | int iCurrentResY; 40 | 41 | // Game info 42 | struct GameInfo 43 | { 44 | std::string GameTitle; 45 | std::string ExeName; 46 | }; 47 | 48 | enum class Game 49 | { 50 | Unknown, 51 | OgreF, // Yakuza 6: The Song of Life 52 | Lexus2, // Yakuza Kiwami 2 53 | Judge, // Judgment 54 | Yazawa, // Yakuza: Like A Dragon 55 | Coyote, // Lost Judgment 56 | Aston, // Like a Dragon Gaiden: The Man Who Erased His Name 57 | Elvis, // Like a Dragon: Infinite Wealth 58 | Sparrow // Like a Dragon: Pirate Yakuza in Hawaii 59 | }; 60 | 61 | const std::map kGames = { 62 | {Game::OgreF, {"Yakuza 6: The Song of Life", "Yakuza6.exe"}}, 63 | {Game::Lexus2, {"Yakuza Kiwami 2", "YakuzaKiwami2.exe"}}, 64 | {Game::Judge, {"Judgment", "Judgment.exe"}}, 65 | {Game::Yazawa, {"Yakuza: Like a Dragon", "YakuzaLikeADragon.exe"}}, 66 | {Game::Coyote, {"Lost Judgment", "LostJudgment.exe"}}, 67 | {Game::Aston, {"Like a Dragon Gaiden: The Man Who Erased His Name", "LikeADragonGaiden.exe"}}, 68 | {Game::Elvis, {"Like a Dragon: Infinite Wealth", "LikeADragon8.exe"}}, 69 | {Game::Sparrow, {"Like a Dragon: Pirate Yakuza in Hawaii", "LikeADragonPirates.exe"}}, 70 | }; 71 | 72 | const GameInfo* game = nullptr; 73 | Game eGameType = Game::Unknown; 74 | 75 | void Logging() 76 | { 77 | // Get path to DLL 78 | WCHAR dllPath[_MAX_PATH] = {0}; 79 | GetModuleFileNameW(thisModule, dllPath, MAX_PATH); 80 | sFixPath = dllPath; 81 | sFixPath = sFixPath.remove_filename(); 82 | 83 | // Get game name and exe path 84 | WCHAR exePath[_MAX_PATH] = {0}; 85 | GetModuleFileNameW(exeModule, exePath, MAX_PATH); 86 | sExePath = exePath; 87 | sExeName = sExePath.filename().string(); 88 | sExePath = sExePath.remove_filename(); 89 | 90 | // Spdlog initialisation 91 | try 92 | { 93 | logger = spdlog::basic_logger_st(sFixName, sExePath.string() + sLogFile, true); 94 | spdlog::set_default_logger(logger); 95 | spdlog::flush_on(spdlog::level::debug); 96 | 97 | spdlog::info("----------"); 98 | spdlog::info("{:s} v{:s} loaded.", sFixName, sFixVersion); 99 | spdlog::info("----------"); 100 | spdlog::info("Log file: {}", sFixPath.string() + sLogFile); 101 | spdlog::info("----------"); 102 | spdlog::info("Module Name: {:s}", sExeName); 103 | spdlog::info("Module Path: {:s}", sExePath.string()); 104 | spdlog::info("Module Address: 0x{:x}", (uintptr_t)exeModule); 105 | spdlog::info("Module Timestamp: {:d}", Memory::ModuleTimestamp(exeModule)); 106 | spdlog::info("----------"); 107 | } 108 | catch (const spdlog::spdlog_ex &ex) 109 | { 110 | AllocConsole(); 111 | FILE *dummy; 112 | freopen_s(&dummy, "CONOUT$", "w", stdout); 113 | std::cout << "Log initialisation failed: " << ex.what() << std::endl; 114 | FreeLibraryAndExitThread(thisModule, 1); 115 | } 116 | } 117 | 118 | void Configuration() 119 | { 120 | // Inipp initialisation 121 | std::ifstream iniFile(sFixPath / sConfigFile); 122 | if (!iniFile) 123 | { 124 | AllocConsole(); 125 | FILE *dummy; 126 | freopen_s(&dummy, "CONOUT$", "w", stdout); 127 | std::cout << "" << sFixName.c_str() << " v" << sFixVersion.c_str() << " loaded." << std::endl; 128 | std::cout << "ERROR: Could not locate config file." << std::endl; 129 | std::cout << "ERROR: Make sure " << sConfigFile.c_str() << " is located in " << sFixPath.string().c_str() << std::endl; 130 | spdlog::error("ERROR: Could not locate config file {}", sConfigFile); 131 | spdlog::shutdown(); 132 | FreeLibraryAndExitThread(thisModule, 1); 133 | } 134 | else 135 | { 136 | spdlog::info("Config file: {}", sFixPath.string() + sConfigFile); 137 | ini.parse(iniFile); 138 | } 139 | 140 | // Parse config 141 | ini.strip_trailing_comments(); 142 | spdlog::info("----------"); 143 | 144 | // Load settings from ini 145 | inipp::get_value(ini.sections["Intro Skip"], "Enabled", bIntroSkip); 146 | inipp::get_value(ini.sections["Shadow Quality"], "Resolution", iShadowResolution); 147 | inipp::get_value(ini.sections["Shadow Quality"], "DrawDistance", bShadowDrawDistance); 148 | inipp::get_value(ini.sections["Adjust LOD"], "Enabled", bAdjustLOD); 149 | inipp::get_value(ini.sections["Disable Pillarboxing"], "CutscenesOnly", bDisableBarsCutscene); 150 | inipp::get_value(ini.sections["Disable Pillarboxing"], "AllScenes", bDisableBarsGlobal); 151 | 152 | // Clamp settings 153 | iShadowResolution = std::clamp(iShadowResolution, 64, 8192); 154 | 155 | // Log ini parse 156 | spdlog_confparse(bIntroSkip); 157 | spdlog_confparse(iShadowResolution); 158 | spdlog_confparse(bShadowDrawDistance); 159 | spdlog_confparse(bAdjustLOD); 160 | spdlog_confparse(bDisableBarsCutscene); 161 | spdlog_confparse(bDisableBarsGlobal); 162 | 163 | spdlog::info("----------"); 164 | } 165 | 166 | bool DetectGame() 167 | { 168 | eGameType = Game::Unknown; 169 | 170 | for (const auto& [type, info] : kGames) 171 | { 172 | if (Util::string_cmp_caseless(info.ExeName, sExeName)) 173 | { 174 | spdlog::info("Detect Game: Detected: {} ({})", info.GameTitle, sExeName); 175 | eGameType = type; 176 | game = &info; 177 | return true; 178 | } 179 | } 180 | 181 | spdlog::error("Detect Game: Failed to detect supported game, {} isn't supported by DragonTweak.", sExeName); 182 | return false; 183 | 184 | } 185 | 186 | bool bHasSkippedIntro = false; 187 | std::string sSceneID; 188 | int iStageID; 189 | const std::string sLexus2SkipID = "lexus2_studio_logo"; 190 | const std::string sOgreFSkipID = "title_logo"; 191 | const std::string sYazawaSkipID = "title_logo"; 192 | const std::string sCoyoteSkipID = "title_photosensitive"; 193 | const std::string sAstonSkipID = "title_photosensitive"; 194 | const std::string sJudgeSkipID = "judge_photosensitive"; 195 | 196 | void IntroSkip() 197 | { 198 | if (bIntroSkip) 199 | { 200 | if (eGameType == Game::Elvis || eGameType == Game::Sparrow) 201 | { 202 | // Intro skip 203 | std::uint8_t* IntroSkipScanResult = Memory::PatternScan(exeModule, "48 89 ?? ?? 31 ?? 48 89 ?? E8 ?? ?? ?? ?? 4C 8B ?? ??"); 204 | if (IntroSkipScanResult) 205 | { 206 | spdlog::info("Intro Skip: Address: {:s}+0x{:x}", sExeName, IntroSkipScanResult - (std::uint8_t*)exeModule); 207 | static SafetyHookMid IntroSkipMidHook{}; 208 | IntroSkipMidHook = safetyhook::create_mid(IntroSkipScanResult, 209 | [](SafetyHookContext &ctx) 210 | { 211 | if (!bHasSkippedIntro) 212 | { 213 | spdlog::info("Intro Skip: Skipping intro logos."); 214 | LPSTR launchArgs = (LPSTR)ctx.rsi; 215 | static std::string sLaunchArgs = launchArgs; 216 | sLaunchArgs += " -skiplogo"; 217 | ctx.rsi = reinterpret_cast(sLaunchArgs.c_str()); 218 | bHasSkippedIntro = true; 219 | } 220 | }); 221 | } 222 | else 223 | { 224 | spdlog::error("Intro Skip: Pattern scan(s) failed."); 225 | } 226 | } 227 | 228 | if (eGameType != Game::Elvis && eGameType != Game::Sparrow) 229 | { 230 | // create_config_scene 231 | std::vector CreateConfigScenePatterns = { 232 | "?? 8B ?? 8B ?? 4C 8B ?? 8B ?? E8 ?? ?? ?? ?? 84 C0 75 ?? 45 33 ?? 45 89 ?? ?? E9 ?? ?? ?? ?? B9 ?? ?? ?? ?? E8 ?? ?? ?? ??", // Yakuza 6/Kiwami 2 233 | "49 8B ?? 8B ?? 4C 8B ?? 8B ?? E8 ?? ?? ?? ?? 84 ?? 75 ?? 33 ?? 41 ?? ?? E9 ?? ?? ?? ??", // Lost Judgment/Gaiden 234 | "8B ?? 4C ?? ?? 85 ?? 0F 84 ?? ?? ?? ?? B9 ?? ?? 00 00 E8 ?? ?? ?? ?? 48 8B ?? 48 85 ??" // LAD7/Judgment 235 | }; 236 | 237 | std::uint8_t* CreateConfigSceneScanResult = Memory::MultiPatternScan(exeModule, CreateConfigScenePatterns); 238 | if (CreateConfigSceneScanResult) 239 | { 240 | spdlog::info("Intro Skip: Create Config Scene: Address: {:s}+0x{:x}", sExeName, CreateConfigSceneScanResult - (std::uint8_t*)exeModule); 241 | static SafetyHookMid CreateConfigSceneMidHook{}; 242 | CreateConfigSceneMidHook = safetyhook::create_mid(CreateConfigSceneScanResult, 243 | [](SafetyHookContext &ctx) 244 | { 245 | if (ctx.rbx && ctx.r8 && ctx.rdi && !bHasSkippedIntro) 246 | { 247 | if (eGameType == Game::OgreF) 248 | sSceneID = *reinterpret_cast(ctx.rdi + 0x10); 249 | else if (eGameType == Game::Lexus2) 250 | sSceneID = *reinterpret_cast(ctx.rbx + 0x08); 251 | else 252 | sSceneID = *reinterpret_cast(ctx.rbx + 0x10); 253 | 254 | iStageID = *reinterpret_cast(ctx.r8 + 0x4); 255 | spdlog::info("Intro Skip: Scene ID = {} (0x{:x}) | Stage: {:x}", sSceneID, ctx.rdx, iStageID); 256 | 257 | // Lost Judgment 258 | if (eGameType == Game::Coyote && Util::string_cmp_caseless(sSceneID, sCoyoteSkipID)) 259 | { 260 | ctx.rdx = 0x10D4; // Set to coyote_title 261 | *reinterpret_cast(ctx.r8 + 0x4) = 0xF4; // Stage change! 262 | bHasSkippedIntro = true; 263 | } 264 | 265 | // Yakuza: Like a Dragon 266 | if (eGameType == Game::Yazawa && Util::string_cmp_caseless(sSceneID, sYazawaSkipID)) 267 | { 268 | ctx.rdx = 0x2096; // Set ID to "yazawa_title" 269 | *reinterpret_cast(ctx.r8 + 0x4) = 0xCF; // Stage change! 270 | bHasSkippedIntro = true; 271 | } 272 | 273 | // Like a Dragon: Gaiden 274 | if (eGameType == Game::Aston && Util::string_cmp_caseless(sSceneID, sAstonSkipID)) 275 | { 276 | ctx.rdx = 0x177; // Set id to "aston_title" 277 | *reinterpret_cast(ctx.r8 + 0x4) = 0xF4; // Stage change! 278 | bHasSkippedIntro = true; 279 | } 280 | 281 | // Judgment 282 | if (eGameType == Game::Judge && Util::string_cmp_caseless(sSceneID, sJudgeSkipID)) 283 | { 284 | ctx.rdx = 0xC88; // Set id to "judge_title" 285 | bHasSkippedIntro = true; 286 | } 287 | 288 | // Yakuza 6 289 | if (eGameType == Game::OgreF && Util::string_cmp_caseless(sSceneID, sOgreFSkipID)) 290 | { 291 | ctx.rdx = 0x62E; // Set id to "title" 292 | bHasSkippedIntro = true; 293 | } 294 | 295 | // Yakuza Kiwami 2 296 | if (eGameType == Game::Lexus2 && Util::string_cmp_caseless(sSceneID, sLexus2SkipID)) 297 | { 298 | ctx.rdx = 0xE10; // Set id to "lexus2_title" 299 | bHasSkippedIntro = true; 300 | } 301 | 302 | if (bHasSkippedIntro) 303 | spdlog::info("Intro Skip: Skipped intro logos."); 304 | } 305 | }); 306 | } 307 | else 308 | { 309 | spdlog::error("Intro Skip: Pattern scan(s) failed."); 310 | } 311 | } 312 | 313 | if (eGameType != Game::Aston && eGameType != Game::Coyote) 314 | { 315 | // Press any key delay 316 | std::vector PressAnyKeyDelayPatterns = { 317 | "84 C0 74 ?? C5 ?? ?? ?? ?? C5 ?? ?? ?? ?? ?? ?? ?? 72 ?? 48 8B ?? ?? 48 85 ?? 74 ?? 48 C7 ?? ?? 00 00 00 00 BA 01 00 00 00", // Yakuza 6 318 | "72 ?? 45 33 ?? 48 8B ?? 41 ?? ?? ?? E8 ?? ?? ?? ?? F3 0F ?? ?? ?? ?? ?? ?? F3 0F ?? ?? ?? F3 0F ?? ?? ?? 48 83 ?? ?? 5B C3", // Kiwami 2 319 | "72 ?? 48 8B ?? E8 ?? ?? ?? ?? C5 ?? ?? ?? ?? ?? ?? ?? C5 ?? ?? ?? ?? C5 ?? ?? ?? ?? 48 83 ?? ?? 5B C3", // LAD7/Judgment 320 | "72 ?? 48 8B ?? E8 ?? ?? ?? ?? C5 ?? 10 ?? ?? C5 ?? ?? ?? ?? ?? ?? ?? C5 ?? ?? ?? ?? ?? C5 ?? 11 ?? ??", // LAD8 321 | "72 ?? 48 8B ?? E8 ?? ?? ?? ?? C5 ?? ?? ?? ?? ?? ?? ?? C5 ?? ?? ?? C5 ?? ?? ?? 48 8B ?? ?? ?? 48 83 ?? ?? ?? C3" // Pirate 322 | }; 323 | 324 | std::uint8_t* PressAnyKeyDelayScanResult = Memory::MultiPatternScan(exeModule, PressAnyKeyDelayPatterns); 325 | if (PressAnyKeyDelayScanResult) 326 | { 327 | // Remove delay on "press any key" appearing 328 | spdlog::info("Intro Skip: Press Any Key Delay: {:s}+0x{:x}", sExeName, PressAnyKeyDelayScanResult - (std::uint8_t*)exeModule); 329 | if (eGameType == Game::OgreF) 330 | Memory::PatchBytes(PressAnyKeyDelayScanResult + 0x11, "\x90\x90", 2); 331 | else 332 | Memory::PatchBytes(PressAnyKeyDelayScanResult, "\x90\x90", 2); 333 | } 334 | else 335 | { 336 | spdlog::error("Intro Skip: Press Any Key Delay: Pattern scan(s) failed."); 337 | } 338 | } 339 | } 340 | } 341 | 342 | void DisablePillarboxing() 343 | { 344 | if (bDisableBarsGlobal) 345 | { 346 | // job_draw_bars() patterns 347 | std::vector DrawBarsPatterns = { 348 | "40 ?? ?? 41 ?? 48 8D ?? ?? ?? 48 81 ?? ?? ?? ?? ?? 48 8B ?? ?? ?? ?? ?? 48 33 ?? 48 89 ?? ?? B9 ?? ?? ?? ??", // Pirate/LAD8 349 | "40 ?? 56 57 41 ?? 41 ?? 48 8D ?? ?? ?? ?? ?? ?? 48 81 ?? ?? ?? ?? ?? 48 8B ?? ?? ?? ?? ?? 48 33 ?? 48 89 ?? ?? ?? ?? ?? 48 8B ?? ?? ?? ?? ?? 45 33 ?? BE ?? ?? ?? ??", // Gaiden 350 | "40 ?? 48 8D ?? ?? ?? 48 81 ?? ?? ?? ?? ?? 48 8B ?? ?? ?? ?? ?? 48 33 ?? 48 89 ?? ?? B9 ?? ?? ?? ?? E8 ?? ?? ?? ?? 84 C0", // LAD7/Judgment 351 | "48 89 ?? ?? ?? 55 56 57 48 8D ?? ?? ?? 48 81 ?? ?? ?? ?? ?? 48 8B ?? E8 ?? ?? ?? ?? 3B ?? ?? ?? ?? ?? 0F 83 ?? ?? ?? ??", // Kiwami2 352 | "40 ?? ?? 41 ?? 41 ?? 41 ?? 48 83 ?? ?? 48 C7 ?? ?? ?? ?? ?? ?? ?? 48 89 ?? ?? ?? 48 89 ?? ?? ?? ?? ?? ?? 4C ?? ?? B9 08 00 00 00", // Yakuza 6 353 | "40 ?? 57 41 ?? 41 ?? 41 ?? 48 8D ?? ?? ?? ?? ?? ?? 48 81 ?? ?? ?? ?? ?? 48 8B ?? ?? ?? ?? ?? 48 33 ?? 48 89 ?? ?? ?? ?? ?? 48 8B ?? ?? ?? ?? ?? 45 33 ??" // Lost Judgment 354 | }; 355 | 356 | // All: Disable pillarboxing/letterboxing everywhere 357 | std::vector DrawBarsScanResults = Memory::MultiPatternScanAll(exeModule, DrawBarsPatterns); 358 | if (!DrawBarsScanResults.empty()) 359 | { 360 | spdlog::info("Disable Pillarboxing/Letterboxing: Global: Found {} pattern match(es).", DrawBarsScanResults.size()); 361 | for (auto& DrawBarsScanResult : DrawBarsScanResults) 362 | { 363 | spdlog::info("Disable Pillarboxing/Letterboxing: Global: Address: {:s}+0x{:x}", sExeName, DrawBarsScanResult - (std::uint8_t*)exeModule); 364 | Memory::PatchBytes(DrawBarsScanResult, "\xC3\x90", 2); 365 | } 366 | } 367 | else 368 | { 369 | spdlog::error("Disable Pillarboxing/Letterboxing: Global: Pattern scan(s) failed."); 370 | } 371 | } 372 | 373 | if (bDisableBarsCutscene) 374 | { 375 | if (eGameType == Game::Sparrow) 376 | { 377 | // Pirate: Cutscene pillarboxing 378 | std::uint8_t* PillarboxingScanResult = Memory::PatternScan(exeModule, "75 ?? BA ?? ?? ?? ?? 48 8B ?? E8 ?? ?? ?? ?? 84 C0 75 ?? BA ?? ?? ?? ?? 48 8B ?? E8 ?? ?? ?? ?? 84 ?? 74 ?? 81 ?? ?? ?? ?? ?? 77 ??"); 379 | if (PillarboxingScanResult) 380 | { 381 | spdlog::info("Disable Pillarboxing: Pillarboxing: Address: {:s}+0x{:x}", sExeName, PillarboxingScanResult - (std::uint8_t*)exeModule); 382 | Memory::PatchBytes(PillarboxingScanResult, "\x90\x90", 2); // Cutscenes 383 | Memory::PatchBytes(PillarboxingScanResult + 0x11, "\x90\x90", 2); // Talk 384 | } 385 | else 386 | { 387 | spdlog::error("Disable Pillarboxing: Pillarboxing: Pattern scan(s) failed."); 388 | } 389 | 390 | // Pirate: Title card pillarboxing 391 | std::uint8_t* TitleCardsScanResult = Memory::PatternScan(exeModule, "C5 F8 ?? ?? 72 ?? 48 39 ?? ?? ?? ?? ?? 75 ?? B9 ?? ?? ?? ?? E8 ?? ?? ?? ??"); 392 | if (TitleCardsScanResult) 393 | { 394 | spdlog::info("Disable Pillarboxing: Title Cards: Address: {:s}+0x{:x}", sExeName, TitleCardsScanResult - (std::uint8_t*)exeModule); 395 | static SafetyHookMid TitleCardsMidHook{}; 396 | TitleCardsMidHook = safetyhook::create_mid(TitleCardsScanResult + 0x24, 397 | [](SafetyHookContext &ctx) 398 | { 399 | if (ctx.rbx) 400 | { 401 | // Set ZF to jump over pillarboxing so that title cards are not pillarboxed 402 | if (*reinterpret_cast(ctx.rbx + 0x8) == 2) 403 | ctx.rflags |= (1ULL << 6); 404 | } 405 | }); 406 | } 407 | else 408 | { 409 | spdlog::error("Disable Pillarboxing: Title Cards: Pattern scan(s) failed."); 410 | } 411 | } 412 | else if (eGameType == Game::Elvis) 413 | { 414 | // IW: Cutscene pillarboxing 415 | std::uint8_t* CutscenePillarboxingScanResult = Memory::PatternScan(exeModule, "74 ?? 32 ?? EB ?? 05 ?? ?? ?? ?? 3D ?? ?? ?? ?? 77 ?? 48 8D ?? ?? ?? ?? ??"); 416 | std::uint8_t* TalkPillarboxingScanResult = Memory::PatternScan(exeModule, "0F 85 ?? ?? ?? ?? 8B ?? ?? ?? 45 ?? ?? 75 ?? 45 ?? ?? 75 ?? 45 ?? ?? 75 ??"); 417 | if (CutscenePillarboxingScanResult && TalkPillarboxingScanResult) 418 | { 419 | spdlog::info("Disable Pillarboxing: Cutscene Pillarboxing: Address: {:s}+0x{:x}", sExeName, CutscenePillarboxingScanResult - (std::uint8_t*)exeModule); 420 | Memory::PatchBytes(CutscenePillarboxingScanResult, "\x90\x90", 2); // Cutscenes 421 | spdlog::info("Disable Pillarboxing: Talk Pillarboxing: Address: {:s}+0x{:x}", sExeName, TalkPillarboxingScanResult - (std::uint8_t*)exeModule); 422 | Memory::PatchBytes(TalkPillarboxingScanResult, "\x90\x90\x90\x90\x90\x90", 6); // Talk 423 | } 424 | else 425 | { 426 | spdlog::error("Disable Pillarboxing: Pillarboxing: Pattern scan(s) failed."); 427 | } 428 | 429 | // IW: Title card pillarboxing 430 | std::uint8_t* TitleCardsScanResult = Memory::PatternScan(exeModule, "C5 F8 ?? ?? 72 ?? 4C 39 ?? ?? ?? ?? ?? 75 ?? 41 ?? ?? ?? E8 ?? ?? ?? ?? 48 89 ??"); 431 | if (TitleCardsScanResult) 432 | { 433 | spdlog::info("Disable Pillarboxing: Title Cards: Address: {:s}+0x{:x}", sExeName, TitleCardsScanResult - (std::uint8_t*)exeModule); 434 | static SafetyHookMid TitleCardsMidHook{}; 435 | TitleCardsMidHook = safetyhook::create_mid(TitleCardsScanResult + 0x23, 436 | [](SafetyHookContext &ctx) 437 | { 438 | if (ctx.rbx) 439 | { 440 | // Set ZF to jump over pillarboxing so that title cards are not pillarboxed 441 | if (*reinterpret_cast(ctx.rbx + 0x8) == 2) 442 | ctx.rflags |= (1ULL << 6); 443 | } 444 | }); 445 | } 446 | else 447 | { 448 | spdlog::error("Disable Pillarboxing: Title Cards: Pattern scan(s) failed."); 449 | } 450 | } 451 | else if (eGameType == Game::Aston || eGameType == Game::Coyote) 452 | { 453 | // Gaiden/LJ: Cutscene pillarboxing 454 | std::uint8_t* CutsceneBarsScanResult = Memory::PatternScan(exeModule, "84 C0 0F 85 ?? ?? ?? ?? B0 01 48 8B ?? ?? ?? 48 83 ?? ?? 41 ??"); 455 | if (CutsceneBarsScanResult) 456 | { 457 | spdlog::info("Disable Pillarboxing/Letterboxing: Cutscene: Address: {:s}+0x{:x}", sExeName, CutsceneBarsScanResult - (std::uint8_t*)exeModule); 458 | Memory::PatchBytes(CutsceneBarsScanResult + 0x3, "\x84", 1); 459 | } 460 | else 461 | { 462 | spdlog::error("Disable Pillarboxing/Letterboxing: Pattern scan(s) failed."); 463 | } 464 | } 465 | else if (eGameType == Game::Yazawa) 466 | { 467 | // LAD7: Cutscene pillarboxing 468 | std::uint8_t* CutsceneBarsScanResult = Memory::PatternScan(exeModule, "0F 85 ?? ?? ?? ?? 44 38 ?? ?? 75 ?? 44 38 ?? ?? 75 ?? 44 38 ?? ?? 75 ??"); 469 | if (CutsceneBarsScanResult) 470 | { 471 | spdlog::info("Disable Pillarboxing/Letterboxing: Cutscene: Address: {:s}+0x{:x}", sExeName, CutsceneBarsScanResult - (std::uint8_t*)exeModule); 472 | Memory::PatchBytes(CutsceneBarsScanResult, "\x90\x90\x90\x90\x90\x90", 6); 473 | } 474 | else 475 | { 476 | spdlog::error("Disable Pillarboxing/Letterboxing: Pattern scan(s) failed."); 477 | } 478 | } 479 | else if (eGameType == Game::Judge) 480 | { 481 | // Judgment: Cutscene pillarboxing 482 | std::uint8_t* CutsceneBarsScanResult = Memory::PatternScan(exeModule, "40 ?? ?? 74 ?? B0 01 EB ?? 32 C0 48 8B ?? ?? ?? 48 8B ?? ?? ?? 48 8B ?? ?? ??"); 483 | if (CutsceneBarsScanResult) 484 | { 485 | spdlog::info("Disable Pillarboxing/Letterboxing: Cutscene: Address: {:s}+0x{:x}", sExeName, CutsceneBarsScanResult - (std::uint8_t*)exeModule); 486 | Memory::PatchBytes(CutsceneBarsScanResult + 0x6, "\x00", 1); 487 | } 488 | else 489 | { 490 | spdlog::error("Disable Pillarboxing/Letterboxing: Pattern scan(s) failed."); 491 | } 492 | } 493 | else if (eGameType == Game::Lexus2) 494 | { 495 | // Kiwami 2: Cutscene pillarboxing 496 | std::uint8_t* CutsceneBarsScanResult = Memory::PatternScan(exeModule, "84 C0 74 ?? B0 01 48 8B ?? ?? ?? 48 83 ?? ?? ?? C3 E8 ?? ?? ?? ??"); 497 | if (CutsceneBarsScanResult) 498 | { 499 | spdlog::info("Disable Pillarboxing/Letterboxing: Cutscene: Address: {:s}+0x{:x}", sExeName, CutsceneBarsScanResult - (std::uint8_t*)exeModule); 500 | Memory::PatchBytes(CutsceneBarsScanResult + 0x5, "\x00", 1); 501 | } 502 | else 503 | { 504 | spdlog::error("Disable Pillarboxing/Letterboxing: Pattern scan(s) failed."); 505 | } 506 | } 507 | else if (eGameType == Game::OgreF) 508 | { 509 | // Yakuza 6: Cutscene pillarboxing 510 | std::uint8_t* CutsceneBarsScanResult = Memory::PatternScan(exeModule, "49 ?? ?? E8 ?? ?? ?? ?? C5 ?? ?? ?? E9 ?? ?? ?? ?? 0F ?? ?? ?? 0F 83 ?? ?? ?? ?? 41 ?? 03 00 00 00"); 511 | if (CutsceneBarsScanResult) 512 | { 513 | spdlog::info("Disable Pillarboxing/Letterboxing: Cutscene: Address: {:s}+0x{:x}", sExeName, CutsceneBarsScanResult - (std::uint8_t*)exeModule); 514 | static SafetyHookMid CutsceneBarsMidHook{}; 515 | CutsceneBarsMidHook = safetyhook::create_mid(CutsceneBarsScanResult, 516 | [](SafetyHookContext &ctx) 517 | { 518 | ctx.xmm2.f32[0] = 1000.00f; 519 | }); 520 | } 521 | else 522 | { 523 | spdlog::error("Disable Pillarboxing/Letterboxing: Pattern scan(s) failed."); 524 | } 525 | } 526 | } 527 | 528 | // Yakuza 6 is a special case in that it forces letterboxing when played at <16:9. 529 | if ((bDisableBarsCutscene || bDisableBarsGlobal) && eGameType == Game::OgreF) 530 | { 531 | // Yakuza 6: Disable letterboxing 532 | std::uint8_t* LetterboxingScanResult = Memory::PatternScan(exeModule, "76 ?? C5 ?? ?? ?? C5 ?? ?? ?? C5 ?? ?? ?? 44 89 ?? C5 ?? ?? ?? ?? 4C 89 ?? ??"); 533 | std::uint8_t* ForcedAspectRatioScanResult = Memory::PatternScan(exeModule, "7E ?? C5 ?? ?? ?? ?? ?? ?? ?? EB ?? C5 ?? ?? ?? C5 ?? ?? ?? ?? ?? ?? ?? 4C 8B ?? ?? ?? ?? ??"); 534 | if (LetterboxingScanResult && ForcedAspectRatioScanResult) 535 | { 536 | spdlog::info("Disable Pillarboxing/Letterboxing: Letterboxing: Address: {:s}+0x{:x}", sExeName, LetterboxingScanResult - (std::uint8_t*)exeModule); 537 | Memory::PatchBytes(LetterboxingScanResult, "\xEB", 1); 538 | spdlog::info("Disable Pillarboxing/Letterboxing: Forced Aspect Ratio: Address: {:s}+0x{:x}", sExeName, ForcedAspectRatioScanResult - (std::uint8_t*)exeModule); 539 | Memory::PatchBytes(ForcedAspectRatioScanResult, "\xEB", 1); 540 | } 541 | else 542 | { 543 | spdlog::error("Disable Pillarboxing/Letterboxing: Pattern scan(s) failed."); 544 | } 545 | } 546 | } 547 | 548 | void Graphics() 549 | { 550 | if (iShadowResolution != 2048) 551 | { 552 | if (eGameType == Game::OgreF) 553 | { 554 | // Yakuza 6: Shadow resolution 555 | std::uint8_t* ShadowResolutionScanResult = Memory::PatternScan(exeModule, "C7 ?? ?? ?? ?? ?? 00 08 00 00 C7 ?? ?? ?? ?? ?? 00 08 00 00 C6 ?? ?? ?? ?? ?? 00"); 556 | if (ShadowResolutionScanResult) 557 | { 558 | spdlog::info("Shadow Resolution: Address: {:s}+0x{:x}", sExeName, ShadowResolutionScanResult - (std::uint8_t*)exeModule); 559 | Memory::Write(ShadowResolutionScanResult + 0x6, iShadowResolution); 560 | Memory::Write(ShadowResolutionScanResult + 0x10, iShadowResolution); 561 | } 562 | else 563 | { 564 | spdlog::error("Shadow Resolution: Pattern scan(s) failed."); 565 | } 566 | } 567 | else if (eGameType == Game::Lexus2) 568 | { 569 | // Kiwami 2: Shadow resolution 570 | std::uint8_t* ShadowResolutionScanResult = Memory::PatternScan(exeModule, "E8 ?? ?? ?? ?? BA 00 08 00 00 41 ?? 00 04 00 00"); 571 | if (ShadowResolutionScanResult) 572 | { 573 | spdlog::info("Shadow Resolution: Address: {:s}+0x{:x}", sExeName, ShadowResolutionScanResult - (std::uint8_t*)exeModule); 574 | Memory::Write(ShadowResolutionScanResult + 0x6, iShadowResolution); 575 | Memory::Write(ShadowResolutionScanResult + 0xC, iShadowResolution / 2); 576 | } 577 | else 578 | { 579 | spdlog::error("Shadow Resolution: Pattern scan(s) failed."); 580 | } 581 | } 582 | else 583 | { 584 | // Newer: Shadow resolution 585 | std::uint8_t* ShadowResolutionScanResult = Memory::PatternScan(exeModule, "39 0D ?? ?? ?? ?? 75 ?? 39 15 ?? ?? ?? ?? ?? ??"); 586 | if (ShadowResolutionScanResult) 587 | { 588 | spdlog::info("Shadow Resolution: Address: {:s}+0x{:x}", sExeName, ShadowResolutionScanResult - (std::uint8_t*)exeModule); 589 | static SafetyHookMid ShadowResolutionMidHook{}; 590 | ShadowResolutionMidHook = safetyhook::create_mid(ShadowResolutionScanResult, 591 | [](SafetyHookContext &ctx) 592 | { 593 | // Check if shadowmap resolution is 2048x2048 594 | if (ctx.rcx == 0x800) 595 | ctx.rcx = ctx.rdx = static_cast(iShadowResolution); 596 | }); 597 | } 598 | else 599 | { 600 | spdlog::error("Shadow Resolution: Pattern scan(s) failed."); 601 | } 602 | } 603 | } 604 | 605 | if (bShadowDrawDistance) 606 | { 607 | std::uint8_t* ShadowDrawDistanceScanResult = nullptr; 608 | 609 | if (eGameType == Game::Sparrow) 610 | { 611 | // Pirate: Shadow draw distance 612 | ShadowDrawDistanceScanResult = Memory::PatternScan(exeModule, "75 ?? C5 ?? 10 ?? ?? ?? ?? ?? C5 ?? ?? ?? 48 8D ?? ?? ?? 49 ?? ?? C5 ?? 11 ?? ?? ??"); 613 | } 614 | else if (eGameType == Game::Elvis || eGameType == Game::Aston || eGameType == Game::Coyote) 615 | { 616 | // IW/Gaiden/LJ: Shadow draw distance 617 | ShadowDrawDistanceScanResult = Memory::PatternScan(exeModule, "75 ?? C5 ?? 57 ?? C4 ?? ?? ?? ?? C5 ?? ?? ?? C5 ?? 57 ?? C5 ?? 10 ??"); 618 | } 619 | else if (eGameType == Game::Yazawa || eGameType == Game::Judge) 620 | { 621 | // LAD7/Judgment: Shadow draw distance 622 | ShadowDrawDistanceScanResult = Memory::PatternScan(exeModule, "75 ?? C5 ?? ?? ?? C5 ?? 57 ?? C5 ?? 10 ?? C5 ?? ?? ?? C5 ?? ?? ?? ?? ?? ?? ?? C5 ?? 10 ?? ?? ??"); 623 | } 624 | else if (eGameType == Game::Lexus2 || eGameType == Game::OgreF) 625 | { 626 | spdlog::info("Shadow Draw Distance: Unsupported game for this feature."); 627 | } 628 | 629 | if (ShadowDrawDistanceScanResult) 630 | { 631 | spdlog::info("Shadow Draw Distance: Address: {:s}+0x{:x}", sExeName, ShadowDrawDistanceScanResult - (std::uint8_t*)exeModule); 632 | Memory::PatchBytes(ShadowDrawDistanceScanResult, "\xEB", 1); 633 | } 634 | else 635 | { 636 | spdlog::error("Shadow Draw Distance: Pattern scan(s) failed."); 637 | } 638 | } 639 | 640 | if (bAdjustLOD) 641 | { 642 | if (eGameType == Game::Sparrow || eGameType == Game::Elvis) 643 | { 644 | // Pirate/IW: LOD 645 | std::uint8_t* ObjectLODSwitchScanResult = Memory::PatternScan(exeModule, "0F 85 ?? ?? ?? ?? 0F B6 ?? ?? ?? 0F 84 ?? ?? ?? ?? 83 ?? 01 0F 84 ?? ?? ?? ??"); 646 | std::uint8_t* FoliageLODSwitchScanResult = Memory::PatternScan(exeModule, "C5 F8 ?? ?? 72 ?? ?? ?? EB ?? C4 C1 ?? ?? ?? ?? C5 F8 ?? ??"); 647 | if (ObjectLODSwitchScanResult && FoliageLODSwitchScanResult) 648 | { 649 | spdlog::info("LOD: Object: Address: {:s}+0x{:x}", sExeName, ObjectLODSwitchScanResult - (std::uint8_t*)exeModule); 650 | if (eGameType == Game::Sparrow) 651 | Memory::PatchBytes(ObjectLODSwitchScanResult + 0x6, "\x31\xC9\x90", 3); // xor ecx,ecx 652 | else if (eGameType == Game::Elvis) 653 | Memory::PatchBytes(ObjectLODSwitchScanResult + 0x6, "\x31\xC1\x90", 3); // xor eax,eax 654 | 655 | spdlog::info("LOD: Foliage: Address: {:s}+0x{:x}", sExeName, FoliageLODSwitchScanResult - (std::uint8_t*)exeModule); 656 | Memory::PatchBytes(FoliageLODSwitchScanResult + 0x4, "\x90\x90", 2); 657 | } 658 | else 659 | { 660 | spdlog::error("LOD: Pattern scan(s) failed."); 661 | } 662 | } 663 | else if (eGameType == Game::Aston || eGameType == Game::Coyote) 664 | { 665 | // Gaiden/LJ: LOD 666 | std::uint8_t* ObjectLODSwitchScanResult = Memory::PatternScan(exeModule, "C5 F8 ?? ?? 72 ?? ?? ?? ?? EB ?? C4 C1 ?? ?? ?? ?? C5 F8 ?? ?? 72 ??"); 667 | std::uint8_t* FoliageLODSwitchScanResult = Memory::PatternScan(exeModule, "76 ?? 41 ?? ?? EB ?? C5 ?? ?? ?? ?? ?? ?? ?? C5 ?? ?? ?? 76 ?? B9 01 00 00 00"); 668 | if (ObjectLODSwitchScanResult && FoliageLODSwitchScanResult) 669 | { 670 | spdlog::info("LOD: Object: Address: {:s}+0x{:x}", sExeName, ObjectLODSwitchScanResult - (std::uint8_t*)exeModule); 671 | Memory::PatchBytes(ObjectLODSwitchScanResult + 0x4, "\x90\x90", 2); 672 | 673 | spdlog::info("LOD: Foliage: Address: {:s}+0x{:x}", sExeName, FoliageLODSwitchScanResult - (std::uint8_t*)exeModule); 674 | Memory::PatchBytes(FoliageLODSwitchScanResult, "\x90\x90", 2); 675 | } 676 | else 677 | { 678 | spdlog::error("LOD: Pattern scan(s) failed."); 679 | } 680 | } 681 | } 682 | } 683 | 684 | std::mutex mainThreadFinishedMutex; 685 | std::condition_variable mainThreadFinishedVar; 686 | bool mainThreadFinished = false; 687 | 688 | DWORD __stdcall Main(void*) 689 | { 690 | Logging(); 691 | Configuration(); 692 | if (DetectGame()) 693 | { 694 | IntroSkip(); 695 | DisablePillarboxing(); 696 | Graphics(); 697 | } 698 | 699 | { 700 | std::lock_guard lock(mainThreadFinishedMutex); 701 | mainThreadFinished = true; 702 | mainThreadFinishedVar.notify_all(); 703 | } 704 | 705 | return true; 706 | } 707 | 708 | std::mutex getCommandLineMutex; 709 | bool getCommandLineHookCalled = false; 710 | LPSTR(WINAPI* GetCommandLineA_Fn)(); 711 | LPSTR WINAPI GetCommandLineA_Hook() 712 | { 713 | std::lock_guard lock(getCommandLineMutex); 714 | if (!getCommandLineHookCalled) 715 | { 716 | getCommandLineHookCalled = true; 717 | Memory::HookIAT(exeModule, "kernel32.dll", GetCommandLineA_Hook, GetCommandLineA_Fn); 718 | if (!mainThreadFinished) 719 | { 720 | std::unique_lock finishedLock(mainThreadFinishedMutex); 721 | mainThreadFinishedVar.wait(finishedLock, [] { return mainThreadFinished; }); 722 | } 723 | } 724 | return GetCommandLineA_Fn(); 725 | } 726 | 727 | BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved) 728 | { 729 | switch (ul_reason_for_call) 730 | { 731 | case DLL_PROCESS_ATTACH: 732 | { 733 | // Detach from "startup.exe" 734 | char exeName[MAX_PATH]; 735 | GetModuleFileNameA(NULL, exeName, MAX_PATH); 736 | std::string exeStr(exeName); 737 | if (exeStr.find("startup.exe") != std::string::npos) 738 | return FALSE; 739 | 740 | thisModule = hModule; 741 | HMODULE kernel32 = GetModuleHandleA("kernel32.dll"); 742 | if (kernel32) 743 | { 744 | GetCommandLineA_Fn = reinterpret_cast(GetProcAddress(kernel32, "GetCommandLineA")); 745 | if (GetCommandLineA_Fn) 746 | { 747 | Memory::HookIAT(exeModule, "kernel32.dll", GetCommandLineA_Fn, GetCommandLineA_Hook); 748 | } 749 | } 750 | 751 | HANDLE mainHandle = CreateThread(NULL, 0, Main, 0, NULL, 0); 752 | if (mainHandle) 753 | { 754 | SetThreadPriority(mainHandle, THREAD_PRIORITY_HIGHEST); 755 | CloseHandle(mainHandle); 756 | } 757 | break; 758 | } 759 | case DLL_THREAD_ATTACH: 760 | case DLL_THREAD_DETACH: 761 | case DLL_PROCESS_DETACH: 762 | break; 763 | } 764 | return TRUE; 765 | } -------------------------------------------------------------------------------- /external/safetyhook/safetyhook.hpp: -------------------------------------------------------------------------------- 1 | // DO NOT EDIT. This file is auto-generated by `amalgamate.py`. 2 | 3 | 4 | // 5 | // Header: safetyhook.hpp 6 | // 7 | 8 | #pragma once 9 | 10 | 11 | // 12 | // Header: safetyhook/easy.hpp 13 | // 14 | // Include stack: 15 | // - safetyhook.hpp 16 | // 17 | 18 | /// @file safetyhook/easy.hpp 19 | /// @brief Easy to use API for creating hooks. 20 | 21 | #pragma once 22 | 23 | 24 | // 25 | // Header: safetyhook/inline_hook.hpp 26 | // 27 | // Include stack: 28 | // - safetyhook.hpp 29 | // - safetyhook/easy.hpp 30 | // 31 | 32 | /// @file safetyhook/inline_hook.hpp 33 | /// @brief Inline hooking class. 34 | 35 | #pragma once 36 | 37 | #ifndef SAFETYHOOK_USE_CXXMODULES 38 | #include 39 | #include 40 | #include 41 | #include 42 | #include 43 | #include 44 | #else 45 | import std.compat; 46 | #endif 47 | 48 | 49 | // 50 | // Header: safetyhook/allocator.hpp 51 | // 52 | // Include stack: 53 | // - safetyhook.hpp 54 | // - safetyhook/easy.hpp 55 | // - safetyhook/inline_hook.hpp 56 | // 57 | 58 | /// @file safetyhook/allocator.hpp 59 | /// @brief Allocator for allocating memory near target addresses. 60 | 61 | #pragma once 62 | 63 | #ifndef SAFETYHOOK_USE_CXXMODULES 64 | #include 65 | #include 66 | #include 67 | #include 68 | #include 69 | #else 70 | import std.compat; 71 | #endif 72 | 73 | namespace safetyhook { 74 | class Allocator; 75 | 76 | /// @brief A memory allocation. 77 | class Allocation final { 78 | public: 79 | Allocation() = default; 80 | Allocation(const Allocation&) = delete; 81 | Allocation(Allocation&& other) noexcept; 82 | Allocation& operator=(const Allocation&) = delete; 83 | Allocation& operator=(Allocation&& other) noexcept; 84 | ~Allocation(); 85 | 86 | /// @brief Frees the allocation. 87 | /// @note This is called automatically when the Allocation object is destroyed. 88 | void free(); 89 | 90 | /// @brief Returns a pointer to the data of the allocation. 91 | /// @return Pointer to the data of the allocation. 92 | [[nodiscard]] uint8_t* data() const noexcept { return m_address; } 93 | 94 | /// @brief Returns the address of the allocation. 95 | /// @return The address of the allocation. 96 | [[nodiscard]] uintptr_t address() const noexcept { return reinterpret_cast(m_address); } 97 | 98 | /// @brief Returns the size of the allocation. 99 | /// @return The size of the allocation. 100 | [[nodiscard]] size_t size() const noexcept { return m_size; } 101 | 102 | /// @brief Tests if the allocation is valid. 103 | /// @return True if the allocation is valid, false otherwise. 104 | explicit operator bool() const noexcept { return m_address != nullptr && m_size != 0; } 105 | 106 | protected: 107 | friend Allocator; 108 | 109 | Allocation(std::shared_ptr allocator, uint8_t* address, size_t size) noexcept; 110 | 111 | private: 112 | std::shared_ptr m_allocator{}; 113 | uint8_t* m_address{}; 114 | size_t m_size{}; 115 | }; 116 | 117 | /// @brief Allocates memory near target addresses. 118 | class Allocator final : public std::enable_shared_from_this { 119 | public: 120 | /// @brief Returns the global Allocator. 121 | /// @return The global Allocator. 122 | [[nodiscard]] static std::shared_ptr global(); 123 | 124 | /// @brief Creates a new Allocator. 125 | /// @return The new Allocator. 126 | [[nodiscard]] static std::shared_ptr create(); 127 | 128 | Allocator(const Allocator&) = delete; 129 | Allocator(Allocator&&) noexcept = delete; 130 | Allocator& operator=(const Allocator&) = delete; 131 | Allocator& operator=(Allocator&&) noexcept = delete; 132 | ~Allocator() = default; 133 | 134 | /// @brief The error type returned by the allocate functions. 135 | enum class Error : uint8_t { 136 | BAD_VIRTUAL_ALLOC, ///< VirtualAlloc failed. 137 | NO_MEMORY_IN_RANGE, ///< No memory in range. 138 | }; 139 | 140 | /// @brief Allocates memory. 141 | /// @param size The size of the allocation. 142 | /// @return The Allocation or an Allocator::Error if the allocation failed. 143 | [[nodiscard]] std::expected allocate(size_t size); 144 | 145 | /// @brief Allocates memory near a target address. 146 | /// @param desired_addresses The target address. 147 | /// @param size The size of the allocation. 148 | /// @param max_distance The maximum distance from the target address. 149 | /// @return The Allocation or an Allocator::Error if the allocation failed. 150 | [[nodiscard]] std::expected allocate_near( 151 | const std::vector& desired_addresses, size_t size, size_t max_distance = 0x7FFF'FFFF); 152 | 153 | protected: 154 | friend Allocation; 155 | 156 | void free(uint8_t* address, size_t size); 157 | 158 | private: 159 | struct FreeNode { 160 | std::unique_ptr next{}; 161 | uint8_t* start{}; 162 | uint8_t* end{}; 163 | }; 164 | 165 | struct Memory { 166 | uint8_t* address{}; 167 | size_t size{}; 168 | std::unique_ptr freelist{}; 169 | 170 | ~Memory(); 171 | }; 172 | 173 | std::vector> m_memory{}; 174 | std::mutex m_mutex{}; 175 | 176 | Allocator() = default; 177 | 178 | [[nodiscard]] std::expected internal_allocate_near( 179 | const std::vector& desired_addresses, size_t size, size_t max_distance = 0x7FFF'FFFF); 180 | void internal_free(uint8_t* address, size_t size); 181 | 182 | static void combine_adjacent_freenodes(Memory& memory); 183 | [[nodiscard]] static std::expected allocate_nearby_memory( 184 | const std::vector& desired_addresses, size_t size, size_t max_distance); 185 | [[nodiscard]] static bool in_range( 186 | uint8_t* address, const std::vector& desired_addresses, size_t max_distance); 187 | }; 188 | } // namespace safetyhook 189 | 190 | // 191 | // Header: safetyhook/common.hpp 192 | // 193 | // Include stack: 194 | // - safetyhook.hpp 195 | // - safetyhook/easy.hpp 196 | // - safetyhook/inline_hook.hpp 197 | // 198 | 199 | #pragma once 200 | 201 | #if defined(_MSC_VER) 202 | #define SAFETYHOOK_COMPILER_MSVC 1 203 | #define SAFETYHOOK_COMPILER_GCC 0 204 | #define SAFETYHOOK_COMPILER_CLANG 0 205 | #elif defined(__GNUC__) 206 | #define SAFETYHOOK_COMPILER_MSVC 0 207 | #define SAFETYHOOK_COMPILER_GCC 1 208 | #define SAFETYHOOK_COMPILER_CLANG 0 209 | #elif defined(__clang__) 210 | #define SAFETYHOOK_COMPILER_MSVC 0 211 | #define SAFETYHOOK_COMPILER_GCC 0 212 | #define SAFETYHOOK_COMPILER_CLANG 1 213 | #else 214 | #error "Unsupported compiler" 215 | #endif 216 | 217 | #if SAFETYHOOK_COMPILER_MSVC 218 | #if defined(_M_IX86) 219 | #define SAFETYHOOK_ARCH_X86_32 1 220 | #define SAFETYHOOK_ARCH_X86_64 0 221 | #elif defined(_M_X64) 222 | #define SAFETYHOOK_ARCH_X86_32 0 223 | #define SAFETYHOOK_ARCH_X86_64 1 224 | #else 225 | #error "Unsupported architecture" 226 | #endif 227 | #elif SAFETYHOOK_COMPILER_GCC || SAFETYHOOK_COMPILER_CLANG 228 | #if defined(__i386__) 229 | #define SAFETYHOOK_ARCH_X86_32 1 230 | #define SAFETYHOOK_ARCH_X86_64 0 231 | #elif defined(__x86_64__) 232 | #define SAFETYHOOK_ARCH_X86_32 0 233 | #define SAFETYHOOK_ARCH_X86_64 1 234 | #else 235 | #error "Unsupported architecture" 236 | #endif 237 | #endif 238 | 239 | #if defined(_WIN32) 240 | #define SAFETYHOOK_OS_WINDOWS 1 241 | #define SAFETYHOOK_OS_LINUX 0 242 | #elif defined(__linux__) 243 | #define SAFETYHOOK_OS_WINDOWS 0 244 | #define SAFETYHOOK_OS_LINUX 1 245 | #else 246 | #error "Unsupported OS" 247 | #endif 248 | 249 | #if SAFETYHOOK_OS_WINDOWS 250 | #if SAFETYHOOK_COMPILER_MSVC 251 | #define SAFETYHOOK_CCALL __cdecl 252 | #define SAFETYHOOK_STDCALL __stdcall 253 | #define SAFETYHOOK_FASTCALL __fastcall 254 | #define SAFETYHOOK_THISCALL __thiscall 255 | #elif SAFETYHOOK_COMPILER_GCC || SAFETYHOOK_COMPILER_CLANG 256 | #define SAFETYHOOK_CCALL __attribute__((cdecl)) 257 | #define SAFETYHOOK_STDCALL __attribute__((stdcall)) 258 | #define SAFETYHOOK_FASTCALL __attribute__((fastcall)) 259 | #define SAFETYHOOK_THISCALL __attribute__((thiscall)) 260 | #endif 261 | #else 262 | #define SAFETYHOOK_CCALL 263 | #define SAFETYHOOK_STDCALL 264 | #define SAFETYHOOK_FASTCALL 265 | #define SAFETYHOOK_THISCALL 266 | #endif 267 | 268 | #if SAFETYHOOK_COMPILER_MSVC 269 | #define SAFETYHOOK_NOINLINE __declspec(noinline) 270 | #elif SAFETYHOOK_COMPILER_GCC || SAFETYHOOK_COMPILER_CLANG 271 | #define SAFETYHOOK_NOINLINE __attribute__((noinline)) 272 | #endif 273 | 274 | // 275 | // Header: safetyhook/utility.hpp 276 | // 277 | // Include stack: 278 | // - safetyhook.hpp 279 | // - safetyhook/easy.hpp 280 | // - safetyhook/inline_hook.hpp 281 | // 282 | 283 | #pragma once 284 | 285 | #ifndef SAFETYHOOK_USE_CXXMODULES 286 | #include 287 | #include 288 | #include 289 | #include 290 | #else 291 | import std.compat; 292 | #endif 293 | 294 | namespace safetyhook { 295 | template constexpr void store(uint8_t* address, const T& value) { 296 | std::copy_n(reinterpret_cast(&value), sizeof(T), address); 297 | } 298 | 299 | template constexpr T address_cast(U address) { 300 | if constexpr (std::is_integral_v && std::is_integral_v) { 301 | return static_cast(address); 302 | } else { 303 | return reinterpret_cast(address); 304 | } 305 | } 306 | 307 | bool is_executable(uint8_t* address); 308 | 309 | class UnprotectMemory { 310 | public: 311 | UnprotectMemory() = delete; 312 | ~UnprotectMemory(); 313 | UnprotectMemory(const UnprotectMemory&) = delete; 314 | UnprotectMemory(UnprotectMemory&& other) noexcept; 315 | UnprotectMemory& operator=(const UnprotectMemory&) = delete; 316 | UnprotectMemory& operator=(UnprotectMemory&& other) noexcept; 317 | 318 | private: 319 | friend std::optional unprotect(uint8_t*, size_t); 320 | 321 | UnprotectMemory(uint8_t* address, size_t size, uint32_t original_protection) 322 | : m_address{address}, m_size{size}, m_original_protection{original_protection} {} 323 | 324 | uint8_t* m_address{}; 325 | size_t m_size{}; 326 | uint32_t m_original_protection{}; 327 | }; 328 | 329 | [[nodiscard]] std::optional unprotect(uint8_t* address, size_t size); 330 | 331 | template constexpr T align_up(T address, size_t align) { 332 | const auto unaligned_address = address_cast(address); 333 | const auto aligned_address = (unaligned_address + align - 1) & ~(align - 1); 334 | return address_cast(aligned_address); 335 | } 336 | 337 | template constexpr T align_down(T address, size_t align) { 338 | const auto unaligned_address = address_cast(address); 339 | const auto aligned_address = unaligned_address & ~(align - 1); 340 | return address_cast(aligned_address); 341 | } 342 | } // namespace safetyhook 343 | 344 | namespace safetyhook { 345 | /// @brief An inline hook. 346 | class InlineHook final { 347 | public: 348 | /// @brief Error type for InlineHook. 349 | struct Error { 350 | /// @brief The type of error. 351 | enum : uint8_t { 352 | BAD_ALLOCATION, ///< An error occurred when allocating memory. 353 | FAILED_TO_DECODE_INSTRUCTION, ///< Failed to decode an instruction. 354 | SHORT_JUMP_IN_TRAMPOLINE, ///< The trampoline contains a short jump. 355 | IP_RELATIVE_INSTRUCTION_OUT_OF_RANGE, ///< An IP-relative instruction is out of range. 356 | UNSUPPORTED_INSTRUCTION_IN_TRAMPOLINE, ///< An unsupported instruction was found in the trampoline. 357 | FAILED_TO_UNPROTECT, ///< Failed to unprotect memory. 358 | NOT_ENOUGH_SPACE, ///< Not enough space to create the hook. 359 | } type; 360 | 361 | /// @brief Extra information about the error. 362 | union { 363 | Allocator::Error allocator_error; ///< Allocator error information. 364 | uint8_t* ip; ///< IP of the problematic instruction. 365 | }; 366 | 367 | /// @brief Create a BAD_ALLOCATION error. 368 | /// @param err The Allocator::Error that failed. 369 | /// @return The new BAD_ALLOCATION error. 370 | [[nodiscard]] static Error bad_allocation(Allocator::Error err) { 371 | return {.type = BAD_ALLOCATION, .allocator_error = err}; 372 | } 373 | 374 | /// @brief Create a FAILED_TO_DECODE_INSTRUCTION error. 375 | /// @param ip The IP of the problematic instruction. 376 | /// @return The new FAILED_TO_DECODE_INSTRUCTION error. 377 | [[nodiscard]] static Error failed_to_decode_instruction(uint8_t* ip) { 378 | return {.type = FAILED_TO_DECODE_INSTRUCTION, .ip = ip}; 379 | } 380 | 381 | /// @brief Create a SHORT_JUMP_IN_TRAMPOLINE error. 382 | /// @param ip The IP of the problematic instruction. 383 | /// @return The new SHORT_JUMP_IN_TRAMPOLINE error. 384 | [[nodiscard]] static Error short_jump_in_trampoline(uint8_t* ip) { 385 | return {.type = SHORT_JUMP_IN_TRAMPOLINE, .ip = ip}; 386 | } 387 | 388 | /// @brief Create a IP_RELATIVE_INSTRUCTION_OUT_OF_RANGE error. 389 | /// @param ip The IP of the problematic instruction. 390 | /// @return The new IP_RELATIVE_INSTRUCTION_OUT_OF_RANGE error. 391 | [[nodiscard]] static Error ip_relative_instruction_out_of_range(uint8_t* ip) { 392 | return {.type = IP_RELATIVE_INSTRUCTION_OUT_OF_RANGE, .ip = ip}; 393 | } 394 | 395 | /// @brief Create a UNSUPPORTED_INSTRUCTION_IN_TRAMPOLINE error. 396 | /// @param ip The IP of the problematic instruction. 397 | /// @return The new UNSUPPORTED_INSTRUCTION_IN_TRAMPOLINE error. 398 | [[nodiscard]] static Error unsupported_instruction_in_trampoline(uint8_t* ip) { 399 | return {.type = UNSUPPORTED_INSTRUCTION_IN_TRAMPOLINE, .ip = ip}; 400 | } 401 | 402 | /// @brief Create a FAILED_TO_UNPROTECT error. 403 | /// @param ip The IP of the problematic instruction. 404 | /// @return The new FAILED_TO_UNPROTECT error. 405 | [[nodiscard]] static Error failed_to_unprotect(uint8_t* ip) { return {.type = FAILED_TO_UNPROTECT, .ip = ip}; } 406 | 407 | /// @brief Create a NOT_ENOUGH_SPACE error. 408 | /// @param ip The IP of the problematic instruction. 409 | /// @return The new NOT_ENOUGH_SPACE error. 410 | [[nodiscard]] static Error not_enough_space(uint8_t* ip) { return {.type = NOT_ENOUGH_SPACE, .ip = ip}; } 411 | }; 412 | 413 | /// @brief Flags for InlineHook. 414 | enum Flags : int { 415 | Default = 0, ///< Default flags. 416 | StartDisabled = 1 << 0, ///< Start the hook disabled. 417 | }; 418 | 419 | /// @brief Create an inline hook. 420 | /// @param target The address of the function to hook. 421 | /// @param destination The destination address. 422 | /// @param flags The flags to use. 423 | /// @return The InlineHook or an InlineHook::Error if an error occurred. 424 | /// @note This will use the default global Allocator. 425 | /// @note If you don't care about error handling, use the easy API (safetyhook::create_inline). 426 | [[nodiscard]] static std::expected create( 427 | void* target, void* destination, Flags flags = Default); 428 | 429 | /// @brief Create an inline hook. 430 | /// @param target The address of the function to hook. 431 | /// @param destination The destination address. 432 | /// @param flags The flags to use. 433 | /// @return The InlineHook or an InlineHook::Error if an error occurred. 434 | /// @note This will use the default global Allocator. 435 | /// @note If you don't care about error handling, use the easy API (safetyhook::create_inline). 436 | template 437 | [[nodiscard]] static std::expected create(T target, U destination, Flags flags = Default) { 438 | return create(reinterpret_cast(target), reinterpret_cast(destination), flags); 439 | } 440 | 441 | /// @brief Create an inline hook with a given Allocator. 442 | /// @param allocator The allocator to use. 443 | /// @param target The address of the function to hook. 444 | /// @param destination The destination address. 445 | /// @param flags The flags to use. 446 | /// @return The InlineHook or an InlineHook::Error if an error occurred. 447 | /// @note If you don't care about error handling, use the easy API (safetyhook::create_inline). 448 | [[nodiscard]] static std::expected create( 449 | const std::shared_ptr& allocator, void* target, void* destination, Flags flags = Default); 450 | 451 | /// @brief Create an inline hook with a given Allocator. 452 | /// @param allocator The allocator to use. 453 | /// @param target The address of the function to hook. 454 | /// @param destination The destination address. 455 | /// @param flags The flags to use. 456 | /// @return The InlineHook or an InlineHook::Error if an error occurred. 457 | /// @note If you don't care about error handling, use the easy API (safetyhook::create_inline). 458 | template 459 | [[nodiscard]] static std::expected create( 460 | const std::shared_ptr& allocator, T target, U destination, Flags flags = Default) { 461 | return create(allocator, reinterpret_cast(target), reinterpret_cast(destination), flags); 462 | } 463 | 464 | InlineHook() = default; 465 | InlineHook(const InlineHook&) = delete; 466 | InlineHook(InlineHook&& other) noexcept; 467 | InlineHook& operator=(const InlineHook&) = delete; 468 | InlineHook& operator=(InlineHook&& other) noexcept; 469 | ~InlineHook(); 470 | 471 | /// @brief Reset the hook. 472 | /// @details This will restore the original function and remove the hook. 473 | /// @note This is called automatically in the destructor. 474 | void reset(); 475 | 476 | /// @brief Get a pointer to the target. 477 | /// @return A pointer to the target. 478 | [[nodiscard]] uint8_t* target() const { return m_target; } 479 | 480 | /// @brief Get the target address. 481 | /// @return The target address. 482 | [[nodiscard]] uintptr_t target_address() const { return reinterpret_cast(m_target); } 483 | 484 | /// @brief Get a pointer ot the destination. 485 | /// @return A pointer to the destination. 486 | [[nodiscard]] uint8_t* destination() const { return m_destination; } 487 | 488 | /// @brief Get the destination address. 489 | /// @return The destination address. 490 | [[nodiscard]] uintptr_t destination_address() const { return reinterpret_cast(m_destination); } 491 | 492 | /// @brief Get the trampoline Allocation. 493 | /// @return The trampoline Allocation. 494 | [[nodiscard]] const Allocation& trampoline() const { return m_trampoline; } 495 | 496 | /// @brief Tests if the hook is valid. 497 | /// @return True if the hook is valid, false otherwise. 498 | explicit operator bool() const { return static_cast(m_trampoline); } 499 | 500 | /// @brief Returns the address of the trampoline to call the original function. 501 | /// @tparam T The type of the function pointer. 502 | /// @return The address of the trampoline to call the original function. 503 | template [[nodiscard]] T original() const { return reinterpret_cast(m_trampoline.address()); } 504 | 505 | /// @brief Returns a vector containing the original bytes of the target function. 506 | /// @return A vector of the original bytes of the target function. 507 | [[nodiscard]] const auto& original_bytes() const { return m_original_bytes; } 508 | 509 | /// @brief Calls the original function. 510 | /// @tparam RetT The return type of the function. 511 | /// @tparam ...Args The argument types of the function. 512 | /// @param ...args The arguments to pass to the function. 513 | /// @return The result of calling the original function. 514 | /// @note This function will use the default calling convention set by your compiler. 515 | template RetT call(Args... args) { 516 | std::scoped_lock lock{m_mutex}; 517 | return m_trampoline ? original()(args...) : RetT(); 518 | } 519 | 520 | /// @brief Calls the original function. 521 | /// @tparam RetT The return type of the function. 522 | /// @tparam ...Args The argument types of the function. 523 | /// @param ...args The arguments to pass to the function. 524 | /// @return The result of calling the original function. 525 | /// @note This function will use the __cdecl calling convention. 526 | template RetT ccall(Args... args) { 527 | std::scoped_lock lock{m_mutex}; 528 | return m_trampoline ? original()(args...) : RetT(); 529 | } 530 | 531 | /// @brief Calls the original function. 532 | /// @tparam RetT The return type of the function. 533 | /// @tparam ...Args The argument types of the function. 534 | /// @param ...args The arguments to pass to the function. 535 | /// @return The result of calling the original function. 536 | /// @note This function will use the __thiscall calling convention. 537 | template RetT thiscall(Args... args) { 538 | std::scoped_lock lock{m_mutex}; 539 | return m_trampoline ? original()(args...) : RetT(); 540 | } 541 | 542 | /// @brief Calls the original function. 543 | /// @tparam RetT The return type of the function. 544 | /// @tparam ...Args The argument types of the function. 545 | /// @param ...args The arguments to pass to the function. 546 | /// @return The result of calling the original function. 547 | /// @note This function will use the __stdcall calling convention. 548 | template RetT stdcall(Args... args) { 549 | std::scoped_lock lock{m_mutex}; 550 | return m_trampoline ? original()(args...) : RetT(); 551 | } 552 | 553 | /// @brief Calls the original function. 554 | /// @tparam RetT The return type of the function. 555 | /// @tparam ...Args The argument types of the function. 556 | /// @param ...args The arguments to pass to the function. 557 | /// @return The result of calling the original function. 558 | /// @note This function will use the __fastcall calling convention. 559 | template RetT fastcall(Args... args) { 560 | std::scoped_lock lock{m_mutex}; 561 | return m_trampoline ? original()(args...) : RetT(); 562 | } 563 | 564 | /// @brief Calls the original function. 565 | /// @tparam RetT The return type of the function. 566 | /// @tparam ...Args The argument types of the function. 567 | /// @param ...args The arguments to pass to the function. 568 | /// @return The result of calling the original function. 569 | /// @note This function will use the default calling convention set by your compiler. 570 | /// @note This function is unsafe because it doesn't lock the mutex. Only use this if you don't care about unhook 571 | /// safety or are worried about the performance cost of locking the mutex. 572 | template RetT unsafe_call(Args... args) { 573 | return original()(args...); 574 | } 575 | 576 | /// @brief Calls the original function. 577 | /// @tparam RetT The return type of the function. 578 | /// @tparam ...Args The argument types of the function. 579 | /// @param ...args The arguments to pass to the function. 580 | /// @return The result of calling the original function. 581 | /// @note This function will use the __cdecl calling convention. 582 | /// @note This function is unsafe because it doesn't lock the mutex. Only use this if you don't care about unhook 583 | /// safety or are worried about the performance cost of locking the mutex. 584 | template RetT unsafe_ccall(Args... args) { 585 | return original()(args...); 586 | } 587 | 588 | /// @brief Calls the original function. 589 | /// @tparam RetT The return type of the function. 590 | /// @tparam ...Args The argument types of the function. 591 | /// @param ...args The arguments to pass to the function. 592 | /// @return The result of calling the original function. 593 | /// @note This function will use the __thiscall calling convention. 594 | /// @note This function is unsafe because it doesn't lock the mutex. Only use this if you don't care about unhook 595 | /// safety or are worried about the performance cost of locking the mutex. 596 | template RetT unsafe_thiscall(Args... args) { 597 | return original()(args...); 598 | } 599 | 600 | /// @brief Calls the original function. 601 | /// @tparam RetT The return type of the function. 602 | /// @tparam ...Args The argument types of the function. 603 | /// @param ...args The arguments to pass to the function. 604 | /// @return The result of calling the original function. 605 | /// @note This function will use the __stdcall calling convention. 606 | /// @note This function is unsafe because it doesn't lock the mutex. Only use this if you don't care about unhook 607 | /// safety or are worried about the performance cost of locking the mutex. 608 | template RetT unsafe_stdcall(Args... args) { 609 | return original()(args...); 610 | } 611 | 612 | /// @brief Calls the original function. 613 | /// @tparam RetT The return type of the function. 614 | /// @tparam ...Args The argument types of the function. 615 | /// @param ...args The arguments to pass to the function. 616 | /// @return The result of calling the original function. 617 | /// @note This function will use the __fastcall calling convention. 618 | /// @note This function is unsafe because it doesn't lock the mutex. Only use this if you don't care about unhook 619 | /// safety or are worried about the performance cost of locking the mutex. 620 | template RetT unsafe_fastcall(Args... args) { 621 | return original()(args...); 622 | } 623 | 624 | /// @brief Enable the hook. 625 | [[nodiscard]] std::expected enable(); 626 | 627 | /// @brief Disable the hook. 628 | [[nodiscard]] std::expected disable(); 629 | 630 | /// @brief Check if the hook is enabled. 631 | [[nodiscard]] bool enabled() const { return m_enabled; } 632 | 633 | private: 634 | friend class MidHook; 635 | 636 | enum class Type { 637 | Unset, 638 | E9, 639 | FF, 640 | }; 641 | 642 | uint8_t* m_target{}; 643 | uint8_t* m_destination{}; 644 | Allocation m_trampoline{}; 645 | std::vector m_original_bytes{}; 646 | uintptr_t m_trampoline_size{}; 647 | std::recursive_mutex m_mutex{}; 648 | bool m_enabled{}; 649 | Type m_type{Type::Unset}; 650 | 651 | std::expected setup( 652 | const std::shared_ptr& allocator, uint8_t* target, uint8_t* destination); 653 | std::expected e9_hook(const std::shared_ptr& allocator); 654 | 655 | #if SAFETYHOOK_ARCH_X86_64 656 | std::expected ff_hook(const std::shared_ptr& allocator); 657 | #endif 658 | 659 | void destroy(); 660 | }; 661 | } // namespace safetyhook 662 | 663 | // 664 | // Header: safetyhook/mid_hook.hpp 665 | // 666 | // Include stack: 667 | // - safetyhook.hpp 668 | // - safetyhook/easy.hpp 669 | // 670 | 671 | /// @file safetyhook/mid_hook.hpp 672 | /// @brief Mid function hooking class. 673 | 674 | #pragma once 675 | 676 | #ifndef SAFETYHOOK_USE_CXXMODULES 677 | #include 678 | #include 679 | #else 680 | import std.compat; 681 | #endif 682 | 683 | 684 | // 685 | // Header: safetyhook/context.hpp 686 | // 687 | // Include stack: 688 | // - safetyhook.hpp 689 | // - safetyhook/easy.hpp 690 | // - safetyhook/mid_hook.hpp 691 | // 692 | 693 | /// @file safetyhook/context.hpp 694 | /// @brief Context structure for MidHook. 695 | 696 | #pragma once 697 | 698 | #ifndef SAFETYHOOK_USE_CXXMODULES 699 | #include 700 | #else 701 | import std.compat; 702 | #endif 703 | 704 | 705 | namespace safetyhook { 706 | union Xmm { 707 | uint8_t u8[16]; 708 | uint16_t u16[8]; 709 | uint32_t u32[4]; 710 | uint64_t u64[2]; 711 | float f32[4]; 712 | double f64[2]; 713 | }; 714 | 715 | /// @brief Context structure for 64-bit MidHook. 716 | /// @details This structure is used to pass the context of the hooked function to the destination allowing full access 717 | /// to the 64-bit registers at the moment the hook is called. 718 | /// @note rip will point to a trampoline containing the replaced instruction(s). 719 | /// @note rsp is read-only. Modifying it will have no effect. Use trampoline_rsp to modify rsp if needed but make sure 720 | /// the top of the stack is the rip you want to resume at. 721 | struct Context64 { 722 | Xmm xmm0, xmm1, xmm2, xmm3, xmm4, xmm5, xmm6, xmm7, xmm8, xmm9, xmm10, xmm11, xmm12, xmm13, xmm14, xmm15; 723 | uintptr_t rflags, r15, r14, r13, r12, r11, r10, r9, r8, rdi, rsi, rdx, rcx, rbx, rax, rbp, rsp, trampoline_rsp, rip; 724 | }; 725 | 726 | /// @brief Context structure for 32-bit MidHook. 727 | /// @details This structure is used to pass the context of the hooked function to the destination allowing full access 728 | /// to the 32-bit registers at the moment the hook is called. 729 | /// @note eip will point to a trampoline containing the replaced instruction(s). 730 | /// @note esp is read-only. Modifying it will have no effect. Use trampoline_esp to modify esp if needed but make sure 731 | /// the top of the stack is the eip you want to resume at. 732 | struct Context32 { 733 | Xmm xmm0, xmm1, xmm2, xmm3, xmm4, xmm5, xmm6, xmm7; 734 | uintptr_t eflags, edi, esi, edx, ecx, ebx, eax, ebp, esp, trampoline_esp, eip; 735 | }; 736 | 737 | /// @brief Context structure for MidHook. 738 | /// @details This structure is used to pass the context of the hooked function to the destination allowing full access 739 | /// to the registers at the moment the hook is called. 740 | /// @note The structure is different depending on architecture. 741 | /// @note The structure only provides access to integer registers. 742 | #if SAFETYHOOK_ARCH_X86_64 743 | using Context = Context64; 744 | #elif SAFETYHOOK_ARCH_X86_32 745 | using Context = Context32; 746 | #endif 747 | 748 | } // namespace safetyhook 749 | 750 | namespace safetyhook { 751 | 752 | /// @brief A MidHook destination function. 753 | using MidHookFn = void (*)(Context& ctx); 754 | 755 | /// @brief A mid function hook. 756 | class MidHook final { 757 | public: 758 | /// @brief Error type for MidHook. 759 | struct Error { 760 | /// @brief The type of error. 761 | enum : uint8_t { 762 | BAD_ALLOCATION, 763 | BAD_INLINE_HOOK, 764 | } type; 765 | 766 | /// @brief Extra error information. 767 | union { 768 | Allocator::Error allocator_error; ///< Allocator error information. 769 | InlineHook::Error inline_hook_error; ///< InlineHook error information. 770 | }; 771 | 772 | /// @brief Create a BAD_ALLOCATION error. 773 | /// @param err The Allocator::Error that failed. 774 | /// @return The new BAD_ALLOCATION error. 775 | [[nodiscard]] static Error bad_allocation(Allocator::Error err) { 776 | return {.type = BAD_ALLOCATION, .allocator_error = err}; 777 | } 778 | 779 | /// @brief Create a BAD_INLINE_HOOK error. 780 | /// @param err The InlineHook::Error that failed. 781 | /// @return The new BAD_INLINE_HOOK error. 782 | [[nodiscard]] static Error bad_inline_hook(InlineHook::Error err) { 783 | return {.type = BAD_INLINE_HOOK, .inline_hook_error = err}; 784 | } 785 | }; 786 | 787 | /// @brief Flags for MidHook. 788 | enum Flags : int { 789 | Default = 0, ///< Default flags. 790 | StartDisabled = 1, ///< Start the hook disabled. 791 | }; 792 | 793 | /// @brief Creates a new MidHook object. 794 | /// @param target The address of the function to hook. 795 | /// @param destination_fn The destination function. 796 | /// @param flags The flags to use. 797 | /// @return The MidHook object or a MidHook::Error if an error occurred. 798 | /// @note This will use the default global Allocator. 799 | /// @note If you don't care about error handling, use the easy API (safetyhook::create_mid). 800 | [[nodiscard]] static std::expected create( 801 | void* target, MidHookFn destination_fn, Flags flags = Default); 802 | 803 | /// @brief Creates a new MidHook object. 804 | /// @param target The address of the function to hook. 805 | /// @param destination_fn The destination function. 806 | /// @param flags The flags to use. 807 | /// @return The MidHook object or a MidHook::Error if an error occurred. 808 | /// @note This will use the default global Allocator. 809 | /// @note If you don't care about error handling, use the easy API (safetyhook::create_mid). 810 | template 811 | [[nodiscard]] static std::expected create( 812 | T target, MidHookFn destination_fn, Flags flags = Default) { 813 | return create(reinterpret_cast(target), destination_fn, flags); 814 | } 815 | 816 | /// @brief Creates a new MidHook object with a given Allocator. 817 | /// @param allocator The Allocator to use. 818 | /// @param target The address of the function to hook. 819 | /// @param destination_fn The destination function. 820 | /// @param flags The flags to use. 821 | /// @return The MidHook object or a MidHook::Error if an error occurred. 822 | /// @note If you don't care about error handling, use the easy API (safetyhook::create_mid). 823 | [[nodiscard]] static std::expected create( 824 | const std::shared_ptr& allocator, void* target, MidHookFn destination_fn, Flags flags = Default); 825 | 826 | /// @brief Creates a new MidHook object with a given Allocator. 827 | /// @tparam T The type of the function to hook. 828 | /// @param allocator The Allocator to use. 829 | /// @param target The address of the function to hook. 830 | /// @param destination_fn The destination function. 831 | /// @param flags The flags to use. 832 | /// @return The MidHook object or a MidHook::Error if an error occurred. 833 | /// @note If you don't care about error handling, use the easy API (safetyhook::create_mid). 834 | template 835 | [[nodiscard]] static std::expected create( 836 | const std::shared_ptr& allocator, T target, MidHookFn destination_fn, Flags flags = Default) { 837 | return create(allocator, reinterpret_cast(target), destination_fn, flags); 838 | } 839 | 840 | MidHook() = default; 841 | MidHook(const MidHook&) = delete; 842 | MidHook(MidHook&& other) noexcept; 843 | MidHook& operator=(const MidHook&) = delete; 844 | MidHook& operator=(MidHook&& other) noexcept; 845 | ~MidHook() = default; 846 | 847 | /// @brief Reset the hook. 848 | /// @details This will remove the hook and free the stub. 849 | /// @note This is called automatically in the destructor. 850 | void reset(); 851 | 852 | /// @brief Get a pointer to the target. 853 | /// @return A pointer to the target. 854 | [[nodiscard]] uint8_t* target() const { return m_target; } 855 | 856 | /// @brief Get the address of the target. 857 | /// @return The address of the target. 858 | [[nodiscard]] uintptr_t target_address() const { return reinterpret_cast(m_target); } 859 | 860 | /// @brief Get the destination function. 861 | /// @return The destination function. 862 | [[nodiscard]] MidHookFn destination() const { return m_destination; } 863 | 864 | /// @brief Returns a vector containing the original bytes of the target function. 865 | /// @return A vector of the original bytes of the target function. 866 | [[nodiscard]] const auto& original_bytes() const { return m_hook.m_original_bytes; } 867 | 868 | /// @brief Tests if the hook is valid. 869 | /// @return true if the hook is valid, false otherwise. 870 | explicit operator bool() const { return static_cast(m_stub); } 871 | 872 | /// @brief Enable the hook. 873 | [[nodiscard]] std::expected enable(); 874 | 875 | /// @brief Disable the hook. 876 | [[nodiscard]] std::expected disable(); 877 | 878 | /// @brief Check if the hook is enabled. 879 | [[nodiscard]] bool enabled() const { return m_hook.enabled(); } 880 | 881 | private: 882 | InlineHook m_hook{}; 883 | uint8_t* m_target{}; 884 | Allocation m_stub{}; 885 | MidHookFn m_destination{}; 886 | 887 | std::expected setup( 888 | const std::shared_ptr& allocator, uint8_t* target, MidHookFn destination); 889 | }; 890 | } // namespace safetyhook 891 | 892 | // 893 | // Header: safetyhook/vmt_hook.hpp 894 | // 895 | // Include stack: 896 | // - safetyhook.hpp 897 | // - safetyhook/easy.hpp 898 | // 899 | 900 | /// @file safetyhook/vmt_hook.hpp 901 | /// @brief VMT hooking classes 902 | 903 | #pragma once 904 | 905 | #ifndef SAFETYHOOK_USE_CXXMODULES 906 | #include 907 | #include 908 | #include 909 | #else 910 | import std.compat; 911 | #endif 912 | 913 | 914 | namespace safetyhook { 915 | /// @brief A hook class that allows for hooking a single method in a VMT. 916 | class VmHook final { 917 | public: 918 | VmHook() = default; 919 | VmHook(const VmHook&) = delete; 920 | VmHook(VmHook&& other) noexcept; 921 | VmHook& operator=(const VmHook&) = delete; 922 | VmHook& operator=(VmHook&& other) noexcept; 923 | ~VmHook(); 924 | 925 | /// @brief Removes the hook. 926 | void reset(); 927 | 928 | /// @brief Gets the original method pointer. 929 | template [[nodiscard]] T original() const { return reinterpret_cast(m_original_vm); } 930 | 931 | /// @brief Calls the original method. 932 | /// @tparam RetT The return type of the method. 933 | /// @tparam Args The argument types of the method. 934 | /// @param args The arguments to pass to the method. 935 | /// @return The return value of the method. 936 | /// @note This will call the original method with the default calling convention. 937 | template RetT call(Args... args) { 938 | return original()(args...); 939 | } 940 | 941 | /// @brief Calls the original method with the __cdecl calling convention. 942 | /// @tparam RetT The return type of the method. 943 | /// @tparam Args The argument types of the method. 944 | /// @param args The arguments to pass to the method. 945 | /// @return The return value of the method. 946 | template RetT ccall(Args... args) { 947 | return original()(args...); 948 | } 949 | 950 | /// @brief Calls the original method with the __thiscall calling convention. 951 | /// @tparam RetT The return type of the method. 952 | /// @tparam Args The argument types of the method. 953 | /// @param args The arguments to pass to the method. 954 | /// @return The return value of the method. 955 | template RetT thiscall(Args... args) { 956 | return original()(args...); 957 | } 958 | 959 | /// @brief Calls the original method with the __stdcall calling convention. 960 | /// @tparam RetT The return type of the method. 961 | /// @tparam Args The argument types of the method. 962 | /// @param args The arguments to pass to the method. 963 | /// @return The return value of the method. 964 | template RetT stdcall(Args... args) { 965 | return original()(args...); 966 | } 967 | 968 | /// @brief Calls the original method with the __fastcall calling convention. 969 | /// @tparam RetT The return type of the method. 970 | /// @tparam Args The argument types of the method. 971 | /// @param args The arguments to pass to the method. 972 | /// @return The return value of the method. 973 | template RetT fastcall(Args... args) { 974 | return original()(args...); 975 | } 976 | 977 | private: 978 | friend class VmtHook; 979 | 980 | uint8_t* m_original_vm{}; 981 | uint8_t* m_new_vm{}; 982 | uint8_t** m_vmt_entry{}; 983 | 984 | // This keeps the allocation alive until the hook is destroyed. 985 | std::shared_ptr m_new_vmt_allocation{}; 986 | 987 | void destroy(); 988 | }; 989 | 990 | /// @brief A hook class that copies an entire VMT for a given object and replaces it. 991 | class VmtHook final { 992 | public: 993 | /// @brief Error type for VmtHook. 994 | struct Error { 995 | /// @brief The type of error. 996 | enum : uint8_t { 997 | BAD_ALLOCATION, ///< An error occurred while allocating memory. 998 | } type; 999 | 1000 | /// @brief Extra error information. 1001 | union { 1002 | Allocator::Error allocator_error; ///< Allocator error information. 1003 | }; 1004 | 1005 | /// @brief Create a BAD_ALLOCATION error. 1006 | /// @param err The Allocator::Error that failed. 1007 | /// @return The new BAD_ALLOCATION error. 1008 | [[nodiscard]] static Error bad_allocation(Allocator::Error err) { 1009 | return {.type = BAD_ALLOCATION, .allocator_error = err}; 1010 | } 1011 | }; 1012 | 1013 | /// @brief Creates a new VmtHook object. Will clone the VMT of the given object and replace it. 1014 | /// @param object The object to hook. 1015 | /// @return The VmtHook object or a VmtHook::Error if an error occurred. 1016 | [[nodiscard]] static std::expected create(void* object); 1017 | 1018 | VmtHook() = default; 1019 | VmtHook(const VmtHook&) = delete; 1020 | VmtHook(VmtHook&& other) noexcept; 1021 | VmtHook& operator=(const VmtHook&) = delete; 1022 | VmtHook& operator=(VmtHook&& other) noexcept; 1023 | ~VmtHook(); 1024 | 1025 | /// @brief Applies the hook. 1026 | /// @param object The object to apply the hook to. 1027 | /// @note This will replace the VMT of the object with the new VMT. You can apply the hook to multiple objects. 1028 | void apply(void* object); 1029 | 1030 | /// @brief Removes the hook. 1031 | /// @param object The object to remove the hook from. 1032 | void remove(void* object); 1033 | 1034 | /// @brief Removes the hook from all objects. 1035 | void reset(); 1036 | 1037 | /// @brief Hooks a method in the VMT. 1038 | /// @param index The index of the method to hook. 1039 | /// @param new_function The new function to use. 1040 | template [[nodiscard]] std::expected hook_method(size_t index, T new_function) { 1041 | VmHook hook{}; 1042 | 1043 | ++index; // Skip RTTI pointer. 1044 | hook.m_original_vm = m_new_vmt[index]; 1045 | store(reinterpret_cast(&hook.m_new_vm), new_function); 1046 | hook.m_vmt_entry = &m_new_vmt[index]; 1047 | hook.m_new_vmt_allocation = m_new_vmt_allocation; 1048 | m_new_vmt[index] = hook.m_new_vm; 1049 | 1050 | return hook; 1051 | } 1052 | 1053 | private: 1054 | // Map of object instance to their original VMT. 1055 | std::unordered_map m_objects{}; 1056 | 1057 | // The allocation is a shared_ptr, so it can be shared with VmHooks to ensure the memory is kept alive. 1058 | std::shared_ptr m_new_vmt_allocation{}; 1059 | uint8_t** m_new_vmt{}; 1060 | 1061 | void destroy(); 1062 | }; 1063 | } // namespace safetyhook 1064 | 1065 | namespace safetyhook { 1066 | /// @brief Easy to use API for creating an InlineHook. 1067 | /// @param target The address of the function to hook. 1068 | /// @param destination The address of the destination function. 1069 | /// @param flags The flags to use. 1070 | /// @return The InlineHook object. 1071 | [[nodiscard]] InlineHook create_inline(void* target, void* destination, InlineHook::Flags flags = InlineHook::Default); 1072 | 1073 | /// @brief Easy to use API for creating an InlineHook. 1074 | /// @param target The address of the function to hook. 1075 | /// @param destination The address of the destination function. 1076 | /// @param flags The flags to use. 1077 | /// @return The InlineHook object. 1078 | template 1079 | [[nodiscard]] InlineHook create_inline(T target, U destination, InlineHook::Flags flags = InlineHook::Default) { 1080 | return create_inline(reinterpret_cast(target), reinterpret_cast(destination), flags); 1081 | } 1082 | 1083 | /// @brief Easy to use API for creating a MidHook. 1084 | /// @param target the address of the function to hook. 1085 | /// @param destination The destination function. 1086 | /// @param flags The flags to use. 1087 | /// @return The MidHook object. 1088 | [[nodiscard]] MidHook create_mid(void* target, MidHookFn destination, MidHook::Flags flags = MidHook::Default); 1089 | 1090 | /// @brief Easy to use API for creating a MidHook. 1091 | /// @param target the address of the function to hook. 1092 | /// @param destination The destination function. 1093 | /// @param flags The flags to use. 1094 | /// @return The MidHook object. 1095 | template 1096 | [[nodiscard]] MidHook create_mid(T target, MidHookFn destination, MidHook::Flags flags = MidHook::Default) { 1097 | return create_mid(reinterpret_cast(target), destination, flags); 1098 | } 1099 | 1100 | /// @brief Easy to use API for creating a VmtHook. 1101 | /// @param object The object to hook. 1102 | /// @return The VmtHook object. 1103 | [[nodiscard]] VmtHook create_vmt(void* object); 1104 | 1105 | /// @brief Easy to use API for creating a VmHook. 1106 | /// @param vmt The VmtHook to use to create the VmHook. 1107 | /// @param index The index of the method to hook. 1108 | /// @param destination The destination function. 1109 | /// @return The VmHook object. 1110 | template [[nodiscard]] VmHook create_vm(VmtHook& vmt, size_t index, T destination) { 1111 | if (auto hook = vmt.hook_method(index, destination)) { 1112 | return std::move(*hook); 1113 | } else { 1114 | return {}; 1115 | } 1116 | } 1117 | 1118 | } // namespace safetyhook 1119 | 1120 | using SafetyHookContext = safetyhook::Context; 1121 | using SafetyHookInline = safetyhook::InlineHook; 1122 | using SafetyHookMid = safetyhook::MidHook; 1123 | using SafetyInlineHook [[deprecated("Use SafetyHookInline instead.")]] = safetyhook::InlineHook; 1124 | using SafetyMidHook [[deprecated("Use SafetyHookMid instead.")]] = safetyhook::MidHook; 1125 | using SafetyHookVmt = safetyhook::VmtHook; 1126 | using SafetyHookVm = safetyhook::VmHook; -------------------------------------------------------------------------------- /external/safetyhook/safetyhook.cpp: -------------------------------------------------------------------------------- 1 | // DO NOT EDIT. This file is auto-generated by `amalgamate.py`. 2 | 3 | #define NOMINMAX 4 | 5 | #include "safetyhook.hpp" 6 | 7 | 8 | // 9 | // Source file: allocator.cpp 10 | // 11 | 12 | #include 13 | #include 14 | #include 15 | 16 | 17 | // 18 | // Header: safetyhook/os.hpp 19 | // 20 | 21 | // This is the OS abstraction layer. 22 | #pragma once 23 | 24 | #ifndef SAFETYHOOK_USE_CXXMODULES 25 | #include 26 | #include 27 | #include 28 | #else 29 | import std.compat; 30 | #endif 31 | 32 | namespace safetyhook { 33 | 34 | enum class OsError { 35 | FAILED_TO_ALLOCATE, 36 | FAILED_TO_PROTECT, 37 | FAILED_TO_QUERY, 38 | FAILED_TO_GET_NEXT_THREAD, 39 | FAILED_TO_GET_THREAD_CONTEXT, 40 | FAILED_TO_SET_THREAD_CONTEXT, 41 | FAILED_TO_FREEZE_THREAD, 42 | FAILED_TO_UNFREEZE_THREAD, 43 | FAILED_TO_GET_THREAD_ID, 44 | }; 45 | 46 | struct VmAccess { 47 | bool read : 1; 48 | bool write : 1; 49 | bool execute : 1; 50 | 51 | constexpr bool operator==(const VmAccess& other) const { 52 | return read == other.read && write == other.write && execute == other.execute; 53 | } 54 | }; 55 | 56 | constexpr VmAccess VM_ACCESS_R{.read = true, .write = false, .execute = false}; 57 | constexpr VmAccess VM_ACCESS_RW{.read = true, .write = true, .execute = false}; 58 | constexpr VmAccess VM_ACCESS_RX{.read = true, .write = false, .execute = true}; 59 | constexpr VmAccess VM_ACCESS_RWX{.read = true, .write = true, .execute = true}; 60 | 61 | struct VmBasicInfo { 62 | uint8_t* address; 63 | size_t size; 64 | VmAccess access; 65 | bool is_free; 66 | }; 67 | 68 | std::expected vm_allocate(uint8_t* address, size_t size, VmAccess access); 69 | void vm_free(uint8_t* address); 70 | std::expected vm_protect(uint8_t* address, size_t size, VmAccess access); 71 | std::expected vm_protect(uint8_t* address, size_t size, uint32_t access); 72 | std::expected vm_query(uint8_t* address); 73 | bool vm_is_readable(uint8_t* address, size_t size); 74 | bool vm_is_writable(uint8_t* address, size_t size); 75 | bool vm_is_executable(uint8_t* address); 76 | 77 | struct SystemInfo { 78 | uint32_t page_size; 79 | uint32_t allocation_granularity; 80 | uint8_t* min_address; 81 | uint8_t* max_address; 82 | }; 83 | 84 | SystemInfo system_info(); 85 | 86 | using ThreadContext = void*; 87 | 88 | void trap_threads(uint8_t* from, uint8_t* to, size_t len, const std::function& run_fn); 89 | 90 | /// @brief Will modify the context of a thread's IP to point to a new address if its IP is at the old address. 91 | /// @param ctx The thread context to modify. 92 | /// @param old_ip The old IP address. 93 | /// @param new_ip The new IP address. 94 | void fix_ip(ThreadContext ctx, uint8_t* old_ip, uint8_t* new_ip); 95 | 96 | } // namespace safetyhook 97 | 98 | 99 | 100 | namespace safetyhook { 101 | Allocation::Allocation(Allocation&& other) noexcept { 102 | *this = std::move(other); 103 | } 104 | 105 | Allocation& Allocation::operator=(Allocation&& other) noexcept { 106 | if (this != &other) { 107 | free(); 108 | 109 | m_allocator = std::move(other.m_allocator); 110 | m_address = other.m_address; 111 | m_size = other.m_size; 112 | 113 | other.m_address = nullptr; 114 | other.m_size = 0; 115 | } 116 | 117 | return *this; 118 | } 119 | 120 | Allocation::~Allocation() { 121 | free(); 122 | } 123 | 124 | void Allocation::free() { 125 | if (m_allocator && m_address != nullptr && m_size != 0) { 126 | m_allocator->free(m_address, m_size); 127 | m_address = nullptr; 128 | m_size = 0; 129 | m_allocator.reset(); 130 | } 131 | } 132 | 133 | Allocation::Allocation(std::shared_ptr allocator, uint8_t* address, size_t size) noexcept 134 | : m_allocator{std::move(allocator)}, m_address{address}, m_size{size} { 135 | } 136 | 137 | std::shared_ptr Allocator::global() { 138 | static std::weak_ptr global_allocator{}; 139 | static std::mutex global_allocator_mutex{}; 140 | 141 | std::scoped_lock lock{global_allocator_mutex}; 142 | 143 | if (auto allocator = global_allocator.lock()) { 144 | return allocator; 145 | } 146 | 147 | auto allocator = Allocator::create(); 148 | 149 | global_allocator = allocator; 150 | 151 | return allocator; 152 | } 153 | 154 | std::shared_ptr Allocator::create() { 155 | return std::shared_ptr{new Allocator{}}; 156 | } 157 | 158 | std::expected Allocator::allocate(size_t size) { 159 | return allocate_near({}, size, std::numeric_limits::max()); 160 | } 161 | 162 | std::expected Allocator::allocate_near( 163 | const std::vector& desired_addresses, size_t size, size_t max_distance) { 164 | std::scoped_lock lock{m_mutex}; 165 | return internal_allocate_near(desired_addresses, size, max_distance); 166 | } 167 | 168 | void Allocator::free(uint8_t* address, size_t size) { 169 | std::scoped_lock lock{m_mutex}; 170 | return internal_free(address, size); 171 | } 172 | 173 | std::expected Allocator::internal_allocate_near( 174 | const std::vector& desired_addresses, size_t size, size_t max_distance) { 175 | // Align to 2 bytes to pass MFP virtual method check 176 | // See https://itanium-cxx-abi.github.io/cxx-abi/abi.html#member-function-pointers 177 | size_t aligned_size = align_up(size, 2); 178 | 179 | // First search through our list of allocations for a free block that is large 180 | // enough. 181 | for (const auto& allocation : m_memory) { 182 | if (allocation->size < aligned_size) { 183 | continue; 184 | } 185 | 186 | for (auto node = allocation->freelist.get(); node != nullptr; node = node->next.get()) { 187 | // Enough room? 188 | if (static_cast(node->end - node->start) < aligned_size) { 189 | continue; 190 | } 191 | 192 | const auto address = node->start; 193 | 194 | // Close enough? 195 | if (!in_range(address, desired_addresses, max_distance)) { 196 | continue; 197 | } 198 | 199 | node->start += aligned_size; 200 | 201 | return Allocation{shared_from_this(), address, size}; 202 | } 203 | } 204 | 205 | // If we didn't find a free block, we need to allocate a new one. 206 | auto allocation_size = align_up(aligned_size, system_info().allocation_granularity); 207 | auto allocation_address = allocate_nearby_memory(desired_addresses, allocation_size, max_distance); 208 | 209 | if (!allocation_address) { 210 | return std::unexpected{allocation_address.error()}; 211 | } 212 | 213 | auto& allocation = m_memory.emplace_back(new Memory); 214 | 215 | allocation->address = *allocation_address; 216 | allocation->size = allocation_size; 217 | allocation->freelist = std::make_unique(); 218 | allocation->freelist->start = *allocation_address + aligned_size; 219 | allocation->freelist->end = *allocation_address + allocation_size; 220 | 221 | return Allocation{shared_from_this(), *allocation_address, size}; 222 | } 223 | 224 | void Allocator::internal_free(uint8_t* address, size_t size) { 225 | // See internal_allocate_near 226 | size = align_up(size, 2); 227 | 228 | for (const auto& allocation : m_memory) { 229 | if (allocation->address > address || allocation->address + allocation->size < address) { 230 | continue; 231 | } 232 | 233 | // Find the right place for our new freenode. 234 | FreeNode* prev{}; 235 | 236 | for (auto node = allocation->freelist.get(); node != nullptr; prev = node, node = node->next.get()) { 237 | if (node->start > address) { 238 | break; 239 | } 240 | } 241 | 242 | // Add new freenode. 243 | auto free_node = std::make_unique(); 244 | 245 | free_node->start = address; 246 | free_node->end = address + size; 247 | 248 | if (prev == nullptr) { 249 | free_node->next.swap(allocation->freelist); 250 | allocation->freelist.swap(free_node); 251 | } else { 252 | free_node->next.swap(prev->next); 253 | prev->next.swap(free_node); 254 | } 255 | 256 | combine_adjacent_freenodes(*allocation); 257 | break; 258 | } 259 | } 260 | 261 | void Allocator::combine_adjacent_freenodes(Memory& memory) { 262 | for (auto prev = memory.freelist.get(), node = prev; node != nullptr; node = node->next.get()) { 263 | if (prev->end == node->start) { 264 | prev->end = node->end; 265 | prev->next.swap(node->next); 266 | node->next.reset(); 267 | node = prev; 268 | } else { 269 | prev = node; 270 | } 271 | } 272 | } 273 | 274 | std::expected Allocator::allocate_nearby_memory( 275 | const std::vector& desired_addresses, size_t size, size_t max_distance) { 276 | if (desired_addresses.empty()) { 277 | if (auto result = vm_allocate(nullptr, size, VM_ACCESS_RWX)) { 278 | return result.value(); 279 | } 280 | 281 | return std::unexpected{Error::BAD_VIRTUAL_ALLOC}; 282 | } 283 | 284 | auto attempt_allocation = [&](uint8_t* p) -> uint8_t* { 285 | if (!in_range(p, desired_addresses, max_distance)) { 286 | return nullptr; 287 | } 288 | 289 | if (auto result = vm_allocate(p, size, VM_ACCESS_RWX)) { 290 | return result.value(); 291 | } 292 | 293 | return nullptr; 294 | }; 295 | 296 | auto si = system_info(); 297 | auto desired_address = desired_addresses[0]; 298 | auto search_start = si.min_address; 299 | auto search_end = si.max_address; 300 | 301 | if (static_cast(desired_address - search_start) > max_distance) { 302 | search_start = desired_address - max_distance; 303 | } 304 | 305 | if (static_cast(search_end - desired_address) > max_distance) { 306 | search_end = desired_address + max_distance; 307 | } 308 | 309 | search_start = std::max(search_start, si.min_address); 310 | search_end = std::min(search_end, si.max_address); 311 | desired_address = align_up(desired_address, si.allocation_granularity); 312 | VmBasicInfo mbi{}; 313 | 314 | // Search backwards from the desired_address. 315 | for (auto p = desired_address; p > search_start && in_range(p, desired_addresses, max_distance); 316 | p = align_down(mbi.address - 1, si.allocation_granularity)) { 317 | auto result = vm_query(p); 318 | 319 | if (!result) { 320 | break; 321 | } 322 | 323 | mbi = result.value(); 324 | 325 | if (!mbi.is_free) { 326 | continue; 327 | } 328 | 329 | if (auto allocation_address = attempt_allocation(p); allocation_address != nullptr) { 330 | return allocation_address; 331 | } 332 | } 333 | 334 | // Search forwards from the desired_address. 335 | for (auto p = desired_address; p < search_end && in_range(p, desired_addresses, max_distance); p += mbi.size) { 336 | auto result = vm_query(p); 337 | 338 | if (!result) { 339 | break; 340 | } 341 | 342 | mbi = result.value(); 343 | 344 | if (!mbi.is_free) { 345 | continue; 346 | } 347 | 348 | if (auto allocation_address = attempt_allocation(p); allocation_address != nullptr) { 349 | return allocation_address; 350 | } 351 | } 352 | 353 | return std::unexpected{Error::NO_MEMORY_IN_RANGE}; 354 | } 355 | 356 | bool Allocator::in_range(uint8_t* address, const std::vector& desired_addresses, size_t max_distance) { 357 | return std::ranges::all_of(desired_addresses, [&](const auto& desired_address) { 358 | const size_t delta = (address > desired_address) ? address - desired_address : desired_address - address; 359 | return delta <= max_distance; 360 | }); 361 | } 362 | 363 | Allocator::Memory::~Memory() { 364 | vm_free(address); 365 | } 366 | } // namespace safetyhook 367 | 368 | // 369 | // Source file: easy.cpp 370 | // 371 | 372 | 373 | namespace safetyhook { 374 | InlineHook create_inline(void* target, void* destination, InlineHook::Flags flags) { 375 | if (auto hook = InlineHook::create(target, destination, flags)) { 376 | return std::move(*hook); 377 | } else { 378 | return {}; 379 | } 380 | } 381 | 382 | MidHook create_mid(void* target, MidHookFn destination, MidHook::Flags flags) { 383 | if (auto hook = MidHook::create(target, destination, flags)) { 384 | return std::move(*hook); 385 | } else { 386 | return {}; 387 | } 388 | } 389 | 390 | VmtHook create_vmt(void* object) { 391 | if (auto hook = VmtHook::create(object)) { 392 | return std::move(*hook); 393 | } else { 394 | return {}; 395 | } 396 | } 397 | } // namespace safetyhook 398 | 399 | // 400 | // Source file: inline_hook.cpp 401 | // 402 | 403 | #include 404 | 405 | #if __has_include("Zydis/Zydis.h") 406 | #include "Zydis/Zydis.h" 407 | #elif __has_include("Zydis.h") 408 | #include "Zydis.h" 409 | #else 410 | #error "Zydis not found" 411 | #endif 412 | 413 | 414 | 415 | namespace safetyhook { 416 | 417 | #pragma pack(push, 1) 418 | struct JmpE9 { 419 | uint8_t opcode{0xE9}; 420 | uint32_t offset{0}; 421 | }; 422 | 423 | #if SAFETYHOOK_ARCH_X86_64 424 | struct JmpFF { 425 | uint8_t opcode0{0xFF}; 426 | uint8_t opcode1{0x25}; 427 | uint32_t offset{0}; 428 | }; 429 | 430 | struct TrampolineEpilogueE9 { 431 | JmpE9 jmp_to_original{}; 432 | JmpFF jmp_to_destination{}; 433 | uint64_t destination_address{}; 434 | }; 435 | 436 | struct TrampolineEpilogueFF { 437 | JmpFF jmp_to_original{}; 438 | uint64_t original_address{}; 439 | }; 440 | #elif SAFETYHOOK_ARCH_X86_32 441 | struct TrampolineEpilogueE9 { 442 | JmpE9 jmp_to_original{}; 443 | JmpE9 jmp_to_destination{}; 444 | }; 445 | #endif 446 | #pragma pack(pop) 447 | 448 | #if SAFETYHOOK_ARCH_X86_64 449 | static auto make_jmp_ff(uint8_t* src, uint8_t* dst, uint8_t* data) { 450 | JmpFF jmp{}; 451 | 452 | jmp.offset = static_cast(data - src - sizeof(jmp)); 453 | store(data, dst); 454 | 455 | return jmp; 456 | } 457 | 458 | [[nodiscard]] static std::expected emit_jmp_ff( 459 | uint8_t* src, uint8_t* dst, uint8_t* data, size_t size = sizeof(JmpFF)) { 460 | if (size < sizeof(JmpFF)) { 461 | return std::unexpected{InlineHook::Error::not_enough_space(dst)}; 462 | } 463 | 464 | if (size > sizeof(JmpFF)) { 465 | std::fill_n(src, size, static_cast(0x90)); 466 | } 467 | 468 | store(src, make_jmp_ff(src, dst, data)); 469 | 470 | return {}; 471 | } 472 | #endif 473 | 474 | constexpr auto make_jmp_e9(uint8_t* src, uint8_t* dst) { 475 | JmpE9 jmp{}; 476 | 477 | jmp.offset = static_cast(dst - src - sizeof(jmp)); 478 | 479 | return jmp; 480 | } 481 | 482 | [[nodiscard]] static std::expected emit_jmp_e9( 483 | uint8_t* src, uint8_t* dst, size_t size = sizeof(JmpE9)) { 484 | if (size < sizeof(JmpE9)) { 485 | return std::unexpected{InlineHook::Error::not_enough_space(dst)}; 486 | } 487 | 488 | if (size > sizeof(JmpE9)) { 489 | std::fill_n(src, size, static_cast(0x90)); 490 | } 491 | 492 | store(src, make_jmp_e9(src, dst)); 493 | 494 | return {}; 495 | } 496 | 497 | static bool decode(ZydisDecodedInstruction* ix, uint8_t* ip) { 498 | ZydisDecoder decoder{}; 499 | ZyanStatus status; 500 | 501 | #if SAFETYHOOK_ARCH_X86_64 502 | status = ZydisDecoderInit(&decoder, ZYDIS_MACHINE_MODE_LONG_64, ZYDIS_STACK_WIDTH_64); 503 | #elif SAFETYHOOK_ARCH_X86_32 504 | status = ZydisDecoderInit(&decoder, ZYDIS_MACHINE_MODE_LEGACY_32, ZYDIS_STACK_WIDTH_32); 505 | #endif 506 | 507 | if (!ZYAN_SUCCESS(status)) { 508 | return false; 509 | } 510 | 511 | return ZYAN_SUCCESS(ZydisDecoderDecodeInstruction(&decoder, nullptr, ip, 15, ix)); 512 | } 513 | 514 | std::expected InlineHook::create(void* target, void* destination, Flags flags) { 515 | return create(Allocator::global(), target, destination, flags); 516 | } 517 | 518 | std::expected InlineHook::create( 519 | const std::shared_ptr& allocator, void* target, void* destination, Flags flags) { 520 | InlineHook hook{}; 521 | 522 | if (const auto setup_result = 523 | hook.setup(allocator, reinterpret_cast(target), reinterpret_cast(destination)); 524 | !setup_result) { 525 | return std::unexpected{setup_result.error()}; 526 | } 527 | 528 | if (!(flags & StartDisabled)) { 529 | if (auto enable_result = hook.enable(); !enable_result) { 530 | return std::unexpected{enable_result.error()}; 531 | } 532 | } 533 | 534 | return hook; 535 | } 536 | 537 | InlineHook::InlineHook(InlineHook&& other) noexcept { 538 | *this = std::move(other); 539 | } 540 | 541 | InlineHook& InlineHook::operator=(InlineHook&& other) noexcept { 542 | if (this != &other) { 543 | destroy(); 544 | 545 | std::scoped_lock lock{m_mutex, other.m_mutex}; 546 | 547 | m_target = other.m_target; 548 | m_destination = other.m_destination; 549 | m_trampoline = std::move(other.m_trampoline); 550 | m_trampoline_size = other.m_trampoline_size; 551 | m_original_bytes = std::move(other.m_original_bytes); 552 | m_enabled = other.m_enabled; 553 | m_type = other.m_type; 554 | 555 | other.m_target = nullptr; 556 | other.m_destination = nullptr; 557 | other.m_trampoline_size = 0; 558 | other.m_enabled = false; 559 | other.m_type = Type::Unset; 560 | } 561 | 562 | return *this; 563 | } 564 | 565 | InlineHook::~InlineHook() { 566 | destroy(); 567 | } 568 | 569 | void InlineHook::reset() { 570 | *this = {}; 571 | } 572 | 573 | std::expected InlineHook::setup( 574 | const std::shared_ptr& allocator, uint8_t* target, uint8_t* destination) { 575 | m_target = target; 576 | m_destination = destination; 577 | 578 | if (auto e9_result = e9_hook(allocator); !e9_result) { 579 | #if SAFETYHOOK_ARCH_X86_64 580 | if (auto ff_result = ff_hook(allocator); !ff_result) { 581 | return ff_result; 582 | } 583 | #elif SAFETYHOOK_ARCH_X86_32 584 | return e9_result; 585 | #endif 586 | } 587 | 588 | return {}; 589 | } 590 | 591 | std::expected InlineHook::e9_hook(const std::shared_ptr& allocator) { 592 | m_original_bytes.clear(); 593 | m_trampoline_size = sizeof(TrampolineEpilogueE9); 594 | 595 | std::vector desired_addresses{m_target}; 596 | ZydisDecodedInstruction ix{}; 597 | 598 | for (auto ip = m_target; ip < m_target + sizeof(JmpE9); ip += ix.length) { 599 | if (!decode(&ix, ip)) { 600 | return std::unexpected{Error::failed_to_decode_instruction(ip)}; 601 | } 602 | 603 | m_trampoline_size += ix.length; 604 | m_original_bytes.insert(m_original_bytes.end(), ip, ip + ix.length); 605 | 606 | const auto is_relative = (ix.attributes & ZYDIS_ATTRIB_IS_RELATIVE) != 0; 607 | 608 | if (is_relative) { 609 | if (ix.raw.disp.size == 32) { 610 | const auto target_address = ip + ix.length + static_cast(ix.raw.disp.value); 611 | desired_addresses.emplace_back(target_address); 612 | } else if (ix.raw.imm[0].size == 32) { 613 | const auto target_address = ip + ix.length + static_cast(ix.raw.imm[0].value.s); 614 | desired_addresses.emplace_back(target_address); 615 | } else if (ix.meta.category == ZYDIS_CATEGORY_COND_BR && ix.meta.branch_type == ZYDIS_BRANCH_TYPE_SHORT) { 616 | const auto target_address = ip + ix.length + static_cast(ix.raw.imm[0].value.s); 617 | desired_addresses.emplace_back(target_address); 618 | m_trampoline_size += 4; // near conditional branches are 4 bytes larger. 619 | } else if (ix.meta.category == ZYDIS_CATEGORY_UNCOND_BR && ix.meta.branch_type == ZYDIS_BRANCH_TYPE_SHORT) { 620 | const auto target_address = ip + ix.length + static_cast(ix.raw.imm[0].value.s); 621 | desired_addresses.emplace_back(target_address); 622 | m_trampoline_size += 3; // near unconditional branches are 3 bytes larger. 623 | } else { 624 | return std::unexpected{Error::unsupported_instruction_in_trampoline(ip)}; 625 | } 626 | } 627 | } 628 | 629 | auto trampoline_allocation = allocator->allocate_near(desired_addresses, m_trampoline_size); 630 | 631 | if (!trampoline_allocation) { 632 | return std::unexpected{Error::bad_allocation(trampoline_allocation.error())}; 633 | } 634 | 635 | m_trampoline = std::move(*trampoline_allocation); 636 | 637 | for (auto ip = m_target, tramp_ip = m_trampoline.data(); ip < m_target + m_original_bytes.size(); ip += ix.length) { 638 | if (!decode(&ix, ip)) { 639 | m_trampoline.free(); 640 | return std::unexpected{Error::failed_to_decode_instruction(ip)}; 641 | } 642 | 643 | const auto is_relative = (ix.attributes & ZYDIS_ATTRIB_IS_RELATIVE) != 0; 644 | 645 | if (is_relative && ix.raw.disp.size == 32) { 646 | std::copy_n(ip, ix.length, tramp_ip); 647 | const auto target_address = ip + ix.length + ix.raw.disp.value; 648 | const auto new_disp = target_address - (tramp_ip + ix.length); 649 | store(tramp_ip + ix.raw.disp.offset, static_cast(new_disp)); 650 | tramp_ip += ix.length; 651 | } else if (is_relative && ix.raw.imm[0].size == 32) { 652 | std::copy_n(ip, ix.length, tramp_ip); 653 | const auto target_address = ip + ix.length + ix.raw.imm[0].value.s; 654 | const auto new_disp = target_address - (tramp_ip + ix.length); 655 | store(tramp_ip + ix.raw.imm[0].offset, static_cast(new_disp)); 656 | tramp_ip += ix.length; 657 | } else if (ix.meta.category == ZYDIS_CATEGORY_COND_BR && ix.meta.branch_type == ZYDIS_BRANCH_TYPE_SHORT) { 658 | const auto target_address = ip + ix.length + ix.raw.imm[0].value.s; 659 | auto new_disp = target_address - (tramp_ip + 6); 660 | 661 | // Handle the case where the target is now in the trampoline. 662 | if (target_address >= m_target && target_address < m_target + m_original_bytes.size()) { 663 | new_disp = static_cast(ix.raw.imm[0].value.s); 664 | } 665 | 666 | *tramp_ip = 0x0F; 667 | *(tramp_ip + 1) = 0x10 + ix.opcode; 668 | store(tramp_ip + 2, static_cast(new_disp)); 669 | tramp_ip += 6; 670 | } else if (ix.meta.category == ZYDIS_CATEGORY_UNCOND_BR && ix.meta.branch_type == ZYDIS_BRANCH_TYPE_SHORT) { 671 | const auto target_address = ip + ix.length + ix.raw.imm[0].value.s; 672 | auto new_disp = target_address - (tramp_ip + 5); 673 | 674 | // Handle the case where the target is now in the trampoline. 675 | if (target_address >= m_target && target_address < m_target + m_original_bytes.size()) { 676 | new_disp = static_cast(ix.raw.imm[0].value.s); 677 | } 678 | 679 | *tramp_ip = 0xE9; 680 | store(tramp_ip + 1, static_cast(new_disp)); 681 | tramp_ip += 5; 682 | } else { 683 | std::copy_n(ip, ix.length, tramp_ip); 684 | tramp_ip += ix.length; 685 | } 686 | } 687 | 688 | auto trampoline_epilogue = reinterpret_cast( 689 | m_trampoline.address() + m_trampoline_size - sizeof(TrampolineEpilogueE9)); 690 | 691 | // jmp from trampoline to original. 692 | auto src = reinterpret_cast(&trampoline_epilogue->jmp_to_original); 693 | auto dst = m_target + m_original_bytes.size(); 694 | 695 | if (auto result = emit_jmp_e9(src, dst); !result) { 696 | return std::unexpected{result.error()}; 697 | } 698 | 699 | // jmp from trampoline to destination. 700 | src = reinterpret_cast(&trampoline_epilogue->jmp_to_destination); 701 | dst = m_destination; 702 | 703 | #if SAFETYHOOK_ARCH_X86_64 704 | auto data = reinterpret_cast(&trampoline_epilogue->destination_address); 705 | 706 | if (auto result = emit_jmp_ff(src, dst, data); !result) { 707 | return std::unexpected{result.error()}; 708 | } 709 | #elif SAFETYHOOK_ARCH_X86_32 710 | if (auto result = emit_jmp_e9(src, dst); !result) { 711 | return std::unexpected{result.error()}; 712 | } 713 | #endif 714 | 715 | m_type = Type::E9; 716 | 717 | return {}; 718 | } 719 | 720 | #if SAFETYHOOK_ARCH_X86_64 721 | std::expected InlineHook::ff_hook(const std::shared_ptr& allocator) { 722 | m_original_bytes.clear(); 723 | m_trampoline_size = sizeof(TrampolineEpilogueFF); 724 | ZydisDecodedInstruction ix{}; 725 | 726 | for (auto ip = m_target; ip < m_target + sizeof(JmpFF) + sizeof(uintptr_t); ip += ix.length) { 727 | if (!decode(&ix, ip)) { 728 | return std::unexpected{Error::failed_to_decode_instruction(ip)}; 729 | } 730 | 731 | // We can't support any instruction that is IP relative here because 732 | // ff_hook should only be called if e9_hook failed indicating that 733 | // we're likely outside the +- 2GB range. 734 | if (ix.attributes & ZYDIS_ATTRIB_IS_RELATIVE) { 735 | return std::unexpected{Error::ip_relative_instruction_out_of_range(ip)}; 736 | } 737 | 738 | m_original_bytes.insert(m_original_bytes.end(), ip, ip + ix.length); 739 | m_trampoline_size += ix.length; 740 | } 741 | 742 | auto trampoline_allocation = allocator->allocate(m_trampoline_size); 743 | 744 | if (!trampoline_allocation) { 745 | return std::unexpected{Error::bad_allocation(trampoline_allocation.error())}; 746 | } 747 | 748 | m_trampoline = std::move(*trampoline_allocation); 749 | 750 | std::copy(m_original_bytes.begin(), m_original_bytes.end(), m_trampoline.data()); 751 | 752 | const auto trampoline_epilogue = 753 | reinterpret_cast(m_trampoline.data() + m_trampoline_size - sizeof(TrampolineEpilogueFF)); 754 | 755 | // jmp from trampoline to original. 756 | auto src = reinterpret_cast(&trampoline_epilogue->jmp_to_original); 757 | auto dst = m_target + m_original_bytes.size(); 758 | auto data = reinterpret_cast(&trampoline_epilogue->original_address); 759 | 760 | if (auto result = emit_jmp_ff(src, dst, data); !result) { 761 | return std::unexpected{result.error()}; 762 | } 763 | 764 | m_type = Type::FF; 765 | 766 | return {}; 767 | } 768 | #endif 769 | 770 | std::expected InlineHook::enable() { 771 | std::scoped_lock lock{m_mutex}; 772 | 773 | if (m_enabled) { 774 | return {}; 775 | } 776 | 777 | std::optional error; 778 | 779 | // jmp from original to trampoline. 780 | trap_threads(m_target, m_trampoline.data(), m_original_bytes.size(), [this, &error] { 781 | if (m_type == Type::E9) { 782 | auto trampoline_epilogue = reinterpret_cast( 783 | m_trampoline.address() + m_trampoline_size - sizeof(TrampolineEpilogueE9)); 784 | 785 | if (auto result = emit_jmp_e9(m_target, 786 | reinterpret_cast(&trampoline_epilogue->jmp_to_destination), m_original_bytes.size()); 787 | !result) { 788 | error = result.error(); 789 | } 790 | } 791 | 792 | #if SAFETYHOOK_ARCH_X86_64 793 | if (m_type == Type::FF) { 794 | if (auto result = emit_jmp_ff(m_target, m_destination, m_target + sizeof(JmpFF), m_original_bytes.size()); 795 | !result) { 796 | error = result.error(); 797 | } 798 | } 799 | #endif 800 | }); 801 | 802 | if (error) { 803 | return std::unexpected{*error}; 804 | } 805 | 806 | m_enabled = true; 807 | 808 | return {}; 809 | } 810 | 811 | std::expected InlineHook::disable() { 812 | std::scoped_lock lock{m_mutex}; 813 | 814 | if (!m_enabled) { 815 | return {}; 816 | } 817 | 818 | trap_threads(m_trampoline.data(), m_target, m_original_bytes.size(), 819 | [this] { std::copy(m_original_bytes.begin(), m_original_bytes.end(), m_target); }); 820 | 821 | m_enabled = false; 822 | 823 | return {}; 824 | } 825 | 826 | void InlineHook::destroy() { 827 | [[maybe_unused]] auto disable_result = disable(); 828 | 829 | std::scoped_lock lock{m_mutex}; 830 | 831 | if (!m_trampoline) { 832 | return; 833 | } 834 | 835 | m_trampoline.free(); 836 | } 837 | } // namespace safetyhook 838 | 839 | // 840 | // Source file: mid_hook.cpp 841 | // 842 | 843 | #include 844 | #include 845 | 846 | 847 | 848 | namespace safetyhook { 849 | 850 | #if SAFETYHOOK_ARCH_X86_64 851 | #if SAFETYHOOK_OS_WINDOWS 852 | constexpr std::array asm_data = {0xFF, 0x35, 0x79, 0x01, 0x00, 0x00, 0x54, 0x54, 0x55, 0x50, 0x53, 0x51, 853 | 0x52, 0x56, 0x57, 0x41, 0x50, 0x41, 0x51, 0x41, 0x52, 0x41, 0x53, 0x41, 0x54, 0x41, 0x55, 0x41, 0x56, 0x41, 0x57, 854 | 0x9C, 0x48, 0x81, 0xEC, 0x00, 0x01, 0x00, 0x00, 0xF3, 0x44, 0x0F, 0x7F, 0xBC, 0x24, 0xF0, 0x00, 0x00, 0x00, 0xF3, 855 | 0x44, 0x0F, 0x7F, 0xB4, 0x24, 0xE0, 0x00, 0x00, 0x00, 0xF3, 0x44, 0x0F, 0x7F, 0xAC, 0x24, 0xD0, 0x00, 0x00, 0x00, 856 | 0xF3, 0x44, 0x0F, 0x7F, 0xA4, 0x24, 0xC0, 0x00, 0x00, 0x00, 0xF3, 0x44, 0x0F, 0x7F, 0x9C, 0x24, 0xB0, 0x00, 0x00, 857 | 0x00, 0xF3, 0x44, 0x0F, 0x7F, 0x94, 0x24, 0xA0, 0x00, 0x00, 0x00, 0xF3, 0x44, 0x0F, 0x7F, 0x8C, 0x24, 0x90, 0x00, 858 | 0x00, 0x00, 0xF3, 0x44, 0x0F, 0x7F, 0x84, 0x24, 0x80, 0x00, 0x00, 0x00, 0xF3, 0x0F, 0x7F, 0x7C, 0x24, 0x70, 0xF3, 859 | 0x0F, 0x7F, 0x74, 0x24, 0x60, 0xF3, 0x0F, 0x7F, 0x6C, 0x24, 0x50, 0xF3, 0x0F, 0x7F, 0x64, 0x24, 0x40, 0xF3, 0x0F, 860 | 0x7F, 0x5C, 0x24, 0x30, 0xF3, 0x0F, 0x7F, 0x54, 0x24, 0x20, 0xF3, 0x0F, 0x7F, 0x4C, 0x24, 0x10, 0xF3, 0x0F, 0x7F, 861 | 0x04, 0x24, 0x48, 0x8B, 0x8C, 0x24, 0x80, 0x01, 0x00, 0x00, 0x48, 0x83, 0xC1, 0x10, 0x48, 0x89, 0x8C, 0x24, 0x80, 862 | 0x01, 0x00, 0x00, 0x48, 0x8D, 0x0C, 0x24, 0x48, 0x89, 0xE3, 0x48, 0x83, 0xEC, 0x30, 0x48, 0x83, 0xE4, 0xF0, 0xFF, 863 | 0x15, 0xA8, 0x00, 0x00, 0x00, 0x48, 0x89, 0xDC, 0xF3, 0x0F, 0x6F, 0x04, 0x24, 0xF3, 0x0F, 0x6F, 0x4C, 0x24, 0x10, 864 | 0xF3, 0x0F, 0x6F, 0x54, 0x24, 0x20, 0xF3, 0x0F, 0x6F, 0x5C, 0x24, 0x30, 0xF3, 0x0F, 0x6F, 0x64, 0x24, 0x40, 0xF3, 865 | 0x0F, 0x6F, 0x6C, 0x24, 0x50, 0xF3, 0x0F, 0x6F, 0x74, 0x24, 0x60, 0xF3, 0x0F, 0x6F, 0x7C, 0x24, 0x70, 0xF3, 0x44, 866 | 0x0F, 0x6F, 0x84, 0x24, 0x80, 0x00, 0x00, 0x00, 0xF3, 0x44, 0x0F, 0x6F, 0x8C, 0x24, 0x90, 0x00, 0x00, 0x00, 0xF3, 867 | 0x44, 0x0F, 0x6F, 0x94, 0x24, 0xA0, 0x00, 0x00, 0x00, 0xF3, 0x44, 0x0F, 0x6F, 0x9C, 0x24, 0xB0, 0x00, 0x00, 0x00, 868 | 0xF3, 0x44, 0x0F, 0x6F, 0xA4, 0x24, 0xC0, 0x00, 0x00, 0x00, 0xF3, 0x44, 0x0F, 0x6F, 0xAC, 0x24, 0xD0, 0x00, 0x00, 869 | 0x00, 0xF3, 0x44, 0x0F, 0x6F, 0xB4, 0x24, 0xE0, 0x00, 0x00, 0x00, 0xF3, 0x44, 0x0F, 0x6F, 0xBC, 0x24, 0xF0, 0x00, 870 | 0x00, 0x00, 0x48, 0x81, 0xC4, 0x00, 0x01, 0x00, 0x00, 0x9D, 0x41, 0x5F, 0x41, 0x5E, 0x41, 0x5D, 0x41, 0x5C, 0x41, 871 | 0x5B, 0x41, 0x5A, 0x41, 0x59, 0x41, 0x58, 0x5F, 0x5E, 0x5A, 0x59, 0x5B, 0x58, 0x5D, 0x48, 0x8D, 0x64, 0x24, 0x08, 872 | 0x5C, 0xC3, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; 873 | #elif SAFETYHOOK_OS_LINUX 874 | constexpr std::array asm_data = {0xFF, 0x35, 0x79, 0x01, 0x00, 0x00, 0x54, 0x54, 0x55, 0x50, 0x53, 0x51, 875 | 0x52, 0x56, 0x57, 0x41, 0x50, 0x41, 0x51, 0x41, 0x52, 0x41, 0x53, 0x41, 0x54, 0x41, 0x55, 0x41, 0x56, 0x41, 0x57, 876 | 0x9C, 0x48, 0x81, 0xEC, 0x00, 0x01, 0x00, 0x00, 0xF3, 0x44, 0x0F, 0x7F, 0xBC, 0x24, 0xF0, 0x00, 0x00, 0x00, 0xF3, 877 | 0x44, 0x0F, 0x7F, 0xB4, 0x24, 0xE0, 0x00, 0x00, 0x00, 0xF3, 0x44, 0x0F, 0x7F, 0xAC, 0x24, 0xD0, 0x00, 0x00, 0x00, 878 | 0xF3, 0x44, 0x0F, 0x7F, 0xA4, 0x24, 0xC0, 0x00, 0x00, 0x00, 0xF3, 0x44, 0x0F, 0x7F, 0x9C, 0x24, 0xB0, 0x00, 0x00, 879 | 0x00, 0xF3, 0x44, 0x0F, 0x7F, 0x94, 0x24, 0xA0, 0x00, 0x00, 0x00, 0xF3, 0x44, 0x0F, 0x7F, 0x8C, 0x24, 0x90, 0x00, 880 | 0x00, 0x00, 0xF3, 0x44, 0x0F, 0x7F, 0x84, 0x24, 0x80, 0x00, 0x00, 0x00, 0xF3, 0x0F, 0x7F, 0x7C, 0x24, 0x70, 0xF3, 881 | 0x0F, 0x7F, 0x74, 0x24, 0x60, 0xF3, 0x0F, 0x7F, 0x6C, 0x24, 0x50, 0xF3, 0x0F, 0x7F, 0x64, 0x24, 0x40, 0xF3, 0x0F, 882 | 0x7F, 0x5C, 0x24, 0x30, 0xF3, 0x0F, 0x7F, 0x54, 0x24, 0x20, 0xF3, 0x0F, 0x7F, 0x4C, 0x24, 0x10, 0xF3, 0x0F, 0x7F, 883 | 0x04, 0x24, 0x48, 0x8B, 0xBC, 0x24, 0x80, 0x01, 0x00, 0x00, 0x48, 0x83, 0xC7, 0x10, 0x48, 0x89, 0xBC, 0x24, 0x80, 884 | 0x01, 0x00, 0x00, 0x48, 0x8D, 0x3C, 0x24, 0x48, 0x89, 0xE3, 0x48, 0x83, 0xEC, 0x30, 0x48, 0x83, 0xE4, 0xF0, 0xFF, 885 | 0x15, 0xA8, 0x00, 0x00, 0x00, 0x48, 0x89, 0xDC, 0xF3, 0x0F, 0x6F, 0x04, 0x24, 0xF3, 0x0F, 0x6F, 0x4C, 0x24, 0x10, 886 | 0xF3, 0x0F, 0x6F, 0x54, 0x24, 0x20, 0xF3, 0x0F, 0x6F, 0x5C, 0x24, 0x30, 0xF3, 0x0F, 0x6F, 0x64, 0x24, 0x40, 0xF3, 887 | 0x0F, 0x6F, 0x6C, 0x24, 0x50, 0xF3, 0x0F, 0x6F, 0x74, 0x24, 0x60, 0xF3, 0x0F, 0x6F, 0x7C, 0x24, 0x70, 0xF3, 0x44, 888 | 0x0F, 0x6F, 0x84, 0x24, 0x80, 0x00, 0x00, 0x00, 0xF3, 0x44, 0x0F, 0x6F, 0x8C, 0x24, 0x90, 0x00, 0x00, 0x00, 0xF3, 889 | 0x44, 0x0F, 0x6F, 0x94, 0x24, 0xA0, 0x00, 0x00, 0x00, 0xF3, 0x44, 0x0F, 0x6F, 0x9C, 0x24, 0xB0, 0x00, 0x00, 0x00, 890 | 0xF3, 0x44, 0x0F, 0x6F, 0xA4, 0x24, 0xC0, 0x00, 0x00, 0x00, 0xF3, 0x44, 0x0F, 0x6F, 0xAC, 0x24, 0xD0, 0x00, 0x00, 891 | 0x00, 0xF3, 0x44, 0x0F, 0x6F, 0xB4, 0x24, 0xE0, 0x00, 0x00, 0x00, 0xF3, 0x44, 0x0F, 0x6F, 0xBC, 0x24, 0xF0, 0x00, 892 | 0x00, 0x00, 0x48, 0x81, 0xC4, 0x00, 0x01, 0x00, 0x00, 0x9D, 0x41, 0x5F, 0x41, 0x5E, 0x41, 0x5D, 0x41, 0x5C, 0x41, 893 | 0x5B, 0x41, 0x5A, 0x41, 0x59, 0x41, 0x58, 0x5F, 0x5E, 0x5A, 0x59, 0x5B, 0x58, 0x5D, 0x48, 0x8D, 0x64, 0x24, 0x08, 894 | 0x5C, 0xC3, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; 895 | #endif 896 | #elif SAFETYHOOK_ARCH_X86_32 897 | constexpr std::array asm_data = {0xFF, 0x35, 0xA7, 0x00, 0x00, 0x00, 0x54, 0x54, 0x55, 0x50, 0x53, 0x51, 898 | 0x52, 0x56, 0x57, 0x9C, 0x81, 0xEC, 0x80, 0x00, 0x00, 0x00, 0xF3, 0x0F, 0x7F, 0x7C, 0x24, 0x70, 0xF3, 0x0F, 0x7F, 899 | 0x74, 0x24, 0x60, 0xF3, 0x0F, 0x7F, 0x6C, 0x24, 0x50, 0xF3, 0x0F, 0x7F, 0x64, 0x24, 0x40, 0xF3, 0x0F, 0x7F, 0x5C, 900 | 0x24, 0x30, 0xF3, 0x0F, 0x7F, 0x54, 0x24, 0x20, 0xF3, 0x0F, 0x7F, 0x4C, 0x24, 0x10, 0xF3, 0x0F, 0x7F, 0x04, 0x24, 901 | 0x8B, 0x8C, 0x24, 0xA0, 0x00, 0x00, 0x00, 0x83, 0xC1, 0x08, 0x89, 0x8C, 0x24, 0xA0, 0x00, 0x00, 0x00, 0x54, 0xFF, 902 | 0x15, 0xA3, 0x00, 0x00, 0x00, 0x83, 0xC4, 0x04, 0xF3, 0x0F, 0x6F, 0x04, 0x24, 0xF3, 0x0F, 0x6F, 0x4C, 0x24, 0x10, 903 | 0xF3, 0x0F, 0x6F, 0x54, 0x24, 0x20, 0xF3, 0x0F, 0x6F, 0x5C, 0x24, 0x30, 0xF3, 0x0F, 0x6F, 0x64, 0x24, 0x40, 0xF3, 904 | 0x0F, 0x6F, 0x6C, 0x24, 0x50, 0xF3, 0x0F, 0x6F, 0x74, 0x24, 0x60, 0xF3, 0x0F, 0x6F, 0x7C, 0x24, 0x70, 0x81, 0xC4, 905 | 0x80, 0x00, 0x00, 0x00, 0x9D, 0x5F, 0x5E, 0x5A, 0x59, 0x5B, 0x58, 0x5D, 0x8D, 0x64, 0x24, 0x04, 0x5C, 0xC3, 0x00, 906 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; 907 | #endif 908 | 909 | std::expected MidHook::create(void* target, MidHookFn destination, Flags flags) { 910 | return create(Allocator::global(), target, destination, flags); 911 | } 912 | 913 | std::expected MidHook::create( 914 | const std::shared_ptr& allocator, void* target, MidHookFn destination, Flags flags) { 915 | MidHook hook{}; 916 | 917 | if (const auto setup_result = hook.setup(allocator, reinterpret_cast(target), destination); 918 | !setup_result) { 919 | return std::unexpected{setup_result.error()}; 920 | } 921 | 922 | if (!(flags & StartDisabled)) { 923 | if (auto enable_result = hook.enable(); !enable_result) { 924 | return std::unexpected{enable_result.error()}; 925 | } 926 | } 927 | 928 | return hook; 929 | } 930 | 931 | MidHook::MidHook(MidHook&& other) noexcept { 932 | *this = std::move(other); 933 | } 934 | 935 | MidHook& MidHook::operator=(MidHook&& other) noexcept { 936 | if (this != &other) { 937 | m_hook = std::move(other.m_hook); 938 | m_target = other.m_target; 939 | m_stub = std::move(other.m_stub); 940 | m_destination = other.m_destination; 941 | 942 | other.m_target = 0; 943 | other.m_destination = nullptr; 944 | } 945 | 946 | return *this; 947 | } 948 | 949 | void MidHook::reset() { 950 | *this = {}; 951 | } 952 | 953 | std::expected MidHook::setup( 954 | const std::shared_ptr& allocator, uint8_t* target, MidHookFn destination_fn) { 955 | m_target = target; 956 | m_destination = destination_fn; 957 | 958 | auto stub_allocation = allocator->allocate(asm_data.size()); 959 | 960 | if (!stub_allocation) { 961 | return std::unexpected{Error::bad_allocation(stub_allocation.error())}; 962 | } 963 | 964 | m_stub = std::move(*stub_allocation); 965 | 966 | std::copy(asm_data.begin(), asm_data.end(), m_stub.data()); 967 | 968 | #if SAFETYHOOK_ARCH_X86_64 969 | store(m_stub.data() + sizeof(asm_data) - 16, m_destination); 970 | #elif SAFETYHOOK_ARCH_X86_32 971 | store(m_stub.data() + sizeof(asm_data) - 8, m_destination); 972 | 973 | // 32-bit has some relocations we need to fix up as well. 974 | store(m_stub.data() + 0x02, m_stub.data() + m_stub.size() - 4); 975 | store(m_stub.data() + 0x59, m_stub.data() + m_stub.size() - 8); 976 | #endif 977 | 978 | auto hook_result = InlineHook::create(allocator, m_target, m_stub.data(), InlineHook::StartDisabled); 979 | 980 | if (!hook_result) { 981 | m_stub.free(); 982 | return std::unexpected{Error::bad_inline_hook(hook_result.error())}; 983 | } 984 | 985 | m_hook = std::move(*hook_result); 986 | 987 | #if SAFETYHOOK_ARCH_X86_64 988 | store(m_stub.data() + sizeof(asm_data) - 8, m_hook.trampoline().data()); 989 | #elif SAFETYHOOK_ARCH_X86_32 990 | store(m_stub.data() + sizeof(asm_data) - 4, m_hook.trampoline().data()); 991 | #endif 992 | 993 | return {}; 994 | } 995 | 996 | std::expected MidHook::enable() { 997 | if (auto enable_result = m_hook.enable(); !enable_result) { 998 | return std::unexpected{Error::bad_inline_hook(enable_result.error())}; 999 | } 1000 | 1001 | return {}; 1002 | } 1003 | 1004 | std::expected MidHook::disable() { 1005 | if (auto disable_result = m_hook.disable(); !disable_result) { 1006 | return std::unexpected{Error::bad_inline_hook(disable_result.error())}; 1007 | } 1008 | 1009 | return {}; 1010 | } 1011 | } // namespace safetyhook 1012 | 1013 | // 1014 | // Source file: os.linux.cpp 1015 | // 1016 | 1017 | 1018 | #if SAFETYHOOK_OS_LINUX 1019 | 1020 | #include 1021 | 1022 | #include 1023 | #include 1024 | 1025 | 1026 | 1027 | namespace safetyhook { 1028 | std::expected vm_allocate(uint8_t* address, size_t size, VmAccess access) { 1029 | int prot = 0; 1030 | int flags = MAP_PRIVATE | MAP_ANONYMOUS; 1031 | 1032 | if (access == VM_ACCESS_R) { 1033 | prot = PROT_READ; 1034 | } else if (access == VM_ACCESS_RW) { 1035 | prot = PROT_READ | PROT_WRITE; 1036 | } else if (access == VM_ACCESS_RX) { 1037 | prot = PROT_READ | PROT_EXEC; 1038 | } else if (access == VM_ACCESS_RWX) { 1039 | prot = PROT_READ | PROT_WRITE | PROT_EXEC; 1040 | } else { 1041 | return std::unexpected{OsError::FAILED_TO_ALLOCATE}; 1042 | } 1043 | 1044 | auto* result = mmap(address, size, prot, flags, -1, 0); 1045 | 1046 | if (result == MAP_FAILED) { 1047 | return std::unexpected{OsError::FAILED_TO_ALLOCATE}; 1048 | } 1049 | 1050 | return static_cast(result); 1051 | } 1052 | 1053 | void vm_free(uint8_t* address) { 1054 | munmap(address, 0); 1055 | } 1056 | 1057 | std::expected vm_protect(uint8_t* address, size_t size, VmAccess access) { 1058 | int prot = 0; 1059 | 1060 | if (access == VM_ACCESS_R) { 1061 | prot = PROT_READ; 1062 | } else if (access == VM_ACCESS_RW) { 1063 | prot = PROT_READ | PROT_WRITE; 1064 | } else if (access == VM_ACCESS_RX) { 1065 | prot = PROT_READ | PROT_EXEC; 1066 | } else if (access == VM_ACCESS_RWX) { 1067 | prot = PROT_READ | PROT_WRITE | PROT_EXEC; 1068 | } else { 1069 | return std::unexpected{OsError::FAILED_TO_PROTECT}; 1070 | } 1071 | 1072 | return vm_protect(address, size, prot); 1073 | } 1074 | 1075 | std::expected vm_protect(uint8_t* address, size_t size, uint32_t protect) { 1076 | auto mbi = vm_query(address); 1077 | 1078 | if (!mbi.has_value()) { 1079 | return std::unexpected{OsError::FAILED_TO_PROTECT}; 1080 | } 1081 | 1082 | uint32_t old_protect = 0; 1083 | 1084 | if (mbi->access.read) { 1085 | old_protect |= PROT_READ; 1086 | } 1087 | 1088 | if (mbi->access.write) { 1089 | old_protect |= PROT_WRITE; 1090 | } 1091 | 1092 | if (mbi->access.execute) { 1093 | old_protect |= PROT_EXEC; 1094 | } 1095 | 1096 | auto* addr = align_down(address, static_cast(sysconf(_SC_PAGESIZE))); 1097 | 1098 | if (mprotect(addr, size, static_cast(protect)) == -1) { 1099 | return std::unexpected{OsError::FAILED_TO_PROTECT}; 1100 | } 1101 | 1102 | return old_protect; 1103 | } 1104 | 1105 | std::expected vm_query(uint8_t* address) { 1106 | auto* maps = fopen("/proc/self/maps", "r"); 1107 | 1108 | if (maps == nullptr) { 1109 | return std::unexpected{OsError::FAILED_TO_QUERY}; 1110 | } 1111 | 1112 | char line[512]; 1113 | unsigned long start; 1114 | unsigned long end; 1115 | char perms[5]; 1116 | unsigned long offset; 1117 | int dev_major; 1118 | int dev_minor; 1119 | unsigned long inode; 1120 | char path[256]; 1121 | unsigned long last_end = 1122 | reinterpret_cast(system_info().min_address); // Track the end address of the last mapping. 1123 | auto addr = reinterpret_cast(address); 1124 | std::optional info = std::nullopt; 1125 | 1126 | while (fgets(line, sizeof(line), maps) != nullptr) { 1127 | path[0] = '\0'; 1128 | 1129 | sscanf(line, "%lx-%lx %4s %lx %x:%x %lu %255[^\n]", &start, &end, perms, &offset, &dev_major, &dev_minor, 1130 | &inode, path); 1131 | 1132 | if (last_end < start && addr >= last_end && addr < start) { 1133 | info = { 1134 | .address = reinterpret_cast(last_end), 1135 | .size = start - last_end, 1136 | .access = VmAccess{}, 1137 | .is_free = true, 1138 | }; 1139 | 1140 | break; 1141 | } 1142 | 1143 | last_end = end; 1144 | 1145 | if (addr >= start && addr < end) { 1146 | info = { 1147 | .address = reinterpret_cast(start), 1148 | .size = end - start, 1149 | .access = VmAccess{}, 1150 | .is_free = false, 1151 | }; 1152 | 1153 | if (perms[0] == 'r') { 1154 | info->access.read = true; 1155 | } 1156 | 1157 | if (perms[1] == 'w') { 1158 | info->access.write = true; 1159 | } 1160 | 1161 | if (perms[2] == 'x') { 1162 | info->access.execute = true; 1163 | } 1164 | 1165 | break; 1166 | } 1167 | } 1168 | 1169 | fclose(maps); 1170 | 1171 | if (!info.has_value()) { 1172 | return std::unexpected{OsError::FAILED_TO_QUERY}; 1173 | } 1174 | 1175 | return info.value(); 1176 | } 1177 | 1178 | bool vm_is_readable(uint8_t* address, [[maybe_unused]] size_t size) { 1179 | return vm_query(address).value_or(VmBasicInfo{}).access.read; 1180 | } 1181 | 1182 | bool vm_is_writable(uint8_t* address, [[maybe_unused]] size_t size) { 1183 | return vm_query(address).value_or(VmBasicInfo{}).access.write; 1184 | } 1185 | 1186 | bool vm_is_executable(uint8_t* address) { 1187 | return vm_query(address).value_or(VmBasicInfo{}).access.execute; 1188 | } 1189 | 1190 | SystemInfo system_info() { 1191 | auto page_size = static_cast(sysconf(_SC_PAGESIZE)); 1192 | 1193 | return { 1194 | .page_size = page_size, 1195 | .allocation_granularity = page_size, 1196 | .min_address = reinterpret_cast(0x10000), 1197 | .max_address = reinterpret_cast(1ull << 47), 1198 | }; 1199 | } 1200 | 1201 | void trap_threads([[maybe_unused]] uint8_t* from, [[maybe_unused]] uint8_t* to, [[maybe_unused]] size_t len, 1202 | const std::function& run_fn) { 1203 | auto from_protect = vm_protect(from, len, VM_ACCESS_RWX).value_or(0); 1204 | auto to_protect = vm_protect(to, len, VM_ACCESS_RWX).value_or(0); 1205 | run_fn(); 1206 | vm_protect(to, len, to_protect); 1207 | vm_protect(from, len, from_protect); 1208 | } 1209 | 1210 | void fix_ip([[maybe_unused]] ThreadContext ctx, [[maybe_unused]] uint8_t* old_ip, [[maybe_unused]] uint8_t* new_ip) { 1211 | } 1212 | 1213 | } // namespace safetyhook 1214 | 1215 | #endif 1216 | 1217 | // 1218 | // Source file: os.windows.cpp 1219 | // 1220 | 1221 | #include 1222 | #include 1223 | #include 1224 | 1225 | 1226 | #if SAFETYHOOK_OS_WINDOWS 1227 | 1228 | #define NOMINMAX 1229 | #if __has_include() 1230 | #include 1231 | #elif __has_include() 1232 | #include 1233 | #else 1234 | #error "Windows.h not found" 1235 | #endif 1236 | 1237 | 1238 | namespace safetyhook { 1239 | std::expected vm_allocate(uint8_t* address, size_t size, VmAccess access) { 1240 | DWORD protect = 0; 1241 | 1242 | if (access == VM_ACCESS_R) { 1243 | protect = PAGE_READONLY; 1244 | } else if (access == VM_ACCESS_RW) { 1245 | protect = PAGE_READWRITE; 1246 | } else if (access == VM_ACCESS_RX) { 1247 | protect = PAGE_EXECUTE_READ; 1248 | } else if (access == VM_ACCESS_RWX) { 1249 | protect = PAGE_EXECUTE_READWRITE; 1250 | } else { 1251 | return std::unexpected{OsError::FAILED_TO_ALLOCATE}; 1252 | } 1253 | 1254 | auto* result = VirtualAlloc(address, size, MEM_COMMIT | MEM_RESERVE, protect); 1255 | 1256 | if (result == nullptr) { 1257 | return std::unexpected{OsError::FAILED_TO_ALLOCATE}; 1258 | } 1259 | 1260 | return static_cast(result); 1261 | } 1262 | 1263 | void vm_free(uint8_t* address) { 1264 | VirtualFree(address, 0, MEM_RELEASE); 1265 | } 1266 | 1267 | std::expected vm_protect(uint8_t* address, size_t size, VmAccess access) { 1268 | DWORD protect = 0; 1269 | 1270 | if (access == VM_ACCESS_R) { 1271 | protect = PAGE_READONLY; 1272 | } else if (access == VM_ACCESS_RW) { 1273 | protect = PAGE_READWRITE; 1274 | } else if (access == VM_ACCESS_RX) { 1275 | protect = PAGE_EXECUTE_READ; 1276 | } else if (access == VM_ACCESS_RWX) { 1277 | protect = PAGE_EXECUTE_READWRITE; 1278 | } else { 1279 | return std::unexpected{OsError::FAILED_TO_PROTECT}; 1280 | } 1281 | 1282 | return vm_protect(address, size, protect); 1283 | } 1284 | 1285 | std::expected vm_protect(uint8_t* address, size_t size, uint32_t protect) { 1286 | DWORD old_protect = 0; 1287 | 1288 | if (VirtualProtect(address, size, protect, &old_protect) == FALSE) { 1289 | return std::unexpected{OsError::FAILED_TO_PROTECT}; 1290 | } 1291 | 1292 | return old_protect; 1293 | } 1294 | 1295 | std::expected vm_query(uint8_t* address) { 1296 | MEMORY_BASIC_INFORMATION mbi{}; 1297 | auto result = VirtualQuery(address, &mbi, sizeof(mbi)); 1298 | 1299 | if (result == 0) { 1300 | return std::unexpected{OsError::FAILED_TO_QUERY}; 1301 | } 1302 | 1303 | VmAccess access{ 1304 | .read = (mbi.Protect & (PAGE_READONLY | PAGE_READWRITE | PAGE_EXECUTE_READ | PAGE_EXECUTE_READWRITE)) != 0, 1305 | .write = (mbi.Protect & (PAGE_READWRITE | PAGE_EXECUTE_READWRITE)) != 0, 1306 | .execute = (mbi.Protect & (PAGE_EXECUTE | PAGE_EXECUTE_READ | PAGE_EXECUTE_READWRITE)) != 0, 1307 | }; 1308 | 1309 | return VmBasicInfo{ 1310 | .address = static_cast(mbi.AllocationBase), 1311 | .size = mbi.RegionSize, 1312 | .access = access, 1313 | .is_free = mbi.State == MEM_FREE, 1314 | }; 1315 | } 1316 | 1317 | bool vm_is_readable(uint8_t* address, size_t size) { 1318 | return IsBadReadPtr(address, size) == FALSE; 1319 | } 1320 | 1321 | bool vm_is_writable(uint8_t* address, size_t size) { 1322 | return IsBadWritePtr(address, size) == FALSE; 1323 | } 1324 | 1325 | bool vm_is_executable(uint8_t* address) { 1326 | LPVOID image_base_ptr; 1327 | 1328 | if (RtlPcToFileHeader(address, &image_base_ptr) == nullptr) { 1329 | return vm_query(address).value_or(VmBasicInfo{}).access.execute; 1330 | } 1331 | 1332 | // Just check if the section is executable. 1333 | const auto* image_base = reinterpret_cast(image_base_ptr); 1334 | const auto* dos_hdr = reinterpret_cast(image_base); 1335 | 1336 | if (dos_hdr->e_magic != IMAGE_DOS_SIGNATURE) { 1337 | return vm_query(address).value_or(VmBasicInfo{}).access.execute; 1338 | } 1339 | 1340 | const auto* nt_hdr = reinterpret_cast(image_base + dos_hdr->e_lfanew); 1341 | 1342 | if (nt_hdr->Signature != IMAGE_NT_SIGNATURE) { 1343 | return vm_query(address).value_or(VmBasicInfo{}).access.execute; 1344 | } 1345 | 1346 | const auto* section = IMAGE_FIRST_SECTION(nt_hdr); 1347 | 1348 | for (auto i = 0; i < nt_hdr->FileHeader.NumberOfSections; ++i, ++section) { 1349 | if (address >= image_base + section->VirtualAddress && 1350 | address < image_base + section->VirtualAddress + section->Misc.VirtualSize) { 1351 | return (section->Characteristics & IMAGE_SCN_MEM_EXECUTE) != 0; 1352 | } 1353 | } 1354 | 1355 | return vm_query(address).value_or(VmBasicInfo{}).access.execute; 1356 | } 1357 | 1358 | SystemInfo system_info() { 1359 | SystemInfo info{}; 1360 | 1361 | SYSTEM_INFO si{}; 1362 | GetSystemInfo(&si); 1363 | 1364 | info.page_size = si.dwPageSize; 1365 | info.allocation_granularity = si.dwAllocationGranularity; 1366 | info.min_address = static_cast(si.lpMinimumApplicationAddress); 1367 | info.max_address = static_cast(si.lpMaximumApplicationAddress); 1368 | 1369 | return info; 1370 | } 1371 | 1372 | struct TrapInfo { 1373 | uint8_t* from_page_start; 1374 | uint8_t* from_page_end; 1375 | uint8_t* from; 1376 | uint8_t* to_page_start; 1377 | uint8_t* to_page_end; 1378 | uint8_t* to; 1379 | size_t len; 1380 | }; 1381 | 1382 | class TrapManager final { 1383 | public: 1384 | static std::mutex mutex; 1385 | static std::unique_ptr instance; 1386 | static bool is_destructed; 1387 | 1388 | TrapManager() { m_trap_veh = AddVectoredExceptionHandler(1, trap_handler); } 1389 | ~TrapManager() { 1390 | if (m_trap_veh != nullptr) { 1391 | RemoveVectoredExceptionHandler(m_trap_veh); 1392 | } 1393 | is_destructed = true; 1394 | } 1395 | 1396 | TrapInfo* find_trap(uint8_t* address) { 1397 | auto search = std::find_if(m_traps.begin(), m_traps.end(), [address](auto& trap) { 1398 | return address >= trap.second.from && address < trap.second.from + trap.second.len; 1399 | }); 1400 | 1401 | if (search == m_traps.end()) { 1402 | return nullptr; 1403 | } 1404 | 1405 | return &search->second; 1406 | } 1407 | 1408 | TrapInfo* find_trap_page(uint8_t* address) { 1409 | auto search = std::find_if(m_traps.begin(), m_traps.end(), [address](auto& trap) { 1410 | return address >= trap.second.from_page_start && address < trap.second.from_page_end; 1411 | }); 1412 | 1413 | if (search != m_traps.end()) { 1414 | return &search->second; 1415 | } 1416 | 1417 | search = std::find_if(m_traps.begin(), m_traps.end(), [address](auto& trap) { 1418 | return address >= trap.second.to_page_start && address < trap.second.to_page_end; 1419 | }); 1420 | 1421 | if (search != m_traps.end()) { 1422 | return &search->second; 1423 | } 1424 | 1425 | return nullptr; 1426 | } 1427 | 1428 | void add_trap(uint8_t* from, uint8_t* to, size_t len) { 1429 | m_traps.insert_or_assign(from, TrapInfo{.from_page_start = align_down(from, 0x1000), 1430 | .from_page_end = align_up(from + len, 0x1000), 1431 | .from = from, 1432 | .to_page_start = align_down(to, 0x1000), 1433 | .to_page_end = align_up(to + len, 0x1000), 1434 | .to = to, 1435 | .len = len}); 1436 | } 1437 | 1438 | private: 1439 | std::map m_traps; 1440 | PVOID m_trap_veh{}; 1441 | 1442 | static LONG CALLBACK trap_handler(PEXCEPTION_POINTERS exp) { 1443 | auto exception_code = exp->ExceptionRecord->ExceptionCode; 1444 | 1445 | if (exception_code != EXCEPTION_ACCESS_VIOLATION) { 1446 | return EXCEPTION_CONTINUE_SEARCH; 1447 | } 1448 | 1449 | std::scoped_lock lock{mutex}; 1450 | auto* faulting_address = reinterpret_cast(exp->ExceptionRecord->ExceptionInformation[1]); 1451 | auto* trap = instance->find_trap(faulting_address); 1452 | 1453 | if (trap == nullptr) { 1454 | if (instance->find_trap_page(faulting_address) != nullptr) { 1455 | return EXCEPTION_CONTINUE_EXECUTION; 1456 | } else { 1457 | return EXCEPTION_CONTINUE_SEARCH; 1458 | } 1459 | } 1460 | 1461 | auto* ctx = exp->ContextRecord; 1462 | 1463 | for (size_t i = 0; i < trap->len; i++) { 1464 | fix_ip(ctx, trap->from + i, trap->to + i); 1465 | } 1466 | 1467 | return EXCEPTION_CONTINUE_EXECUTION; 1468 | } 1469 | }; 1470 | 1471 | std::mutex TrapManager::mutex; 1472 | std::unique_ptr TrapManager::instance; 1473 | bool TrapManager::is_destructed = false; 1474 | 1475 | void find_me() { 1476 | } 1477 | 1478 | void trap_threads(uint8_t* from, uint8_t* to, size_t len, const std::function& run_fn) { 1479 | MEMORY_BASIC_INFORMATION find_me_mbi{}; 1480 | MEMORY_BASIC_INFORMATION from_mbi{}; 1481 | MEMORY_BASIC_INFORMATION to_mbi{}; 1482 | 1483 | VirtualQuery(reinterpret_cast(find_me), &find_me_mbi, sizeof(find_me_mbi)); 1484 | VirtualQuery(from, &from_mbi, sizeof(from_mbi)); 1485 | VirtualQuery(to, &to_mbi, sizeof(to_mbi)); 1486 | 1487 | auto new_protect = PAGE_READWRITE; 1488 | 1489 | if (from_mbi.AllocationBase == find_me_mbi.AllocationBase || to_mbi.AllocationBase == find_me_mbi.AllocationBase) { 1490 | new_protect = PAGE_EXECUTE_READWRITE; 1491 | } 1492 | 1493 | auto si = system_info(); 1494 | auto *from_page_start = align_down(from, si.page_size); 1495 | auto *from_page_end = align_up(from + len, si.page_size); 1496 | auto *vp_start = reinterpret_cast(&VirtualProtect); 1497 | auto *vp_end = vp_start + 0x20; 1498 | 1499 | if (!(from_page_end < vp_start || vp_end < from_page_start)){ 1500 | new_protect = PAGE_EXECUTE_READWRITE; 1501 | } 1502 | 1503 | if (!TrapManager::is_destructed) { 1504 | std::scoped_lock lock{TrapManager::mutex}; 1505 | 1506 | if (TrapManager::instance == nullptr) { 1507 | TrapManager::instance = std::make_unique(); 1508 | } 1509 | 1510 | TrapManager::instance->add_trap(from, to, len); 1511 | } 1512 | 1513 | DWORD from_protect; 1514 | DWORD to_protect; 1515 | 1516 | VirtualProtect(from, len, new_protect, &from_protect); 1517 | VirtualProtect(to, len, new_protect, &to_protect); 1518 | 1519 | if (run_fn) { 1520 | run_fn(); 1521 | } 1522 | 1523 | VirtualProtect(to, len, to_protect, &to_protect); 1524 | VirtualProtect(from, len, from_protect, &from_protect); 1525 | } 1526 | 1527 | void fix_ip(ThreadContext thread_ctx, uint8_t* old_ip, uint8_t* new_ip) { 1528 | auto* ctx = reinterpret_cast(thread_ctx); 1529 | 1530 | #if SAFETYHOOK_ARCH_X86_64 1531 | auto ip = ctx->Rip; 1532 | #elif SAFETYHOOK_ARCH_X86_32 1533 | auto ip = ctx->Eip; 1534 | #endif 1535 | 1536 | if (ip == reinterpret_cast(old_ip)) { 1537 | ip = reinterpret_cast(new_ip); 1538 | } 1539 | 1540 | #if SAFETYHOOK_ARCH_X86_64 1541 | ctx->Rip = ip; 1542 | #elif SAFETYHOOK_ARCH_X86_32 1543 | ctx->Eip = ip; 1544 | #endif 1545 | } 1546 | 1547 | } // namespace safetyhook 1548 | 1549 | #endif 1550 | 1551 | // 1552 | // Source file: utility.cpp 1553 | // 1554 | 1555 | 1556 | 1557 | namespace safetyhook { 1558 | bool is_executable(uint8_t* address) { 1559 | return vm_is_executable(address); 1560 | } 1561 | 1562 | UnprotectMemory::~UnprotectMemory() { 1563 | if (m_address != nullptr) { 1564 | vm_protect(m_address, m_size, m_original_protection); 1565 | } 1566 | } 1567 | 1568 | UnprotectMemory::UnprotectMemory(UnprotectMemory&& other) noexcept { 1569 | *this = std::move(other); 1570 | } 1571 | 1572 | UnprotectMemory& UnprotectMemory::operator=(UnprotectMemory&& other) noexcept { 1573 | if (this != &other) { 1574 | m_address = other.m_address; 1575 | m_size = other.m_size; 1576 | m_original_protection = other.m_original_protection; 1577 | other.m_address = nullptr; 1578 | other.m_size = 0; 1579 | other.m_original_protection = 0; 1580 | } 1581 | 1582 | return *this; 1583 | } 1584 | 1585 | std::optional unprotect(uint8_t* address, size_t size) { 1586 | auto old_protection = vm_protect(address, size, VM_ACCESS_RWX); 1587 | 1588 | if (!old_protection) { 1589 | return std::nullopt; 1590 | } 1591 | 1592 | return UnprotectMemory{address, size, old_protection.value()}; 1593 | } 1594 | 1595 | } // namespace safetyhook 1596 | 1597 | // 1598 | // Source file: vmt_hook.cpp 1599 | // 1600 | 1601 | 1602 | 1603 | namespace safetyhook { 1604 | VmHook::VmHook(VmHook&& other) noexcept { 1605 | *this = std::move(other); 1606 | } 1607 | 1608 | VmHook& VmHook::operator=(VmHook&& other) noexcept { 1609 | destroy(); 1610 | m_original_vm = other.m_original_vm; 1611 | m_new_vm = other.m_new_vm; 1612 | m_vmt_entry = other.m_vmt_entry; 1613 | m_new_vmt_allocation = std::move(other.m_new_vmt_allocation); 1614 | other.m_original_vm = nullptr; 1615 | other.m_new_vm = nullptr; 1616 | other.m_vmt_entry = nullptr; 1617 | return *this; 1618 | } 1619 | 1620 | VmHook::~VmHook() { 1621 | destroy(); 1622 | } 1623 | 1624 | void VmHook::reset() { 1625 | *this = {}; 1626 | } 1627 | 1628 | void VmHook::destroy() { 1629 | if (m_original_vm != nullptr) { 1630 | *m_vmt_entry = m_original_vm; 1631 | m_original_vm = nullptr; 1632 | m_new_vm = nullptr; 1633 | m_vmt_entry = nullptr; 1634 | m_new_vmt_allocation.reset(); 1635 | } 1636 | } 1637 | 1638 | std::expected VmtHook::create(void* object) { 1639 | VmtHook hook{}; 1640 | 1641 | const auto original_vmt = *reinterpret_cast(object); 1642 | hook.m_objects.emplace(object, original_vmt); 1643 | 1644 | // Count the number of virtual method pointers. We start at one to account for the RTTI pointer. 1645 | auto num_vmt_entries = 1; 1646 | 1647 | for (auto vm = original_vmt; is_executable(*vm); ++vm) { 1648 | ++num_vmt_entries; 1649 | } 1650 | 1651 | // Allocate memory for the new VMT. 1652 | auto allocation = Allocator::global()->allocate(num_vmt_entries * sizeof(uint8_t*)); 1653 | 1654 | if (!allocation) { 1655 | return std::unexpected{Error::bad_allocation(allocation.error())}; 1656 | } 1657 | 1658 | hook.m_new_vmt_allocation = std::make_shared(std::move(*allocation)); 1659 | hook.m_new_vmt = reinterpret_cast(hook.m_new_vmt_allocation->data()); 1660 | 1661 | // Copy pointer to RTTI. 1662 | hook.m_new_vmt[0] = original_vmt[-1]; 1663 | 1664 | // Copy virtual method pointers. 1665 | for (auto i = 0; i < num_vmt_entries - 1; ++i) { 1666 | hook.m_new_vmt[i + 1] = original_vmt[i]; 1667 | } 1668 | 1669 | *reinterpret_cast(object) = &hook.m_new_vmt[1]; 1670 | 1671 | return hook; 1672 | } 1673 | 1674 | VmtHook::VmtHook(VmtHook&& other) noexcept { 1675 | *this = std::move(other); 1676 | } 1677 | 1678 | VmtHook& VmtHook::operator=(VmtHook&& other) noexcept { 1679 | destroy(); 1680 | m_objects = std::move(other.m_objects); 1681 | m_new_vmt_allocation = std::move(other.m_new_vmt_allocation); 1682 | m_new_vmt = other.m_new_vmt; 1683 | other.m_new_vmt = nullptr; 1684 | return *this; 1685 | } 1686 | 1687 | VmtHook::~VmtHook() { 1688 | destroy(); 1689 | } 1690 | 1691 | void VmtHook::apply(void* object) { 1692 | m_objects.emplace(object, *reinterpret_cast(object)); 1693 | *reinterpret_cast(object) = &m_new_vmt[1]; 1694 | } 1695 | 1696 | void VmtHook::remove(void* object) { 1697 | const auto search = m_objects.find(object); 1698 | 1699 | if (search == m_objects.end()) { 1700 | return; 1701 | } 1702 | 1703 | const auto original_vmt = search->second; 1704 | 1705 | if (!vm_is_writable(reinterpret_cast(object), sizeof(void*))) { 1706 | m_objects.erase(search); 1707 | return; 1708 | } 1709 | 1710 | if (*reinterpret_cast(object) != &m_new_vmt[1]) { 1711 | m_objects.erase(search); 1712 | return; 1713 | } 1714 | 1715 | *reinterpret_cast(object) = original_vmt; 1716 | 1717 | m_objects.erase(search); 1718 | } 1719 | 1720 | void VmtHook::reset() { 1721 | *this = {}; 1722 | } 1723 | 1724 | void VmtHook::destroy() { 1725 | for (const auto [object, original_vmt] : m_objects) { 1726 | if (!vm_is_writable(reinterpret_cast(object), sizeof(void*))) { 1727 | continue; 1728 | } 1729 | 1730 | if (*reinterpret_cast(object) != &m_new_vmt[1]) { 1731 | continue; 1732 | } 1733 | 1734 | *reinterpret_cast(object) = original_vmt; 1735 | } 1736 | 1737 | m_objects.clear(); 1738 | m_new_vmt_allocation.reset(); 1739 | m_new_vmt = nullptr; 1740 | } 1741 | } // namespace safetyhook --------------------------------------------------------------------------------