Sekiro: Shadows Die Twice is a souls-like game produced by FromSoftware with additional ACT features and I quite like it, as well as other souls-like games, namely Bloodborne, Elden Ring. Anyway, recently I’m inspired by some Streamers who tried to mod the game in order to add more fun, therefore I’m also in to analyze this ModEngine to unveil its technical details. I may also create or update some mods if I have enough time.
Sekiro: Shadows Die Twice ModEngine Analysis
Prelude
Before the release of this ModEngine which exposes several quite useful APIs to help modders creating mods in an easier way, most of them have to use CheatEngine to do the scripting work. Both of them use the same technique, namely memory patch, but obviously the former one is surely more user-friendly.
Environment
I’ve done my source code analysis work on Visual Studio 2019, and the version of the ModEngine is 0.1.16 which is for Sekiro 1.06 version.
Project Structure
1 | ├── DS3ModEngine |
Then let’s dug deeper into each of its components in a sequential order.
AOBScanner
This is simply a helper class commonly seen in every game hacking techniques. Its full name would be “Array Of Bytes Scanner”, meaning it is used to search arrays of bytes inside the memory of the game process, and replace them with modified ones if needed.
Just look at the AOBSCanner.h
, this is all we need to know.
1 | class AOBScanner |
Two interesting methods are Scan
and FindAndReplace
. Namely speaking, we can use these two methods to scan bytes in memory and replace them.
Game
This is a quite simple one, but actually I have no idea about what this thing is doing. In Game.h
, we have:
1 | typedef enum |
then in Game.c
:
1 | #include "Game.h" |
The weird thing is, this is Sekiro, not Darksouls2, but this function returns an enum referring to the latter anyway. I predict that this isn’t quite important, so returning anything would be okay.
GameplayPatcher
tips: skip “unimportant” functions.
In this class, we have several tool functions that are related to modifying the game features. Look at the file GameplayPatcher.h
:
1 | BOOL ApplyGameplayPatches(); |
In this header file, we only spot 6 functions. However, in the source file, we found other functions that are not exposed, like ApplyBonfileSacrificePatch
. I presume that this ModEngine is migrated from the one for the Dark Souls 2, and these functions weren’t removed. But I’m going to analyze all of them in any case.
ApplyBonfireSacrificePatch(postponed)
1 | BOOL ApplyBonfireSacrificePatch() |
Just a simple function that replaces an array of bytes into another self-defined one using VirtualProtect
, AOBSCanner::Scan
and memcpy
.
According to static analysis through IDA, I couldn’t find the scanBytes
array in the whole program. Therefore I guess this function is truly for DS2. So lets postpone its analysis.
ApplyGameplayPatches(postponed)
1 | BOOL ApplyGameplayPatches() |
This function simply reads the user profile and then invokes the previous ApplyBonfireSacrificePatch
. However, I failed to find restoreBonfireSacrifice
in that profile.
Just like the statements I’ve made above, let’s postpone its analysis.
ApplyNoLogoPatch
This is the byte sequence to be modified: “7430488d542430488bcde810010010010090bb1000895c242044fb64e4”
The first two bytes are “7430”, meaning je 0x32
in x64 ISA. The function firstly will find the array’s address, and then increase the first byte with 1, making it “7530”, meaning jne 0x32
. The following sequence can’t be disassembled correctly, but I presume its just a pattern and doesn’t have an actual meaning.
Anyway, it’s just a patch on the jump instruction.
1 | BOOL ApplyNoLogoPatch() |
tFXRConstructor(unimportant)
1 | typedef void* (*FXRCONSTRUCTOR)(LPVOID, LPVOID, wchar_t*, LPVOID, UINT64); |
I have no idea about what this function does. The only place where it is invoked has been commented.
tFXR1(unimportant)
1 | typedef void* (*FXR1)(LPVOID, LPVOID); |
tMSBHitConstructor(unimportant)
1 | typedef void* (*MSBHITCONSTRUCTOR)(LPVOID, LPVOID, LPVOID, LPVOID, char); |
Called when the game is DS3. pass.
tMemoryAllocate(unimportant)
1 | typedef void* (*MEMORYALLOCATE)(UINT32, UINT32, LPVOID); |
tVirtualAlloc
1 | BOOL gPatchedAllocatorLimits = false; |
A VirtualAlloc hook API. Will call ApplyDS3SekiroAllocatorLimitPatch
once.
ApplyAllocationTracer(unimportant)
1 | BOOL ApplyAllocationTracer() |
Another heritage from the ModEngine for Dark Souls 2. Not invoked.
ApplyMiscPatches
A function for miscellaneous patches.
1 | BOOL ApplyMiscPatches() |
Firstly it calls ApplyNoLogoPatch
to skip the prelude, CG and logos.
Then there comes an “if” statement, but I think it’s assertion can’t be satisfied as the GetGameType
function will return theGAME_DARKSOULS_2_SOTFS
enumeration, which isn’t equal to GAME_DARKSOULS_3
. But let’s just mark this place for further review.
The “if” statement is unlikely to be met.
Finally it invokes ApplyDS3SekiroAllocatorLimitPatch
. Details of this function will be discussed later.
ApplyShadowMapResolutionPatches(unimportant)
According to the function’s name, may be it’s used to change the in-game shadow or map resolutions. But it’s not invoked. Let’s review this later.
This function will be invoked in dllmain.cpp
if and only if the game is Dark Souls 2.
tFmodMemoryAllocate(unimportant)
1 | typedef void* (*FMODMEMORYALLOCATE)(UINT32, UINT32, LPVOID); |
Used in DS3. pass.
ApplyFModHooks(unimportant)
Not invoked anyway.
1 | BOOL ApplyFModHooks() |
Used in DS3. Pass.
ApplyDS3SekiroAllocatorLimitPatch
1 | BOOL ApplyDS3SekiroAllocatorLimitPatch() |
Search a byte sequence called “table” with a length of 56 and then multiply the 9th and 10th QWORD by 3.
This function is called from ApplyMiscPatches function, and is also used to hook VirtualAlloc API as well.
I’ve failed to find this table in IDA. But according to the function’s name, It’s just used to unlock memory limit which isn’t quite important.
ApplyAllocatorLimitPatchVA
1 | BOOL ApplyAllocatorLimitPatchVA() |
Used to hook kernel32.VirtualAlloc
API with self-defined one to unlock memory limit. Not quite important.
Minhook
I have to mention this helper class first as it’s a very important one.
It’s a very large class so I guess I can explain this in detail in another blog. But now I will talk about it’s functionality in brief.
According to New Bing:
MinHook is a minimalistic x86/x64 API hooking library for Windows. It provides the basic part of Microsoft Detours functionality for both x64/x86 environments. It can be used to intercept calls to exported functions.
TsudaKageyu/minhook: The Minimalistic x86/x64 API Hooking Library for Windows (github.com)
To be honest, let’s just look at the MinHook.h
. It has all the information we need.
MH_CreateHook
1 | MH_STATUS WINAPI MH_CreateHook(LPVOID pTarget, LPVOID pDetour, LPVOID *ppOriginal); |
Creates a Hook for the specified API function, in disabled state. Therefore we need to invoke MH_EnableHook
as well to enable the hook.
MH_CreatHookApi
1 | MH_STATUS WINAPI MH_CreateHookApi( |
Creates a Hook for the specified API function, in disabled state. Therefore we need to invoke MH_EnableHook
as well to enable the hook.
These two functions also have a extended one respectively with an “Ex” postfix.
MH_EnableHook
1 | MH_STATUS WINAPI MH_EnableHook(LPVOID pTarget); |
Just enable the hook.
HideThreadFromDebugger
The only function used is BypassHideThreadFromDebugger
.
1 | BOOL BypassHideThreadFromDebugger() |
This function hooks the Windows API NtSetInformationThread
with a self-defined one.
1 | // Detour function |
The function checks the second argument “threadInfoClass” and avoids invoking the original API if the argument is 0x11, meaning the ThreadHideFromDebugger
enumeration.
If we don’t hook this API, the NtSetInformaitonThread
with ThreadHideFromDebugger
will make the whole thread undetectable to a debugger, which is needed for a mod.
ImGui
Let’s just skip this class because it’s just a GUI library. If interested, check this web: ocornut/imgui: Dear ImGui: Bloat-free Graphical User interface for C++ with minimal dependencies (github.com)
ImGuiHook
This class includes the following files(both source files and header files):
- d3d11hook
- imgui_impl_dx11
- InputHook
- Menu
- stdafx
I guess it’s used for the in-game menu.
d3d11hook
What the ModEngine is using is an old one. The author actually has integrated them into the following:
But we can also refer to the following demo projects:
lxfly2000/D3D11Hook: Direct3D 11 Hook (github.com)
imgui_impl_dx11
Actually it’s a subset of d3d11hook.
InputHook
Used to get the keyboard inputs. Skip.
Menu
In-game menu’s specification. Skip.
LooseParams
LooseParamsPatch(unimportant)
This function still has nothing to do with Sekiro. pass.
ModLoader
The whole point of this class is to hook File related functions(Windows API or in-game functions etc.) to load modders’ self-defined functionalities. (I guess)
HookModLoader
1 | BOOL HookModLoader(bool loadUXMFiles, bool useModOverride, bool cachePaths, wchar_t *modOverrideDirectory); |
Firstly it gets the ArchiveFunction
‘s address:
1 | LPVOID hookAddress = GetArchiveFunctionAddress(); |
If found:
1 | if (GetGameType() == GAME_SEKIRO) |
Details of tFuckSekiro
will be covered later.
Then, It hooks Windows File Create/Open API:
1 | else |
Details of tCreateFileW
will be covered later.
GetArchiveFunctionAddress(postponed)
1 | else if (game == GAME_SEKIRO) |
Sadly I’ve failed to search this array in static analysis. But it’s actually not that important.
tFuckSekiro
1 | void* tFuckSekiro(SekiroString *path, UINT64 p2, UINT64 p3, DLString *p4, UINT64 p5, UINT64 p6) |
Invokes ReplaceFileLoadPath
.
ReplaceFileLoadPath
Details of this function isn’t that important XD. Just like its name.
NetworkBlocker
This class is used to block network access to prevent from being banned.
1 | INT __stdcall tWSAStartup(WORD wVersionRequested, void* lpWSAData) |
It’s quite an easy one, just hook the WSAStartup
function and refrain the network library from initializing itself.
StackWalker
JochenKalmbach/StackWalker: Walking the callstack in windows applications (github.com)
Walk a callstack.
dllmain⭐
DllMain⭐
1 | if (GetPrivateProfileIntW(L"debug", L"showDebugLog", 0, L".\\modengine.ini") == 1) |
Firstly it checks your profile about whether you need a debug log. If so, it will call AllocConsole
. I guess It will open a new terminal for logging.
Then it will call InitInstance
function while the DLL is attaching to the game process:
1 | InitInstance(hModule); |
Next, it will call ApplyAllocatorLimitPatchVA
which has already been discussed.
Finally it may create a new thread:
1 | if (GetGameType() == GAME_DARKSOULS_3) |
To be honest, the GetGameType
function is really wrecked. I thought this “if” statement should be invoked because it’s for DS3, but there truly are some functionalities related to Sekiro, so… anyway.
InitInstance
Firstly, it checks chainDInput8DLLPath
to load other DLLs that are also used to hook dinput8.dll.
1 | GetPrivateProfileStringW(L"misc", L"chainDInput8DLLPath", L"", chainPath, MAX_PATH, L".\\modengine.ini"); |
Then it initialize MinHook:
1 | // Initialize MinHook |
Then it blocks network access:
1 | // Do early hook of WSA stuff |
Sekiro may not need this because it’s truly a single player game.
Then it hooks the entry of steam_api64.dll
if the game is on Steam:
1 | if (GetGameType() == GAME_SEKIRO || GetGameType() == GAME_DARKSOULS_3 || GetGameType() == GAME_DARKSOULS_REMASTERED || GetGameType() == GAME_DARKSOULS_2_SOTFS) |
The hook function:
1 | // SteamAPI hook |
It calls ApplyPostUnpackHooks
which will be discussed later.
Finally it does something to the DS2’s shadow rendering but lets skip this.
MainThread
Firstly, it unlocks the game’s memory limit:
1 | ApplyDS3SekiroAllocatorLimitPatch(); |
Then it tries to find the game’s window and title:
1 | if (GetGameType() == GAME_SEKIRO) |
But I actually don’t think it does anything useful.
Finally, it calls ApplyPostUnpackHooks
as well.
ApplyPostUnpackHooks
Firstly it checks Sekiro’s version:
1 | // Check Sekiro version |
Then there are some statements related to acquiring the profile’s options, but lets just skip that. And we also skip anything unrelated to Sekiro.
Next:
1 | if (!ApplyMiscPatches()) |
Finally, there should be a LoadPlugins
function called but somehow I found this function is commented:
1 | void LoadPlugins() |
Weird enough.
Conclusion
To be honest, this mod engine actually doesn’t do a lot of things, just some simple patches, nor does it succeeds in wrapping and exposing helpful APIs to modders. I suppose I have to read other’s mod for further study.
- 本文作者: Taardis
- 本文链接: https://taardisaa.github.io/2023/04/09/Sekiro_ModEngine_Analysis/
- 版权声明: 本博客所有文章除特别声明外,均采用 Apache License 2.0 许可协议。转载请注明出处!