Official site anti-cheat Ultra Core Protector

Home Download F.A.Q. Addons Monitor Forum Support Advertise English version site UCP Anti-Cheat  Russian version site UCP Anti-Cheat
Ultra Core Protector - is the client-server anti-cheat freeware, for server protection from unscrupulous players.

Abilities Supported games  
Half-Life
Condition Zero
Counter-Strike 1.6
Day of Defeat
Adrenaline Gamer
Team Fortress Classic
Counter-Strike Source
MU Online
Ragnarok Online
Half-Life 2 Deathmatch
Adrenaline Gamer 2
Team Fortress 2
[Linux] Journey of finding bSendPacket on the stack

This is more of an informational post on how to get things done in a similar situation and where I failed. I am releasing the process, because the process allows you to learn, not the expected result. If you just came here for a TL;DR it’s right here:

*(unsigned int*)(**(unsigned long***)rbp - 0x1) = 0;

So first we need to understand what we are dealing with. Let’s take a look at an old aged source code of an Aimtux fork. Here’s what’s inside hooker.cpp:

void Hooker::FindSendPacket()
{
#ifdef __linux__
uintptr_t bool_address = PatternFinder::FindPatternInModule("engine_client.so", (char*) BSENDPACKET_SIGNATURE);
bool_address = GetAbsoluteAddress(bool_address, 2, 1);

bSendPacket = reinterpret_cast(bool_address);
Util::ProtectAddr(bSendPacket, PROT_READ | PROT_WRITE | PROT_EXEC);
DebugPrint("bSendPacket", (void*)bSendPacket);
#endif
}

And the sig is in hooker.h:

#define BSENDPACKET_SIGNATURE "41 BD 01 ? ? ? E9 2A FE"

Okay, so we are dealing with a not really good thing – .text bytepatching. This basically asks Valve to give you a VAC ban. If you search for this pattern in IDA, you will end up on this line inside CL_Move:

img

This makes it evident of what the current way does. It modifies the value that bSendPacket gets set to. You can also take a look at the 2007 SDK to get an idea of what bSendPacket is intended for. I did not really know the reason why it existed in the first place before doing all of this. But wait, what else does CL_Move do? It also calls CreateMove, after setting bSendPacket:

img2

So now we know exactly what happens: bSendPacket gets set, CreateMove gets called and then, if bSendPacket is not equal to 0, the move is sent, else, the current command gets stored to be sent out later on. Here comes the second catch of the original method: the change is not instant. When you set bSendPacket to whatever value you set it to, it is going to take a full tick until the value becomes effective.

So, on windows people have 2 other options, 1) Grab the offset from the stack and 2) Naked hook CreateMove, read bSendPacket from a register and pass it over to your hook. The second one not only already has too much of asm, but while I am not sure if Clang supports it, but GCC will definitely only support naked attribute on x86 platform in the upcoming GCC 8. So I will stick to the option number one.

Here comes the first real problem, I have never done this, nor I have a broad understanding how things work on a very low level.

So, let’s start from my dumbest try – LLDB attach to csgo, set a breakpoint inside our CreateMove, check whats the value of bSendPacket, and start printing the offset from the frame pointer (register RBP) from 0x1 to 0x5f and make educated guesses on which offset might be right. Fun huh? Not really, this will never lead to anywhere. And sure it didn’t.

Okay, so another try was to make my cheat print the values of each offset while flipping bSendPacket. This lead me to some really interesting results. So at first, I would check for value equality to bSendPacket, and I got these offsets that there valid when it was flipping between 0 and 1: 0x8 0x18 0x30, okay, so none of these worked, lets try to check for inequality, and the offsets are 0x8 0x18 0x30 (LMAO what?!). Of course we gotta do something else.

So, lets take a deeper look on our stuff. Stepping out of our hook (finish command in LLDB) leads us somewhere (CreateMove inheritance chain) inside client_client.so, finish the second time to land somewhere on client_client.so as well. Finish out of that and we land inside engine_client.so, and if we disassemble the function inside LLDB we sure land right after CreateMove call. Okay, if we scroll up to the place where bSendPacket is set (match it with IDA) we see that it is stored inside register r13d. I just set a breakpoint on the instruction to use later.

Okay, so skipping 20 minutes of wasted time, I finally got back on the right track. I set the bSendPacket register to a random number (like 420):

p $r13d = 420
(unsigned int) $63 = 420

And then I would break on the cheat’s CreateMove hook and then print the offsets from *$rbp to check which one is correct. And, none of the offsets from 0x0 to 0x5f (or maybe more) was correct. So I got back to check what I did before. Remember that I had to exit out of 2 client_client.so subroutines to get back to CL_Move? So this is what I did, I tried to triple dereference rbp and then try the offsets again. Okay, it didn’t work, let’s try the double dereference, and sure enough, offset 0x1 was correct! First try! Kind of.

p $r13d = 360420
(unsigned int) $220 = 360420
(lldb) c
Process 29824 resuming
Process 29824 stopped
* thread #1, name = 'csgo_linux64', stop reason = breakpoint 1.1
frame #0: 0x00007fff849f79e0 libNyctum.so`Hooks::CreateMove(thisptr=0x00007fffdf655220, flInputSampleTime=0.015625, cmd=0x00007fffca3f6938) at CreateMove.cpp:18
15
16 bool OWin(__stdcall) Hooks::CreateMove(OLin(void* thisptr COMMA) float flInputSampleTime, CUserCmd* cmd)
17 {
-> 18 clientModeVMT->GetOriginalMethod(LinWin(25, 24))(LinWin(thisptr, clientMode), flInputSampleTime, cmd);
19 if (!initialized) {
20 for (int i = 0; i < 30; i++) 21 timeArr[i] = 5.0f; (lldb) p *(unsigned int*)(**(unsigned long***)$rbp - 0x1) (unsigned int) $221 = 360420

Just convert it to the code, grabbing rbp the same way people grab ebp on Windows, and you are done! This was it. This might help you in a similar situation, I don't know. But this was how I got bSendPacket from the stack.

Anyways, it's good to be back on the Linux side after a short 2 months break.