a.k.a how to get started writing an aimbot in CS:GO.
CUserCmd
is a class that holds player buttons, viewangles, position, and other movement specific data. These user commands are stored in a circular buffer, where they are then sent to the server. In Counter Strike: Global Offensive however, these data structures have changed as well as some intrinsics. This circular buffer is part of the CInput class, which we can get the global instance of by reversing CHLClient->IN_ActivateMouse()
. The first instruction in this virtual function references a pointer to an instance of CInput, and we can get this pointer by doing the following:
void *pClient = 0xDEADBEEF; //you should already have this pointer
DWORD **pClientVtable = *(DWORD ***)(*pClient); //client+0x0 contains the vtable pointer
DWORD *pIN_ACTIVATEMOUSE = pClientVtable[16]; //16th vfunc
CInput *pgInput = *(CInput **)(pIN_ACTIVATEMOUSE + 0x2);
Once we get this pointer, we can disassemble pgInput->GetUserCmd(int, int)
. (The function now takes 2 parameters, rather than one, but after taking a look at it you should just pass 0
or 1
into the function). After dumping the virtual table in IDA, we see that GetUserCmd
is now at vtable index 8
. We can get the pointer to the function like above, then disassemble it in IDA (or OllyDbg).
Here is my disassembly with some renamed variables as well as some annotations/comments:
From this we can see first of all, the size of the CUserCmd structure has increased, from 64 bytes in Source07 to 100 bytes in this engine. We can also see that the size of the circular buffer increased from 90
to 150
(MULTIPLAYER_BACKUP
).
We can also see the location where the circular buffer is stored, but I’ll explain that after this segment:
.text:102480C0 ; int __thiscall CInput__GetUserCmd(void *this, int nSlot, signed int sequence_number)
.text:102480C0 CInput__GetUserCmd proc near ; DATA XREF: .rdata:105F2C74o
.text:102480C0 ; .rdata:1061F4B4o
.text:102480C0
.text:102480C0 nSlot = dword ptr 8
.text:102480C0 sequence_number = dword ptr 0Ch
.text:102480C0
.text:102480C0 push ebp
.text:102480C1 mov ebp, esp
.text:102480C3 mov eax, [ebp+nSlot]
.text:102480C6 push esi
.text:102480C7 cmp eax, 0FFFFFFFFh ; CMP -1
.text:102480CA jnz short loc_102480D1
.text:102480CC lea esi, [ecx+38h]
.text:102480CF jmp short loc_102480DB
.text:102480D1 ; ---------------------------------------------------------------------------
.text:102480D1
.text:102480D1 loc_102480D1: ; CODE XREF: CInput__GetUserCmd+Aj
.text:102480D1 imul eax, 0D4h
.text:102480D7 lea esi, [eax+ecx+38h]
.text:102480DB
.text:102480DB loc_102480DB: ; CODE XREF: CInput__GetUserCmd+Fj
.text:102480DB mov ecx, [ebp+sequence_number]
.text:102480DE mov eax, 1B4E81B5h
.text:102480E3 imul ecx
.text:102480E5 sar edx, 4
.text:102480E8 mov eax, edx
.text:102480EA shr eax, 1Fh
.text:102480ED add edx, eax
.text:102480EF imul edx, 96h ; MULTIPLAYER_BACKUP = 150 decimal
.text:102480F5 mov eax, ecx
.text:102480F7 sub eax, edx
.text:102480F9 imul eax, 64h ; sizeof(CUserCmd) = 100
.text:102480FC add eax, [esi+0ACh]
.text:10248102 xor edx, edx
.text:10248104 cmp [eax+4], ecx
.text:10248107 pop esi
.text:10248108 setnz dl
.text:1024810B dec edx
.text:1024810C and eax, edx
.text:1024810E pop ebp
.text:1024810F retn 8
.text:1024810F CInput__GetUserCmd endp
Looking at this disassembly here is the equivalent C++ code I have reversed (compacted by adding a few constants together)
CUserCmd* GetUserCmd(void *thisptr, int nSlot, int sequence_number)
{
CUserCmd *pCircularBuffer = 0;
if (nSlot == -1)
pCircularBuffer = (CUserCmd *)((char *)thisptr + 0xE4);
else
pCircularBuffer = (CUserCmd *)((char *)thisptr + (nSlot * 212) + 0xE4);
return &pCircularBuffer[sequence_number % 150];
}
As you can see, the circular buffer is stored at gInput + 0xE4
and has a size of 150
.
Now by hooking CreateMove, you can alter the viewangles of your player, your buttons, and anything else. But there’s a catch: You can’t just directly modify the command now, since Valve inserted a verification system for these commands. We can deduce a CVerifiedUserCmd
structure as follows:
class CInput
{
//...
CUserCmd *pCircBuf;
CVerifiedUserCmd *pCircBuf2;
//...
};
Since we reversed that pCircBuf
was at the 0xE4
offset, we can deduce that pCircBuf2
is at 0xE8
. Now we can easily modify the values by modifying the command, copying it to the CVerifiedUserCmd
buffer and recalculating the CRC32 value.