Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

[Launching Reloaded-II from Launcher Fails when used with Special K Local Install when Steam Embedded (.bind) DRM is present due error when Hooking Invalid Function Exports in Special K] #308

Closed
Mas9ue opened this issue Feb 21, 2024 · 11 comments
Labels
untriaged No decision has been made by the developers.

Comments

@Mas9ue
Copy link

Mas9ue commented Feb 21, 2024

Failed to Load Reloaded-Il.
Reloaded Hooks: Internal Error in lnternal/lcedPatcher. Failed to re-encode code
for new address. Process will probably die.Error: Can't encode an invalid
instruction : 0x7FFD91CE7917 (bad)
at Reloaded.HooksInternal.lcedPatcher.EncodeForNewAddress(UlntPtr
newAddress)
at Reloaded.Hooks.AsmHook.MakeHookstub(MemoryBuffer buffer, lcedPatcher
patcher, Bytel] asmCode, Int32 originalCodeLength, UlntPtr jumpBackAddress.
AsmHookBehaviour behaviour)
at Reloaded.Hooks.AsmHook.MakeAsmHook(Bytell asmCode, UlntPtr
functionAddress, AsmHookOptions options, Bytel originalFunction,
MemoryBuffer buffer, Int32 codeAlignment, Untptr jumpBackAddress)
at Reloaded.Hooks.AsmHook.e>c DisplayClass17 0.<.ctor>b 00
at Reloaded.Memory.Buffers.MemoryBuffer.ExecuteWithLockT
at Reloaded.Hooks.AsmHook..ctor(Bytel] asmCode, UntPtr functionAddress,
AsmHookOptions options)
at Reloaded.Hooks.ReloadedHooks.CreateAsmHook(String!] asmCode, Int64
functionAddress)
at Reloaded.Mod.Loader,Utilities,Delaylnjector.CreateHook(lnt64 address, Int32
dllOrdinal, Int32 functionOrdinal, IReloadedHooks hooks)
at Reloaded.Mod.Loader,Utilities.Delaylnjector..ctor(ReloadedHooks hooks.
Action action, Logger logger)
at Reloaded.Mod.Loader.EntryPoint.LoadMods(lReloadedHooks hooks)
at
Reloaded.Mod.LoaderEntryPoint,s>c DisplayClass14 0sSetupLoader2>b 10
at Reloaded.Mod.Loader.EntryPoint.ExecuteTimed(String text, Action action)
at Reloaded.Mod.Loader.EntryPoint.SetupLoader2(EntryPointParameters
parameters)
at Reloaded.Mod.Loader.EntryPoint.SetupLoader(EntryPointParameters
parameters)
A log is available at

@Mas9ue Mas9ue added the untriaged No decision has been made by the developers. label Feb 21, 2024
@Mas9ue
Copy link
Author

Mas9ue commented Feb 21, 2024

QQ截图20240221221851

@Sewer56
Copy link
Member

Sewer56 commented Feb 21, 2024

I am going to guess the following:

  • You're playing GB: Relink
  • You're using Special K
  • Process fails w hen using both at once

I haven't been able to debug this recently, I've been spending all my time fixing small bugs and rewriting the whole DLL injection backend due to 50+ reports of false positive virus detection. (Defender started randomly flagging a part of Reloaded after being unchanged for 4 years)

I did ask someone with the game to at least give me a screenshot of what's in the memory of that specific address, but I've still got no response. I'd buy the game and look into it myself, but I'm currently too swamped.

If you want a temporary workaround, stripping the Steam DRM from the main EXE with https://github.com/atom0s/Steamless Steamless, would probably work in the meantime.

@Mas9ue
Copy link
Author

Mas9ue commented Feb 22, 2024

Oh, you're right. I didn't notice at first that I was using a mod via SPECIAL K. Now I changed to Reloaded and it seems to be no problem.
Those who playing GBF Relink can check if you are using the Controller Button Prompts via SPECIAL K mod.

