Reverse engineering a CS:GO cheating software

TL;DR: Technical low-level analysis of the cheat, also including the licensing and differences between public and private version.

CS:GO is one of the most popular competitive online games, it has 520.285 current players as I write these lines. As in any other competition-driven game, cheaters arise, and specially in the CS community, they have become a serious problem.

Today we are taking a look at the public and private version of a cheat for this game!

I won't mention the name of the cheat to avoid giving them free advertisement and because it's not necessary for this post, but if you're into this topic, you'll probably guess.

Before we start, it's important to mention that I managed to get a private version build using an alternative channel ūüėą. This means I've never paid to the developer, so I didn't support their business in any way! Damn you, cheaters!

Public vs Private version

This cheat is quite accessible, as the developer provides a public (free) version with all the capabilities for the users to try. The most important "downside", is that the public cheat is obviously detected by VAC, so if you use it in a VAC-protected server, it's a matter of time that your account gets VAC-banned.

Here is where the paid private version comes into play: Customers get a unique build that is guaranteed to be undetected.

Licensing

Each private version build of the cheat is tied to a machine, to avoid piracy, reselling, ...

The license procedure gets the SystemDrive environment variable, and using DeviceIoControl with the parameter IOCTL_DISK_GET_DRIVE_GEOMETRY, reads the technical capabilities of the hard drive. Then the Processor Brand String is also read using the cpuid instruction.

This information is formatted into a string, hashed with SHA1, and mutated with a custom ASCII rotation algorithm:

for ( i = 0; i < v16; v16 = strlen((const char *)&sha1_hex) ) {
v18 = *((char *)&sha1_hex + i);
if ( (unsigned int)(v18 - '0') > 9 )
*((_BYTE *)&sha1_hex + i) = v18 + 5;
else
*((_BYTE *)&sha1_hex + i) = v18 + '!';
++i;
}

The resulting string is your unique license, which is sent to the cheat developer when you buy it, and in return you get a build that only works in the computer that generated this license.

How the cheat works

This cheat is an external cheat, which means all the work is done out of the CS:GO process (no DLL injection).

The first thing it does is open the csgo.exe process, and get the base addresses of client.dll and engine.dll.

Then it uses patterns to find game structures (offsets) in the memory, these patterns usually match opcodes of the game binaries, where memory pointers are referenced, or other useful information. They also use patterns to find game functions and strings.

For example, one of the patterns is:

89 0D ? ? ? ? 8B 0D ? ? ? ? 8B F2 8B C1 83 CE 08

If we look for these bytes in the client.dll file, we get the following hit:

 0x102bdf1d 890de815f214 mov dword [0x14f215e8], ecx
0x102bdf23 8b0d5ccaec12 mov ecx, dword [0x12ecca5c]
0x102bdf29 8bf2 mov esi, edx
0x102bdf2b 8bc1 mov eax, ecx
0x102bdf2d 83ce08 or esi, 8

Which means this pattern is looking for one of those global memory references present in the first two disassembly lines.

As we said, they also use patterns to locate game functions, for instance with the following pattern, the cheat locates the start of the function used by the game to execute console commands in-game:

55 8B EC 8B ? ? ? ? ? 81 F9 ? ? ? ? 75 0C A1 ? ? ? ? 35 ? ? ? ? EB 05 8B 01 FF 50 34 50 A1

This one is found in engine.dll:

      0x100aa300 55           push ebp
0x100aa301 8bec mov ebp, esp
0x100aa303 8b0d54345b10 mov ecx, dword [0x105b3454]
0x100aa309 81f938345b10 cmp ecx, 0x105b3438
,=< 0x100aa30f 750c jne 0x100aa31d
| 0x100aa311 a168345b10 mov eax, dword [0x105b3468]
| 0x100aa316 3538345b10 xor eax, 0x105b3438
,==< 0x100aa31b eb05 jmp 0x100aa322
|`-> 0x100aa31d 8b01 mov eax, dword [ecx]
| 0x100aa31f ff5034 call dword [eax + 0x34]
`--> 0x100aa322 50 push eax
0x100aa323 a1f8325a10 mov eax, dword [0x105a32f8]
[...]

