Hand-made cheat: Looking through walls and aiming automatically in a 3D shooter

Date: 22/01/2025

Today you will learn how to write a cheat for an online shooter. This cheat will implement such hacks as extrasensory perception (ESP) and aimbot. ESP displays player information above their heads (e.g. player’s health, name, or current weapon); while aimbot automatically aims your weapons at other players.

Game selection

My game of choice is AssaultCube, a free multiplayer first-person shooter based on the CUBE engine. Also, it uses the OpenGL graphics library.

warning

The use of cheats violates the user agreement and may result in legal prosecution. The cheat discussed in this article was created for educational purposes only. Neither the author, nor the Editorial Board can be held liable for potential consequences arising from the use and distribution of such software.

Searching for values

First, let’s run the game and select the windowed mode in its settings (since you need to see other stuff on the screen as well).

Windowed mode
Windowed mode

Also, you have to create a local server and configure it (namely, set the playing time so that the timer does not rush you). The path to the config is as follows:

C:\Program Files (x86)\AssaultCube 1.3.0.2\config\maprot.cfg
Default time settings
Default time settings

Set the maximum time: 100 minutes.

New time settings
New time settings

Then start the server…

Starting the server
Starting the server

…and connect to it.

Connecting to the server
Connecting to the server

Run Cheat Engine and attach it to the game process.

Attaching to the game process
Attaching to the game process

Searching for the health variable

For testing purposes, you’ll need a second player. You can join the game from another device or, as I did, from a virtual PC. To find the health variable, set scanning parameters in Cheat Engine and use the second player to deal some damage to the first player. After that, you can start searching for HP in Cheat Engine.

Some damage inflicted
Some damage inflicted

Continue dealing damage until you find the address where your player’s HP are stored.

HP address
HP address

Your goal is to implement extrasensory perception and aimbot. To do this, you have to find out the player class and static address. To find out the class, right-click on your address and select “Find out what writes to this address” (or press F6).

Searching for the location from where data are written to this address
Searching for the location from where data are written to this address

A new window pops-up: it will show opcodes that write data to the above address. To make them appear, use the second player to inflict damage to the first player again.

Instruction found!
Instruction found!

Only one instruction writes data to the health variable; it’s located at offset 0xEC. Accordingly, the value stored in the edx register is the address of the player class (although it’s a heap address, not a static one). So, let’s add it manually.

The structure
The structure

Static address of the player object

Cheat Engine can be used to find out the static address. Right-click on the previously added address 0x6AED20 and select “Pointer scan for this address.”

Running a pointer scan for the found address
Running a pointer scan for the found address

There are plenty of pointer scan parameters, but you are interested in Max level. Its value determines how many times your pointer (i.e. static address) will be dereferenced. And it will help you to find the desired address.

Default pointer scan settings
Default pointer scan settings

The executable file of the game is small and contains not much code. This indicates that the game features not many classes and structures, and the number of offsets won’t be large. Therefore, let’s limit the search depth to one.

New pointer scan settings
New pointer scan settings

The scan results are shown below. You are interested in addresses displayed in the format "ac_client.exe"+YYYYYYYY; while addresses in the format "THREADSTACKX"-YYYYYYYY indicate that the desired address was found on the stack.

Pointer scan results
Pointer scan results

Let’s add the five found addresses to the list.

Added pointers
Added pointers

And set an offset to the health variable for each address.

Added pointer to HP
Added pointer to HP

Now restart the game, and Cheat Engine will suggest to save your addresses. Follow this advice so that you can load them when you attach again.

Saving the cheat table
Saving the cheat table

After reattaching to the game, you see that only two addresses on the list point to the health variable: "ac_client.exe"+0017E0A8 and "ac_client.exe"+0018AC00.

After the restart
After the restart

Let’s take some damage again and see what happens to other addresses. As can be seen, the health variable appears in two more addresses. This means only the two above-mentioned addresses should be left on the list.

Taking damage again
Taking damage again

Add the static address "ac_client.exe"+0017E0A8 under the name Player_ptr_1

Adding first pointer
Adding first pointer

…and the static address "ac_client.exe"+0018AC00, under the name Player_ptr_2.

Adding second pointer
Adding second pointer

Player class

Let’s assume that the static address Player_ptr_1 is what you are looking for (in fact, this is not true; the correct static address is Player_ptr_2, but you’ll find this out later). To view the player class in memory, right-click on the address and select “Browse this memory region” (or press Ctrl-B).

Browsing memory region
Browsing memory region

Good news is that Cheat Engine includes a tool making it possible to visualize memory structures instead of examining bytes in the dump. Right-click on the selected byte and select “Open in dissect data/structure.”

Open in dissect data/structure
Open in dissect data/structure

