Hi there! The last two days I had a lot of fun hacking a game called X Rebirth. The task seemed simple: I wanted to see my current in game money on e.g. my tablet. Another use case would be to use excel to analyze the changes of my in game money or draw diagrams based on the money.
So, how can one access the in game money? One could use the in game scripting engine and write debug logs (german thread in the official game forum). This was the way I used in previous X-games. Although that was my first approach, I think I had more trouble getting into the game scripting engine / modding, than …
- write a DLL which dumps numbers into a file
- inject it into the game
- hook the money-setter to call the DLL function
and, honestly, this approach was a lot more fun!
Step 0: Discarded approaches
- Use the in game lua scripting engine, run it periodically and write the money value to a file (debug/os/io libraries are unfortunately not available)
- Follow a chain of pointers to find the current in game money and read it (I did not find a valid pointer chain, the address of the money changed each start of the game)
Step 1: Find a good location for the hook
Step 2: Write a DLL which dumps numbers into a file and inject it
I opened Visual Studio and wrote a little DLL with one function, which is called fnTestDll
. The function takes exactly one parameter (unsigned int money
) and writes it to a file. Note: I had some trouble injecting the DLL into the game. The reason was, that I compiled it as 32-bit DLL, while the game is 64-bit compiled. I know, I know … but hey, as soon as I realized and fixed it, it was injectable via Cheat Engine.
#include "stdafx.h"
#include <stdio.h>
#include <stddef.h>
#include <stdlib.h>
#include <wchar.h>
__declspec(dllexport) void __fastcall fnTestDll(unsigned int money)
{
FILE* file;
// yeah, I know, not exactly best practices ... but it does work ;-)
fopen_s(&file, "C:\\Users\\trampi\\Desktop\\temp\\temp_rebirth.txt", "w+");
fprintf(file, "%u", money);
fclose(file);
return;
}
BOOL APIENTRY DllMain(HMODULE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
case DLL_PROCESS_DETACH:
break;
}
return TRUE;
}
Step 3: Rewrite the instructions to call the dump function
So, I injected the DLL into the game. Now all I needed to do was to rewrite the mov [rdi+00000108],rbx
to call my function after setting the value. Cheat Engine had here again some convenience functionality, which overrides a given instruction with a jump to a nearby Code Cave, in which you can place instructions as you like. I selected the mov
-instruction and choose Tools -> Auto Assemble. In the now opened code window, I selected the template Code injection.
After this, I lost some time. I can now enter code to call my DLL. But how do I do this? Cheat Engine can import symbols from other places, (e.g. my DLL), but how do I call the function correctly? Some googling and a question in the Cheat Engine-Forum later I read about the x64 Microsoft calling conventions. The for me relevant part is:
First 4 parameters – RCX, RDX, R8, R9. Others passed on stack.
What I instead tried at this point was PUSHing the argument on the stack, in the hope that the compiled DLL would POP the arguments from the stack. If I remember correctly, that was how it was done in MMIX. I crashed the game like 60 times at that point, because I had no idea how to call my DLL. Instead of looking it up in the first place, I tried to understand the compiled DLLs instructions. Silly me.
The following code is the complete auto assemble script necessary:
[ENABLE] // my modified instructions
// assert that the instruction we overwrite is the mov mentioned earlier
assert(XRebirth.xmlInitializeDict+A809A, 48 8B 5C 24 30)
alloc(newmem,2048,XRebirth.xmlInitializeDict+A8093)
label(returnhere)
loadlibrary(C:\Users\trampi\Desktop\XRebirthWriteToFile\x64\Debug\XRebirthWriteToFile.dll)
newmem: //this is allocated memory, you have read,write,execute access
mov [rdi+00000108],rbx // execute the original mov
mov rcx,rbx // the x64 Microsoft calling conventions state
// that the first parameter shall be stored in rcx
// if it is an integer or a pointer
call XRebirthWriteToFile.fnTestDll // the debug print function
jmp returnhere // return to the normal program flow
XRebirth.xmlInitializeDict+A8093: // overwrite the original instruction
jmp newmem // jump to newmem
nop // the original mov was 7 bytes long. The jmp instruction above is
nop // 5 bytes long. so we need to nop to get the right alignment of the
// following code
returnhere: // this points right after the original instruction
[DISABLE] // the original instructions
XRebirth.xmlInitializeDict+A8093:
mov [rdi+00000108],rbx // the original code
The above script can be enabled and disabled in Cheat Engine on the fly.
It is done. It works. It does export the money in the given file whenever my money changes. And: the game does not crash anymore!
Step 4, bonus: A little node.js web-app to display it on my tablet
npm install express-ws express
app.js:
var express = require('express');
var app = express();
var expressWs = require('express-ws')(app);
var fs = require('fs');
app.use(express.static('static'));
var server = app.listen(3000, function () {
var host = server.address().address;
var port = server.address().port;
console.log('Example app listening at http://%s:%s', host, port);
});
app.ws('/echo', function(ws, req) {
});
var listener = expressWs.getWss('/echo');
var watchedFileName = "../temp_rebirth.txt";
var oldMoneyValue = null;
setInterval(function() {
fs.readFile(watchedFileName, {encoding: "utf8"}, function(err, newMoneyValue) {
if (oldMoneyValue != newMoneyValue) {
oldMoneyValue = newMoneyValue;
listener.clients.forEach(function(client) {
client.send(newMoneyValue);
});
}
});
}, 500);
static/index.html:
// ... snip ...
var ws = new WebSocket("ws://192.168.178.100:3000/echo");
ws.onmessage = function(message) {
$("#credits-container").html("<span id='credits'>" + convertToThousandSeparated(message.data) + "</span>");
}
// ... snip ...
Run it with node app.js
.
Summary
I have learned a lot from this. This was my first time with assembler for a real CPU, my first DLL and the first time I have called a DLL. Finding and patching the addresses was also a lot of fun.
One of the most valuable experience from this little project was to never give up. At some points I almost gave up. But I did not - I left my comfort zone to learn some new amazing things.
Questions? Suggestions? Comments? I would love to hear from you!