If the cheat wants to run an in-game console command, it can allocate memory in the game process, pass the arguments to the function using this memory, and create a new thread using CreateRemoteThread at the beginning of the procedure.

When the cheat has located all it needs to work, it will start a bunch of threads that implement each of the functionalities. These threads are in charge of monitoring and manipulate the game memory using the functions ReadProcessMemory and WriteProcessMemory.

Changing the values of the internal game structures at will, the cheat can achieve the functionalities it offers.

I have identified some of the functions and renamed them in my pseudocode:

CreateThread(0, 0, (LPTHREAD_START_ROUTINE)aimassist, 0, 0, 0);
CreateThread(0, 0, (LPTHREAD_START_ROUTINE)aimlock, 0, 0, 0);
CreateThread(0, 0, (LPTHREAD_START_ROUTINE)bunnyhop, 0, 0, 0);
CreateThread(0, 0, (LPTHREAD_START_ROUTINE)anti_flash, 0, 0, 0);
CreateThread(0, 0, (LPTHREAD_START_ROUTINE)sub_403F0E, 0, 0, 0);
CreateThread(0, 0, (LPTHREAD_START_ROUTINE)esp_hack, 0, 0, 0);
CreateThread(0, 0, (LPTHREAD_START_ROUTINE)radar_hack, 0, 0, 0);
CreateThread(0, 0, (LPTHREAD_START_ROUTINE)kill_message, 0, 0, 0);
while ( !byte_4F1081
|| !byte_4F1054
|| !byte_4F1082
|| !byte_4F10C9
|| !byte_4F1062
|| !byte_4F1040
|| !byte_4F1090
|| !byte_4F1028 )
Sleep(0x64u);
// Default config
cfg_antiflash = 1;
cfg_aimlock = 1;
cfg_killmessage = 1;
cfg_radarhack = 1;
byte_4F1032 = 0;
cfg_glowesp = 1;
byte_4F10C0 = 0;
cfg_bunnyhop = 1;
cfg_aimassist = 1;
cfg_reload();
while ( WaitForSingleObject(csgo_prochandler, 0) != 0 )
cfg_changes_loop();
CloseHandle(csgo_prochandler);
j_exit(0);

Private version protection

The public version is poorly protected, they just encrypted the strings with a simple algorithm but it has no code obfuscation or PE packing.

On the other side, the private version is protected with Themida, a commercial packer that, depending on its configuration, can be quite effective protecting executables.

It's very likely that they use Themida for two purposes:

  1. Protect the cheat license from being patched. The program can be manipulated to validate any license when running in a computer, but reconstruct a fully working version of the packed executable and patch it may be quite tricky.
  2. The second and most important, avoid the VAC signatures from detecting their cheat when running. Themida can protect the original opcodes of the program when it's loaded in memory and running, and writing signatures (patterns) for those opcodes is one of the methods VAC uses to detect cheaters.

Closing

If we compare it to other cheats, this one is simple in terms of functionality, but still quite effective.

Bear in mind that the CSGO binaries used for the analysis are not from the latest game update, as I wrote this one week ago. The binaries I used are:

942fa5d3ef9e0328157b34666327461cee10aae74b26af135b8589dc35a5abc7 client.dll
e6f3eda5877f2584aeb43122a85d0861946c7fb4222d0cb6f3bc30034e4d3e24 engine.dll
1a5bb2b0ae9f2e6ef757c834eeb2c360a59dce274b2e4137706031f629e6455f csgo.exe

This means that the cheat signatures may have been slightly modified to work with the new executables, and the offsets probably won't be the same if these binaries changed in the latest version of the game.