@Sewer56
Copy link
Member

Sewer56 commented Mar 1, 2024

So anyway, I bought the game out of my own pocket to look at the issue.

I was struggling to reproduce the error for a while, but think I figured it out.
It happens on local install when you inject SpecialK via the EXE import table (i.e. as d3d11.dll etc.)

Gonna look into it right now.
Edit: Once I get back home*

@Sewer56 Sewer56 changed the title Help: Failed to Load Reloaded-II. Reloaded hooks [Launching Reloaded-II from Launcher Fails when used with Special K when Steam Embedded (.bind) DRM is present] Mar 1, 2024
@Sewer56 Sewer56 changed the title [Launching Reloaded-II from Launcher Fails when used with Special K when Steam Embedded (.bind) DRM is present] [Launching Reloaded-II from Launcher Fails when used with Special K when Steam Embedded (.bind) DRM is present due to Invalid d3dkmt Exports in Special K] Mar 2, 2024
@Sewer56 Sewer56 changed the title [Launching Reloaded-II from Launcher Fails when used with Special K when Steam Embedded (.bind) DRM is present due to Invalid d3dkmt Exports in Special K] [Launching Reloaded-II from Launcher Fails when used with Special K when Steam Embedded (.bind) DRM is present due to Invalid Function Exports in Special K] Mar 2, 2024
@Sewer56
Copy link
Member

Sewer56 commented Mar 2, 2024

After poking around for give-take 40 mins, I figured out the problem, here's a detailed writeup

Reproduction

This error can be replicated under the following conditions:

  • You launch game with mods via Reloaded Launcher.
  • You have Special K installed via DLL Hijacking (a.k.a. 'Local Injection' in SK). For instance as d3d11.dll.
  • Game has Steam Embedded DRM (.bind) section.

Cause

When running a game via the Reloaded Launcher, the following happens:

  • Process is started as suspended.
  • Launcher DLL Injects into the process.
  • The injected loader loads the mods.
  • Launcher unsuspends process.

The expectation from R2, is that user mods can execute before any game logic runs. Normally this works fine, with Steam Embedded DRM, that is a bit more tricky. The game code is encrypted on boot, so mods can't apply their hooks.

Reloaded-II works around this in a rather hacky way.

  • Reads a list of entry APIs for various libraries (i.e. 'create' entry point functions)
  • Checks which have been loaded via import table (GetModuleHandle + GetProcAddress). And hooks them all, using this stub.
    • This stub fires a callback, while preserving core x86 registers used in stdcall (x86) or Microsoft (x64) call convention.
  • During callback the loader initializes, and undoes all of its hooks.
    • During unhook, this stub is replaced with branch to original, so this won't have issues when stacking hooks with other libraries. (It's safer, as the function entry point is not overwritten again).

This works because Steam DRM decrypts in place and doesn't call external APIs. While not perfect (as mods don't get to run before game instruction 0), Reloaded can load within the first couple hundred instructions of game execution reliably.

In any case, the error occurs when Reloaded tries to perform one of these hooks down the road. For example, when Special K is local installed as d3d11.dll (or similar), Reloaded calling GetModuleHandle(d3d11) and GetProcAddress(d3d11, D3DKMTCloseAdapter) for results in the address 0x18098F8D0. This address corresponds to this export in the Special K source code.

Unfortunately, there's no code at that address (this should be a function export), only a pointer to the actual target function which is assigned here in the Special K source code. Special K does place a hook on GetProcAddress but said hook does not fix the function address (at least in this context/scenario).

Note

tl;dr: Special K is exporting some functions, but these exports contain pointers instead of code. And there's either not a built-in fix for this, or it's not working quite right.

Resolution

(My thoughts on how could this be fixed)

Although you could partially resolve this inside GetProcAddress hook, that's a hack. If someone decides to parse your PE header in memory to get the export, it won't yield the right address (unless you also want to patch your own header). I wouldn't recommend it. That's why I didn't dig much further into the hook code.

