Reverse engineering CUserCmd in Counter Strike: Global Offensive

October 03, 2012

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 nSlot           = dword ptr  8
.text:102480C0 sequence_number = dword ptr  0Ch
.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 loc_102480D1:                           ; CODE XREF: CInput__GetUserCmd+Aj
.text:102480D1                 imul    eax, 0D4h
.text:102480D7                 lea     esi, [eax+ecx+38h]
.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);
        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.