├── .gitattributes
├── .github
├── FUNDING.yml
└── workflows
│ └── create_release.yaml
├── .gitignore
├── .gitmodules
├── DDDAFix.filters
├── DDDAFix.ini
├── DDDAFix.sln
├── DDDAFix.vcxproj
├── LICENSE.md
├── README.md
├── external
└── safetyhook
│ ├── Zydis.c
│ ├── Zydis.h
│ ├── safetyhook.cpp
│ └── safetyhook.hpp
└── src
├── dllmain.cpp
├── helper.hpp
└── stdafx.h
/.gitattributes:
--------------------------------------------------------------------------------
1 | ###############################################################################
2 | # Set default behavior to automatically normalize line endings.
3 | ###############################################################################
4 | * text=auto
5 |
6 | ###############################################################################
7 | # Set default behavior for command prompt diff.
8 | #
9 | # This is need for earlier builds of msysgit that does not have it on by
10 | # default for csharp files.
11 | # Note: This is only used by command line
12 | ###############################################################################
13 | #*.cs diff=csharp
14 |
15 | ###############################################################################
16 | # Set the merge driver for project and solution files
17 | #
18 | # Merging from the command prompt will add diff markers to the files if there
19 | # are conflicts (Merging from VS is not affected by the settings below, in VS
20 | # the diff markers are never inserted). Diff markers may cause the following
21 | # file extensions to fail to load in VS. An alternative would be to treat
22 | # these files as binary and thus will always conflict and require user
23 | # intervention with every merge. To do so, just uncomment the entries below
24 | ###############################################################################
25 | #*.sln merge=binary
26 | #*.csproj merge=binary
27 | #*.vbproj merge=binary
28 | #*.vcxproj merge=binary
29 | #*.vcproj merge=binary
30 | #*.dbproj merge=binary
31 | #*.fsproj merge=binary
32 | #*.lsproj merge=binary
33 | #*.wixproj merge=binary
34 | #*.modelproj merge=binary
35 | #*.sqlproj merge=binary
36 | #*.wwaproj merge=binary
37 |
38 | ###############################################################################
39 | # behavior for image files
40 | #
41 | # image files are treated as binary by default.
42 | ###############################################################################
43 | #*.jpg binary
44 | #*.png binary
45 | #*.gif binary
46 |
47 | ###############################################################################
48 | # diff behavior for common document formats
49 | #
50 | # Convert binary document formats to text before diffing them. This feature
51 | # is only available from the command line. Turn it on by uncommenting the
52 | # entries below.
53 | ###############################################################################
54 | #*.doc diff=astextplain
55 | #*.DOC diff=astextplain
56 | #*.docx diff=astextplain
57 | #*.DOCX diff=astextplain
58 | #*.dot diff=astextplain
59 | #*.DOT diff=astextplain
60 | #*.pdf diff=astextplain
61 | #*.PDF diff=astextplain
62 | #*.rtf diff=astextplain
63 | #*.RTF diff=astextplain
64 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | patreon: Wintermance
2 | ko_fi: lyall
3 |
--------------------------------------------------------------------------------
/.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 | steps:
20 | - uses: actions/checkout@v4
21 | with:
22 | submodules: 'true'
23 |
24 | - name: setup-msbuild
25 | uses: microsoft/setup-msbuild@v1.1
26 |
27 | - run: msbuild ${{ github.event.repository.name }}.sln -t:rebuild -verbosity:diag -property:Configuration=Release -property:Platform=x86
28 | - run: cp Release\${{ github.event.repository.name }}.asi ${{ github.event.repository.name }}.asi
29 |
30 | - uses: robinraju/release-downloader@v1.8
31 | with:
32 | repository: "ThirteenAG/Ultimate-ASI-Loader"
33 | latest: true
34 | fileName: "Ultimate-ASI-Loader.zip"
35 |
36 | - name: Prepare Ultimate ASI Loader
37 | run: |
38 | unzip Ultimate-ASI-Loader.zip -d .\
39 | C:\msys64\usr\bin\wget.exe -O .\UltimateASILoader_LICENSE.md https://raw.githubusercontent.com/ThirteenAG/Ultimate-ASI-Loader/master/license
40 |
41 | - name: Create Directory Structure
42 | run: |
43 | mkdir .\zip\
44 |
45 | # Release build
46 | - name: Prepare Release Files
47 | run: |
48 | cp ${{ github.event.repository.name }}.asi .\zip\
49 | cp ${{ github.event.repository.name }}.ini .\zip\
50 | cp dinput8.dll .\zip\winmm.dll
51 | cp UltimateASILoader_LICENSE.md .\zip\
52 | New-Item -Path ".\zip\EXTRACT_TO_GAME_FOLDER" -ItemType File
53 |
54 | - name: Create Release Zip
55 | run: |
56 | cd .\zip
57 | 7z a -r -tzip ..\${{ github.event.repository.name }}_${{ github.event.inputs.version }}.zip .\*
58 |
59 | - uses: ncipollo/release-action@v1
60 | with:
61 | artifacts: "${{ github.event.repository.name }}_${{ github.event.inputs.version }}.zip"
62 | token: ${{ secrets.GITHUB_TOKEN }}
63 | tag: ${{ github.event.inputs.version }}
64 | name: "${{ github.event.inputs.version }}"
65 | draft: true
66 | generateReleaseNotes: true
67 | artifactErrorsFailBuild: true
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | ## Ignore Visual Studio temporary files, build results, and
2 | ## files generated by popular Visual Studio add-ons.
3 | ##
4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
5 |
6 | # User-specific files
7 | *.rsuser
8 | *.suo
9 | *.user
10 | *.userosscache
11 | *.sln.docstates
12 |
13 | # User-specific files (MonoDevelop/Xamarin Studio)
14 | *.userprefs
15 |
16 | # Mono auto generated files
17 | mono_crash.*
18 |
19 | # Build results
20 | [Dd]ebug/
21 | [Dd]ebugPublic/
22 | [Rr]elease/
23 | [Rr]eleases/
24 | x64/
25 | x86/
26 | [Ww][Ii][Nn]32/
27 | [Aa][Rr][Mm]/
28 | [Aa][Rr][Mm]64/
29 | bld/
30 | [Bb]in/
31 | [Oo]bj/
32 | [Oo]ut/
33 | [Ll]og/
34 | [Ll]ogs/
35 |
36 | # Visual Studio 2015/2017 cache/options directory
37 | .vs/
38 | # Uncomment if you have tasks that create the project's static files in wwwroot
39 | #wwwroot/
40 |
41 | # Visual Studio 2017 auto generated files
42 | Generated\ Files/
43 |
44 | # MSTest test Results
45 | [Tt]est[Rr]esult*/
46 | [Bb]uild[Ll]og.*
47 |
48 | # NUnit
49 | *.VisualState.xml
50 | TestResult.xml
51 | nunit-*.xml
52 |
53 | # Build Results of an ATL Project
54 | [Dd]ebugPS/
55 | [Rr]eleasePS/
56 | dlldata.c
57 |
58 | # Benchmark Results
59 | BenchmarkDotNet.Artifacts/
60 |
61 | # .NET Core
62 | project.lock.json
63 | project.fragment.lock.json
64 | artifacts/
65 |
66 | # ASP.NET Scaffolding
67 | ScaffoldingReadMe.txt
68 |
69 | # StyleCop
70 | StyleCopReport.xml
71 |
72 | # Files built by Visual Studio
73 | *_i.c
74 | *_p.c
75 | *_h.h
76 | *.ilk
77 | *.meta
78 | *.obj
79 | *.iobj
80 | *.pch
81 | *.pdb
82 | *.ipdb
83 | *.pgc
84 | *.pgd
85 | *.rsp
86 | *.sbr
87 | *.tlb
88 | *.tli
89 | *.tlh
90 | *.tmp
91 | *.tmp_proj
92 | *_wpftmp.csproj
93 | *.log
94 | *.vspscc
95 | *.vssscc
96 | .builds
97 | *.pidb
98 | *.svclog
99 | *.scc
100 |
101 | # Chutzpah Test files
102 | _Chutzpah*
103 |
104 | # Visual C++ cache files
105 | ipch/
106 | *.aps
107 | *.ncb
108 | *.opendb
109 | *.opensdf
110 | *.sdf
111 | *.cachefile
112 | *.VC.db
113 | *.VC.VC.opendb
114 |
115 | # Visual Studio profiler
116 | *.psess
117 | *.vsp
118 | *.vspx
119 | *.sap
120 |
121 | # Visual Studio Trace Files
122 | *.e2e
123 |
124 | # TFS 2012 Local Workspace
125 | $tf/
126 |
127 | # Guidance Automation Toolkit
128 | *.gpState
129 |
130 | # ReSharper is a .NET coding add-in
131 | _ReSharper*/
132 | *.[Rr]e[Ss]harper
133 | *.DotSettings.user
134 |
135 | # TeamCity is a build add-in
136 | _TeamCity*
137 |
138 | # DotCover is a Code Coverage Tool
139 | *.dotCover
140 |
141 | # AxoCover is a Code Coverage Tool
142 | .axoCover/*
143 | !.axoCover/settings.json
144 |
145 | # Coverlet is a free, cross platform Code Coverage Tool
146 | coverage*.json
147 | coverage*.xml
148 | coverage*.info
149 |
150 | # Visual Studio code coverage results
151 | *.coverage
152 | *.coveragexml
153 |
154 | # NCrunch
155 | _NCrunch_*
156 | .*crunch*.local.xml
157 | nCrunchTemp_*
158 |
159 | # MightyMoose
160 | *.mm.*
161 | AutoTest.Net/
162 |
163 | # Web workbench (sass)
164 | .sass-cache/
165 |
166 | # Installshield output folder
167 | [Ee]xpress/
168 |
169 | # DocProject is a documentation generator add-in
170 | DocProject/buildhelp/
171 | DocProject/Help/*.HxT
172 | DocProject/Help/*.HxC
173 | DocProject/Help/*.hhc
174 | DocProject/Help/*.hhk
175 | DocProject/Help/*.hhp
176 | DocProject/Help/Html2
177 | DocProject/Help/html
178 |
179 | # Click-Once directory
180 | publish/
181 |
182 | # Publish Web Output
183 | *.[Pp]ublish.xml
184 | *.azurePubxml
185 | # Note: Comment the next line if you want to checkin your web deploy settings,
186 | # but database connection strings (with potential passwords) will be unencrypted
187 | *.pubxml
188 | *.publishproj
189 |
190 | # Microsoft Azure Web App publish settings. Comment the next line if you want to
191 | # checkin your Azure Web App publish settings, but sensitive information contained
192 | # in these scripts will be unencrypted
193 | PublishScripts/
194 |
195 | # NuGet Packages
196 | *.nupkg
197 | # NuGet Symbol Packages
198 | *.snupkg
199 | # The packages folder can be ignored because of Package Restore
200 | **/[Pp]ackages/*
201 | # except build/, which is used as an MSBuild target.
202 | !**/[Pp]ackages/build/
203 | # Uncomment if necessary however generally it will be regenerated when needed
204 | #!**/[Pp]ackages/repositories.config
205 | # NuGet v3's project.json files produces more ignorable files
206 | *.nuget.props
207 | *.nuget.targets
208 |
209 | # Microsoft Azure Build Output
210 | csx/
211 | *.build.csdef
212 |
213 | # Microsoft Azure Emulator
214 | ecf/
215 | rcf/
216 |
217 | # Windows Store app package directories and files
218 | AppPackages/
219 | BundleArtifacts/
220 | Package.StoreAssociation.xml
221 | _pkginfo.txt
222 | *.appx
223 | *.appxbundle
224 | *.appxupload
225 |
226 | # Visual Studio cache files
227 | # files ending in .cache can be ignored
228 | *.[Cc]ache
229 | # but keep track of directories ending in .cache
230 | !?*.[Cc]ache/
231 |
232 | # Others
233 | ClientBin/
234 | ~$*
235 | *~
236 | *.dbmdl
237 | *.dbproj.schemaview
238 | *.jfm
239 | *.pfx
240 | *.publishsettings
241 | orleans.codegen.cs
242 |
243 | # Including strong name files can present a security risk
244 | # (https://github.com/github/gitignore/pull/2483#issue-259490424)
245 | #*.snk
246 |
247 | # Since there are multiple workflows, uncomment next line to ignore bower_components
248 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
249 | #bower_components/
250 |
251 | # RIA/Silverlight projects
252 | Generated_Code/
253 |
254 | # Backup & report files from converting an old project file
255 | # to a newer Visual Studio version. Backup files are not needed,
256 | # because we have git ;-)
257 | _UpgradeReport_Files/
258 | Backup*/
259 | UpgradeLog*.XML
260 | UpgradeLog*.htm
261 | ServiceFabricBackup/
262 | *.rptproj.bak
263 |
264 | # SQL Server files
265 | *.mdf
266 | *.ldf
267 | *.ndf
268 |
269 | # Business Intelligence projects
270 | *.rdl.data
271 | *.bim.layout
272 | *.bim_*.settings
273 | *.rptproj.rsuser
274 | *- [Bb]ackup.rdl
275 | *- [Bb]ackup ([0-9]).rdl
276 | *- [Bb]ackup ([0-9][0-9]).rdl
277 |
278 | # Microsoft Fakes
279 | FakesAssemblies/
280 |
281 | # GhostDoc plugin setting file
282 | *.GhostDoc.xml
283 |
284 | # Node.js Tools for Visual Studio
285 | .ntvs_analysis.dat
286 | node_modules/
287 |
288 | # Visual Studio 6 build log
289 | *.plg
290 |
291 | # Visual Studio 6 workspace options file
292 | *.opt
293 |
294 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
295 | *.vbw
296 |
297 | # Visual Studio LightSwitch build output
298 | **/*.HTMLClient/GeneratedArtifacts
299 | **/*.DesktopClient/GeneratedArtifacts
300 | **/*.DesktopClient/ModelManifest.xml
301 | **/*.Server/GeneratedArtifacts
302 | **/*.Server/ModelManifest.xml
303 | _Pvt_Extensions
304 |
305 | # Paket dependency manager
306 | .paket/paket.exe
307 | paket-files/
308 |
309 | # FAKE - F# Make
310 | .fake/
311 |
312 | # CodeRush personal settings
313 | .cr/personal
314 |
315 | # Python Tools for Visual Studio (PTVS)
316 | __pycache__/
317 | *.pyc
318 |
319 | # Cake - Uncomment if you are using it
320 | # tools/**
321 | # !tools/packages.config
322 |
323 | # Tabs Studio
324 | *.tss
325 |
326 | # Telerik's JustMock configuration file
327 | *.jmconfig
328 |
329 | # BizTalk build output
330 | *.btp.cs
331 | *.btm.cs
332 | *.odx.cs
333 | *.xsd.cs
334 |
335 | # OpenCover UI analysis results
336 | OpenCover/
337 |
338 | # Azure Stream Analytics local run output
339 | ASALocalRun/
340 |
341 | # MSBuild Binary and Structured Log
342 | *.binlog
343 |
344 | # NVidia Nsight GPU debugger configuration file
345 | *.nvuser
346 |
347 | # MFractors (Xamarin productivity tool) working folder
348 | .mfractor/
349 |
350 | # Local History for Visual Studio
351 | .localhistory/
352 |
353 | # BeatPulse healthcheck temp database
354 | healthchecksdb
355 |
356 | # Backup folder for Package Reference Convert tool in Visual Studio 2017
357 | MigrationBackup/
358 |
359 | # Ionide (cross platform F# VS Code tools) working folder
360 | .ionide/
361 |
362 | # Fody - auto-generated XML schema
363 | FodyWeavers.xsd
--------------------------------------------------------------------------------
/.gitmodules:
--------------------------------------------------------------------------------
1 |
2 | [submodule "external/inipp"]
3 | path = external/inipp
4 | url = https://github.com/mcmtroffaes/inipp
5 | [submodule "external/spdlog"]
6 | path = external/spdlog
7 | url = https://github.com/gabime/spdlog
8 |
--------------------------------------------------------------------------------
/DDDAFix.filters:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | {4FC737F1-C7A5-4376-A066-2A32D752A2FF}
6 | cpp;c;cc;cxx;c++;cppm;ixx;def;odl;idl;hpj;bat;asm;asmx
7 |
8 |
9 | {93995380-89BD-4b04-88EB-625FBE52EBFB}
10 | h;hh;hpp;hxx;h++;hm;inl;inc;ipp;xsd
11 |
12 |
13 | {67DA6AB6-F800-4c08-8B7A-83BB121AAD01}
14 | rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms
15 |
16 |
17 |
18 |
19 | Source Files
20 |
21 |
22 | Source Files
23 |
24 |
25 |
26 |
27 | Header Files
28 |
29 |
30 | Header Files
31 |
32 |
33 |
--------------------------------------------------------------------------------
/DDDAFix.ini:
--------------------------------------------------------------------------------
1 | [DDDAFix Parameters]
2 | ; Injection delay in milliseconds.
3 | InjectionDelay = 1000
4 |
5 | ;;;;;;;;;; General ;;;;;;;;;;
6 |
7 | [Raise Framerate Cap]
8 | ; Raises "variable" framerate setting from 150fps to 1000fps.
9 | Enabled = false
10 |
11 | [Disable Pause on Focus Loss]
12 | ; Set to true to stop the game from pausing when alt+tabbed.
13 | Enabled = false
14 |
15 | [Borderless Windowed Mode]
16 | ; Set to true to enable borderless fullscreen when the game is set to "windowed" mode.
17 | Enabled = true
18 |
19 | ;;;;;;;;;; Ultrawide/Narrower Fixes ;;;;;;;;;;
20 |
21 | [Fix HUD]
22 | ; Centres HUD to 16:9 and fixes many HUD issues.
23 | Enabled = true
24 |
25 | [Fix FOV]
26 | ; Fixes FOV.
27 | Enabled = true
--------------------------------------------------------------------------------
/DDDAFix.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio Version 17
4 | VisualStudioVersion = 17.3.32901.215
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "DDDAFix", "DDDAFix.vcxproj", "{C6644269-B721-4F94-BE7F-77BFB2343BA5}"
7 | EndProject
8 | Global
9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
10 | Debug|x86 = Debug|x86
11 | Release|x86 = Release|x86
12 | EndGlobalSection
13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
14 | {C6644269-B721-4F94-BE7F-77BFB2343BA5}.Debug|x86.ActiveCfg = Debug|Win32
15 | {C6644269-B721-4F94-BE7F-77BFB2343BA5}.Debug|x86.Build.0 = Debug|Win32
16 | {C6644269-B721-4F94-BE7F-77BFB2343BA5}.Release|x86.ActiveCfg = Release|Win32
17 | {C6644269-B721-4F94-BE7F-77BFB2343BA5}.Release|x86.Build.0 = Release|Win32
18 | EndGlobalSection
19 | GlobalSection(SolutionProperties) = preSolution
20 | HideSolutionNode = FALSE
21 | EndGlobalSection
22 | GlobalSection(ExtensibilityGlobals) = postSolution
23 | SolutionGuid = {BD882CE7-25BA-4751-8EB4-4B14C55192CA}
24 | EndGlobalSection
25 | EndGlobal
26 |
--------------------------------------------------------------------------------
/DDDAFix.vcxproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Debug
6 | Win32
7 |
8 |
9 | Release
10 | Win32
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 | 16.0
26 | Win32Proj
27 | {c6644269-b721-4f94-be7f-77bfb2343ba5}
28 | DDDAFix
29 | 10.0
30 |
31 |
32 |
33 | DynamicLibrary
34 | true
35 | v143
36 | Unicode
37 |
38 |
39 | DynamicLibrary
40 | false
41 | v143
42 | true
43 | Unicode
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 | .asi
59 | external\inipp;external\spdlog\include;external\safetyhook;$(VC_IncludePath);$(WindowsSDK_IncludePath);
60 | $(SolutionDir)$(Configuration)\
61 |
62 |
63 | $(SolutionDir)$(Configuration)\
64 | .asi
65 | external\inipp;external\spdlog\include;external\safetyhook;$(VC_IncludePath);$(WindowsSDK_IncludePath);
66 |
67 |
68 |
69 | Level3
70 | true
71 | WIN32;_DEBUG;DDDAFix_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions);_SILENCE_STDEXT_ARR_ITERS_DEPRECATION_WARNING;_CRT_SECURE_NO_WARNINGS
72 | true
73 | NotUsing
74 | pch.h
75 | stdcpplatest
76 |
77 |
78 | Windows
79 | true
80 | false
81 |
82 |
83 |
84 |
85 | Level3
86 | true
87 | true
88 | true
89 | WIN32;NDEBUG;DDDAFix_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions);_CRT_SECURE_NO_WARNINGS
90 | true
91 | NotUsing
92 | pch.h
93 | stdcpplatest
94 | Default
95 |
96 |
97 | Windows
98 | true
99 | true
100 | true
101 | false
102 |
103 |
104 |
105 |
106 |
107 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 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 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Dragon's Dogma: Dark Arisen Fix
2 | [](https://www.patreon.com/Wintermance) [](https://ko-fi.com/W7W01UAI9)
3 | [](https://github.com/Lyall/DDDAFix/releases)
4 |
5 | This is a fix for Dragon's Dogma: Dark Arisen that attempts to solve various issues related to non-16:9 display support and more.
6 |
7 | ## Ultrawide/Narrower Features
8 | - Fixed broken depth of field.
9 | - 16:9 centred HUD.
10 | - Fixed many broken/misaligned HUD elements.
11 | - Corrected mouse input for interacting with UI.
12 | - Fixed cropped cutscene FOV.
13 | - Spanned backgrounds for various HUD elements like fades etc.
14 | - Correctly scaled FMVs.
15 |
16 | ## Other Features
17 | - Option to uncap 150fps "variable" framerate cap.
18 | - Borderless windowed mode.
19 | - Option to disable pausing when game is alt+tabbed.
20 |
21 | ## Installation
22 | - Grab the latest release of DDDAFix from [here.](https://github.com/Lyall/DDDAFix/releases)
23 | - Extract the contents of the release zip in to the the game folder.
(e.g. "**steamapps\common\DDDA**" for Steam).
24 |
25 | ### Steam Deck/Linux Additional Instructions
26 | 🚩**You do not need to do this if you are using Windows!**
27 | - Open up the game properties in Steam and add `WINEDLLOVERRIDES="winmm=n,b" %command%` to the launch options.
28 |
29 | ## Configuration
30 | - See **DDDAFix.ini** to adjust settings for the fix.
31 |
32 | ## Known Issues
33 | Please report any issues you see.
34 | This list will contain bugs which may or may not be fixed.
35 | - Loaded map area is limited to 16:9.
36 |
37 | ## Screenshots
38 |
39 | |  |
40 | |:--:|
41 | | Gameplay |
42 |
43 | ## Credits
44 | Thanks to [@p1xel8ted](https://github.com/p1xel8ted) for testing!
45 | [Ultimate ASI Loader](https://github.com/ThirteenAG/Ultimate-ASI-Loader) for ASI loading.
46 | [inipp](https://github.com/mcmtroffaes/inipp) for ini reading.
47 | [spdlog](https://github.com/gabime/spdlog) for logging.
48 | [safetyhook](https://github.com/cursey/safetyhook) for hooking.
49 |
--------------------------------------------------------------------------------
/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 ThreadId = uint32_t;
87 | using ThreadHandle = void*;
88 | using ThreadContext = void*;
89 |
90 | /// @brief Executes a function while all other threads are frozen. Also allows for visiting each frozen thread and
91 | /// modifying it's context.
92 | /// @param run_fn The function to run while all other threads are frozen.
93 | /// @param visit_fn The function that will be called for each frozen thread.
94 | /// @note The visit function will be called in the order that the threads were frozen.
95 | /// @note The visit function will be called before the run function.
96 | /// @note Keep the logic inside run_fn and visit_fn as simple as possible to avoid deadlocks.
97 | void execute_while_frozen(const std::function& run_fn,
98 | const std::function& visit_fn = {});
99 |
100 | /// @brief Will modify the context of a thread's IP to point to a new address if its IP is at the old address.
101 | /// @param ctx The thread context to modify.
102 | /// @param old_ip The old IP address.
103 | /// @param new_ip The new IP address.
104 | void fix_ip(ThreadContext ctx, uint8_t* old_ip, uint8_t* new_ip);
105 |
106 | } // namespace safetyhook
107 |
108 |
109 | namespace safetyhook {
110 | Allocation::Allocation(Allocation&& other) noexcept {
111 | *this = std::move(other);
112 | }
113 |
114 | Allocation& Allocation::operator=(Allocation&& other) noexcept {
115 | if (this != &other) {
116 | free();
117 |
118 | m_allocator = std::move(other.m_allocator);
119 | m_address = other.m_address;
120 | m_size = other.m_size;
121 |
122 | other.m_address = nullptr;
123 | other.m_size = 0;
124 | }
125 |
126 | return *this;
127 | }
128 |
129 | Allocation::~Allocation() {
130 | free();
131 | }
132 |
133 | void Allocation::free() {
134 | if (m_allocator && m_address != nullptr && m_size != 0) {
135 | m_allocator->free(m_address, m_size);
136 | m_address = nullptr;
137 | m_size = 0;
138 | m_allocator.reset();
139 | }
140 | }
141 |
142 | Allocation::Allocation(std::shared_ptr allocator, uint8_t* address, size_t size) noexcept
143 | : m_allocator{std::move(allocator)}, m_address{address}, m_size{size} {
144 | }
145 |
146 | std::shared_ptr Allocator::global() {
147 | static std::weak_ptr global_allocator{};
148 | static std::mutex global_allocator_mutex{};
149 |
150 | std::scoped_lock lock{global_allocator_mutex};
151 |
152 | if (auto allocator = global_allocator.lock()) {
153 | return allocator;
154 | }
155 |
156 | auto allocator = Allocator::create();
157 |
158 | global_allocator = allocator;
159 |
160 | return allocator;
161 | }
162 |
163 | std::shared_ptr Allocator::create() {
164 | return std::shared_ptr{new Allocator{}};
165 | }
166 |
167 | std::expected Allocator::allocate(size_t size) {
168 | return allocate_near({}, size, std::numeric_limits::max());
169 | }
170 |
171 | std::expected Allocator::allocate_near(
172 | const std::vector& desired_addresses, size_t size, size_t max_distance) {
173 | std::scoped_lock lock{m_mutex};
174 | return internal_allocate_near(desired_addresses, size, max_distance);
175 | }
176 |
177 | void Allocator::free(uint8_t* address, size_t size) {
178 | std::scoped_lock lock{m_mutex};
179 | return internal_free(address, size);
180 | }
181 |
182 | std::expected Allocator::internal_allocate_near(
183 | const std::vector& desired_addresses, size_t size, size_t max_distance) {
184 | // First search through our list of allocations for a free block that is large
185 | // enough.
186 | for (const auto& allocation : m_memory) {
187 | if (allocation->size < size) {
188 | continue;
189 | }
190 |
191 | for (auto node = allocation->freelist.get(); node != nullptr; node = node->next.get()) {
192 | // Enough room?
193 | if (static_cast(node->end - node->start) < size) {
194 | continue;
195 | }
196 |
197 | const auto address = node->start;
198 |
199 | // Close enough?
200 | if (!in_range(address, desired_addresses, max_distance)) {
201 | continue;
202 | }
203 |
204 | node->start += size;
205 |
206 | return Allocation{shared_from_this(), address, size};
207 | }
208 | }
209 |
210 | // If we didn't find a free block, we need to allocate a new one.
211 | auto allocation_size = align_up(size, system_info().allocation_granularity);
212 | auto allocation_address = allocate_nearby_memory(desired_addresses, allocation_size, max_distance);
213 |
214 | if (!allocation_address) {
215 | return std::unexpected{allocation_address.error()};
216 | }
217 |
218 | auto& allocation = m_memory.emplace_back(new Memory);
219 |
220 | allocation->address = *allocation_address;
221 | allocation->size = allocation_size;
222 | allocation->freelist = std::make_unique();
223 | allocation->freelist->start = *allocation_address + size;
224 | allocation->freelist->end = *allocation_address + allocation_size;
225 |
226 | return Allocation{shared_from_this(), *allocation_address, size};
227 | }
228 |
229 | void Allocator::internal_free(uint8_t* address, size_t size) {
230 | for (const auto& allocation : m_memory) {
231 | if (allocation->address > address || allocation->address + allocation->size < address) {
232 | continue;
233 | }
234 |
235 | // Find the right place for our new freenode.
236 | FreeNode* prev{};
237 |
238 | for (auto node = allocation->freelist.get(); node != nullptr; prev = node, node = node->next.get()) {
239 | if (node->start > address) {
240 | break;
241 | }
242 | }
243 |
244 | // Add new freenode.
245 | auto free_node = std::make_unique();
246 |
247 | free_node->start = address;
248 | free_node->end = address + size;
249 |
250 | if (prev == nullptr) {
251 | free_node->next.swap(allocation->freelist);
252 | allocation->freelist.swap(free_node);
253 | } else {
254 | free_node->next.swap(prev->next);
255 | prev->next.swap(free_node);
256 | }
257 |
258 | combine_adjacent_freenodes(*allocation);
259 | break;
260 | }
261 | }
262 |
263 | void Allocator::combine_adjacent_freenodes(Memory& memory) {
264 | for (auto prev = memory.freelist.get(), node = prev; node != nullptr; node = node->next.get()) {
265 | if (prev->end == node->start) {
266 | prev->end = node->end;
267 | prev->next.swap(node->next);
268 | node->next.reset();
269 | node = prev;
270 | } else {
271 | prev = node;
272 | }
273 | }
274 | }
275 |
276 | std::expected Allocator::allocate_nearby_memory(
277 | const std::vector& desired_addresses, size_t size, size_t max_distance) {
278 | if (desired_addresses.empty()) {
279 | if (auto result = vm_allocate(nullptr, size, VM_ACCESS_RWX)) {
280 | return result.value();
281 | }
282 |
283 | return std::unexpected{Error::BAD_VIRTUAL_ALLOC};
284 | }
285 |
286 | auto attempt_allocation = [&](uint8_t* p) -> uint8_t* {
287 | if (!in_range(p, desired_addresses, max_distance)) {
288 | return nullptr;
289 | }
290 |
291 | if (auto result = vm_allocate(p, size, VM_ACCESS_RWX)) {
292 | return result.value();
293 | }
294 |
295 | return nullptr;
296 | };
297 |
298 | auto si = system_info();
299 | auto desired_address = desired_addresses[0];
300 | auto search_start = si.min_address;
301 | auto search_end = si.max_address;
302 |
303 | if (static_cast(desired_address - search_start) > max_distance) {
304 | search_start = desired_address - max_distance;
305 | }
306 |
307 | if (static_cast(search_end - desired_address) > max_distance) {
308 | search_end = desired_address + max_distance;
309 | }
310 |
311 | search_start = std::max(search_start, si.min_address);
312 | search_end = std::min(search_end, si.max_address);
313 | desired_address = align_up(desired_address, si.allocation_granularity);
314 | VmBasicInfo mbi{};
315 |
316 | // Search backwards from the desired_address.
317 | for (auto p = desired_address; p > search_start && in_range(p, desired_addresses, max_distance);
318 | p = align_down(mbi.address - 1, si.allocation_granularity)) {
319 | auto result = vm_query(p);
320 |
321 | if (!result) {
322 | break;
323 | }
324 |
325 | mbi = result.value();
326 |
327 | if (!mbi.is_free) {
328 | continue;
329 | }
330 |
331 | if (auto allocation_address = attempt_allocation(p); allocation_address != nullptr) {
332 | return allocation_address;
333 | }
334 | }
335 |
336 | // Search forwards from the desired_address.
337 | for (auto p = desired_address; p < search_end && in_range(p, desired_addresses, max_distance); p += mbi.size) {
338 | auto result = vm_query(p);
339 |
340 | if (!result) {
341 | break;
342 | }
343 |
344 | mbi = result.value();
345 |
346 | if (!mbi.is_free) {
347 | continue;
348 | }
349 |
350 | if (auto allocation_address = attempt_allocation(p); allocation_address != nullptr) {
351 | return allocation_address;
352 | }
353 | }
354 |
355 | return std::unexpected{Error::NO_MEMORY_IN_RANGE};
356 | }
357 |
358 | bool Allocator::in_range(uint8_t* address, const std::vector& desired_addresses, size_t max_distance) {
359 | return std::ranges::all_of(desired_addresses, [&](const auto& desired_address) {
360 | const size_t delta = (address > desired_address) ? address - desired_address : desired_address - address;
361 | return delta <= max_distance;
362 | });
363 | }
364 |
365 | Allocator::Memory::~Memory() {
366 | vm_free(address);
367 | }
368 | } // namespace safetyhook
369 |
370 | //
371 | // Source file: easy.cpp
372 | //
373 |
374 |
375 | namespace safetyhook {
376 | InlineHook create_inline(void* target, void* destination) {
377 | if (auto hook = InlineHook::create(target, destination)) {
378 | return std::move(*hook);
379 | } else {
380 | return {};
381 | }
382 | }
383 |
384 | MidHook create_mid(void* target, MidHookFn destination) {
385 | if (auto hook = MidHook::create(target, destination)) {
386 | return std::move(*hook);
387 | } else {
388 | return {};
389 | }
390 | }
391 |
392 | VmtHook create_vmt(void* object) {
393 | if (auto hook = VmtHook::create(object)) {
394 | return std::move(*hook);
395 | } else {
396 | return {};
397 | }
398 | }
399 | } // namespace safetyhook
400 |
401 | //
402 | // Source file: inline_hook.cpp
403 | //
404 |
405 | #include
406 |
407 | #if __has_include("Zydis/Zydis.h")
408 | #include "Zydis/Zydis.h"
409 | #elif __has_include("Zydis.h")
410 | #include "Zydis.h"
411 | #else
412 | #error "Zydis not found"
413 | #endif
414 |
415 |
416 |
417 | namespace safetyhook {
418 |
419 | #pragma pack(push, 1)
420 | struct JmpE9 {
421 | uint8_t opcode{0xE9};
422 | uint32_t offset{0};
423 | };
424 |
425 | #if SAFETYHOOK_ARCH_X86_64
426 | struct JmpFF {
427 | uint8_t opcode0{0xFF};
428 | uint8_t opcode1{0x25};
429 | uint32_t offset{0};
430 | };
431 |
432 | struct TrampolineEpilogueE9 {
433 | JmpE9 jmp_to_original{};
434 | JmpFF jmp_to_destination{};
435 | uint64_t destination_address{};
436 | };
437 |
438 | struct TrampolineEpilogueFF {
439 | JmpFF jmp_to_original{};
440 | uint64_t original_address{};
441 | };
442 | #elif SAFETYHOOK_ARCH_X86_32
443 | struct TrampolineEpilogueE9 {
444 | JmpE9 jmp_to_original{};
445 | JmpE9 jmp_to_destination{};
446 | };
447 | #endif
448 | #pragma pack(pop)
449 |
450 | #if SAFETYHOOK_ARCH_X86_64
451 | static auto make_jmp_ff(uint8_t* src, uint8_t* dst, uint8_t* data) {
452 | JmpFF jmp{};
453 |
454 | jmp.offset = static_cast(data - src - sizeof(jmp));
455 | store(data, dst);
456 |
457 | return jmp;
458 | }
459 |
460 | [[nodiscard]] static std::expected emit_jmp_ff(
461 | uint8_t* src, uint8_t* dst, uint8_t* data, size_t size = sizeof(JmpFF)) {
462 | if (size < sizeof(JmpFF)) {
463 | return std::unexpected{InlineHook::Error::not_enough_space(dst)};
464 | }
465 |
466 | auto um = unprotect(src, size);
467 |
468 | if (!um) {
469 | return std::unexpected{InlineHook::Error::failed_to_unprotect(src)};
470 | }
471 |
472 | if (size > sizeof(JmpFF)) {
473 | std::fill_n(src, size, static_cast(0x90));
474 | }
475 |
476 | store(src, make_jmp_ff(src, dst, data));
477 |
478 | return {};
479 | }
480 | #endif
481 |
482 | constexpr auto make_jmp_e9(uint8_t* src, uint8_t* dst) {
483 | JmpE9 jmp{};
484 |
485 | jmp.offset = static_cast(dst - src - sizeof(jmp));
486 |
487 | return jmp;
488 | }
489 |
490 | [[nodiscard]] static std::expected emit_jmp_e9(
491 | uint8_t* src, uint8_t* dst, size_t size = sizeof(JmpE9)) {
492 | if (size < sizeof(JmpE9)) {
493 | return std::unexpected{InlineHook::Error::not_enough_space(dst)};
494 | }
495 |
496 | auto um = unprotect(src, size);
497 |
498 | if (!um) {
499 | return std::unexpected{InlineHook::Error::failed_to_unprotect(src)};
500 | }
501 |
502 | if (size > sizeof(JmpE9)) {
503 | std::fill_n(src, size, static_cast(0x90));
504 | }
505 |
506 | store(src, make_jmp_e9(src, dst));
507 |
508 | return {};
509 | }
510 |
511 | static bool decode(ZydisDecodedInstruction* ix, uint8_t* ip) {
512 | ZydisDecoder decoder{};
513 | ZyanStatus status;
514 |
515 | #if SAFETYHOOK_ARCH_X86_64
516 | status = ZydisDecoderInit(&decoder, ZYDIS_MACHINE_MODE_LONG_64, ZYDIS_STACK_WIDTH_64);
517 | #elif SAFETYHOOK_ARCH_X86_32
518 | status = ZydisDecoderInit(&decoder, ZYDIS_MACHINE_MODE_LEGACY_32, ZYDIS_STACK_WIDTH_32);
519 | #endif
520 |
521 | if (!ZYAN_SUCCESS(status)) {
522 | return false;
523 | }
524 |
525 | return ZYAN_SUCCESS(ZydisDecoderDecodeInstruction(&decoder, nullptr, ip, 15, ix));
526 | }
527 |
528 | std::expected InlineHook::create(void* target, void* destination) {
529 | return create(Allocator::global(), target, destination);
530 | }
531 |
532 | std::expected InlineHook::create(
533 | const std::shared_ptr& allocator, void* target, void* destination) {
534 | InlineHook hook{};
535 |
536 | if (const auto setup_result =
537 | hook.setup(allocator, reinterpret_cast(target), reinterpret_cast(destination));
538 | !setup_result) {
539 | return std::unexpected{setup_result.error()};
540 | }
541 |
542 | return hook;
543 | }
544 |
545 | InlineHook::InlineHook(InlineHook&& other) noexcept {
546 | *this = std::move(other);
547 | }
548 |
549 | InlineHook& InlineHook::operator=(InlineHook&& other) noexcept {
550 | if (this != &other) {
551 | destroy();
552 |
553 | std::scoped_lock lock{m_mutex, other.m_mutex};
554 |
555 | m_target = other.m_target;
556 | m_destination = other.m_destination;
557 | m_trampoline = std::move(other.m_trampoline);
558 | m_trampoline_size = other.m_trampoline_size;
559 | m_original_bytes = std::move(other.m_original_bytes);
560 |
561 | other.m_target = nullptr;
562 | other.m_destination = nullptr;
563 | other.m_trampoline_size = 0;
564 | }
565 |
566 | return *this;
567 | }
568 |
569 | InlineHook::~InlineHook() {
570 | destroy();
571 | }
572 |
573 | void InlineHook::reset() {
574 | *this = {};
575 | }
576 |
577 | std::expected InlineHook::setup(
578 | const std::shared_ptr& allocator, uint8_t* target, uint8_t* destination) {
579 | m_target = target;
580 | m_destination = destination;
581 |
582 | if (auto e9_result = e9_hook(allocator); !e9_result) {
583 | #if SAFETYHOOK_ARCH_X86_64
584 | if (auto ff_result = ff_hook(allocator); !ff_result) {
585 | return ff_result;
586 | }
587 | #elif SAFETYHOOK_ARCH_X86_32
588 | return e9_result;
589 | #endif
590 | }
591 |
592 | return {};
593 | }
594 |
595 | std::expected InlineHook::e9_hook(const std::shared_ptr& allocator) {
596 | m_original_bytes.clear();
597 | m_trampoline_size = sizeof(TrampolineEpilogueE9);
598 |
599 | std::vector desired_addresses{m_target};
600 | ZydisDecodedInstruction ix{};
601 |
602 | for (auto ip = m_target; ip < m_target + sizeof(JmpE9); ip += ix.length) {
603 | if (!decode(&ix, ip)) {
604 | return std::unexpected{Error::failed_to_decode_instruction(ip)};
605 | }
606 |
607 | m_trampoline_size += ix.length;
608 | m_original_bytes.insert(m_original_bytes.end(), ip, ip + ix.length);
609 |
610 | const auto is_relative = (ix.attributes & ZYDIS_ATTRIB_IS_RELATIVE) != 0;
611 |
612 | if (is_relative) {
613 | if (ix.raw.disp.size == 32) {
614 | const auto target_address = ip + ix.length + static_cast(ix.raw.disp.value);
615 | desired_addresses.emplace_back(target_address);
616 | } else if (ix.raw.imm[0].size == 32) {
617 | const auto target_address = ip + ix.length + static_cast(ix.raw.imm[0].value.s);
618 | desired_addresses.emplace_back(target_address);
619 | } else if (ix.meta.category == ZYDIS_CATEGORY_COND_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 += 4; // near conditional branches are 4 bytes larger.
623 | } else if (ix.meta.category == ZYDIS_CATEGORY_UNCOND_BR && ix.meta.branch_type == ZYDIS_BRANCH_TYPE_SHORT) {
624 | const auto target_address = ip + ix.length + static_cast(ix.raw.imm[0].value.s);
625 | desired_addresses.emplace_back(target_address);
626 | m_trampoline_size += 3; // near unconditional branches are 3 bytes larger.
627 | } else {
628 | return std::unexpected{Error::unsupported_instruction_in_trampoline(ip)};
629 | }
630 | }
631 | }
632 |
633 | auto trampoline_allocation = allocator->allocate_near(desired_addresses, m_trampoline_size);
634 |
635 | if (!trampoline_allocation) {
636 | return std::unexpected{Error::bad_allocation(trampoline_allocation.error())};
637 | }
638 |
639 | m_trampoline = std::move(*trampoline_allocation);
640 |
641 | for (auto ip = m_target, tramp_ip = m_trampoline.data(); ip < m_target + m_original_bytes.size(); ip += ix.length) {
642 | if (!decode(&ix, ip)) {
643 | m_trampoline.free();
644 | return std::unexpected{Error::failed_to_decode_instruction(ip)};
645 | }
646 |
647 | const auto is_relative = (ix.attributes & ZYDIS_ATTRIB_IS_RELATIVE) != 0;
648 |
649 | if (is_relative && ix.raw.disp.size == 32) {
650 | std::copy_n(ip, ix.length, tramp_ip);
651 | const auto target_address = ip + ix.length + ix.raw.disp.value;
652 | const auto new_disp = target_address - (tramp_ip + ix.length);
653 | store(tramp_ip + ix.raw.disp.offset, static_cast(new_disp));
654 | tramp_ip += ix.length;
655 | } else if (is_relative && ix.raw.imm[0].size == 32) {
656 | std::copy_n(ip, ix.length, tramp_ip);
657 | const auto target_address = ip + ix.length + ix.raw.imm[0].value.s;
658 | const auto new_disp = target_address - (tramp_ip + ix.length);
659 | store(tramp_ip + ix.raw.imm[0].offset, static_cast(new_disp));
660 | tramp_ip += ix.length;
661 | } else if (ix.meta.category == ZYDIS_CATEGORY_COND_BR && ix.meta.branch_type == ZYDIS_BRANCH_TYPE_SHORT) {
662 | const auto target_address = ip + ix.length + ix.raw.imm[0].value.s;
663 | auto new_disp = target_address - (tramp_ip + 6);
664 |
665 | // Handle the case where the target is now in the trampoline.
666 | if (target_address < m_target + m_original_bytes.size()) {
667 | new_disp = static_cast(ix.raw.imm[0].value.s);
668 | }
669 |
670 | *tramp_ip = 0x0F;
671 | *(tramp_ip + 1) = 0x10 + ix.opcode;
672 | store(tramp_ip + 2, static_cast(new_disp));
673 | tramp_ip += 6;
674 | } else if (ix.meta.category == ZYDIS_CATEGORY_UNCOND_BR && ix.meta.branch_type == ZYDIS_BRANCH_TYPE_SHORT) {
675 | const auto target_address = ip + ix.length + ix.raw.imm[0].value.s;
676 | auto new_disp = target_address - (tramp_ip + 5);
677 |
678 | // Handle the case where the target is now in the trampoline.
679 | if (target_address < m_target + m_original_bytes.size()) {
680 | new_disp = static_cast(ix.raw.imm[0].value.s);
681 | }
682 |
683 | *tramp_ip = 0xE9;
684 | store(tramp_ip + 1, static_cast(new_disp));
685 | tramp_ip += 5;
686 | } else {
687 | std::copy_n(ip, ix.length, tramp_ip);
688 | tramp_ip += ix.length;
689 | }
690 | }
691 |
692 | auto trampoline_epilogue = reinterpret_cast(
693 | m_trampoline.address() + m_trampoline_size - sizeof(TrampolineEpilogueE9));
694 |
695 | // jmp from trampoline to original.
696 | auto src = reinterpret_cast(&trampoline_epilogue->jmp_to_original);
697 | auto dst = m_target + m_original_bytes.size();
698 |
699 | if (auto result = emit_jmp_e9(src, dst); !result) {
700 | return std::unexpected{result.error()};
701 | }
702 |
703 | // jmp from trampoline to destination.
704 | src = reinterpret_cast(&trampoline_epilogue->jmp_to_destination);
705 | dst = m_destination;
706 |
707 | #if SAFETYHOOK_ARCH_X86_64
708 | auto data = reinterpret_cast(&trampoline_epilogue->destination_address);
709 |
710 | if (auto result = emit_jmp_ff(src, dst, data); !result) {
711 | return std::unexpected{result.error()};
712 | }
713 | #elif SAFETYHOOK_ARCH_X86_32
714 | if (auto result = emit_jmp_e9(src, dst); !result) {
715 | return std::unexpected{result.error()};
716 | }
717 | #endif
718 |
719 | std::optional error;
720 |
721 | // jmp from original to trampoline.
722 | execute_while_frozen(
723 | [this, &trampoline_epilogue, &error] {
724 | if (auto result = emit_jmp_e9(m_target,
725 | reinterpret_cast(&trampoline_epilogue->jmp_to_destination), m_original_bytes.size());
726 | !result) {
727 | error = result.error();
728 | }
729 | },
730 | [this](auto, auto, auto ctx) {
731 | for (size_t i = 0; i < m_original_bytes.size(); ++i) {
732 | fix_ip(ctx, m_target + i, m_trampoline.data() + i);
733 | }
734 | });
735 |
736 | if (error) {
737 | return std::unexpected{*error};
738 | }
739 |
740 | return {};
741 | }
742 |
743 | #if SAFETYHOOK_ARCH_X86_64
744 | std::expected InlineHook::ff_hook(const std::shared_ptr& allocator) {
745 | m_original_bytes.clear();
746 | m_trampoline_size = sizeof(TrampolineEpilogueFF);
747 | ZydisDecodedInstruction ix{};
748 |
749 | for (auto ip = m_target; ip < m_target + sizeof(JmpFF) + sizeof(uintptr_t); ip += ix.length) {
750 | if (!decode(&ix, ip)) {
751 | return std::unexpected{Error::failed_to_decode_instruction(ip)};
752 | }
753 |
754 | // We can't support any instruction that is IP relative here because
755 | // ff_hook should only be called if e9_hook failed indicating that
756 | // we're likely outside the +- 2GB range.
757 | if (ix.attributes & ZYDIS_ATTRIB_IS_RELATIVE) {
758 | return std::unexpected{Error::ip_relative_instruction_out_of_range(ip)};
759 | }
760 |
761 | m_original_bytes.insert(m_original_bytes.end(), ip, ip + ix.length);
762 | m_trampoline_size += ix.length;
763 | }
764 |
765 | auto trampoline_allocation = allocator->allocate(m_trampoline_size);
766 |
767 | if (!trampoline_allocation) {
768 | return std::unexpected{Error::bad_allocation(trampoline_allocation.error())};
769 | }
770 |
771 | m_trampoline = std::move(*trampoline_allocation);
772 |
773 | std::copy(m_original_bytes.begin(), m_original_bytes.end(), m_trampoline.data());
774 |
775 | const auto trampoline_epilogue =
776 | reinterpret_cast(m_trampoline.data() + m_trampoline_size - sizeof(TrampolineEpilogueFF));
777 |
778 | // jmp from trampoline to original.
779 | auto src = reinterpret_cast(&trampoline_epilogue->jmp_to_original);
780 | auto dst = m_target + m_original_bytes.size();
781 | auto data = reinterpret_cast(&trampoline_epilogue->original_address);
782 |
783 | if (auto result = emit_jmp_ff(src, dst, data); !result) {
784 | return std::unexpected{result.error()};
785 | }
786 |
787 | std::optional error;
788 |
789 | // jmp from original to trampoline.
790 | execute_while_frozen(
791 | [this, &error] {
792 | if (auto result = emit_jmp_ff(m_target, m_destination, m_target + sizeof(JmpFF), m_original_bytes.size());
793 | !result) {
794 | error = result.error();
795 | }
796 | },
797 | [this](auto, auto, auto ctx) {
798 | for (size_t i = 0; i < m_original_bytes.size(); ++i) {
799 | fix_ip(ctx, m_target + i, m_trampoline.data() + i);
800 | }
801 | });
802 |
803 | if (error) {
804 | return std::unexpected{*error};
805 | }
806 |
807 | return {};
808 | }
809 | #endif
810 |
811 | void InlineHook::destroy() {
812 | std::scoped_lock lock{m_mutex};
813 |
814 | if (!m_trampoline) {
815 | return;
816 | }
817 |
818 | execute_while_frozen(
819 | [this] {
820 | if (auto um = unprotect(m_target, m_original_bytes.size())) {
821 | std::copy(m_original_bytes.begin(), m_original_bytes.end(), m_target);
822 | }
823 | },
824 | [this](auto, auto, auto ctx) {
825 | for (size_t i = 0; i < m_original_bytes.size(); ++i) {
826 | fix_ip(ctx, m_trampoline.data() + i, m_target + i);
827 | }
828 | });
829 |
830 | m_trampoline.free();
831 | }
832 | } // namespace safetyhook
833 |
834 | //
835 | // Source file: mid_hook.cpp
836 | //
837 |
838 | #include
839 | #include
840 |
841 |
842 |
843 | namespace safetyhook {
844 |
845 | #if SAFETYHOOK_ARCH_X86_64
846 | #if SAFETYHOOK_OS_WINDOWS
847 | constexpr std::array asm_data = {0xFF, 0x35, 0x79, 0x01, 0x00, 0x00, 0x54, 0x54, 0x55, 0x50, 0x53, 0x51,
848 | 0x52, 0x56, 0x57, 0x41, 0x50, 0x41, 0x51, 0x41, 0x52, 0x41, 0x53, 0x41, 0x54, 0x41, 0x55, 0x41, 0x56, 0x41, 0x57,
849 | 0x9C, 0x48, 0x81, 0xEC, 0x00, 0x01, 0x00, 0x00, 0xF3, 0x44, 0x0F, 0x7F, 0xBC, 0x24, 0xF0, 0x00, 0x00, 0x00, 0xF3,
850 | 0x44, 0x0F, 0x7F, 0xB4, 0x24, 0xE0, 0x00, 0x00, 0x00, 0xF3, 0x44, 0x0F, 0x7F, 0xAC, 0x24, 0xD0, 0x00, 0x00, 0x00,
851 | 0xF3, 0x44, 0x0F, 0x7F, 0xA4, 0x24, 0xC0, 0x00, 0x00, 0x00, 0xF3, 0x44, 0x0F, 0x7F, 0x9C, 0x24, 0xB0, 0x00, 0x00,
852 | 0x00, 0xF3, 0x44, 0x0F, 0x7F, 0x94, 0x24, 0xA0, 0x00, 0x00, 0x00, 0xF3, 0x44, 0x0F, 0x7F, 0x8C, 0x24, 0x90, 0x00,
853 | 0x00, 0x00, 0xF3, 0x44, 0x0F, 0x7F, 0x84, 0x24, 0x80, 0x00, 0x00, 0x00, 0xF3, 0x0F, 0x7F, 0x7C, 0x24, 0x70, 0xF3,
854 | 0x0F, 0x7F, 0x74, 0x24, 0x60, 0xF3, 0x0F, 0x7F, 0x6C, 0x24, 0x50, 0xF3, 0x0F, 0x7F, 0x64, 0x24, 0x40, 0xF3, 0x0F,
855 | 0x7F, 0x5C, 0x24, 0x30, 0xF3, 0x0F, 0x7F, 0x54, 0x24, 0x20, 0xF3, 0x0F, 0x7F, 0x4C, 0x24, 0x10, 0xF3, 0x0F, 0x7F,
856 | 0x04, 0x24, 0x48, 0x8B, 0x8C, 0x24, 0x80, 0x01, 0x00, 0x00, 0x48, 0x83, 0xC1, 0x10, 0x48, 0x89, 0x8C, 0x24, 0x80,
857 | 0x01, 0x00, 0x00, 0x48, 0x8D, 0x0C, 0x24, 0x48, 0x89, 0xE3, 0x48, 0x83, 0xEC, 0x30, 0x48, 0x83, 0xE4, 0xF0, 0xFF,
858 | 0x15, 0xA8, 0x00, 0x00, 0x00, 0x48, 0x89, 0xDC, 0xF3, 0x0F, 0x6F, 0x04, 0x24, 0xF3, 0x0F, 0x6F, 0x4C, 0x24, 0x10,
859 | 0xF3, 0x0F, 0x6F, 0x54, 0x24, 0x20, 0xF3, 0x0F, 0x6F, 0x5C, 0x24, 0x30, 0xF3, 0x0F, 0x6F, 0x64, 0x24, 0x40, 0xF3,
860 | 0x0F, 0x6F, 0x6C, 0x24, 0x50, 0xF3, 0x0F, 0x6F, 0x74, 0x24, 0x60, 0xF3, 0x0F, 0x6F, 0x7C, 0x24, 0x70, 0xF3, 0x44,
861 | 0x0F, 0x6F, 0x84, 0x24, 0x80, 0x00, 0x00, 0x00, 0xF3, 0x44, 0x0F, 0x6F, 0x8C, 0x24, 0x90, 0x00, 0x00, 0x00, 0xF3,
862 | 0x44, 0x0F, 0x6F, 0x94, 0x24, 0xA0, 0x00, 0x00, 0x00, 0xF3, 0x44, 0x0F, 0x6F, 0x9C, 0x24, 0xB0, 0x00, 0x00, 0x00,
863 | 0xF3, 0x44, 0x0F, 0x6F, 0xA4, 0x24, 0xC0, 0x00, 0x00, 0x00, 0xF3, 0x44, 0x0F, 0x6F, 0xAC, 0x24, 0xD0, 0x00, 0x00,
864 | 0x00, 0xF3, 0x44, 0x0F, 0x6F, 0xB4, 0x24, 0xE0, 0x00, 0x00, 0x00, 0xF3, 0x44, 0x0F, 0x6F, 0xBC, 0x24, 0xF0, 0x00,
865 | 0x00, 0x00, 0x48, 0x81, 0xC4, 0x00, 0x01, 0x00, 0x00, 0x9D, 0x41, 0x5F, 0x41, 0x5E, 0x41, 0x5D, 0x41, 0x5C, 0x41,
866 | 0x5B, 0x41, 0x5A, 0x41, 0x59, 0x41, 0x58, 0x5F, 0x5E, 0x5A, 0x59, 0x5B, 0x58, 0x5D, 0x48, 0x8D, 0x64, 0x24, 0x08,
867 | 0x5C, 0xC3, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
868 | #elif SAFETYHOOK_OS_LINUX
869 | constexpr std::array asm_data = {0xFF, 0x35, 0x79, 0x01, 0x00, 0x00, 0x54, 0x54, 0x55, 0x50, 0x53, 0x51,
870 | 0x52, 0x56, 0x57, 0x41, 0x50, 0x41, 0x51, 0x41, 0x52, 0x41, 0x53, 0x41, 0x54, 0x41, 0x55, 0x41, 0x56, 0x41, 0x57,
871 | 0x9C, 0x48, 0x81, 0xEC, 0x00, 0x01, 0x00, 0x00, 0xF3, 0x44, 0x0F, 0x7F, 0xBC, 0x24, 0xF0, 0x00, 0x00, 0x00, 0xF3,
872 | 0x44, 0x0F, 0x7F, 0xB4, 0x24, 0xE0, 0x00, 0x00, 0x00, 0xF3, 0x44, 0x0F, 0x7F, 0xAC, 0x24, 0xD0, 0x00, 0x00, 0x00,
873 | 0xF3, 0x44, 0x0F, 0x7F, 0xA4, 0x24, 0xC0, 0x00, 0x00, 0x00, 0xF3, 0x44, 0x0F, 0x7F, 0x9C, 0x24, 0xB0, 0x00, 0x00,
874 | 0x00, 0xF3, 0x44, 0x0F, 0x7F, 0x94, 0x24, 0xA0, 0x00, 0x00, 0x00, 0xF3, 0x44, 0x0F, 0x7F, 0x8C, 0x24, 0x90, 0x00,
875 | 0x00, 0x00, 0xF3, 0x44, 0x0F, 0x7F, 0x84, 0x24, 0x80, 0x00, 0x00, 0x00, 0xF3, 0x0F, 0x7F, 0x7C, 0x24, 0x70, 0xF3,
876 | 0x0F, 0x7F, 0x74, 0x24, 0x60, 0xF3, 0x0F, 0x7F, 0x6C, 0x24, 0x50, 0xF3, 0x0F, 0x7F, 0x64, 0x24, 0x40, 0xF3, 0x0F,
877 | 0x7F, 0x5C, 0x24, 0x30, 0xF3, 0x0F, 0x7F, 0x54, 0x24, 0x20, 0xF3, 0x0F, 0x7F, 0x4C, 0x24, 0x10, 0xF3, 0x0F, 0x7F,
878 | 0x04, 0x24, 0x48, 0x8B, 0xBC, 0x24, 0x80, 0x01, 0x00, 0x00, 0x48, 0x83, 0xC7, 0x10, 0x48, 0x89, 0xBC, 0x24, 0x80,
879 | 0x01, 0x00, 0x00, 0x48, 0x8D, 0x3C, 0x24, 0x48, 0x89, 0xE3, 0x48, 0x83, 0xEC, 0x30, 0x48, 0x83, 0xE4, 0xF0, 0xFF,
880 | 0x15, 0xA8, 0x00, 0x00, 0x00, 0x48, 0x89, 0xDC, 0xF3, 0x0F, 0x6F, 0x04, 0x24, 0xF3, 0x0F, 0x6F, 0x4C, 0x24, 0x10,
881 | 0xF3, 0x0F, 0x6F, 0x54, 0x24, 0x20, 0xF3, 0x0F, 0x6F, 0x5C, 0x24, 0x30, 0xF3, 0x0F, 0x6F, 0x64, 0x24, 0x40, 0xF3,
882 | 0x0F, 0x6F, 0x6C, 0x24, 0x50, 0xF3, 0x0F, 0x6F, 0x74, 0x24, 0x60, 0xF3, 0x0F, 0x6F, 0x7C, 0x24, 0x70, 0xF3, 0x44,
883 | 0x0F, 0x6F, 0x84, 0x24, 0x80, 0x00, 0x00, 0x00, 0xF3, 0x44, 0x0F, 0x6F, 0x8C, 0x24, 0x90, 0x00, 0x00, 0x00, 0xF3,
884 | 0x44, 0x0F, 0x6F, 0x94, 0x24, 0xA0, 0x00, 0x00, 0x00, 0xF3, 0x44, 0x0F, 0x6F, 0x9C, 0x24, 0xB0, 0x00, 0x00, 0x00,
885 | 0xF3, 0x44, 0x0F, 0x6F, 0xA4, 0x24, 0xC0, 0x00, 0x00, 0x00, 0xF3, 0x44, 0x0F, 0x6F, 0xAC, 0x24, 0xD0, 0x00, 0x00,
886 | 0x00, 0xF3, 0x44, 0x0F, 0x6F, 0xB4, 0x24, 0xE0, 0x00, 0x00, 0x00, 0xF3, 0x44, 0x0F, 0x6F, 0xBC, 0x24, 0xF0, 0x00,
887 | 0x00, 0x00, 0x48, 0x81, 0xC4, 0x00, 0x01, 0x00, 0x00, 0x9D, 0x41, 0x5F, 0x41, 0x5E, 0x41, 0x5D, 0x41, 0x5C, 0x41,
888 | 0x5B, 0x41, 0x5A, 0x41, 0x59, 0x41, 0x58, 0x5F, 0x5E, 0x5A, 0x59, 0x5B, 0x58, 0x5D, 0x48, 0x8D, 0x64, 0x24, 0x08,
889 | 0x5C, 0xC3, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
890 | #endif
891 | #elif SAFETYHOOK_ARCH_X86_32
892 | constexpr std::array asm_data = {0xFF, 0x35, 0xA7, 0x00, 0x00, 0x00, 0x54, 0x54, 0x55, 0x50, 0x53, 0x51,
893 | 0x52, 0x56, 0x57, 0x9C, 0x81, 0xEC, 0x80, 0x00, 0x00, 0x00, 0xF3, 0x0F, 0x7F, 0x7C, 0x24, 0x70, 0xF3, 0x0F, 0x7F,
894 | 0x74, 0x24, 0x60, 0xF3, 0x0F, 0x7F, 0x6C, 0x24, 0x50, 0xF3, 0x0F, 0x7F, 0x64, 0x24, 0x40, 0xF3, 0x0F, 0x7F, 0x5C,
895 | 0x24, 0x30, 0xF3, 0x0F, 0x7F, 0x54, 0x24, 0x20, 0xF3, 0x0F, 0x7F, 0x4C, 0x24, 0x10, 0xF3, 0x0F, 0x7F, 0x04, 0x24,
896 | 0x8B, 0x8C, 0x24, 0xA0, 0x00, 0x00, 0x00, 0x83, 0xC1, 0x08, 0x89, 0x8C, 0x24, 0xA0, 0x00, 0x00, 0x00, 0x54, 0xFF,
897 | 0x15, 0xA3, 0x00, 0x00, 0x00, 0x83, 0xC4, 0x04, 0xF3, 0x0F, 0x6F, 0x04, 0x24, 0xF3, 0x0F, 0x6F, 0x4C, 0x24, 0x10,
898 | 0xF3, 0x0F, 0x6F, 0x54, 0x24, 0x20, 0xF3, 0x0F, 0x6F, 0x5C, 0x24, 0x30, 0xF3, 0x0F, 0x6F, 0x64, 0x24, 0x40, 0xF3,
899 | 0x0F, 0x6F, 0x6C, 0x24, 0x50, 0xF3, 0x0F, 0x6F, 0x74, 0x24, 0x60, 0xF3, 0x0F, 0x6F, 0x7C, 0x24, 0x70, 0x81, 0xC4,
900 | 0x80, 0x00, 0x00, 0x00, 0x9D, 0x5F, 0x5E, 0x5A, 0x59, 0x5B, 0x58, 0x5D, 0x8D, 0x64, 0x24, 0x04, 0x5C, 0xC3, 0x00,
901 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
902 | #endif
903 |
904 | std::expected MidHook::create(void* target, MidHookFn destination) {
905 | return create(Allocator::global(), target, destination);
906 | }
907 |
908 | std::expected MidHook::create(
909 | const std::shared_ptr& allocator, void* target, MidHookFn destination) {
910 | MidHook hook{};
911 |
912 | if (const auto setup_result = hook.setup(allocator, reinterpret_cast(target), destination);
913 | !setup_result) {
914 | return std::unexpected{setup_result.error()};
915 | }
916 |
917 | return hook;
918 | }
919 |
920 | MidHook::MidHook(MidHook&& other) noexcept {
921 | *this = std::move(other);
922 | }
923 |
924 | MidHook& MidHook::operator=(MidHook&& other) noexcept {
925 | if (this != &other) {
926 | m_hook = std::move(other.m_hook);
927 | m_target = other.m_target;
928 | m_stub = std::move(other.m_stub);
929 | m_destination = other.m_destination;
930 |
931 | other.m_target = 0;
932 | other.m_destination = nullptr;
933 | }
934 |
935 | return *this;
936 | }
937 |
938 | void MidHook::reset() {
939 | *this = {};
940 | }
941 |
942 | std::expected MidHook::setup(
943 | const std::shared_ptr& allocator, uint8_t* target, MidHookFn destination_fn) {
944 | m_target = target;
945 | m_destination = destination_fn;
946 |
947 | auto stub_allocation = allocator->allocate(asm_data.size());
948 |
949 | if (!stub_allocation) {
950 | return std::unexpected{Error::bad_allocation(stub_allocation.error())};
951 | }
952 |
953 | m_stub = std::move(*stub_allocation);
954 |
955 | std::copy(asm_data.begin(), asm_data.end(), m_stub.data());
956 |
957 | #if SAFETYHOOK_ARCH_X86_64
958 | store(m_stub.data() + sizeof(asm_data) - 16, m_destination);
959 | #elif SAFETYHOOK_ARCH_X86_32
960 | store(m_stub.data() + sizeof(asm_data) - 8, m_destination);
961 |
962 | // 32-bit has some relocations we need to fix up as well.
963 | store(m_stub.data() + 0x02, m_stub.data() + m_stub.size() - 4);
964 | store(m_stub.data() + 0x59, m_stub.data() + m_stub.size() - 8);
965 | #endif
966 |
967 | auto hook_result = InlineHook::create(allocator, m_target, m_stub.data());
968 |
969 | if (!hook_result) {
970 | m_stub.free();
971 | return std::unexpected{Error::bad_inline_hook(hook_result.error())};
972 | }
973 |
974 | m_hook = std::move(*hook_result);
975 |
976 | #if SAFETYHOOK_ARCH_X86_64
977 | store(m_stub.data() + sizeof(asm_data) - 8, m_hook.trampoline().data());
978 | #elif SAFETYHOOK_ARCH_X86_32
979 | store(m_stub.data() + sizeof(asm_data) - 4, m_hook.trampoline().data());
980 | #endif
981 |
982 | return {};
983 | }
984 | } // namespace safetyhook
985 |
986 | //
987 | // Source file: os.linux.cpp
988 | //
989 |
990 |
991 | #if SAFETYHOOK_OS_LINUX
992 |
993 | #include
994 |
995 | #include
996 | #include
997 |
998 |
999 |
1000 | namespace safetyhook {
1001 | std::expected vm_allocate(uint8_t* address, size_t size, VmAccess access) {
1002 | int prot = 0;
1003 | int flags = MAP_PRIVATE | MAP_ANONYMOUS;
1004 |
1005 | if (access == VM_ACCESS_R) {
1006 | prot = PROT_READ;
1007 | } else if (access == VM_ACCESS_RW) {
1008 | prot = PROT_READ | PROT_WRITE;
1009 | } else if (access == VM_ACCESS_RX) {
1010 | prot = PROT_READ | PROT_EXEC;
1011 | } else if (access == VM_ACCESS_RWX) {
1012 | prot = PROT_READ | PROT_WRITE | PROT_EXEC;
1013 | } else {
1014 | return std::unexpected{OsError::FAILED_TO_ALLOCATE};
1015 | }
1016 |
1017 | auto* result = mmap(address, size, prot, flags, -1, 0);
1018 |
1019 | if (result == MAP_FAILED) {
1020 | return std::unexpected{OsError::FAILED_TO_ALLOCATE};
1021 | }
1022 |
1023 | return static_cast(result);
1024 | }
1025 |
1026 | void vm_free(uint8_t* address) {
1027 | munmap(address, 0);
1028 | }
1029 |
1030 | std::expected vm_protect(uint8_t* address, size_t size, VmAccess access) {
1031 | int prot = 0;
1032 |
1033 | if (access == VM_ACCESS_R) {
1034 | prot = PROT_READ;
1035 | } else if (access == VM_ACCESS_RW) {
1036 | prot = PROT_READ | PROT_WRITE;
1037 | } else if (access == VM_ACCESS_RX) {
1038 | prot = PROT_READ | PROT_EXEC;
1039 | } else if (access == VM_ACCESS_RWX) {
1040 | prot = PROT_READ | PROT_WRITE | PROT_EXEC;
1041 | } else {
1042 | return std::unexpected{OsError::FAILED_TO_PROTECT};
1043 | }
1044 |
1045 | return vm_protect(address, size, prot);
1046 | }
1047 |
1048 | std::expected vm_protect(uint8_t* address, size_t size, uint32_t protect) {
1049 | auto mbi = vm_query(address);
1050 |
1051 | if (!mbi.has_value()) {
1052 | return std::unexpected{OsError::FAILED_TO_PROTECT};
1053 | }
1054 |
1055 | uint32_t old_protect = 0;
1056 |
1057 | if (mbi->access.read) {
1058 | old_protect |= PROT_READ;
1059 | }
1060 |
1061 | if (mbi->access.write) {
1062 | old_protect |= PROT_WRITE;
1063 | }
1064 |
1065 | if (mbi->access.execute) {
1066 | old_protect |= PROT_EXEC;
1067 | }
1068 |
1069 | auto* addr = align_down(address, static_cast(sysconf(_SC_PAGESIZE)));
1070 |
1071 | if (mprotect(addr, size, static_cast(protect)) == -1) {
1072 | return std::unexpected{OsError::FAILED_TO_PROTECT};
1073 | }
1074 |
1075 | return old_protect;
1076 | }
1077 |
1078 | std::expected vm_query(uint8_t* address) {
1079 | auto* maps = fopen("/proc/self/maps", "r");
1080 |
1081 | if (maps == nullptr) {
1082 | return std::unexpected{OsError::FAILED_TO_QUERY};
1083 | }
1084 |
1085 | char line[512];
1086 | unsigned long start;
1087 | unsigned long end;
1088 | char perms[5];
1089 | unsigned long offset;
1090 | int dev_major;
1091 | int dev_minor;
1092 | unsigned long inode;
1093 | char path[256];
1094 | unsigned long last_end =
1095 | reinterpret_cast(system_info().min_address); // Track the end address of the last mapping.
1096 | auto addr = reinterpret_cast(address);
1097 | std::optional info = std::nullopt;
1098 |
1099 | while (fgets(line, sizeof(line), maps) != nullptr) {
1100 | path[0] = '\0';
1101 |
1102 | sscanf(line, "%lx-%lx %4s %lx %x:%x %lu %255[^\n]", &start, &end, perms, &offset, &dev_major, &dev_minor,
1103 | &inode, path);
1104 |
1105 | if (last_end < start && addr >= last_end && addr < start) {
1106 | info = {
1107 | .address = reinterpret_cast(last_end),
1108 | .size = start - last_end,
1109 | .access = VmAccess{},
1110 | .is_free = true,
1111 | };
1112 |
1113 | break;
1114 | }
1115 |
1116 | last_end = end;
1117 |
1118 | if (addr >= start && addr < end) {
1119 | info = {
1120 | .address = reinterpret_cast(start),
1121 | .size = end - start,
1122 | .access = VmAccess{},
1123 | .is_free = false,
1124 | };
1125 |
1126 | if (perms[0] == 'r') {
1127 | info->access.read = true;
1128 | }
1129 |
1130 | if (perms[1] == 'w') {
1131 | info->access.write = true;
1132 | }
1133 |
1134 | if (perms[2] == 'x') {
1135 | info->access.execute = true;
1136 | }
1137 |
1138 | break;
1139 | }
1140 | }
1141 |
1142 | fclose(maps);
1143 |
1144 | if (!info.has_value()) {
1145 | return std::unexpected{OsError::FAILED_TO_QUERY};
1146 | }
1147 |
1148 | return info.value();
1149 | }
1150 |
1151 | bool vm_is_readable(uint8_t* address, [[maybe_unused]] size_t size) {
1152 | return vm_query(address).value_or(VmBasicInfo{}).access.read;
1153 | }
1154 |
1155 | bool vm_is_writable(uint8_t* address, [[maybe_unused]] size_t size) {
1156 | return vm_query(address).value_or(VmBasicInfo{}).access.write;
1157 | }
1158 |
1159 | bool vm_is_executable(uint8_t* address) {
1160 | return vm_query(address).value_or(VmBasicInfo{}).access.execute;
1161 | }
1162 |
1163 | SystemInfo system_info() {
1164 | auto page_size = static_cast(sysconf(_SC_PAGESIZE));
1165 |
1166 | return {
1167 | .page_size = page_size,
1168 | .allocation_granularity = page_size,
1169 | .min_address = reinterpret_cast(0x10000),
1170 | .max_address = reinterpret_cast(1ull << 47),
1171 | };
1172 | }
1173 |
1174 | void execute_while_frozen(const std::function& run_fn,
1175 | [[maybe_unused]] const std::function& visit_fn) {
1176 | run_fn();
1177 | }
1178 |
1179 | void fix_ip([[maybe_unused]] ThreadContext ctx, [[maybe_unused]] uint8_t* old_ip, [[maybe_unused]] uint8_t* new_ip) {
1180 | }
1181 |
1182 | } // namespace safetyhook
1183 |
1184 | #endif
1185 |
1186 | //
1187 | // Source file: os.windows.cpp
1188 | //
1189 |
1190 |
1191 | #if SAFETYHOOK_OS_WINDOWS
1192 |
1193 | #define NOMINMAX
1194 | #if __has_include()
1195 | #include
1196 | #elif __has_include()
1197 | #include
1198 | #else
1199 | #error "Windows.h not found"
1200 | #endif
1201 |
1202 | #include
1203 |
1204 |
1205 | #pragma comment(lib, "ntdll")
1206 |
1207 | extern "C" {
1208 | NTSTATUS
1209 | NTAPI
1210 | NtGetNextThread(HANDLE ProcessHandle, HANDLE ThreadHandle, ACCESS_MASK DesiredAccess, ULONG HandleAttributes,
1211 | ULONG Flags, PHANDLE NewThreadHandle);
1212 | }
1213 |
1214 | namespace safetyhook {
1215 | std::expected vm_allocate(uint8_t* address, size_t size, VmAccess access) {
1216 | DWORD protect = 0;
1217 |
1218 | if (access == VM_ACCESS_R) {
1219 | protect = PAGE_READONLY;
1220 | } else if (access == VM_ACCESS_RW) {
1221 | protect = PAGE_READWRITE;
1222 | } else if (access == VM_ACCESS_RX) {
1223 | protect = PAGE_EXECUTE_READ;
1224 | } else if (access == VM_ACCESS_RWX) {
1225 | protect = PAGE_EXECUTE_READWRITE;
1226 | } else {
1227 | return std::unexpected{OsError::FAILED_TO_ALLOCATE};
1228 | }
1229 |
1230 | auto* result = VirtualAlloc(address, size, MEM_COMMIT | MEM_RESERVE, protect);
1231 |
1232 | if (result == nullptr) {
1233 | return std::unexpected{OsError::FAILED_TO_ALLOCATE};
1234 | }
1235 |
1236 | return static_cast(result);
1237 | }
1238 |
1239 | void vm_free(uint8_t* address) {
1240 | VirtualFree(address, 0, MEM_RELEASE);
1241 | }
1242 |
1243 | std::expected vm_protect(uint8_t* address, size_t size, VmAccess access) {
1244 | DWORD protect = 0;
1245 |
1246 | if (access == VM_ACCESS_R) {
1247 | protect = PAGE_READONLY;
1248 | } else if (access == VM_ACCESS_RW) {
1249 | protect = PAGE_READWRITE;
1250 | } else if (access == VM_ACCESS_RX) {
1251 | protect = PAGE_EXECUTE_READ;
1252 | } else if (access == VM_ACCESS_RWX) {
1253 | protect = PAGE_EXECUTE_READWRITE;
1254 | } else {
1255 | return std::unexpected{OsError::FAILED_TO_PROTECT};
1256 | }
1257 |
1258 | return vm_protect(address, size, protect);
1259 | }
1260 |
1261 | std::expected vm_protect(uint8_t* address, size_t size, uint32_t protect) {
1262 | DWORD old_protect = 0;
1263 |
1264 | if (VirtualProtect(address, size, protect, &old_protect) == FALSE) {
1265 | return std::unexpected{OsError::FAILED_TO_PROTECT};
1266 | }
1267 |
1268 | return old_protect;
1269 | }
1270 |
1271 | std::expected vm_query(uint8_t* address) {
1272 | MEMORY_BASIC_INFORMATION mbi{};
1273 | auto result = VirtualQuery(address, &mbi, sizeof(mbi));
1274 |
1275 | if (result == 0) {
1276 | return std::unexpected{OsError::FAILED_TO_QUERY};
1277 | }
1278 |
1279 | VmAccess access{
1280 | .read = (mbi.Protect & (PAGE_READONLY | PAGE_READWRITE | PAGE_EXECUTE_READ | PAGE_EXECUTE_READWRITE)) != 0,
1281 | .write = (mbi.Protect & (PAGE_READWRITE | PAGE_EXECUTE_READWRITE)) != 0,
1282 | .execute = (mbi.Protect & (PAGE_EXECUTE | PAGE_EXECUTE_READ | PAGE_EXECUTE_READWRITE)) != 0,
1283 | };
1284 |
1285 | return VmBasicInfo{
1286 | .address = static_cast(mbi.AllocationBase),
1287 | .size = mbi.RegionSize,
1288 | .access = access,
1289 | .is_free = mbi.State == MEM_FREE,
1290 | };
1291 | }
1292 |
1293 | bool vm_is_readable(uint8_t* address, size_t size) {
1294 | return IsBadReadPtr(address, size) == FALSE;
1295 | }
1296 |
1297 | bool vm_is_writable(uint8_t* address, size_t size) {
1298 | return IsBadWritePtr(address, size) == FALSE;
1299 | }
1300 |
1301 | bool vm_is_executable(uint8_t* address) {
1302 | LPVOID image_base_ptr;
1303 |
1304 | if (RtlPcToFileHeader(address, &image_base_ptr) == nullptr) {
1305 | return vm_query(address).value_or(VmBasicInfo{}).access.execute;
1306 | }
1307 |
1308 | // Just check if the section is executable.
1309 | const auto* image_base = reinterpret_cast(image_base_ptr);
1310 | const auto* dos_hdr = reinterpret_cast(image_base);
1311 |
1312 | if (dos_hdr->e_magic != IMAGE_DOS_SIGNATURE) {
1313 | return vm_query(address).value_or(VmBasicInfo{}).access.execute;
1314 | }
1315 |
1316 | const auto* nt_hdr = reinterpret_cast(image_base + dos_hdr->e_lfanew);
1317 |
1318 | if (nt_hdr->Signature != IMAGE_NT_SIGNATURE) {
1319 | return vm_query(address).value_or(VmBasicInfo{}).access.execute;
1320 | }
1321 |
1322 | const auto* section = IMAGE_FIRST_SECTION(nt_hdr);
1323 |
1324 | for (auto i = 0; i < nt_hdr->FileHeader.NumberOfSections; ++i, ++section) {
1325 | if (address >= image_base + section->VirtualAddress &&
1326 | address < image_base + section->VirtualAddress + section->Misc.VirtualSize) {
1327 | return (section->Characteristics & IMAGE_SCN_MEM_EXECUTE) != 0;
1328 | }
1329 | }
1330 |
1331 | return vm_query(address).value_or(VmBasicInfo{}).access.execute;
1332 | }
1333 |
1334 | SystemInfo system_info() {
1335 | SystemInfo info{};
1336 |
1337 | SYSTEM_INFO si{};
1338 | GetSystemInfo(&si);
1339 |
1340 | info.page_size = si.dwPageSize;
1341 | info.allocation_granularity = si.dwAllocationGranularity;
1342 | info.min_address = static_cast(si.lpMinimumApplicationAddress);
1343 | info.max_address = static_cast(si.lpMaximumApplicationAddress);
1344 |
1345 | return info;
1346 | }
1347 |
1348 | void execute_while_frozen(
1349 | const std::function& run_fn, const std::function& visit_fn) {
1350 | // Freeze all threads.
1351 | int num_threads_frozen;
1352 | auto first_run = true;
1353 |
1354 | do {
1355 | num_threads_frozen = 0;
1356 | HANDLE thread{};
1357 |
1358 | while (true) {
1359 | HANDLE next_thread{};
1360 | const auto status = NtGetNextThread(GetCurrentProcess(), thread,
1361 | THREAD_QUERY_LIMITED_INFORMATION | THREAD_SUSPEND_RESUME | THREAD_GET_CONTEXT | THREAD_SET_CONTEXT, 0,
1362 | 0, &next_thread);
1363 |
1364 | if (thread != nullptr) {
1365 | CloseHandle(thread);
1366 | }
1367 |
1368 | if (!NT_SUCCESS(status)) {
1369 | break;
1370 | }
1371 |
1372 | thread = next_thread;
1373 |
1374 | const auto thread_id = GetThreadId(thread);
1375 |
1376 | if (thread_id == 0 || thread_id == GetCurrentThreadId()) {
1377 | continue;
1378 | }
1379 |
1380 | const auto suspend_count = SuspendThread(thread);
1381 |
1382 | if (suspend_count == static_cast(-1)) {
1383 | continue;
1384 | }
1385 |
1386 | // Check if the thread was already frozen. Only resume if the thread was already frozen, and it wasn't the
1387 | // first run of this freeze loop to account for threads that may have already been frozen for other reasons.
1388 | if (suspend_count != 0 && !first_run) {
1389 | ResumeThread(thread);
1390 | continue;
1391 | }
1392 |
1393 | CONTEXT thread_ctx{};
1394 |
1395 | thread_ctx.ContextFlags = CONTEXT_FULL;
1396 |
1397 | if (GetThreadContext(thread, &thread_ctx) == FALSE) {
1398 | continue;
1399 | }
1400 |
1401 | if (visit_fn) {
1402 | visit_fn(static_cast(thread_id), static_cast(thread),
1403 | static_cast(&thread_ctx));
1404 | }
1405 |
1406 | SetThreadContext(thread, &thread_ctx);
1407 |
1408 | ++num_threads_frozen;
1409 | }
1410 |
1411 | first_run = false;
1412 | } while (num_threads_frozen != 0);
1413 |
1414 | // Run the function.
1415 | if (run_fn) {
1416 | run_fn();
1417 | }
1418 |
1419 | // Resume all threads.
1420 | HANDLE thread{};
1421 |
1422 | while (true) {
1423 | HANDLE next_thread{};
1424 | const auto status = NtGetNextThread(GetCurrentProcess(), thread,
1425 | THREAD_QUERY_LIMITED_INFORMATION | THREAD_SUSPEND_RESUME | THREAD_GET_CONTEXT | THREAD_SET_CONTEXT, 0, 0,
1426 | &next_thread);
1427 |
1428 | if (thread != nullptr) {
1429 | CloseHandle(thread);
1430 | }
1431 |
1432 | if (!NT_SUCCESS(status)) {
1433 | break;
1434 | }
1435 |
1436 | thread = next_thread;
1437 |
1438 | const auto thread_id = GetThreadId(thread);
1439 |
1440 | if (thread_id == 0 || thread_id == GetCurrentThreadId()) {
1441 | continue;
1442 | }
1443 |
1444 | ResumeThread(thread);
1445 | }
1446 | }
1447 |
1448 | void fix_ip(ThreadContext thread_ctx, uint8_t* old_ip, uint8_t* new_ip) {
1449 | auto* ctx = reinterpret_cast(thread_ctx);
1450 |
1451 | #if SAFETYHOOK_ARCH_X86_64
1452 | auto ip = ctx->Rip;
1453 | #elif SAFETYHOOK_ARCH_X86_32
1454 | auto ip = ctx->Eip;
1455 | #endif
1456 |
1457 | if (ip == reinterpret_cast(old_ip)) {
1458 | ip = reinterpret_cast(new_ip);
1459 | }
1460 |
1461 | #if SAFETYHOOK_ARCH_X86_64
1462 | ctx->Rip = ip;
1463 | #elif SAFETYHOOK_ARCH_X86_32
1464 | ctx->Eip = ip;
1465 | #endif
1466 | }
1467 |
1468 | } // namespace safetyhook
1469 |
1470 | #endif
1471 |
1472 | //
1473 | // Source file: utility.cpp
1474 | //
1475 |
1476 |
1477 |
1478 | namespace safetyhook {
1479 | bool is_executable(uint8_t* address) {
1480 | return vm_is_executable(address);
1481 | }
1482 |
1483 | UnprotectMemory::~UnprotectMemory() {
1484 | if (m_address != nullptr) {
1485 | vm_protect(m_address, m_size, m_original_protection);
1486 | }
1487 | }
1488 |
1489 | UnprotectMemory::UnprotectMemory(UnprotectMemory&& other) noexcept {
1490 | *this = std::move(other);
1491 | }
1492 |
1493 | UnprotectMemory& UnprotectMemory::operator=(UnprotectMemory&& other) noexcept {
1494 | if (this != &other) {
1495 | m_address = other.m_address;
1496 | m_size = other.m_size;
1497 | m_original_protection = other.m_original_protection;
1498 | other.m_address = nullptr;
1499 | other.m_size = 0;
1500 | other.m_original_protection = 0;
1501 | }
1502 |
1503 | return *this;
1504 | }
1505 |
1506 | std::optional unprotect(uint8_t* address, size_t size) {
1507 | auto old_protection = vm_protect(address, size, VM_ACCESS_RWX);
1508 |
1509 | if (!old_protection) {
1510 | return std::nullopt;
1511 | }
1512 |
1513 | return UnprotectMemory{address, size, old_protection.value()};
1514 | }
1515 |
1516 | } // namespace safetyhook
1517 |
1518 | //
1519 | // Source file: vmt_hook.cpp
1520 | //
1521 |
1522 |
1523 |
1524 | namespace safetyhook {
1525 | VmHook::VmHook(VmHook&& other) noexcept {
1526 | *this = std::move(other);
1527 | }
1528 |
1529 | VmHook& VmHook::operator=(VmHook&& other) noexcept {
1530 | destroy();
1531 | m_original_vm = other.m_original_vm;
1532 | m_new_vm = other.m_new_vm;
1533 | m_vmt_entry = other.m_vmt_entry;
1534 | m_new_vmt_allocation = std::move(other.m_new_vmt_allocation);
1535 | other.m_original_vm = nullptr;
1536 | other.m_new_vm = nullptr;
1537 | other.m_vmt_entry = nullptr;
1538 | return *this;
1539 | }
1540 |
1541 | VmHook::~VmHook() {
1542 | destroy();
1543 | }
1544 |
1545 | void VmHook::reset() {
1546 | *this = {};
1547 | }
1548 |
1549 | void VmHook::destroy() {
1550 | if (m_original_vm != nullptr) {
1551 | *m_vmt_entry = m_original_vm;
1552 | m_original_vm = nullptr;
1553 | m_new_vm = nullptr;
1554 | m_vmt_entry = nullptr;
1555 | m_new_vmt_allocation.reset();
1556 | }
1557 | }
1558 |
1559 | std::expected VmtHook::create(void* object) {
1560 | VmtHook hook{};
1561 |
1562 | const auto original_vmt = *reinterpret_cast(object);
1563 | hook.m_objects.emplace(object, original_vmt);
1564 |
1565 | // Count the number of virtual method pointers. We start at one to account for the RTTI pointer.
1566 | auto num_vmt_entries = 1;
1567 |
1568 | for (auto vm = original_vmt; is_executable(*vm); ++vm) {
1569 | ++num_vmt_entries;
1570 | }
1571 |
1572 | // Allocate memory for the new VMT.
1573 | auto allocation = Allocator::global()->allocate(num_vmt_entries * sizeof(uint8_t*));
1574 |
1575 | if (!allocation) {
1576 | return std::unexpected{Error::bad_allocation(allocation.error())};
1577 | }
1578 |
1579 | hook.m_new_vmt_allocation = std::make_shared(std::move(*allocation));
1580 | hook.m_new_vmt = reinterpret_cast(hook.m_new_vmt_allocation->data());
1581 |
1582 | // Copy pointer to RTTI.
1583 | hook.m_new_vmt[0] = original_vmt[-1];
1584 |
1585 | // Copy virtual method pointers.
1586 | for (auto i = 0; i < num_vmt_entries - 1; ++i) {
1587 | hook.m_new_vmt[i + 1] = original_vmt[i];
1588 | }
1589 |
1590 | *reinterpret_cast(object) = &hook.m_new_vmt[1];
1591 |
1592 | return hook;
1593 | }
1594 |
1595 | VmtHook::VmtHook(VmtHook&& other) noexcept {
1596 | *this = std::move(other);
1597 | }
1598 |
1599 | VmtHook& VmtHook::operator=(VmtHook&& other) noexcept {
1600 | destroy();
1601 | m_objects = std::move(other.m_objects);
1602 | m_new_vmt_allocation = std::move(other.m_new_vmt_allocation);
1603 | m_new_vmt = other.m_new_vmt;
1604 | other.m_new_vmt = nullptr;
1605 | return *this;
1606 | }
1607 |
1608 | VmtHook::~VmtHook() {
1609 | destroy();
1610 | }
1611 |
1612 | void VmtHook::apply(void* object) {
1613 | m_objects.emplace(object, *reinterpret_cast(object));
1614 | *reinterpret_cast(object) = &m_new_vmt[1];
1615 | }
1616 |
1617 | void VmtHook::remove(void* object) {
1618 | const auto search = m_objects.find(object);
1619 |
1620 | if (search == m_objects.end()) {
1621 | return;
1622 | }
1623 |
1624 | const auto original_vmt = search->second;
1625 |
1626 | execute_while_frozen([&] {
1627 | if (!vm_is_writable(reinterpret_cast(object), sizeof(void*))) {
1628 | return;
1629 | }
1630 |
1631 | if (*reinterpret_cast(object) != &m_new_vmt[1]) {
1632 | return;
1633 | }
1634 |
1635 | *reinterpret_cast(object) = original_vmt;
1636 | });
1637 |
1638 | m_objects.erase(search);
1639 | }
1640 |
1641 | void VmtHook::reset() {
1642 | *this = {};
1643 | }
1644 |
1645 | void VmtHook::destroy() {
1646 | execute_while_frozen([this] {
1647 | for (const auto [object, original_vmt] : m_objects) {
1648 | if (!vm_is_writable(reinterpret_cast(object), sizeof(void*))) {
1649 | return;
1650 | }
1651 |
1652 | if (*reinterpret_cast(object) != &m_new_vmt[1]) {
1653 | return;
1654 | }
1655 |
1656 | *reinterpret_cast(object) = original_vmt;
1657 | }
1658 | });
1659 |
1660 | m_objects.clear();
1661 | m_new_vmt_allocation.reset();
1662 | m_new_vmt = nullptr;
1663 | }
1664 | } // namespace safetyhook
--------------------------------------------------------------------------------
/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 (uintptr_t)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
300 | concept FnPtr = requires(T f) { std::is_pointer_v&& std::is_function_v>; };
301 |
302 | bool is_executable(uint8_t* address);
303 |
304 | class UnprotectMemory {
305 | public:
306 | UnprotectMemory() = delete;
307 | ~UnprotectMemory();
308 | UnprotectMemory(const UnprotectMemory&) = delete;
309 | UnprotectMemory(UnprotectMemory&& other) noexcept;
310 | UnprotectMemory& operator=(const UnprotectMemory&) = delete;
311 | UnprotectMemory& operator=(UnprotectMemory&& other) noexcept;
312 |
313 | private:
314 | friend std::optional unprotect(uint8_t*, size_t);
315 |
316 | UnprotectMemory(uint8_t* address, size_t size, uint32_t original_protection)
317 | : m_address{address}, m_size{size}, m_original_protection{original_protection} {}
318 |
319 | uint8_t* m_address{};
320 | size_t m_size{};
321 | uint32_t m_original_protection{};
322 | };
323 |
324 | [[nodiscard]] std::optional unprotect(uint8_t* address, size_t size);
325 |
326 | template constexpr T align_up(T address, size_t align) {
327 | const auto unaligned_address = (uintptr_t)address;
328 | const auto aligned_address = (unaligned_address + align - 1) & ~(align - 1);
329 | return (T)aligned_address;
330 | }
331 |
332 | template constexpr T align_down(T address, size_t align) {
333 | const auto unaligned_address = (uintptr_t)address;
334 | const auto aligned_address = unaligned_address & ~(align - 1);
335 | return (T)aligned_address;
336 | }
337 | } // namespace safetyhook
338 |
339 | namespace safetyhook {
340 | /// @brief An inline hook.
341 | class InlineHook final {
342 | public:
343 | /// @brief Error type for InlineHook.
344 | struct Error {
345 | /// @brief The type of error.
346 | enum : uint8_t {
347 | BAD_ALLOCATION, ///< An error occurred when allocating memory.
348 | FAILED_TO_DECODE_INSTRUCTION, ///< Failed to decode an instruction.
349 | SHORT_JUMP_IN_TRAMPOLINE, ///< The trampoline contains a short jump.
350 | IP_RELATIVE_INSTRUCTION_OUT_OF_RANGE, ///< An IP-relative instruction is out of range.
351 | UNSUPPORTED_INSTRUCTION_IN_TRAMPOLINE, ///< An unsupported instruction was found in the trampoline.
352 | FAILED_TO_UNPROTECT, ///< Failed to unprotect memory.
353 | NOT_ENOUGH_SPACE, ///< Not enough space to create the hook.
354 | } type;
355 |
356 | /// @brief Extra information about the error.
357 | union {
358 | Allocator::Error allocator_error; ///< Allocator error information.
359 | uint8_t* ip; ///< IP of the problematic instruction.
360 | };
361 |
362 | /// @brief Create a BAD_ALLOCATION error.
363 | /// @param err The Allocator::Error that failed.
364 | /// @return The new BAD_ALLOCATION error.
365 | [[nodiscard]] static Error bad_allocation(Allocator::Error err) {
366 | return {.type = BAD_ALLOCATION, .allocator_error = err};
367 | }
368 |
369 | /// @brief Create a FAILED_TO_DECODE_INSTRUCTION error.
370 | /// @param ip The IP of the problematic instruction.
371 | /// @return The new FAILED_TO_DECODE_INSTRUCTION error.
372 | [[nodiscard]] static Error failed_to_decode_instruction(uint8_t* ip) {
373 | return {.type = FAILED_TO_DECODE_INSTRUCTION, .ip = ip};
374 | }
375 |
376 | /// @brief Create a SHORT_JUMP_IN_TRAMPOLINE error.
377 | /// @param ip The IP of the problematic instruction.
378 | /// @return The new SHORT_JUMP_IN_TRAMPOLINE error.
379 | [[nodiscard]] static Error short_jump_in_trampoline(uint8_t* ip) {
380 | return {.type = SHORT_JUMP_IN_TRAMPOLINE, .ip = ip};
381 | }
382 |
383 | /// @brief Create a IP_RELATIVE_INSTRUCTION_OUT_OF_RANGE error.
384 | /// @param ip The IP of the problematic instruction.
385 | /// @return The new IP_RELATIVE_INSTRUCTION_OUT_OF_RANGE error.
386 | [[nodiscard]] static Error ip_relative_instruction_out_of_range(uint8_t* ip) {
387 | return {.type = IP_RELATIVE_INSTRUCTION_OUT_OF_RANGE, .ip = ip};
388 | }
389 |
390 | /// @brief Create a UNSUPPORTED_INSTRUCTION_IN_TRAMPOLINE error.
391 | /// @param ip The IP of the problematic instruction.
392 | /// @return The new UNSUPPORTED_INSTRUCTION_IN_TRAMPOLINE error.
393 | [[nodiscard]] static Error unsupported_instruction_in_trampoline(uint8_t* ip) {
394 | return {.type = UNSUPPORTED_INSTRUCTION_IN_TRAMPOLINE, .ip = ip};
395 | }
396 |
397 | /// @brief Create a FAILED_TO_UNPROTECT error.
398 | /// @param ip The IP of the problematic instruction.
399 | /// @return The new FAILED_TO_UNPROTECT error.
400 | [[nodiscard]] static Error failed_to_unprotect(uint8_t* ip) { return {.type = FAILED_TO_UNPROTECT, .ip = ip}; }
401 |
402 | /// @brief Create a NOT_ENOUGH_SPACE error.
403 | /// @param ip The IP of the problematic instruction.
404 | /// @return The new NOT_ENOUGH_SPACE error.
405 | [[nodiscard]] static Error not_enough_space(uint8_t* ip) { return {.type = NOT_ENOUGH_SPACE, .ip = ip}; }
406 | };
407 |
408 | /// @brief Create an inline hook.
409 | /// @param target The address of the function to hook.
410 | /// @param destination The destination address.
411 | /// @return The InlineHook or an InlineHook::Error if an error occurred.
412 | /// @note This will use the default global Allocator.
413 | /// @note If you don't care about error handling, use the easy API (safetyhook::create_inline).
414 | [[nodiscard]] static std::expected create(void* target, void* destination);
415 |
416 | /// @brief Create an inline hook.
417 | /// @param target The address of the function to hook.
418 | /// @param destination The destination address.
419 | /// @return The InlineHook or an InlineHook::Error if an error occurred.
420 | /// @note This will use the default global Allocator.
421 | /// @note If you don't care about error handling, use the easy API (safetyhook::create_inline).
422 | [[nodiscard]] static std::expected create(FnPtr auto target, FnPtr auto destination) {
423 | return create(reinterpret_cast(target), reinterpret_cast(destination));
424 | }
425 |
426 | /// @brief Create an inline hook with a given Allocator.
427 | /// @param allocator The allocator to use.
428 | /// @param target The address of the function to hook.
429 | /// @param destination The destination address.
430 | /// @return The InlineHook or an InlineHook::Error if an error occurred.
431 | /// @note If you don't care about error handling, use the easy API (safetyhook::create_inline).
432 | [[nodiscard]] static std::expected create(
433 | const std::shared_ptr& allocator, void* target, void* destination);
434 |
435 | /// @brief Create an inline hook with a given Allocator.
436 | /// @param allocator The allocator to use.
437 | /// @param target The address of the function to hook.
438 | /// @param destination The destination address.
439 | /// @return The InlineHook or an InlineHook::Error if an error occurred.
440 | /// @note If you don't care about error handling, use the easy API (safetyhook::create_inline).
441 | [[nodiscard]] static std::expected create(
442 | const std::shared_ptr& allocator, FnPtr auto target, FnPtr auto destination) {
443 | return create(allocator, reinterpret_cast(target), reinterpret_cast(destination));
444 | }
445 |
446 | InlineHook() = default;
447 | InlineHook(const InlineHook&) = delete;
448 | InlineHook(InlineHook&& other) noexcept;
449 | InlineHook& operator=(const InlineHook&) = delete;
450 | InlineHook& operator=(InlineHook&& other) noexcept;
451 | ~InlineHook();
452 |
453 | /// @brief Reset the hook.
454 | /// @details This will restore the original function and remove the hook.
455 | /// @note This is called automatically in the destructor.
456 | void reset();
457 |
458 | /// @brief Get a pointer to the target.
459 | /// @return A pointer to the target.
460 | [[nodiscard]] uint8_t* target() const { return m_target; }
461 |
462 | /// @brief Get the target address.
463 | /// @return The target address.
464 | [[nodiscard]] uintptr_t target_address() const { return reinterpret_cast(m_target); }
465 |
466 | /// @brief Get a pointer ot the destination.
467 | /// @return A pointer to the destination.
468 | [[nodiscard]] uint8_t* destination() const { return m_destination; }
469 |
470 | /// @brief Get the destination address.
471 | /// @return The destination address.
472 | [[nodiscard]] uintptr_t destination_address() const { return reinterpret_cast(m_destination); }
473 |
474 | /// @brief Get the trampoline Allocation.
475 | /// @return The trampoline Allocation.
476 | [[nodiscard]] const Allocation& trampoline() const { return m_trampoline; }
477 |
478 | /// @brief Tests if the hook is valid.
479 | /// @return True if the hook is valid, false otherwise.
480 | explicit operator bool() const { return static_cast(m_trampoline); }
481 |
482 | /// @brief Returns the address of the trampoline to call the original function.
483 | /// @tparam T The type of the function pointer.
484 | /// @return The address of the trampoline to call the original function.
485 | template [[nodiscard]] T original() const { return reinterpret_cast(m_trampoline.address()); }
486 |
487 | /// @brief Returns a vector containing the original bytes of the target function.
488 | /// @return A vector of the original bytes of the target function.
489 | [[nodiscard]] const auto& original_bytes() const { return m_original_bytes; }
490 |
491 | /// @brief Calls the original function.
492 | /// @tparam RetT The return type of the function.
493 | /// @tparam ...Args The argument types of the function.
494 | /// @param ...args The arguments to pass to the function.
495 | /// @return The result of calling the original function.
496 | /// @note This function will use the default calling convention set by your compiler.
497 | template RetT call(Args... args) {
498 | std::scoped_lock lock{m_mutex};
499 | return m_trampoline ? original()(args...) : RetT();
500 | }
501 |
502 | /// @brief Calls the original function.
503 | /// @tparam RetT The return type of the function.
504 | /// @tparam ...Args The argument types of the function.
505 | /// @param ...args The arguments to pass to the function.
506 | /// @return The result of calling the original function.
507 | /// @note This function will use the __cdecl calling convention.
508 | template RetT ccall(Args... args) {
509 | std::scoped_lock lock{m_mutex};
510 | return m_trampoline ? original()(args...) : RetT();
511 | }
512 |
513 | /// @brief Calls the original function.
514 | /// @tparam RetT The return type of the function.
515 | /// @tparam ...Args The argument types of the function.
516 | /// @param ...args The arguments to pass to the function.
517 | /// @return The result of calling the original function.
518 | /// @note This function will use the __thiscall calling convention.
519 | template RetT thiscall(Args... args) {
520 | std::scoped_lock lock{m_mutex};
521 | return m_trampoline ? original()(args...) : RetT();
522 | }
523 |
524 | /// @brief Calls the original function.
525 | /// @tparam RetT The return type of the function.
526 | /// @tparam ...Args The argument types of the function.
527 | /// @param ...args The arguments to pass to the function.
528 | /// @return The result of calling the original function.
529 | /// @note This function will use the __stdcall calling convention.
530 | template RetT stdcall(Args... args) {
531 | std::scoped_lock lock{m_mutex};
532 | return m_trampoline ? original()(args...) : RetT();
533 | }
534 |
535 | /// @brief Calls the original function.
536 | /// @tparam RetT The return type of the function.
537 | /// @tparam ...Args The argument types of the function.
538 | /// @param ...args The arguments to pass to the function.
539 | /// @return The result of calling the original function.
540 | /// @note This function will use the __fastcall calling convention.
541 | template RetT fastcall(Args... args) {
542 | std::scoped_lock lock{m_mutex};
543 | return m_trampoline ? original()(args...) : RetT();
544 | }
545 |
546 | /// @brief Calls the original function.
547 | /// @tparam RetT The return type of the function.
548 | /// @tparam ...Args The argument types of the function.
549 | /// @param ...args The arguments to pass to the function.
550 | /// @return The result of calling the original function.
551 | /// @note This function will use the default calling convention set by your compiler.
552 | /// @note This function is unsafe because it doesn't lock the mutex. Only use this if you don't care about unhook
553 | /// safety or are worried about the performance cost of locking the mutex.
554 | template RetT unsafe_call(Args... args) {
555 | return original()(args...);
556 | }
557 |
558 | /// @brief Calls the original function.
559 | /// @tparam RetT The return type of the function.
560 | /// @tparam ...Args The argument types of the function.
561 | /// @param ...args The arguments to pass to the function.
562 | /// @return The result of calling the original function.
563 | /// @note This function will use the __cdecl calling convention.
564 | /// @note This function is unsafe because it doesn't lock the mutex. Only use this if you don't care about unhook
565 | /// safety or are worried about the performance cost of locking the mutex.
566 | template RetT unsafe_ccall(Args... args) {
567 | return original()(args...);
568 | }
569 |
570 | /// @brief Calls the original function.
571 | /// @tparam RetT The return type of the function.
572 | /// @tparam ...Args The argument types of the function.
573 | /// @param ...args The arguments to pass to the function.
574 | /// @return The result of calling the original function.
575 | /// @note This function will use the __thiscall calling convention.
576 | /// @note This function is unsafe because it doesn't lock the mutex. Only use this if you don't care about unhook
577 | /// safety or are worried about the performance cost of locking the mutex.
578 | template RetT unsafe_thiscall(Args... args) {
579 | return original()(args...);
580 | }
581 |
582 | /// @brief Calls the original function.
583 | /// @tparam RetT The return type of the function.
584 | /// @tparam ...Args The argument types of the function.
585 | /// @param ...args The arguments to pass to the function.
586 | /// @return The result of calling the original function.
587 | /// @note This function will use the __stdcall calling convention.
588 | /// @note This function is unsafe because it doesn't lock the mutex. Only use this if you don't care about unhook
589 | /// safety or are worried about the performance cost of locking the mutex.
590 | template RetT unsafe_stdcall(Args... args) {
591 | return original()(args...);
592 | }
593 |
594 | /// @brief Calls the original function.
595 | /// @tparam RetT The return type of the function.
596 | /// @tparam ...Args The argument types of the function.
597 | /// @param ...args The arguments to pass to the function.
598 | /// @return The result of calling the original function.
599 | /// @note This function will use the __fastcall calling convention.
600 | /// @note This function is unsafe because it doesn't lock the mutex. Only use this if you don't care about unhook
601 | /// safety or are worried about the performance cost of locking the mutex.
602 | template RetT unsafe_fastcall(Args... args) {
603 | return original()(args...);
604 | }
605 |
606 | private:
607 | friend class MidHook;
608 |
609 | uint8_t* m_target{};
610 | uint8_t* m_destination{};
611 | Allocation m_trampoline{};
612 | std::vector m_original_bytes{};
613 | uintptr_t m_trampoline_size{};
614 | std::recursive_mutex m_mutex{};
615 |
616 | std::expected setup(
617 | const std::shared_ptr& allocator, uint8_t* target, uint8_t* destination);
618 | std::expected e9_hook(const std::shared_ptr& allocator);
619 |
620 | #if SAFETYHOOK_ARCH_X86_64
621 | std::expected ff_hook(const std::shared_ptr& allocator);
622 | #endif
623 |
624 | void destroy();
625 | };
626 | } // namespace safetyhook
627 |
628 | //
629 | // Header: safetyhook/mid_hook.hpp
630 | //
631 | // Include stack:
632 | // - safetyhook.hpp
633 | // - safetyhook/easy.hpp
634 | //
635 |
636 | /// @file safetyhook/mid_hook.hpp
637 | /// @brief Mid function hooking class.
638 |
639 | #pragma once
640 |
641 | #ifndef SAFETYHOOK_USE_CXXMODULES
642 | #include
643 | #include
644 | #else
645 | import std.compat;
646 | #endif
647 |
648 |
649 | //
650 | // Header: safetyhook/context.hpp
651 | //
652 | // Include stack:
653 | // - safetyhook.hpp
654 | // - safetyhook/easy.hpp
655 | // - safetyhook/mid_hook.hpp
656 | //
657 |
658 | /// @file safetyhook/context.hpp
659 | /// @brief Context structure for MidHook.
660 |
661 | #pragma once
662 |
663 | #ifndef SAFETYHOOK_USE_CXXMODULES
664 | #include
665 | #else
666 | import std.compat;
667 | #endif
668 |
669 |
670 | namespace safetyhook {
671 | union Xmm {
672 | uint8_t u8[16];
673 | uint16_t u16[8];
674 | uint32_t u32[4];
675 | uint64_t u64[2];
676 | float f32[4];
677 | double f64[2];
678 | };
679 |
680 | /// @brief Context structure for 64-bit MidHook.
681 | /// @details This structure is used to pass the context of the hooked function to the destination allowing full access
682 | /// to the 64-bit registers at the moment the hook is called.
683 | /// @note rip will point to a trampoline containing the replaced instruction(s).
684 | /// @note rsp is read-only. Modifying it will have no effect. Use trampoline_rsp to modify rsp if needed but make sure
685 | /// the top of the stack is the rip you want to resume at.
686 | struct Context64 {
687 | Xmm xmm0, xmm1, xmm2, xmm3, xmm4, xmm5, xmm6, xmm7, xmm8, xmm9, xmm10, xmm11, xmm12, xmm13, xmm14, xmm15;
688 | uintptr_t rflags, r15, r14, r13, r12, r11, r10, r9, r8, rdi, rsi, rdx, rcx, rbx, rax, rbp, rsp, trampoline_rsp, rip;
689 | };
690 |
691 | /// @brief Context structure for 32-bit MidHook.
692 | /// @details This structure is used to pass the context of the hooked function to the destination allowing full access
693 | /// to the 32-bit registers at the moment the hook is called.
694 | /// @note eip will point to a trampoline containing the replaced instruction(s).
695 | /// @note esp is read-only. Modifying it will have no effect. Use trampoline_esp to modify esp if needed but make sure
696 | /// the top of the stack is the eip you want to resume at.
697 | struct Context32 {
698 | Xmm xmm0, xmm1, xmm2, xmm3, xmm4, xmm5, xmm6, xmm7;
699 | uintptr_t eflags, edi, esi, edx, ecx, ebx, eax, ebp, esp, trampoline_esp, eip;
700 | };
701 |
702 | /// @brief Context structure for MidHook.
703 | /// @details This structure is used to pass the context of the hooked function to the destination allowing full access
704 | /// to the registers at the moment the hook is called.
705 | /// @note The structure is different depending on architecture.
706 | /// @note The structure only provides access to integer registers.
707 | #if SAFETYHOOK_ARCH_X86_64
708 | using Context = Context64;
709 | #elif SAFETYHOOK_ARCH_X86_32
710 | using Context = Context32;
711 | #endif
712 |
713 | } // namespace safetyhook
714 |
715 | namespace safetyhook {
716 |
717 | /// @brief A MidHook destination function.
718 | using MidHookFn = void (*)(Context& ctx);
719 |
720 | /// @brief A mid function hook.
721 | class MidHook final {
722 | public:
723 | /// @brief Error type for MidHook.
724 | struct Error {
725 | /// @brief The type of error.
726 | enum : uint8_t {
727 | BAD_ALLOCATION,
728 | BAD_INLINE_HOOK,
729 | } type;
730 |
731 | /// @brief Extra error information.
732 | union {
733 | Allocator::Error allocator_error; ///< Allocator error information.
734 | InlineHook::Error inline_hook_error; ///< InlineHook error information.
735 | };
736 |
737 | /// @brief Create a BAD_ALLOCATION error.
738 | /// @param err The Allocator::Error that failed.
739 | /// @return The new BAD_ALLOCATION error.
740 | [[nodiscard]] static Error bad_allocation(Allocator::Error err) {
741 | return {.type = BAD_ALLOCATION, .allocator_error = err};
742 | }
743 |
744 | /// @brief Create a BAD_INLINE_HOOK error.
745 | /// @param err The InlineHook::Error that failed.
746 | /// @return The new BAD_INLINE_HOOK error.
747 | [[nodiscard]] static Error bad_inline_hook(InlineHook::Error err) {
748 | return {.type = BAD_INLINE_HOOK, .inline_hook_error = err};
749 | }
750 | };
751 |
752 | /// @brief Creates a new MidHook object.
753 | /// @param target The address of the function to hook.
754 | /// @param destination_fn The destination function.
755 | /// @return The MidHook object or a MidHook::Error if an error occurred.
756 | /// @note This will use the default global Allocator.
757 | /// @note If you don't care about error handling, use the easy API (safetyhook::create_mid).
758 | [[nodiscard]] static std::expected create(void* target, MidHookFn destination_fn);
759 |
760 | /// @brief Creates a new MidHook object.
761 | /// @param target The address of the function to hook.
762 | /// @param destination_fn The destination function.
763 | /// @return The MidHook object or a MidHook::Error if an error occurred.
764 | /// @note This will use the default global Allocator.
765 | /// @note If you don't care about error handling, use the easy API (safetyhook::create_mid).
766 | [[nodiscard]] static std::expected create(FnPtr auto target, MidHookFn destination_fn) {
767 | return create(reinterpret_cast(target), destination_fn);
768 | }
769 |
770 | /// @brief Creates a new MidHook object with a given Allocator.
771 | /// @param allocator The Allocator to use.
772 | /// @param target The address of the function to hook.
773 | /// @param destination_fn The destination function.
774 | /// @return The MidHook object or a MidHook::Error if an error occurred.
775 | /// @note If you don't care about error handling, use the easy API (safetyhook::create_mid).
776 | [[nodiscard]] static std::expected create(
777 | const std::shared_ptr& allocator, void* target, MidHookFn destination_fn);
778 |
779 | /// @brief Creates a new MidHook object with a given Allocator.
780 | /// @tparam T The type of the function to hook.
781 | /// @param allocator The Allocator to use.
782 | /// @param target The address of the function to hook.
783 | /// @param destination_fn The destination function.
784 | /// @return The MidHook object or a MidHook::Error if an error occurred.
785 | /// @note If you don't care about error handling, use the easy API (safetyhook::create_mid).
786 | [[nodiscard]] static std::expected create(
787 | const std::shared_ptr& allocator, FnPtr auto target, MidHookFn destination_fn) {
788 | return create(allocator, reinterpret_cast(target), destination_fn);
789 | }
790 |
791 | MidHook() = default;
792 | MidHook(const MidHook&) = delete;
793 | MidHook(MidHook&& other) noexcept;
794 | MidHook& operator=(const MidHook&) = delete;
795 | MidHook& operator=(MidHook&& other) noexcept;
796 | ~MidHook() = default;
797 |
798 | /// @brief Reset the hook.
799 | /// @details This will remove the hook and free the stub.
800 | /// @note This is called automatically in the destructor.
801 | void reset();
802 |
803 | /// @brief Get a pointer to the target.
804 | /// @return A pointer to the target.
805 | [[nodiscard]] uint8_t* target() const { return m_target; }
806 |
807 | /// @brief Get the address of the target.
808 | /// @return The address of the target.
809 | [[nodiscard]] uintptr_t target_address() const { return reinterpret_cast(m_target); }
810 |
811 | /// @brief Get the destination function.
812 | /// @return The destination function.
813 | [[nodiscard]] MidHookFn destination() const { return m_destination; }
814 |
815 | /// @brief Returns a vector containing the original bytes of the target function.
816 | /// @return A vector of the original bytes of the target function.
817 | [[nodiscard]] const auto& original_bytes() const { return m_hook.m_original_bytes; }
818 |
819 | /// @brief Tests if the hook is valid.
820 | /// @return true if the hook is valid, false otherwise.
821 | explicit operator bool() const { return static_cast(m_stub); }
822 |
823 | private:
824 | InlineHook m_hook{};
825 | uint8_t* m_target{};
826 | Allocation m_stub{};
827 | MidHookFn m_destination{};
828 |
829 | std::expected setup(
830 | const std::shared_ptr& allocator, uint8_t* target, MidHookFn destination);
831 | };
832 | } // namespace safetyhook
833 |
834 | //
835 | // Header: safetyhook/vmt_hook.hpp
836 | //
837 | // Include stack:
838 | // - safetyhook.hpp
839 | // - safetyhook/easy.hpp
840 | //
841 |
842 | /// @file safetyhook/vmt_hook.hpp
843 | /// @brief VMT hooking classes
844 |
845 | #pragma once
846 |
847 | #ifndef SAFETYHOOK_USE_CXXMODULES
848 | #include
849 | #include
850 | #include
851 | #else
852 | import std.compat;
853 | #endif
854 |
855 |
856 | namespace safetyhook {
857 | /// @brief A hook class that allows for hooking a single method in a VMT.
858 | class VmHook final {
859 | public:
860 | VmHook() = default;
861 | VmHook(const VmHook&) = delete;
862 | VmHook(VmHook&& other) noexcept;
863 | VmHook& operator=(const VmHook&) = delete;
864 | VmHook& operator=(VmHook&& other) noexcept;
865 | ~VmHook();
866 |
867 | /// @brief Removes the hook.
868 | void reset();
869 |
870 | /// @brief Gets the original method pointer.
871 | template [[nodiscard]] T original() const { return reinterpret_cast(m_original_vm); }
872 |
873 | /// @brief Calls the original method.
874 | /// @tparam RetT The return type of the method.
875 | /// @tparam Args The argument types of the method.
876 | /// @param args The arguments to pass to the method.
877 | /// @return The return value of the method.
878 | /// @note This will call the original method with the default calling convention.
879 | template RetT call(Args... args) {
880 | return original()(args...);
881 | }
882 |
883 | /// @brief Calls the original method with the __cdecl calling convention.
884 | /// @tparam RetT The return type of the method.
885 | /// @tparam Args The argument types of the method.
886 | /// @param args The arguments to pass to the method.
887 | /// @return The return value of the method.
888 | template RetT ccall(Args... args) {
889 | return original()(args...);
890 | }
891 |
892 | /// @brief Calls the original method with the __thiscall calling convention.
893 | /// @tparam RetT The return type of the method.
894 | /// @tparam Args The argument types of the method.
895 | /// @param args The arguments to pass to the method.
896 | /// @return The return value of the method.
897 | template RetT thiscall(Args... args) {
898 | return original()(args...);
899 | }
900 |
901 | /// @brief Calls the original method with the __stdcall calling convention.
902 | /// @tparam RetT The return type of the method.
903 | /// @tparam Args The argument types of the method.
904 | /// @param args The arguments to pass to the method.
905 | /// @return The return value of the method.
906 | template RetT stdcall(Args... args) {
907 | return original()(args...);
908 | }
909 |
910 | /// @brief Calls the original method with the __fastcall calling convention.
911 | /// @tparam RetT The return type of the method.
912 | /// @tparam Args The argument types of the method.
913 | /// @param args The arguments to pass to the method.
914 | /// @return The return value of the method.
915 | template RetT fastcall(Args... args) {
916 | return original()(args...);
917 | }
918 |
919 | private:
920 | friend class VmtHook;
921 |
922 | uint8_t* m_original_vm{};
923 | uint8_t* m_new_vm{};
924 | uint8_t** m_vmt_entry{};
925 |
926 | // This keeps the allocation alive until the hook is destroyed.
927 | std::shared_ptr m_new_vmt_allocation{};
928 |
929 | void destroy();
930 | };
931 |
932 | /// @brief A hook class that copies an entire VMT for a given object and replaces it.
933 | class VmtHook final {
934 | public:
935 | /// @brief Error type for VmtHook.
936 | struct Error {
937 | /// @brief The type of error.
938 | enum : uint8_t {
939 | BAD_ALLOCATION, ///< An error occurred while allocating memory.
940 | } type;
941 |
942 | /// @brief Extra error information.
943 | union {
944 | Allocator::Error allocator_error; ///< Allocator error information.
945 | };
946 |
947 | /// @brief Create a BAD_ALLOCATION error.
948 | /// @param err The Allocator::Error that failed.
949 | /// @return The new BAD_ALLOCATION error.
950 | [[nodiscard]] static Error bad_allocation(Allocator::Error err) {
951 | return {.type = BAD_ALLOCATION, .allocator_error = err};
952 | }
953 | };
954 |
955 | /// @brief Creates a new VmtHook object. Will clone the VMT of the given object and replace it.
956 | /// @param object The object to hook.
957 | /// @return The VmtHook object or a VmtHook::Error if an error occurred.
958 | [[nodiscard]] static std::expected create(void* object);
959 |
960 | VmtHook() = default;
961 | VmtHook(const VmtHook&) = delete;
962 | VmtHook(VmtHook&& other) noexcept;
963 | VmtHook& operator=(const VmtHook&) = delete;
964 | VmtHook& operator=(VmtHook&& other) noexcept;
965 | ~VmtHook();
966 |
967 | /// @brief Applies the hook.
968 | /// @param object The object to apply the hook to.
969 | /// @note This will replace the VMT of the object with the new VMT. You can apply the hook to multiple objects.
970 | void apply(void* object);
971 |
972 | /// @brief Removes the hook.
973 | /// @param object The object to remove the hook from.
974 | void remove(void* object);
975 |
976 | /// @brief Removes the hook from all objects.
977 | void reset();
978 |
979 | /// @brief Hooks a method in the VMT.
980 | /// @param index The index of the method to hook.
981 | /// @param new_function The new function to use.
982 | [[nodiscard]] std::expected