Instead, it would probably be better to focus on correctness. In particular, making sure that these exports are valid functions as opposed to pointers to target functions, instead of playing whack-a-mole with various methods to get exported function addresses.

I suggest making the exports look like this (assembly):

x86:

# Relative branch. In x86 relative branch can get you from any
# address to any other address, thanks to overflow.
jmp 0x123456
# 5 bytes

x86_64 (if target out of 2GiB range):

# Branch to absolute address stored directly after this instruction
jmp qword [rip+0x0] 
.dq 0xDEADBEEFDEADBEEF # target

# 14 bytes, cache friendly and fits in CPU instruction fetch window.

In other words, export 8 bytes for x86, and 16 bytes (due to padding/alignment) for x86_64, and fixup these exports at some point in DllMain on boot.

For your function wrappers, either store target in separate fields for relative jump case (saves ~2 instructions) or just extract from the function. This fix will probably increase DLL size by about 600 bytes for x64, or 0.009%.

(CC @Aemony , for opinion/feedback/comments)
(Not pinging main dev, as I've had to deal with unsolicited trash talk for years relating to things out of my control, and I don't want to deal with any more of that.)

Misc Note

Unfortunately, this is a classic case where copy-protection only hurts legitimate consumers. In this case:

  • Steam DRM makes it considerably harder to debug the issue.
    • Steam DRM deliberately makes games crash when a debugger is attached.
    • Good luck debugging .NET code with native debugging tools.
  • Time is also wasted by game self-decrypting at boot (slower boot).
    • and by DRM specific workarounds in loader... :(

In the past I've considered turning Steamless into a library and stripping the decryption part of the DRM from the launcher directly. Unfortunately in the past there were caveats.

  • Steamless hasn't historically worked on 100% of the games (only like 99%, so another workaround still needed)
  • Steamless didn't have a license (at the time), so I couldn't use it.

Although it's possible now, you wouldn't be seeing it from me as Reloaded-II is in maintenance mode (only bug fixes), while I spend all of my spare time over ~2 years working on a successor.

@Sewer56 Sewer56 changed the title [Launching Reloaded-II from Launcher Fails when used with Special K when Steam Embedded (.bind) DRM is present due to Invalid Function Exports in Special K] [Launching Reloaded-II from Launcher Fails when used with Special K when Steam Embedded (.bind) DRM is present due error when Hooking Invalid Function Exports in Special K] Mar 2, 2024
@Sewer56 Sewer56 changed the title [Launching Reloaded-II from Launcher Fails when used with Special K when Steam Embedded (.bind) DRM is present due error when Hooking Invalid Function Exports in Special K] [Launching Reloaded-II from Launcher Fails when used with Special K Local Install when Steam Embedded (.bind) DRM is present due error when Hooking Invalid Function Exports in Special K] Mar 2, 2024
@Aemony
Copy link

Aemony commented Mar 2, 2024

Thanks for taking the time to look into and figure this all out, and for making such a detailed write up. It's much appreciated!

You're not the only one that has contemplated turning Steamless into something more easily usable either... Its importance only keeps rising as more and more users get modern high resolution displays that are more likely to run into the stupid 32-bit memory limit when playing older games, and it's infuriating that legitimate users cannot even do something basic as applying the 4GB/LAA flag to a game without needing Steamless to deobfuscate the executable first.

I'm not super-knowledgeable about the intricacies of DLL exports and the like so I'll have to bring this up with Kal and see what he thinks about it, though I can at least mention that the local injection method has for a long time not been a major concern due to lower compatibility in general. Hopefully we'll be able to improve this aspect of it, though.

I think our global injection (works through a CBT hook) has some minor compatibility issues of its own with R2 that I've been meaning to look into though I think they're much simpler in comparison (we probably just need to blacklist R2's processes I think?). But in general we tend to recommend users to use global injection over local due to its aforementioned lower compatibility (these DLL exports notwithstanding).

Cheers!

@Sewer56
Copy link
Member

Sewer56 commented Mar 2, 2024

I think our global injection (works through a CBT hook) has some minor compatibility issues of its own with R2 that I've been meaning to look into though I think they're much simpler in comparison

Are there? If you have a reproduction, I won't mind helping looking at that too.
I'm not personally aware of there having been any, historically. Not with base loader anyway.


There was that one time where a 3rd party R2 mod for Persona 4 Golden (32-bit) didn't run quite right when using Special K with CBT hooks. Here's the details as I remember them.

That was misattributed as a memory issue on the SK forums by Kal, but that was not the cause. The game was not out of virtual address space (though it came close over prolonged gameplay at 4K+ resolutions before it got LAA patched). The .NET Runtime didn't either need patching for LAA (it's always been LAA out of the box, and the Reloaded libraries would use the upper addresses when available, such as in the game hotfix that enabled LAA). [Note: Testing that in a pain in the butt, you have to patch the test runner binary for .NET itself to be LAA, so I never got around to doing that in CI, only locally]

It was some oddity with that mod's built in XACT (.xwb , .xsb) file emulator. ('file emulator' in this context is hooking Windows API to simulate files that don't exist, as means of producing virtual files that don't actually exist).

The crash point was at WMFDemuxFilter::CreateInstance, and it was some weird timing oddity. The game would prepare video playback at around same time as creating a window, and some strange interaction between the file emulator, audio/video hooks in SK and the game preparing for video playback right after the window caused the issue.

Injecting SpecialK as a native Reloaded mod or injecting Reloaded's bootstrapper as a SpecialK plugin worked as a workaround, and I've shown how to do that to some curious end users. (I recommend the former, as that avoids creating the hooks as game begins video playback, latter could still cause rare CTD, while former initializes both components before any game logic executed, sidestepping the issue). Alternatively you could use a patch to skip the video at startup, that worked too (future videos would play just fine).

I did let the mod author in question know back then, but they've been AWOL/inactive by that time. I did look at their code, and did some debugging, but I wasn't sure on the exact cause of this either.

Strangely crashes at WMFDemuxFilter::CreateInstance also happened for some end users on unmodded game copies (at least on Steam Forums), during prolonged gameplay. Maybe in those cases it could have been memory (if they experienced it before game got LAA patched), but it certainly wasn't that at startup, there was a lot of virtual address space to still go around.

Edit: Oh actually, there you are, so I guess you now know. I was a bit angry at the time, so I refrained from commenting back then.


Outside of that I'm not aware of any other cases.

@Kaldaien
Copy link

Kaldaien commented Mar 2, 2024

You can fix most of these problems if you check whether the page of memory that GetProcAddress (...) returns is all readable, executable and in a resident page. This is an essential check that I think you are missing.

  MEMORY_BASIC_INFORMATION mi;
  VirtualQuery (pAddress, &mi, sizeof (MEMORY_BASIC_INFORMATION));

  const bool bIsExecutable =
    ( mi.State == MEM_COMMIT && ( mi.Protect & (PAGE_EXECUTE | PAGE_EXECUTE_READ | PAGE_EXECUTE_READWRITE | PAGE_EXECUTE_WRITECOPY) ) != 0 );

A rogue uninitialized pointer will rarely satisfy all of these conditions. If you encounter an address that fails this, do not bother hooking that API at all. This is important when dealing with the Steam overlay because it hooks GetProcAddress (...) and can change the runtime behavior in weird ways.


I have worked around this problem in Special K by simply not exporting the private DLL symbols. There is no game in existence that relies on those symbols being exported, anything that uses them will always call GetProcAddress (...) at runtime.

So there was no reason for SK to be exporting them in the first place.


Also

(Not pinging main dev, as I've had to deal with unsolicited trash talk for years relating to things out of my control, and I don't want to deal with any more of that.)

I have no idea what you are talking about. I have never even talked to you before, if you approach me on Discord or GitHub, I am happy to work with you.

@Sewer56
Copy link
Member

Sewer56 commented Mar 2, 2024

You can fix most of these problems if you check whether the page of memory that GetProcAddress (...) returns is all readable, executable and in a resident page. This is an essential check that I think you are missing.

  MEMORY_BASIC_INFORMATION mi;
  VirtualQuery (pAddress, &mi, sizeof (MEMORY_BASIC_INFORMATION));

  const bool bIsExecutable =
    ( mi.State == MEM_COMMIT && ( mi.Protect & (PAGE_EXECUTE | PAGE_EXECUTE_READ | PAGE_EXECUTE_READWRITE | PAGE_EXECUTE_WRITECOPY) ) != 0 );

A rogue uninitialized pointer will rarely satisfy all of these conditions. If you encounter an address that fails this, do not bother hooking that API at all. This is important when dealing with the Steam overlay because it hooks GetProcAddress (...) and can change the runtime behavior in weird ways.

I have worked around this problem in Special K by simply not exporting the private DLL symbols. There is no game in existence that relies on those symbols being exported, anything that uses them will always call GetProcAddress (...) at runtime.

So there was no reason for SK to be exporting them in the first place.

Working around this from my end is no problem, and the approach you gave should work just fine.

I've made a patch for this before going to sleep yesterday, it actually does the exact same thing as you suggested, i.e. checking if the memory page is executable. That's sufficient to fix the problem; I'll probably get it out to users later today. As SpecialK exports these as variables which are then mutated, they get put in .data, so have default perms of rw-; so the check works just fine.

My concern is more of the fact that we should be striving for correctness in general. Even if the API is some benign function that no sane person should ever call or be concerned about, it's still technically possible that some silly person (I guess that's me in that case), will touch it. KMT? Probably nobody, but someone calling D3D11On12CreateDevice for instance, be it for experimental reasons, or otherwise isn't unthinkable.