A new window with the address of the selected byte will open. Press “Structures” and then “Define new structure” (or Ctrl-N).

Creating a structure
Creating a structure

Let’s call this structure Player and leave the default remaining settings (i.e. Guessed Field Type and Structure Size (4096)).

Creating a player class
Creating a player class

Since you left the “Guess Field Types” box checked, Cheat Engine has successfully performed this task.

Resulting structure
Resulting structure

Now go to offset 0xEC to make sure that your player’s health is located there.

Checking the class
Checking the class

Also, you can find the player’s name at offset 0x205.

Player
Player’s name

You’ll need this class in the future; however, Cheat Engine doesn’t allow to export such structures, while doing it manually would be a nightmare. Fortunately, there is a ready-made tool called ReClass.NET. It enables you to directly export this structure in the form of C++ code. So, you download it, install, and attach to the game process.

Attaching to the process
Attaching to the process

After attaching to the process, a class is created at base address 400000; it’s name is N0000004E. Double-click on the address and specify the one you need: 0066ED48. In a similar way, you change the class name to Player and add 4096 more bytes to be displayed.

In this context, Cheat Engine treats the displayed number of bytes as the class you are exporting. If the size of your class is larger than the standard number of displayed bytes, then you have to increase this number manually.

Configuring ReClass.NET
Configuring ReClass.NET

Now go to offset 0xEC (i.e. to the health variable). Change the field type value to DWORD (you have to set values of types for offsets to be able to export the class with required fields).

Changing field type
Changing field type

Searching for coordinates

Now you have to find values ​​required to implement ESP and aimbot. For ESP, you need coordinates of the player and player’s head in three-dimensional space.

To implement aimbot, you need pitch, yaw, and roll values. You have no idea about such things, right? No problem, I will explain them using the Godot game engine as an example.

XYZ coordinates
XYZ coordinates

Depending on the engine, coordinates can be presented in different formats. For instance, directions of some axes and the axis taken as height often differ.

Variations in coordinates
Variations in coordinates

For demonstration purposes, let’s take a ready-made character on GitHub and see how it moves when you change its coordinates.

Character at zero point
Character at zero point

Pitch represents character movements along the X axis. The lower arrow shows the actual character movement at the present moment; while the upper one represents the alternative movement option.

Pitch
Pitch

Yaw represents character’s movements along the Y axis.

Yaw
Yaw

Roll represents character’s movements along the Z axis.

Roll
Roll

Searching for player coordinates

Now that you are armed with new knowledge, let’s go back to the game and the Cheat Engine window. Start moving around the map and concurrently move the mouse to and from, as well as up and down. In the Cheat Engine window, you can see three sequences of floating point values that change during your actions. This means that these sequences represent player coordinates, coordinates of the player’s head, and its rotation relative to the axes.

You can notice that two sets of values, out of the three, repeat each other in terms of X and Y; while their Z coordinates are different. This indicates that the first set represents player coordinates; while the second one, player’s head coordinates. Since OpenGL uses the Z coordinate as height, let’s try to change height for each set. After an attempt to change Z from -0.5 to another value in the first set, it returns to its initial value. This means that these are the head coordinates; the second set represents player coordinates; while the third set represents movements relative to the axes. But you still have to make certain of this.

Incorrect value selected
Incorrect value selected

Now let’s perform the same operations for the second set.

Correct selection
Correct selection

For demonstration purposes, let’s stand on a box and look at the value at offset 0x30 in Cheat Engine.

Taking a higher position
Taking a higher position

If you try to change this value, you’ll see that the character falls through the box.

Player
Player’s Z coordinate changed

This means that your assumption is correct.

Your coordinates
Your coordinates

Let’s go back to the ReClass.NET window and select types for these offsets.

Renaming offsets and assigning types to them
Renaming offsets and assigning types to them

Searching for pitch, yaw, and roll values

Let’s perform the same check again. Move the mouse to and from and then up and down. Now try to change values, and it turns out that your assumptions were correct.

Pitch and yaw values
Pitch and yaw values

Go back to the ReClass.NET window and select types for these offsets.

Renaming offsets and assigning types to them
Renaming offsets and assigning types to them

Now your class looks as shown below. But what is VTable? Actually, this field isn’t required, and I added it as an eye candy. For more information, see the article Understanding Virtual Tables in C++.

Your class
Your class

Now let’s export this class (right-click on it and select the respective menu item). As you can see, there are some fields that you didn’t look for: armor, team, etc. If you want, you can find them yourself.

Exporting the class
Exporting the class

Entity list

Done with your player, but what about other players? Normally, there should be a long list of all game entities, including player characters. It can also be assumed that the game code includes a loop that iterates through the players, and the game logic somehow interacts with their data. Let’s use the health variable as the basis and check what opcodes request it.

