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.

A full update function on Windows
A full update function on Linux
So, I heard you like compiler differences?

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.

Found the argument passed to the full update function.

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 We'll store this in a variable for convenience.

$ grep -m1 -ioP '[\da-f]+(?=-.+engine_client\.so)' /proc/$(pidof csgo_linux64)/maps

Pro-tip: Starting the game with 'DEBUGGER=gdb' will disable address space layout randomization (ASLR) by default. This means the 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
Calling full update with an unknown argument from gdb. Working as intended.

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.

String search for 'cl_fullupdate' in IDA.

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'.

The callback function for 'cl_fullupdate' in IDA.

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
Calling the cl_fullupdate callback function from gdb.

So now we're on par with Windows. But wait, there's more!

__int64 __fastcall ForceFullUpdate(__int64 a1) {
  if (*(_DWORD *)(a1 + 0x1FC) != -1) {
    *(_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
Invoking a full update by manipulating memory from gdb.

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.

Our cl_fullupdate 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!

GetLocalPlayer in CEngineClient. GetLocalPlayer in the 12th position of the CEngineClient virtual table.

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.

Last updated Friday, 17 January 2020 at 07:16 PM.