How does game cracking work?
Today we are going to explore how cracking games works in depth. Especially for Steam games.
For this post, I'm going to target A little to the left , a paid Unity Game on steam.
#1 Digital Rights Management
In order to understand how cracking works, we first have to understand what DRM is, in our case the steam DRM.
Digital Rights Management, are a set of technologies used to protect copyrighted digital content, such as games.
For the steam DRM, these technologies are implemented inside a DLL (Dynamic Link Library), namely steam_api64.dll (64-bit game) which is loaded upon game execution and prevents the launch of the game if not owned.
There are multiple ways to bypass these checks: hooking functions, modifying code or even emulating the steam api itself.
In this post we are going to focus on the modification of the executable code.
To understand how and what to patch, we first have to understand how Unity Games work.
#2 Unity Game Architecture
Unity Games can either use the Mono backend or the IL2CPP scripting backend.
Mono
When compiling .NET code, in our case C#, it is turned either into a module or assembly.
An assembly is the primary unit of development, it's usually a DLL or an EXE file.
Modules are physical files that can be part of an assembly. They contain all of the code types (classes, namespaces, struct, ...).
An assembly can theoretically consist of one or more modules, however in practice, almost all C# projects are single-module assemblies. And all the dll's in your game directories are assemblies.
With mono, the Assembly-CSharp.dll assembly file contains: all of the custom C# game scripts, any namespaces, classes defined in the project and attributes, enum, structs, ... defined in the code.
You can also find the assemblies containing Unity's functionality such as UnityEngine, UnityEditor, ...
These assemblies reference each other via metadata (called assembly references), so that the main game assembly can call external functions defined in other assemblies classes.
Managed C# Assemblies contain code compiled into IL (Intermediate Language) code, along with C# metadata for the data types defined, and references to external assemblies or native functions.
These types of assemblies are pure managed code, meaning they run inside the mono virtual machine.
Here is an example of a main's game managed assembly file opened into an IL disassembler.
However, there is also a native C++ unity binary, UnityPlayer.dll that has the actual unity implementation (low-level rendering, physics, input, etc.... )
When the game runs, the unity mono runtime loads the game's main assembly Assembly-CSharp.dll , the Unity's managed engine assemblies such as , UnityEngine.dll, UnityEngine.CoreModule.dll, UnityEngine.PhysicsModule.dll, .... and then the mono JIT (Just-In-Time) Compiler, converts IL code stored in the managed assemblies into native code (code that can be directly executed by the CPU, assembly) in memory, just before it's being used.
IL2CPP
When using the IL2CPP backend however, things are different.
All of the IL code from the managed DLL assemblies is converted to native C++ source code at build time and then compiled and linked with Unity's native engine.
This process is called AOT (Ahead of time) compilation.
The result, is a fully native executable, without IL or Mono runtime included.
Generally called GameAssembly.dll. This is the file that we are going to patch.
However, you may notice, that the game still contains a mono folder in it's directory.
This is because, even though IL2CPP eliminates Mono JIT runtime and compiles all CIL code into native C++, Unity still reuses parts of the Mono runtime's infrastructure.
- Runtime support libraries
- .NET/Mono class libraries (System.dll (core .net framework), mscorlib.dll (Microsoft Common Object Runtime Library), etc...)
- Interoperability and metadata reading
- Configuration files
The IL2CPP runtime can interpret Mono-style metadata and call into the same BCL (Base class libraries), it still needs the same libraries and metadata layout that the BCL expects.
When the game runs, the IL2CPP runtime initiliazes its environnment:
- Loading the Mono-style configuration files
- Initializing the managed heap and GC (Garbage collector, Sgen mono garbage collector)
- Sets up globalization, locality and I/O based on mono data
The previously generated native machine code executes.
When calls to managed API's are used, they depend on configuration metadata from mono.
#3 Steamworks API client library
The Steamworks API client library DLL or steam_api64.dll serves two purposes
- Steamworks API bridge: Used to talk to the steam client for achievement, stats, friends list, Workshop, etc...
- DRM: Performs authentication between the game process and the running steam client, in other words, verifies ownership
When the game runs, the function SteamAPI_Init() is called.
This function:
- Locates and connects to the running steam client process via Shared memory or IPC (InterProcess Communication, such as File mappings, Pipes or sockets).
- Verifies the App ID (using the steam_appid.txt file or with embedded data)
- Checks the current user owns the game and is logged in
- Initializes the Steamworks services
If validation fails, the function returns false, and the game exits.
In essence, the steam DRM consists of a runtime ownership check.
Some other interesting Steamworks API functions include:
- SteamAPI_Shutdown() : cleans up on exit
- SteamAPI_RunCallbacks() : processes async callbacks from the Steam client. Sync = code blocks until result comes back. Async = code requests something, code continues running, result is delivered later via a callback function. Async is used so that the game doesn't freeze while waiting for steam servers.
- SteamUser() : Give access to individual the API interface for our steam user
- SteamAPI_RestartAppIfNecessary(AppId_t): Ensures the game is launched through Steam; if not, it restarts via the Steam client. If used, should be called before SteamAPI_Init(). Can be considered as an additional DRM feature
This DLL can either be loaded statically by the game at startup using the executable's import table or dynamically with the LoadLibrary*() winapi function. For the latter, the functions are then fetched using the GetProcAddress() winapi function, this makes the analysis a bit longer.
#4 The analysis
Let's open our GameAssembly.dll file and check its contents.
To display its contents in a readable manner we can use DUMPBIN from the MSVC build tools command line.
First, let's check if this DLL statically imports the Steamworks api, let's type C:\>dumpbin /dependents GameAssembly.dll and we get back
Dump of file C:\GameAssembly.dll
File Type: DLL
Image has the following dependencies:
KERNEL32.dll
USER32.dll
ADVAPI32.dll
ole32.dll
OLEAUT32.dll
SHELL32.dll
WS2_32.dll
IPHLPAPI.DLL
baselib.dll
This confirms that the DLL is dynamically loaded into our game executable at runtime.
To confirm this we can check for the strings in the DLL starting with SteamAPI_
To do this, we can use the strings tool, let's type C:\>strings.exe -a GameAssembly.dll | findstr "^SteamAPI_", we get back
SteamAPI_Init
SteamAPI_Shutdown
SteamAPI_RestartAppIfNecessary
SteamAPI_GetHSteamPipe
SteamAPI_GetHSteamUser
...
Great! Those are the functions we talked about earlier (I omitted the other ones).
PS: In case you don't find any match, try using the -u flag instead of -a, they might be stored as unicode strings.
It's now time to analyze the actual native code inside this DLL.
For this, we can use disassembler tool like IDA.
Let's first find the SteamAPI_RestartAppIfNecessary string, remember if used, it should be called before the SteamAPI_Init function, so this is the starting point of our analysis.
We press shift+f12 and search.
We then double click on it to go it's actual location in the DLL.
We see that IDA shows the section where the string is located (.rdata) and has already created a symbol for this address aSteamapiRestar accessible through the whole app.
Let's then press the letter x while selecting the symbol to get the cross references (xref) to this address.
Removing the if condition will lead to the game never restarting even if not launched from steam.
Let's continue our investigation in the code below.
Bingo! We found the code that we wanted!
Now come the modifications
Using basic assembly, we invert the first if condition for SteamAPI_RestartAppIfNecessary(), so that the dll execution goes to the else statement if the executable is not launched from steam. Doing of the opposite of what it was doing originally.
Next we do the same for the SteamAPI_Init() condition
Let's apply the modifications and test things out.
The game runs without quitting even if I log out from my steam account!
You'll notice that steam does still launch after some time, there must be some other code calling the Steamworks API, more digging and modification is needed.
However, as this is an introduction, I won't go further and I'll let you experiment on your own :)
See you on the next post !
Comments
Post a Comment