Introduction
In this post, I will explain how I realized that writing 480 in steam_appid.txt would enable us to bypass Family Sharing restrictions.My original goal when reversing Halo was to find a way to play the game with my siblings without buying the game. My first approach was to find a way to start the game successfully without Steam.
Steamworks SDK
One approach to do that would be to look for the first library call that MCC-Win64-Shipping.exe makes in the Steam API module. A way to do that might be to go to Symbols tab in x64dbg, select steam_api64.dll, and break on all the exported functions. I might have done this.The easier way to do this is to download the Steamworks SDK and read the Steamworks SDK documentation. https://partner.steamgames.com/doc/sdk/api/example
After downloading the SDK, go to Main.cpp. There is a function called RealMain with a comment above it that says "Real main entry point for the program".
SteamAPI_RestartAppIfNecessary
The first function that is called is SteamAPI_RestartAppIfNecessary. To find this in x64dbg, go to Symbols tab, select steam_api64.dll, search for the function. We want to break on this function because we want to find the function in MCC-Win64-Shipping.exe that calls this SteamAPI_RestartAppIfNecessary.
Restart the program and continue until we get to the function.
Then look at the callstack to figure out which function in MCC called this library function. It will be the first mcc-win64-shipping function underneath the steam_api64.dll function.
Double-click on the function to follow it in the CPU window. It should look like this picture below.
The instruction test al, al sets the Zero flag if the value in AL is zero. (See here for the reason why: https://en.wikipedia.org/wiki/TEST_(x86_instruction))
Recall that __fastcall convention places the return value in the RAX register. AL is the lower 16-bits of that register. That means the program is testing whether SteamAPI_RestartAppIfNecessary returned 0.
On the line after the test, (where I commented "jmp if returned false"), the je instruction means "jump if equal", and it will jump if the Zero flag is set. (In some disassemblers, the instruction will be disassembled into jz for jump zero instead of je.) This means that if SteamAPI_RestartAppIfNecessary returned 0, then we will take the jump.
Now the question is, do we want to take the jump?
Take the jump?
The instruction directly after the je is executed if we don't take the jump. xor al, al will clear the bits in AL. There is an unconditional jump after that instruction, which takes us to the line right after the line that has the comment "return 1 (success)". These instructions just clean up the stack, so that means if we don't take the jump, then we immediately return 0.
Technically, there is another function that runs just before we return. See this function in IDA by subtracting the address of this function from the base address of MCC-Win64-Shipping.exe. (Get the base address in Symbols tab. It is the entry in the column labeled "Base", which is left of the mcc-win64-shipping.exe entry.)
Copy the offset, which should be 0x234b2b0. In IDA, press g, paste, and click enter. You should get the same function that has a __security_check_cookie label. This is here because Visual C++ compiler will insert checks for buffer overruns if you compile with /GS. This function will not change the result of AL, so we do not need to worry about it.
Back to figuring out whether we want to take the jump.
Since not taking the jump causes us to return 0, the question now is, do we want to return 0.
Return 0?
To find out, go to the call stack to figure out who calls the function that calls SteamAPI_RestartAppIfNecessary and double click to follow. You should get something like the picture below.
We return to a test al, al again, but this time there is a jne instruction after the test. This instruction is "jump if not equal", which is the opposite of je from before. That means if AL is 1 then we will jump because test al, al will not set the Zero flag.
If we do not jump then we will xor ecx, ecx and call the function at 0x7FF7F413D1EC. Click the function and press enter to follow.
This function terminates the process. We don't want to terminate the process. That means we want to take the jump. In order to take the jump, we need to return 1 in the function on the line labeled "the function that calls RealMain".
How to return 1?
Now in order to return 1, we need to execute the line that says mov al, 1 in the function that calls SteamAPI_RestartAppIfNecessary. This is the line highlighted in the picture below.
The red and blue arrows on the left indicate that there are three ways to get here. Set a breakpoint on all three ways. You can try all three ways, but I will tell you that without changing anything, the only jump that does not cause a Fatal Error is the last one.
Jumping from the other two will cause us to crash here with an Exception Access Violation.
SteamInternal_ContextInit Tangent
Notice that before the fatal error, there was a call to SteamInternal_ContextInit. Double click on it and set a breakpoint. Restart the game and follow the first jump that goes to the mov al, 1 instruction. (In x64dbg, on the right side pane with the registers, under RFLAGS, there is a ZF. Double click the 0 next to the ZF to set the Zero flag.) Press continue. The game should start loading and then break at SteamInternal_ContextInit.Stepping over the instructions, we can see that we eventually reach a je instruction that jumps to the exit. From reversing other video games, I would say that we usually want to enter the Critical Sections, so let's not take this jump.
To confirm that we want to enter the Critical Section, if you keep stepping, you will see that we copy rcx + 0x10 into rax. The value at this address is 0, so when we dereference the value at rax, we get an exception because it is dereferencing 0, which is not a valid address.
To save time, I will tell you that if we force execution to not take the jumps, the program will then call SteamInternal_FindOrCreateUserInterface.
Stepping through that, it will take the second je and fail with an error that complains that SteamAPI_Init has not yet succeeded. Notice the test rcx, rcx above that, and the mov rcx, [0x00007FFFC7A73298]. Since we took the jump, that means that the value at 0x00007FFFC7A73298 was 0. The error messages indicate that 0x00007FFFC7A73298 is likely the hardcoded pointer to the Steam interface.
This could be useful in the future. Maybe we want to call Steam interface functions. To figure out the offset, subtract this address from the Base address of steam_api64.dll. You should get 0x43298.
This is probably a dead-end because we cannot easily fake the Steam Interface. But let's not leave any stones unturned.
Fake the SteamAPI?
Run the program again while logged into Steam this time. Continue until we get to SteamInternal_ContextInit.Go to SteamInternal_FindOrCreateUserInterface to get the address of the Steam API. Open up ReclassEx64.exe and paste the address there.
Opening the first pointer in ReClass, we get an array of addresses. On the plus side, these are not runtime addresses. Perhaps we can just give the program the values at 0x00007FFFC7A73298?
I tried this, but I still got the Fatal Error (at a different place) because there was nothing at 7FFFA41AAA08.
Back to MCC
Now if we get to the last je and decide to not take it, you will get a Sign In prompt. If you click No, everything still seems to work as expected. This suggests that the call above the je checks for an active steam user. This also suggests that once you make it to the game lobby, MCC does not seem to really care whether you are logged in to Steam or not.SteamAPI_RestartAppIfNecessary must return 0
The entire point of the tangent was to rule out taking the first two jumps to the mov al, 1 instruction. By now, we understand that we need SteamAPI_RestartAppIfNecessary to return 0. I'll show the section of code again with better comments.
Let's take a look at SteamAPI_RestartAppIfNecessary. Notice the early return at the beginning of the function. There are three arrows going to it.
Pan right to see what happens if we don't early return. It seems that execution looks for the installation path of steam.exe in the registry. If you look farther down, you will see the ShellExecute calls. There are no returns once we take this path, and there is no path to the last return that does not call ShellExecute.
For a better view of the function, you can open steam_api64.dll in IDA, go to Export, and find the function.
At this point, I had included the steam_appid.txt file and wrote the app id for Path of Exile. When I tried executing the binary, it took the early return, but the app exited immediately.
This was when I started reading the Unreal Engine 4 documentation for the Steam Online Subsystem.
Very interesting. I tried 480, and the game launched :)