Edit: I checked a93a97, looks good to me. Removing the exports themselves prevents the case of someone parsing PE header manually getting the wrong function. There's still the edge case of someone doing that actually expecting a function to exist; but the chance of someone doing that is extremely unlikely.

@Sewer56
Copy link
Member

Sewer56 commented Mar 2, 2024

Also

(Not pinging main dev, as I've had to deal with unsolicited trash talk for years relating to things out of my control, and I don't want to deal with any more of that.)

I have no idea what you are talking about. I have never even talked to you before, if you approach me on Discord or GitHub, I am happy to work with you.

Random Examples:

  • https://steamcommunity.com/app/1687950/discussions/0/3549427890059593325/

  • https://steamcommunity.com/app/1687950/discussions/0/3489752656792175866/?ctp=6#c3549427890052910748 (+ previous pages)

  • https://discourse.differentk.fyi/t/using-special-k-in-combination-with-reloaded-ii-mod-loader-with-steam-hook-mod-enabled-and-persona-4-mod-loader-will-trigger-the-warning-and-close-the-game/140/9

    Edit: This one personally bothers me because it's misinformation and considerable effort was spent on optimizing this thing. So please, let me explain.

    Yes .NET is not ideal, but I wanted to provide a 'safe' working environment for people where newbies wouldn't shoot themselves in the foot easily; which is why I used it.

    That said, I've gone very, very far out of my way to ensure this framework is efficient (both CPU and RAM) within the limitations I have. I do many things that aren't even officially supported by the runtime, such as hacking the SDK targets to eliminate dead code from non-selfcontained applications (dumb .NET doesn't support this).

    Things are also very aggressively dynamically linked, across mods, including logging, hooking library etc. Each (1st party) mod winds up being around 40-50KB of memory on average (~35KiB DLL size composed of IL and some pre-jitted code AND code jitted at runtime. Some common DLLs in mod folders are also shared/deduped and not double loaded). All mods also share same GC, so allocations are shared. So things scale OK past the initial heavy runtime load.

    Last I measured, loading R2 with a heavy setup of ~20 independent (mostly 1st party) code mods ontop of it amounted to less than 95MiB usage of virtual address space [reserved+committed]. I tested on an old game (Sonic Heroes, was the game with most DLL mods at the time), going really heavily out of my way, including loading etc. XInput DLL from a mod, and even a D2D powered Window, which also counted towards this number. Although the numbers aren't close to what a fully native optimized to the bone solution could achieve, a lot of effort was put towards this; so although the runtime itself is heavily, the framework does scale (provided everyone removes dead code).

    User's specific setup in that thread used ~40% in committed pages compared to what SpecialK used, so I wasn't particularly pleased with being blamed for address space exhaustion. [Measured by diffing modded/unmodded with VMMap] (Although note SpecialK can save some address space by deduping textures down the road at runtime). The user's crash as was not even caused by address space exhaustion, either.

    In any case, in the Rust driven successor, I'll be targeting sub 1MiB of code+alloc for loader+corelibs and hopefully sub 2MiB with core mods/middleware. That's my target for purely native setups anyway.

And the occasional Discord mention.
I got these links from other people in the past, so I always assumed there was more.

I take it you were probably frustrated with end users being end users, but well, it's not an ideal way to act.


To provide context for that specific scenario. Usually my injection method is DLL Inject into suspended process.

However, you can't do that with GamePass, as the actual binary is protected with OS-level DRM scheme (you're not even allowed to read it). Starting the main binary launches a completely unrelated launcher binary, which then loads the actual real game binary; so injection is moot [injects to wrong binary]. (I've never looked too deep into what goes on under the hood)

So my only real options I know of that give the guarantee 'your code loads before game runs any substantial amount of logic' are

  • AVRF (if that even works with GamePass)
  • LoadAppInit_DLLs (registry, and a huge no-no).
  • Local Shim

Other approaches like CBT hooks are too late into execution.

Usually, I tell the GamePass people to use a shim in a situation like this (just load Reloaded.Mod.Loader.Bootstrapper.dll through your favourite shim of choice). I don't do any hacky stuff, no funny Windows Loader locking shenanigans, etc. if it can load the binary, it just works. There's also 1 click option for end users to set up UltimateAsiLoader as a shim, I wrote some code to parse the PE header of a binary by hand, I read the import section, figure out appropriate DLL name etc. I never wrote a 1st party shim, as I've never had a good reason to in the past. This thing was originally built for 2-3 old non-launcher games, until people unexpectedly started using it for other things 😅

Now UAL was borked for P5R for quite a while, so I told the GamePass people to use Special K Local Install to bootstrap, I really don't understand why I needed to be roasted so badly for that, be it on that thread or Discord.


In the case of P5R, the Persona folks used R2 because I wrote a generic CRI FileSystem V2 Hook a while back, so people can load loose files into 2009+ CRI games. The alternative is packing entire .cpk archives at once (requires simple hook to extend .cpk limit and another to load custom ones), or repack almost the whole game every time they want to test a mod.

So when you make a post like this:
20240302_17h39m04s

You can imagine people in game's community were not particularly pleased.

[Also Note: P5R Reloaded stuff always worked on GamePass, just needs a shim to bootstrap, never understood why people had misconceptions there]


Edit: I added a note to one of the bullets above. (marked with 'Edit')

@Sewer56
Copy link
Member

Sewer56 commented Mar 2, 2024

This is resolved in ca10702 on Reloaded end (for older SK versions) and in a93a97 on Special-K end.
So I'll close this. If you need me, I'll throw myself in the SpecialK Discord, ping me there.

# for free to join this conversation on GitHub. Already have an account? # to comment
Labels
untriaged No decision has been made by the developers.
Projects
None yet
Development

No branches or pull requests

4 participants