Access to the address where health is located
Access to the address where health is located

At first glance, it’s difficult to find something, but a trained eye can notice that access to your player’s health goes via some static address. Furthermore, it turns out that the address you have selected is wrong! The correct one is: "ac_client.exe"+0018AC00.

Correct static address
Correct static address

Now let’s search based on player’s name located at offset 0x205

Player
Player’s name

…and you get the list of instructions that access player’s name.

Adding the address
Adding the address
Opcodes that access this address
Opcodes that access this address

Iterate through the possible variants, and you’ll find one where access occurs in a loop.

Entity loop
Entity loop

If you are familiar with the assembly language, you understand that the ebx register contains the address of the first element; esi is the index; while the multiplier after the index indicates the data type: in this particular case, it’s 4 (DWORD).

Accordingly, the static address of the entity list is:

"ac_client.exe"+0018AC04
Entity list
Entity list

To verify this, you can check the other player’s name at offset 0x205.

Other player
Other player’s name

You also need the total number of players. Browse through the disassembler output, and you’ll see a check at the end of the loop. The edi register stores the current number of players.

Number of players
Number of players

After some debugging, you can see that the static address for the number of players is located above the loop: "ac_client.exe"+0018AC0C.

Static address of the player list
Static address of the player list

Let’s restart the game and check whether these addresses are correct.

Searching for view matrix

