-
-
Notifications
You must be signed in to change notification settings - Fork 87
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
Comments
I am going to guess the following:
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. |
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. |
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. Gonna look into it right now. |
After poking around for give-take 40 mins, I figured out the problem, here's a detailed writeup ReproductionThis error can be replicated under the following conditions:
CauseWhen running a game via the Reloaded Launcher, the following happens:
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.
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 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 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) Misc NoteUnfortunately, this is a classic case where copy-protection only hurts legitimate consumers. In this case:
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.
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. |
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! |
Are there? If you have a reproduction, I won't mind helping looking at that too. 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 The crash point was at 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 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. |
You can fix most of these problems if you check whether the page of memory that 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 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 So there was no reason for SK to be exporting them in the first place. Also
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. |
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 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 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. |
Random Examples:
And the occasional Discord mention. 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
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 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 So when you make a post like this: 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') |
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
The text was updated successfully, but these errors were encountered: