Reloaded-Project / Reloaded-III

[WIP] Formal Specification for the Next Iteration of Reloaded
https://reloaded-project.github.io/Reloaded-III/
6 stars 0 forks source link

Idea: Add an Import Directly to PE Import Table #12

Open Sewer56 opened 6 months ago

Sewer56 commented 6 months ago

Made easy via: https://secana.github.io/PeNet/articles/imports.html

For ELF (Linux), this will be a bit more involved (as in: not available via existing library), but not too bad hopefully, it's actually easier there.

RibShark commented 6 months ago

I kinda don't like the idea of modifying files in this way. I'd rather not patch the EXE directly, it causes all manner of headaches (for one, it will cause issues when scanning for corrupted files on Steam games).

Sewer56 commented 6 months ago

Note:

It's just an idea. Like with the other notes, I made this issue so I remember what to add to the documentation when I get back to working on this. i.e. Another section to add to the Windows and Linux sidebar in the docs

I've kinda spent the last 4 months working on hooking library, so R3 stuff is getting hugely delayed in any case. I'll probably spend another 2 months working on it at this rate.

In any case, in my personal opinion, I would rather not edit the game folder in any capacity at all. However there are user scenarios and situations where that ideal cannot be achieved.

Some people for example, rely on Steam Input for their controller support. Launching outside of Steam therefore breaks their controller as Steam doesn't get to DLL Inject its own code; and that unfortunately is entirely my problem from a user's POV. If a broken controller is the user's first experience after installing mod loader; that isn't a particularly good first impression.


In any case, to give some rationale, this injection method is mainly an idea for reliably modding native games on more niche platforms, such as Linux.

From a user's POV:

Unfortunately, there are some gotchas involved there.

As a start, some of these titles need to run sandboxed due to compatibility reasons, making it harder for us to run our own code. As for the general options...

Idiomatic Way

'Idiomatic way' to load additional code is LD_PRELOAD, however Steam is the one launching the game; we don't have control of the commandline. Without editing Steam configs...

Code Injection

.so (DLL equivalent) injection is unreliable.

  1. Some distros' security policies may disable ptrace() by default, which is required for injection.

  2. Potential deadlock, described well in this 3rd party library.

Note: You also need to overwrite already existing memory (risky!). However, this is actually ok, as you can usually safely override libc entry point as that'll only ever get called once.

In additional, injection on Linux requires shellcode to be written inside the target process. This can be problematic if people want to mod games on new architectures for science purposes. I want for Reloaded to be as portable as possible, so if someone wants to mod Super Tux Kart on RISC-V; they should be able to without investing much upfront effort.

In any case, even if we were able to get past all of these limitations, the Steam games that require sandboxing will still need to be started by Steam. Steam will not start our process suspended, thus loading our code will be asynchronous; which is a no-go for injection.

Shared Library Hijacking

a.k.a. The ASI Loader Approach

Simply said; today, the application will load our own library as requested.

Tomorrow, the game will get sandboxed, and the sandbox will dictate which DLL to load, skipping our DLL entirely; now an end user trying to follow an old guide will have issues getting things to run; and that doesn't give them a good first impression.

The Problem

[General Problem, not Linux/Steam Specific]

The common problem here is having to rely/interact with an external dependency (Steam).

Steam itself could change anytime; and we don't get any input or warning over it. The configs could move, or change in format. Some users may also have an outdated version of Steam, while some have a newer one; so there may be a need to support multiple config formats at once if that happens; this is difficult.

And it is not a hypothetical. This has happened before, in 2021 and 2022, on two separate occasions, I had to edit a no longer maintained library for parsing Steam VDF files, because first, the format, and later the format constraints on the files which contain the info about the user's Steam libraries have changed.

And I needed to parse that to find the AppID at runtime, to force Steam API to not reboot the game via Steam for those people who wanted to run a modded game without touching their game folder. (Reloaded-II's default behaviour).

This led to some people being unable to run some games dependent on Steam API for at least 14 hours without using the ASI Loader method, before I dropped everything I was doing and fixed it with a quick hotfix. (That required me to get a copy of Steam from someone else, as I didn't even get the format changing update yet).

For an end user to wake up and have their modded game suddenly broken by a Steam update is simply unacceptable. Additionally, if an end user tries to mod their game for the first time during that period, they will run into unexpected issues. They likely won't get support as the issue is 'new', they will blame my software, and it will ultimately be considered my fault.

I want to avoid situations like this if at all possible.

Sewer56 commented 6 months ago

The other reason I want to make this an opt-in possibility for end users does actually concern the common windows games.

There's always the risk that inevitably, one day we'll encounter a game where:

There are cases for old indies where patching the binary may be required; where the end user requires that they launch the target process for Steam Input support, but the game has insufficient imports out of the box.

Some games also dynamically link their dependencies via LoadLibrary + GetProcAddress at runtime. Reloaded needs to provide a guarantee that all mod code is executed before any of the game code runs, regardless of the user's injection method. There can't be discrepancies such as 'this game can only be modded when using DLL Injection method' if I can avoid it.

Sometimes things in the game's entry method need patching, for example, a game may perform a check for multiple instances and quit. Now if you're writing an online multiplayer mod, you need to patch this check out so you can test.

In the rare case the end user explicitly needs SteamInput support (which means launching target EXE directly from Steam) AND the import table is insufficient, drastic measures like this need to exist.

Some examples of things I want to avoid:

Creating new DLL Wrappers/Hijackers on a Per Game Basis

For example Half Life and their derivatives require DLL Hijacking of WSOCK32.dll as it's the only viable entry point.

So suppose if someone wanted to mod that; I would need to go out of the way to create a wrapper around WSOCK32 if I didn't have one already.

Okay, now someone wants to run this on Linux; but the default Proton configuration might just happen to be set up to prefer its own builtin version of WSOCK32. Now we have to go back to parsing Steam configs to fix that; which is prone to breaking any day if something about Steam changes. We don't want to be introducing an unnecessary failure point.

The user should not be expected to know about WINEPREFIX(es) etc. so a breakage there, however unlikely is not acceptable. I need to provide solutions that work with any game out of the box; without requiring any developer intervention (code) whatsoever. They need to work exactly as the user expects them; no matter how niche their setup is.

Late Binding of Dependencies (e.g. Techland Games)

e.g. Pet Soccer https://youtu.be/pYpTM_hSekI?t=29

These Polish games that would often ship in magazines and cereal boxes have no imports that can be hijacked at boot time.

They do have dependencies that are hijackable at runtime such as vorbisfile, however that happens at runtime, so Reloaded couldn't provide the 'I execute code before game does' with a DLL Hijacking approach.

Now suppose this game needs a patch to work on modern hardware too (or to remove a broken CD check, sometimes DRM doesn't work on modern hardware). Now you have a modded EXE anyway.

If DLL Injection is not an option because the user e.g. wants to use Steam Input, thus we're not the one launching the game, AND they want to mod the game, but import table is insufficient, a hardmod of the binary is needed; because we should not be relying on Steam's Implementation detail.

tl;dr

I don't care how statistically unlikely a given scenario may be; I want to support it if I can. This is a 'I need to support every kind of game imaginable' sort of issue.

I may have impossibly high standards, I may sound insane sometimes; but. if I want to claim to deliver the truly universal solution, I need to cover every single base I can think of. It's a matter of pride.

RibShark commented 6 months ago

For some reason I was under the impression that this was intended to be the main form of injection. Given that this isn't the case I think supporting this is actually a great idea.

Though given your point regarding Windows games with few/no imports, Application Verifier Custom Providers are injected early, are persistent without being launched from a specific program, and so you could make a AVRF provider that acts as a bootstrap to inject some other DLL.

Additionally, there is the "InjectDLL" compatibility shim which can inject any DLL, which can be installed either through some apphelp.dll APIs, or I believe it is possible to set a registry entry to enable it too; sadly this shim is not implemented for 64-bit executables, so AVRF would be the more universal method here.

Obviously neither of these are going to work on Wine, but for Windows users at least this could be a good option.

Sewer56 commented 6 months ago

Though given your point regarding Windows games with few/no imports, Application Verifier Custom Providers are injected early,

Wow, this is actually pretty cool. I was not aware of this tech. I think this might particularly come in handy with Microsoft Store (Gamepass) titles. There's a chance this injection method might just work for th em.

Store binaries have built-in OS DRM that doesn't let you read or modify the game binaries in any capacity whatsoever. DLL Injection into them is also a no-go, because they launch some custom launcher stub when you launch their binaries. However this trick maybe might just work as an approach for code injection without editing game folder.

For some reason I was under the impression that this was intended to be the main form of injection.

Not natively on Windows, definitely not; there's no real benefit to it. My priority is maximizing the chances of things 'just working'; whenever possible.

I'm thinking something along the lines of:

Table extension is required due to likelihood of sandboxing for Steam based Native Linux titles.

On Windows, while I'm not a fan of modifying the game directory, I do have to prioritize things on 'just working'. Which means that if the title is a Steam title, there's a non-zero chance the user is relying on Steam Input, in which case it means Steam must be the one to start the game.

Natively on Windows, there isn't much value in patching the binary directly. DLL Hijack provides a cleaner uninstall experience. On Linux, there is a bit of value. If I'm unable to find/parse Steam's config to inject the Wine DLL Override to load mine instead of the original, then it's a good fallback. Alternatively, if the application doesn't have many injection points (e.g. only d3d), it may be a good idea to suggest patching, as the user may wish to replace the built-in dxvk (d3d9/10/11.dll) with a custom build.

Of course, as per usual, users will be able to change the default injection method.