The view matrix is required for ESP to work correctly (for more information, see the OpenGL tutorial.

Sequence of transformations
Sequence of transformations
  1. Local coordinates are coordinates of an object measured relative to the point where the object is located;
  2. At the next step, local coordinates are converted to world space coordinates. They are plotted from some point that is universal for all other objects located in world space;
  3. Next, world coordinates are transformed into view space coordinates so that each vertex looks as if viewed from the camera or from the observer’s perspective;
  4. Once the coordinates are converted to view space, they are projected into clip coordinates. Clip coordinates range from -1.0 to 1.0 and determine which vertices appear on the screen; and 
  5. Finally, a process called viewport transformation is used to transform clip coordinates ranging from -1.0 to 1.0 into the area of screen coordinates defined by the glViewport function.

To search for the view matrix, use clip coordinates that vary in the range from -1.0 to 1.0 as the basis. Look straight up in the game and set search parameters in CE as shown below.

Looking up
Looking up

Now look straight down and set new search parameters. Alternate these steps until you get a sufficient number of results. In this particular case, it doesn’t take that many iterations to find two static addresses. Let’s verify them.

Looking down
Looking down

Open the first address in the dump (for convenience, change the type of displayed data from bytes to float.

Changing display type
Changing display type

Now it looks as shown below.

New displayed output type
New displayed output type

Now you have to point the weapon up and down and check how the values ​​change. You can see that something has changed in the area marked in red. Interestingly, 1.0 and -1.0 occur only in the selected area; while the world matrix and the transformation matrix, respectively, are located prior to it (based on the size of these matrices). Therefore, the static address of the view matrix is "ac_client.exe"+17DFD0.

View matrix
View matrix

Don’t forget to restart the game and double-check whether your findings are correct.

Writing cheat

Since you are writing an internal cheat, you need not only the library, but also a special tool to inject it into the game process. This injector will retrieve the list of processes, find the game process, allocate memory in it, and write your internal cheat there. And then it will create a remote thread inside the game to execute the cheat code.

Injector

The injector code is as follows.

#include <windows.h>
#include <tlhelp32.h>
// Name of injected DLL
const char* dll_path = "internal_cheat_ac.dll";
int main(void) {
HANDLE process;
void* alloc_base_addr;
HMODULE kernel32_base;
LPTHREAD_START_ROUTINE LoadLibraryA_addr;
HANDLE thread;
HANDLE snapshot = 0;
PROCESSENTRY32 pe32 = { 0 };
DWORD exitCode = 0;
pe32.dwSize = sizeof(PROCESSENTRY32);
// Make snapshot of current processes
snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
Process32First(snapshot, &pe32);
do {
// You will interact only with the AC process
if (wcscmp(pe32.szExeFile, L"ac_client.exe") == 0) {
// First, you need process handle to use it in subsequent calls
process = OpenProcess(PROCESS_ALL_ACCESS, true, pe32.th32ProcessID);
// To avoid memory corruption, allocate additional memory to store the path to your DLL
alloc_base_addr = VirtualAllocEx(process, NULL, strlen(dll_path) + 1, MEM_COMMIT, PAGE_READWRITE);
// Write path to your DLL into memory that was just allocated inside the game
WriteProcessMemory(process, alloc_base_addr, dll_path, strlen(dll_path) + 1, NULL);
// Create a remote thread inside the game; it will execute LoadLibraryA
// Full path to your DLL injected into the game is passed to this LoadLibraryA call
kernel32_base = GetModuleHandle(L"kernel32.dll");
LoadLibraryA_addr = (LPTHREAD_START_ROUTINE)GetProcAddress(kernel32_base, "LoadLibraryA");
thread = CreateRemoteThread(process, NULL, 0, LoadLibraryA_addr, alloc_base_addr, 0, NULL);
// To make sure that your DLL has been successfully injected, the following two calls can be used for synchronization
WaitForSingleObject(thread, INFINITE);
GetExitCodeThread(thread, &exitCode);
// Finally, the process memory and handles are released
VirtualFreeEx(process, alloc_base_addr, 0, MEM_RELEASE);
CloseHandle(thread);
CloseHandle(process);
break;
}
// Iterate processes contained in the snapshot
} while (Process32Next(snapshot, &pe32));
return 0;
}

DLL

Your library will consist of the following modules:

  • main module – dllmain.cpp;
  • in-game offsets and game structures module – structures.h;
  • hook module – hook.cpp and hook.h; and 
  • drawing module – gl_draw.cpp and gl_draw.h.

Main module

When your library is loaded using the LoadLibraryA function, a thread is created, and the main logic of your cheat will operate within this thread, including:

  • getting pointers to fields you need;
  • setting a hook to the scene rendering function;
  • a loop making it possible to enable and disable the hacks and exit the cheat;
  • a function that draws the cheat menu; and 
  • ESP and aimbot hacks.

Let’s begin!

include <iostream>
#include <string>
#include <tchar.h>
#include <thread>
#include <mutex>
#define _USE_MATH_DEFINES
#include <math.h>
#include "hook.h"
#include "structures.h"
#include "gl_draw.h"
// Player variable
Player* player;
// Synchronization variable
std::mutex hook_mutex;
// Typedef for the wglSwapBuffers function
typedef BOOL(__stdcall* twglSwapBuffers) (HDC hDc);
Tramp_Hook* esp_hook;
Player** player_list = nullptr;
int* player_list_size = nullptr;
static int list_size = 0;
static Vector3 screen_position;
static Vector3 world_position;
static Vector3 player_position;
static float* view_matrix;
bool aimbot_enabled = false;
// Menu
void draw_menu(bool flag_esp) {
std::string esp = "ESP is ";
std::string aimbot = "Aimbot is ";
if (flag_esp) {
esp += "ON press F1 to OFF";
}
else {
esp += "OFF press F1 to ON";
}
if (aimbot_enabled) {
aimbot += "ON press F2 to OFF";
}
else {
aimbot += "OFF press F2 to ON";
}
GL::print_gl(50, 1200, rgb::gray, esp.c_str());
GL::print_gl(50, 1300, rgb::gray, aimbot.c_str());
GL::print_gl(50, 1400, rgb::gray, "Exit cheat press F3");
}
// Your function called by the hook you've set
BOOL _stdcall hooked_wglSwapBuffers(HDC hDc) {
hook_mutex.lock();
draw_menu(esp_hook->is_enabled());
if (esp_hook->is_enabled()) {
// Set up orthographic regime
GL::setup_orthographic();
if (*player_list_size == list_size) {
for (int i = 0; i < list_size; ++i) {
// Check whether player address is valid
if (!player_list[i]) {
continue;
}
if (player_list[i]->hp > 0 && player_list[i]->hp < 200) {
// Save positions of other players in Vector3
world_position.x = player_list[i]->x_y_z_player.x;
world_position.y = player_list[i]->x_y_z_player.y;
world_position.z = player_list[i]->x_y_z_player.z;
// Save positions of other players' heads in Vector3
player_position.x = player->x_y_z_head.x;
player_position.y = player->x_y_z_head.y;
player_position.z = player->x_y_z_head.z;
// Calculate distance to other player
float distance = sqrtf((player_position.x - world_position.x) * (player_position.x - world_position.x) + (player_position.y - world_position.y) * (player_position.y - world_position.y) + (player_position.z - world_position.z) * (player_position.z - world_position.z));
// Check whether the player is visible
if (distance > 5.0f && GL::world_to_screen(world_position, screen_position, view_matrix)) {
// Check whether the other player is a member of your team
if (player_list[i]->team != player->team) {
// Opposing team
GL::draw_esp_box(screen_position.x, screen_position.y, distance, rgb::red, player_list[i]->name, player_list[i]->hp, player_list[i]->armor);
}
else {
// Your team
GL::draw_esp_box(screen_position.x, screen_position.y, distance, rgb::green, player_list[i]->name, player_list[i]->hp, player_list[i]->armor);
}
}
}
}
}
else {
list_size = *player_list_size;
}
GL::restore_gl(); // Restore initial regime
}
if (aimbot_enabled && GetAsyncKeyState(VK_RBUTTON)) {
if (*player_list_size == list_size) {
// These variables will be used to hold the closest enemy at gunpoint
float closest_player = -1.0f;
float closest_yaw = 0.0f;
float closest_pitch = 0.0f;
// Select closest enemy as target
for (int i = 0; i < list_size; ++i) {
// Check whether the player's address is valid; skip players from your team; and check HP
if (!player_list[i] || (player_list[i]->team == player->team) || (player_list[i]->hp < 0)) {
continue;
}
// Calculate the enemy's absolute position away from you (to ensure that your future calculations are correct and based on the initial zero point)
float abspos_x = player_list[i]->x_y_z_player.x - player->x_y_z_player.x;
float abspos_y = player_list[i]->x_y_z_player.y - player->x_y_z_player.y;
float abspos_z = player_list[i]->x_y_z_player.z - player->x_y_z_player.z;
// Calculate distance to the enemy
float temp_distance = sqrtf((abspos_x * abspos_x) + (abspos_y * abspos_y));
// If this is the closest enemy, calculate yaw and pitch to aim at it
if (closest_player == -1.0f || temp_distance < closest_player) {
closest_player = temp_distance;
// Calculate yaw
float azimuth_xy = atan2f(abspos_y, abspos_x);
// Convert to degrees
float yaw = (float)(azimuth_xy * (180.0 / M_PI));
// Add 90 since the game believes that direct north is equal to 90 degrees
closest_yaw = yaw + 90;
// Calculate pitch
// Since Z varies is a very narrow range, select a higher value between X and Y to make sure you're not looking directly at the sky when you're close to the enemy
if (abspos_y < 0) {
abspos_y *= -1;
}
if (abspos_y < 5) {
if (abspos_x < 0) {
abspos_x *= -1;
}
abspos_y = abspos_x;
}
float azimuth_z = atan2f(abspos_z, abspos_y);
// Convert the value to degrees
closest_pitch = (float)(azimuth_z * (180.0 / M_PI));
}
}
// When your loop ends, set yaw and pitch to the closest values
player->yaw_pitch_roll.x = closest_yaw;
player->yaw_pitch_roll.y = closest_pitch;
}
else {
list_size = *player_list_size;
}
}
// Call hooked function
BOOL ret_value = ((twglSwapBuffers)esp_hook->get_gateway())(hDc);
hook_mutex.unlock();
return ret_value;
}
DWORD WINAPI injected_thread(HMODULE hMod) {
// Get address at which .exe was loaded inside your game process
uintptr_t moduleBase = (uintptr_t)GetModuleHandle(0);
// Address of pointer to your player
player = *reinterpret_cast<Player**>(moduleBase + player_offset);
// Pointer to player list
player_list = *reinterpret_cast<Player***>(moduleBase + entity_offset);
// Pointer to player list size
player_list_size = reinterpret_cast<int*>(moduleBase + entity_count_offset);
// Pointer to view matrix
view_matrix = reinterpret_cast<float*>(moduleBase + view_matrix_offset);
// Get handle to OpenGL module
HMODULE open_gl = GetModuleHandleA("opengl32.dll");
if (!open_gl) {
return -1; // OpenGL not loaded
}
// Get address of wglSwapBuffers
void* orig_wglSwapBuffers = GetProcAddress(open_gl, "wglSwapBuffers");
// Set hook to wglSwapBuffers
esp_hook = new Tramp_Hook(orig_wglSwapBuffers, hooked_wglSwapBuffers, 5);
while (!GetAsyncKeyState(VK_F3)) {
if (GetAsyncKeyState(VK_F1) & 1) { // Enable ESP
esp_hook->is_enabled() ? esp_hook->disable() : esp_hook->enable();
}
if (GetAsyncKeyState(VK_F2) & 1) { // Enable aimbot
aimbot_enabled = !aimbot_enabled;
}
Sleep(50);
}
hook_mutex.lock();
// Delete hook
delete esp_hook;
hook_mutex.unlock();
// Release your library
FreeLibraryAndExitThread(hMod, 0);
return 0;
}
BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved) {
switch (ul_reason_for_call) {
case DLL_PROCESS_ATTACH:
CreateThread(0, 0, (LPTHREAD_START_ROUTINE)injected_thread, hModule, 0, 0);
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
case DLL_PROCESS_DETACH:
break;
}
return TRUE;
}

In-game offsets and game structures module

This module contains all the required player fields, as well as offsets.

#pragma once
#include "gl_draw.h"
class Player {
public:
DWORD* vftable; // 0x00
Vector3 x_y_z_head; // 0x04
BYTE pad_0010[24]; // 0x10
Vector3 x_y_z_player; // 0x28
Vector3 yaw_pitch_roll; // 0x34
BYTE pad_0040[172]; // 0x40
DWORD hp; // 0xEC
DWORD armor; // 0xF0
BYTE pad_00F4[273]; // 0xF4
BYTE name[16]; // 0x205
BYTE pad_0215[247]; // 0x215
BYTE team; // 0x30C
};
DWORD player_offset = 0x018AC00;
DWORD entity_offset = 0x018AC04;
DWORD entity_count_offset = 0x018AC0C;
DWORD view_matrix_offset = 0x17DFD0;

Hook module

The purpose of this module is to hook the wglSwapBuffers function responsible for scene rendering. It’s required to draw your menu and ESP boxes.

hook.h
#pragma once
#include <windows.h>
#include <memory>
// Hook class
class Hook {
// Pointer to hook
void* this_to_hook;
// Saved old opcodes
std::unique_ptr<char[]> old_opcodes;
// Length of overwritten instructions
int this_len;
// Hook enabled?
bool enabled;
public:
// Constructor for hook
Hook(void* to_hook, void* our_func, int len);
// Destructor to restore initial code
~Hook();
// Enable hook
void enable();
// Disable hook
void disable();
// Hook enabled?
bool is_enabled();
};
// Class that implements inline hook
class Tramp_Hook {
void* gateway;
Hook* managed_hook;
public:
Tramp_Hook(void* to_hook, void* our_func, int len);
// Restores initial code
~Tramp_Hook();
void enable();
void disable();
bool is_enabled();
void* get_gateway();
};
hook.cpp
#include "hook.h"
Hook::Hook(void* to_hook, void* our_func, int len) : this_to_hook{to_hook }, old_opcodes{ nullptr }, this_len{ len }, enabled{ false } {
// Jmp instruction is 5 bytes in size. Memory area you are overwriting must be at least this size
if (len < 5) {
return;
}
DWORD curr_protection;
// Make memory that stores code you want to overwrite writable
VirtualProtect(to_hook, len, PAGE_EXECUTE_READWRITE, &curr_protection);
// Save current bytes to character array
old_opcodes = std::make_unique<char[]>(len);
if (old_opcodes != nullptr) {
for (int i = 0; i < len; ++i) {
old_opcodes[i] = ((char*)to_hook)[i];
}
}
// Overwrite the area you want to hook with nop instructions
memset(to_hook, 0x90, len);
// Calculate relative address for the jump
DWORD rva_addr = ((DWORD)our_func - (DWORD)to_hook) - 5;
// Write opcode for the jmp instruction
*(BYTE*)to_hook = 0xE9;
// Write address to jump to
*(DWORD*)((DWORD)to_hook + 1) = rva_addr;
// Restore old code protection
VirtualProtect(to_hook, len, curr_protection, &curr_protection);
}
Hook::~Hook() {
if (old_opcodes != nullptr) {
DWORD curr_protection;
// Make memory writable
VirtualProtect(this_to_hook, this_len, PAGE_EXECUTE_READWRITE, &curr_protection);
// Write old opcodes back to the hooked location
for (int i = 0; i < this_len; ++i) {
((char*)this_to_hook)[i] = Hook::old_opcodes[i];
}
// Restore old memory protection
VirtualProtect(this_to_hook, this_len, curr_protection, &curr_protection);
}
}
void Hook::enable() {
this->enabled = true;
}
void Hook::disable() {
this->enabled = false;
}
bool Hook::is_enabled() {
return enabled;
}
Tramp_Hook::Tramp_Hook(void* to_hook, void* our_func, int len) : gateway{ nullptr }, managed_hook{ nullptr } {
// jmp is 5 bytes in size
if (len < 5) {
return;
}
// Allocate memory for your trampoline
gateway = VirtualAlloc(0, len + 5, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
// Save bytes that will be overwritten in the trampoline
memcpy_s(gateway, len, to_hook, len);
// Get return address
uintptr_t ret_addr = (BYTE*)to_hook - (BYTE*)gateway - 5;
// Put jmp opcode at the end of the trampoline
*(BYTE*)((uintptr_t)gateway + len) = 0xE9;
// Put return address after jmp
*(uintptr_t*)((uintptr_t)gateway + len + 1) = ret_addr;
// Create hook
managed_hook = new Hook(to_hook, our_func, len);
}
Tramp_Hook::~Tramp_Hook() {
managed_hook->disable();
delete managed_hook;
VirtualFree(gateway, 0, MEM_RELEASE);
}
void Tramp_Hook::enable() {
managed_hook->enable();
}
void Tramp_Hook::disable() {
managed_hook->disable();
}
bool Tramp_Hook::is_enabled() {
return managed_hook->is_enabled();
}
void* Tramp_Hook::get_gateway() {
return gateway;
}

Drawing module

This module contains functions that draw the cheat menu and ESP boxes.

gl_draw.h
#pragma once
#pragma comment(lib, "OpenGL32.lib")
#include <windows.h>
#include<gl/GL.h>
struct Vector3 {
float x, y, z;
};
struct Vector4 {
float x, y, z, w;
};
// Namespace for colors used in drawing
namespace rgb {
const GLubyte red[3] = { 255,0,0 };
const GLubyte green[3] = { 0,255,0 };
const GLubyte blue[3] = { 0,0,255 };
const GLubyte gray[3] = { 55,55,55 };
const GLubyte light_gray[3] = { 192,192,192 };
const GLubyte yellow[3] = { 255, 255, 0 };
const GLubyte black[3] = { 0,0,0 };
}
// Namespace for functions used to draw cheat menu and ESP hack
namespace GL {
void setup_orthographic();
void restore_gl();
void build_font();
void draw_filled_rectangle(float x, float y, float width, float height, const GLubyte color[3]);
void draw_out_line(float x, float y, float width, float height, float line_width, const GLubyte color[3]);
void draw_line(float fromX, float fromY, float toX, float toY, float line_width, const GLubyte color[3]);
void draw_esp_box(float pos_x, float pos_y, float distance, const GLubyte color[3], const BYTE* text, const int health_percent = -1, const int armor_percent = -1);
void print_gl(float x, float y, const GLubyte color[3], const char* fmt, ...);
bool world_to_screen(Vector3 pos, Vector3& screen, float matrix[16]);
}
gl_draw.cpp
#include "gl_draw.h"
#include <corecrt_math.h>
#include <stdio.h>
HDC h_DC;
HFONT h_old_font;
HFONT h_font;
UINT font_base;
bool b_font_build = 0;
void GL::setup_orthographic() {
// Save attributes
glPushAttrib(GL_ALL_ATTRIB_BITS);
// Save view matrix
glPushMatrix();
// Screen dimensions
GLint view_port[4];
// Get screen dimensions
glGetIntegerv(GL_VIEWPORT, view_port);
// Set screen dimensions
glViewport(0, 0, view_port[2], view_port[3]);
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
// Set up orthographic regime
glOrtho(0, view_port[2], view_port[3], 0, -1, 1);
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();
// Disable GL depth test
glDisable(GL_DEPTH_TEST);
}
void GL::restore_gl() {
// Restore view matrix
glPopMatrix();
// Restore all attributes
glPopAttrib();
}
void GL::draw_filled_rectangle(float x, float y, float width, float height, const GLubyte color[3]) {
glColor3ub(color[0], color[1], color[2]);
glBegin(GL_QUADS);
glVertex2f(x, y);
glVertex2f(x + width, y);
glVertex2f(x + width, y + height);
glVertex2f(x, y + height);
glEnd();
}
void GL::draw_out_line(float x, float y, float width, float height, float line_width, const GLubyte color[3]) {
glLineWidth(line_width);
glBegin(GL_LINE_STRIP);
glColor3ub(color[0], color[1], color[2]);
glVertex2f(x - 0.5f, y - 0.5f);
glVertex2f(x + width + 0.5f, y - 0.5f);
glVertex2f(x + width + 0.5f, y + height + 0.5f);
glVertex2f(x - 0.5f, y + height + 0.5f);
glVertex2f(x - 0.5f, y - 0.5f);
glEnd();
}
void GL::draw_line(float fromX, float fromY, float toX, float toY, float line_width, const GLubyte color[3]) {
glLineWidth(line_width);
glBegin(GL_LINES);
glColor3ub(color[0], color[1], color[2]);
glVertex2f(fromX, fromY);
glVertex2f(toX, toY);
glEnd();
}
void GL::build_font()
{
h_DC = wglGetCurrentDC();
font_base = glGenLists(96);
h_font = CreateFont(-12, 0, 0, 0, FW_NORMAL, FALSE, FALSE, FALSE, ANSI_CHARSET, OUT_TT_PRECIS, CLIP_DEFAULT_PRECIS, PROOF_QUALITY, FF_DONTCARE | DEFAULT_PITCH, L"Courier");
h_old_font = (HFONT)SelectObject(h_DC, h_font);
wglUseFontBitmaps(h_DC, 32, 96, font_base);
SelectObject(h_DC, h_old_font);
DeleteObject(h_font);
b_font_build = true;
}
void GL::print_gl(float x, float y, const GLubyte color[3], const char* fmt, ...)
{
if (!b_font_build) {
GL::build_font();
}
if (fmt == NULL) {
return;
}
glColor3f(color[0], color[1], color[2]);
glRasterPos2i(x, y);
char text[256];
va_list ap;
va_start(ap, fmt);
vsprintf(text, fmt, ap);
va_end(ap);
glPushAttrib(GL_LIST_BIT);
glListBase(font_base - 32);
glCallLists(strlen(text), GL_UNSIGNED_BYTE, text);
glPopAttrib();
}
void GL::draw_esp_box(float pos_x, float pos_y, float distance, const GLubyte color[3], const BYTE* text, const int health, const int armor) {
float line_width = 0.5f; // Line width
GLint view_port[4];
glGetIntegerv(GL_VIEWPORT, view_port);
float height = (view_port[3] / distance) * 3; // Box height
float width = (view_port[2] / distance); // Box width
// Snap lines
GL::draw_line(view_port[2] / 2.0f, (float)view_port[3], pos_x, pos_y, line_width + 2.0f, rgb::black);
GL::draw_line(view_port[2] / 2.0f, (float)view_port[3], pos_x, pos_y, line_width, color);
// Shape
GL::draw_out_line(pos_x - (width / 2), pos_y - height, width, height, line_width + 2.0f, rgb::black);
GL::draw_out_line(pos_x - (width / 2), pos_y - height, width, height, line_width, color);
// Health
if (health != -1) {
float perc = (width / 100);
float curr = perc * health;
GL::draw_filled_rectangle(pos_x - (width / 2) - 1, ((pos_y - (height / 10)) - 1) - height, width + 2, (height / 15) + 2, rgb::black);
GL::draw_filled_rectangle(pos_x - (width / 2), (pos_y - (height / 10)) - height, width, height / 15, rgb::light_gray);
GLubyte Hcolor[3]{ static_cast<GLubyte>(255 - (2.5f * health)), static_cast<GLubyte>(health * 2.5f), 0 };
GL::draw_filled_rectangle(pos_x - (width / 2), (pos_y - (height / 10)) - height, curr, height / 15, Hcolor);
}
// Armor
if (armor != -1) {
float perc = (width / 100);
float curr = perc * armor;
GL::draw_filled_rectangle(pos_x - (width / 2) - 1, ((pos_y - (height / 5)) - 1) - height, width + 2, (height / 15) + 2, rgb::black);
GL::draw_filled_rectangle(pos_x - (width / 2), (pos_y - (height / 5)) - height, width, height / 15, rgb::light_gray);
GL::draw_filled_rectangle(pos_x - (width / 2), (pos_y - (height / 5)) - height, curr, height / 15, rgb::blue);
}
// Name
GL::print_gl(pos_x - (width / 2), (pos_y - (height / 4)) - height, rgb::yellow,(char *)text);
}
bool GL::world_to_screen(Vector3 pos, Vector3& screen, float matrix[16]) {
// Get screen height and weight
GLint view_port[4];
glGetIntegerv(GL_VIEWPORT, view_port);
int window_width = view_port[2];
int window_height = view_port[3];
// Matrix-vector result multiplying world (eye) coordinates by projection matrix (clip_coords)
Vector4 clip_coords;
clip_coords.x = pos.x * matrix[0] + pos.y * matrix[4] + pos.z * matrix[8] + matrix[12];
clip_coords.y = pos.x * matrix[1] + pos.y * matrix[5] + pos.z * matrix[9] + matrix[13];
clip_coords.z = pos.x * matrix[2] + pos.y * matrix[6] + pos.z * matrix[10] + matrix[14];
clip_coords.w = pos.x * matrix[3] + pos.y * matrix[7] + pos.z * matrix[11] + matrix[15];
// If coordinates not on the screen
if (clip_coords.w < 0.1f) {
return false;
}
// Perspective division, division by clip.W (i.e. normalized device coordinates)
Vector3 NDC;
NDC.x = clip_coords.x / clip_coords.w;
NDC.y = clip_coords.y / clip_coords.w;
NDC.z = clip_coords.z / clip_coords.w;
// Convert to screen coordinates
screen.x = (window_width / 2 * NDC.x) + (NDC.x + window_width / 2);
screen.y = -(window_height / 2 * NDC.y) + (NDC.y + window_height / 2);
return true;
}

Functional test

Time to run the cheat.

Cheat menu
Cheat menu

To enable the ESP hack, press F1; to disable it, press F1 again.

ESP functional test
ESP functional test

To enable aimbot, press F2; to activate it, hold down the right mouse button. To disable the hack, press F2 again.

Aimbot functional test
Aimbot functional test

Conclusions

Congrats! You’ve learned that Cheat Engine can be used not only to search for player class fields, but also for the player class per se, for the list of players, their number, and view matrix. You have also implemented basic hacks suitable for any online shooter. Your cheat is almost perfect. To become fully functional, it must be able to bypass anticheats. Next time, I will explain how to do this.


Leave a Reply

XHTML: You can use these tags: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>