As I hastily hacked various code together to get Chameleon NG running on Linux I came across a signature scan that needed replacing. Before I knew it, the solution had gone from pretty bad, to decent, and finally, better than its Windows counterpart.
First, a little bit of context. I'm searching for a way to request a full update from the server. Chameleon NG uses this to instantly apply skin changes to a weapon. This was implemented by Emma in the original project and I'm told it was as simple as searching for the 'Requesting full game update' string and following the only reference.
Unfortunately, we already have a problem. The above image on the left shows the function (let's call it ForceFullUpdate) on Windows while the right shows Linux, an extra argument has appeared! Not a problem, but it means we're potentially dealing with two signatures now - one for the function and one for the argument passed to it.
A single jump to cross-reference later and we've found the argument. Now we can call 0x4477A0
and pass the result to ForceFullUpdate. You could make signatures for both functions and call it a day but I've got a few more methods lined up and trust me, it only gets better. It's still worth testing and we don't need to write any code to do it. Check out this gdb magic.
First we'll need the base address of engine_client.so. We'll store this in a variable for convenience.
$ grep -m1 -ioP '[\da-f]+(?=-.+engine_client\.so)' /proc/$(pidof csgo_linux64)/maps 7fffecdd7000
Pro-tip: Starting the game with 'DEBUGGER=gdb' will disable address space layout randomization (ASLR) by default. This means the engine_client.so library among other things will always load at the same address.
# gdb -p $(pidof csgo_linux64) (gdb) set $engine_addr = 0x7fffecdd7000
Recall the addresses of both functions: ForceFullUpdate was 0x3B78A0
while the return value of 0x4477A0
was passed as the argument. These will also be assigned to variables alongside their function signatures so gdb knows how to call them.
(gdb) set $unknown = (int64_t(*)()) $engine_addr+0x4477a0 (gdb) set $fullupdate = (int64_t(*)(int64_t)) $engine_addr+0x3b78a0
Finally, we call the fullupdate function.
(gdb) call $fullupdate($unknown()) $1 = 0 (gdb) continue
That was my first attempt. There's nothing wrong with this method - I've actually seen some projects using it. It's easy to find, easy to invoke and requires no extra classes or structures. I noticed something a little odd with the console output though.
Requesting full game update ()...
The 'reason' for the update is missing when we call the function directly. If you can't see this in your game, make sure you have the developer console variable set to 1. We saw a 'recording demo' string in the disassembly of one of the functions that used ForceFullUpdate so maybe there's another one for cl_fullupdate.
One string search for 'cl_fullupdate' later and we have two results. One used in registering the console command and another used as the reason for requesting the update. Follow 'cl_fullupdate command'.
We'll simply call this function 'cl_fullupdate' since it's the callback for the console command. Now we've brought down the amount of required signatures down to one. Once again, we can quickly test this with gdb.
(gdb) set $fullupdate = (int64_t(*)()) $engine_addr+0x4394C0 (gdb) call $fullupdate() $2 = 0
So now we're on par with Windows. But wait, there's more!
__int64 __fastcall ForceFullUpdate(__int64 a1) {
if (*(_DWORD *)(a1 + 0x1FC) != -1) {
sub_3B7830(a1);
*(_DWORD *)(a1 + 0x1FC) = -1;
return DevMsg("Requesting full game update (%s)...\n");
}
}
This is the decompiled version of the ForceFullUpdate function. It's pretty simple, isn't it? Just like we forced the model index for knives, we can try forcing a1 + 0x1FC to -1 and see if it causes a full update to occur. First, where does a1 come from?
__int64 cl_fullupdate() {
return ForceFullUpdate(sub_447780(0xFFFFFFFF));
}
We don't have to look any further than our cl_fullupdate function. Whatever is being returned from sub_447780 is the key to forcing the update. Once again, gdb comes in handy. Seriously, what can't gdb do?!
(gdb) set $unknown = (int(*)(int)) $engine_addr+0x447780 (gdb) set $update = $unknown(-1) (gdb) set *(int*)($update + 0x1fc) = -1
It works, but I suppose this is no better than our previous method of calling the cl_fullupdate function. Actually, this is probably worse since we need a hard-coded offset and a signature scan. It wouldn't be so bad if we could get rid of the scanning part since the offset is unlikely to change any time soon.
Let us consult the elder wisdom of the Mac binaries with symbols.
Turns out the unknown function at 0x447780
is called GetLocalClient and it's used in several functions in the CEngineClient class. Not only have we used this class in a previous tutorial but we've used one of these functions before too!
Here's the disassembled version of GetLocalPlayer from CEngineClient.
.text:405520 55 push rbp .text:405521 BF FF FF FF FF mov edi, 0FFFFFFFFh .text:405526 48 89 E5 mov rbp, rsp .text:405529 E8 52 22 04 00 call GetLocalClient .text:40552E 5D pop rbp .text:40552F 8B 80 08 02 00 00 mov eax, [rax+208h] .text:405535 83 C0 01 add eax, 1 .text:405538 C3 retn
We're dealing with RIP-relative addressing once again. In order to get the absolute address of GetLocalClient we need to move the instruction pointer 9 bytes into the function to 0x405529
then read the 4 byte relative address after the E8 opcode. I've talked about this before in the ClientMode hooking tutorial so make sure to read that for a slightly better explanation.
0x405529 + 0x42252 + 5 = 0x447780
I'll be using my GetVirtualFunction and GetAbsoluteAddress helper functions to make things a little easier. The following code will assume you've captured the VEngineClient interface into engine.
typedef CBaseClientState* (*GetLocalClient_t) (int);
uintptr_t GetLocalPlayer = GetVirtualFunction<uintptr_t>(engine, 12);
GetLocalClient_t GetLocalClient = reinterpret_cast<GetLocalClient_t>(GetAbsoluteAddress(GetLocalPlayer + 9, 1, 5));
*reinterpret_cast<int*>(uintptr_t(GetLocalClient(-1)) + 0x1FC) = -1;
And just like that, no signatures! This is my favorite way to find those pesky pointers that aren't exported in any interface. Just take a look through the Source SDK 2013 code or the Mac binaries and see what you can find.
Need some suggestions? Try finding IClientMode from HudProcessInput, CGlobalVarsBase from HudUpdate and CInput from IN_ActivateMouse using the same method. Good luck and have fun!
Oh, and just in case you thought I was missing another method..
engine->ClientCmd_Unrestricted("record x;stop");
Don't even think about it.