Creating Aimbot for Half-Life 2

In this post, we will walk you through the process of creating an aimbot, a program that automatically aims at enemies in a first-person shooter (FPS) game. We will create an aimbot for the game

Half Life 2

running on the engine

Source

. Aimbot will work inside the game process and use reverse engineered internal functions of the game for its work (unlike other systems that work outside the game and scan the screen).

To begin with, let’s study Source SDK and use it as a guide to reverse-engineer the Half-Life 2 executable. We’ll then use the resulting data to create a bot. By the end of the article, we will have written an aimbot that binds the player’s sight to the nearest enemy.

▍ Reverse engineering and Source SDK

Let’s look at how to reverse engineer an executable to find the information we need. To create code that will automatically aim at a target, you need to know the following:

  • The position of the player’s eyes
  • The position of the nearest enemy’s eyes
  • The vector from the player’s eye to the enemy’s eye (obtained from the two previous points)
  • A way to change the position of the player’s camera so that the player’s eye looks along the vector towards the enemy’s eye

For everything except the third point, it is required to receive information from the state of the running game. This information is obtained by reverse engineering the game to find the classes containing the corresponding fields. This usually proves to be an extremely time consuming task with a lot of trial and error; however, in this case, we have access to

Source SDK

which we use as a guide.

Let’s start the search by finding references to the position of the eyes in the repository. After examining several pages of search results, we will reach a huge class CBaseEntity. Inside this class there are two functions:

virtual Vector EyePosition(void);
virtual const QAngle &EyeAngles(void);

Because

CBaseEntity

is the base class from which everything is derived

entities

(entity) of the game, and it contains members for the eye position and camera angles, it looks like this is what we need to work with. Next, we need to see where these functions are referenced from. Searching the GitHub Source SDK a bit again, we find the interface

IServerTools

which has some very promising features:

virtual IServerEntity *GetIServerEntity(IClientEntity *pClientEntity) = 0;
virtual bool SnapPlayerToPosition(const Vector &org, const QAngle &ang, IClientEntity *pClientPlayer = NULL) = 0;
virtual bool GetPlayerPosition(Vector &org, QAngle &ang, IClientEntity  *pClientPlayer = NULL) = 0;

// ...

virtual CBaseEntity *FirstEntity(void) = 0;
virtual CBaseEntity *NextEntity(CBaseEntity *pEntity) = 0;

What’s great about this interface is that it provides access to the local player’s position, which allows you to snap the player to a different position and viewpoint, and also provides the ability to iteratively traverse entities. In addition, its instance is created globally and is tied to a hard-coded line in the code.

#define VSERVERTOOLS_INTERFACE_VERSION_1	"VSERVERTOOLS001"
#define VSERVERTOOLS_INTERFACE_VERSION_2	"VSERVERTOOLS002"
#define VSERVERTOOLS_INTERFACE_VERSION		"VSERVERTOOLS003"
#define VSERVERTOOLS_INTERFACE_VERSION_INT	3

// ...

EXPOSE_SINGLE_INTERFACE_GLOBALVAR(CServerTools, IServerTools001, VSERVERTOOLS_INTERFACE_VERSION_1, g_ServerTools);
EXPOSE_SINGLE_INTERFACE_GLOBALVAR(CServerTools, IServerTools, VSERVERTOOLS_INTERFACE_VERSION, g_ServerTools);

We can start developing aimbot by looking for this class in memory. Launching Half-Life 2 and connecting to it

debugger

look for string references to

VSERVERTOOLS

.

We see where they are referenced from:

7BCAB090 | 68 88FA1F7C              | push server.7C1FFA88                    | 7C1FFA88:"VSERVERTOOLS001"
7BCAB095 | 68 00C4087C              | push server.7C08C400                    |
7BCAB09A | B9 B02A337C              | mov ecx,server.7C332AB0                 |
7BCAB09F | E8 8CCA3F00              | call server.7C0A7B30                    |
7BCAB0A4 | C3                       | ret                                     |
7BCAB0A5 | CC                       | int3                                    |
7BCAB0A6 | CC                       | int3                                    |
7BCAB0A7 | CC                       | int3                                    |
7BCAB0A8 | CC                       | int3                                    |
7BCAB0A9 | CC                       | int3                                    |
7BCAB0AA | CC                       | int3                                    |
7BCAB0AB | CC                       | int3                                    |
7BCAB0AC | CC                       | int3                                    |
7BCAB0AD | CC                       | int3                                    |
7BCAB0AE | CC                       | int3                                    |
7BCAB0AF | CC                       | int3                                    |
7BCAB0B0 | 68 98FA1F7C              | push server.7C1FFA98                    | 7C1FFA98:"VSERVERTOOLS002"
7BCAB0B5 | 68 00C4087C              | push server.7C08C400                    |
7BCAB0BA | B9 BC2A337C              | mov ecx,server.7C332ABC                 |
7BCAB0BF | E8 6CCA3F00              | call server.7C0A7B30                    |
7BCAB0C4 | C3                       | ret                                     |

The assembly listing shows that the member function

server.7C0A7B30

called in

server.7C332AB0

and

server.7C332ABC

. This function takes two arguments, one of which is the string name of the interface. After studying the debugger, it becomes clear that the second parameter is a static instance of something.

Looking at what the macro does in the code

EXPOSE_SINGLE_INTERFACE_GLOBALVAR

it becomes clearer that this is a singleton

CServerTools

The provided as a global interface. Knowing this, we can easily get a pointer to this singleton at runtime: we simply take the address of this pseudo-function that moves the pointer to

EAX

, and call it directly. To do this, we can write the following generic code, which we will continue to use for new uses of other functions:

template <typename T>
T GetFunctionPointer(const std::string moduleName, const DWORD_PTR offset) {

    auto moduleBaseAddress{ GetModuleHandleA(moduleName.c_str()) };
    if (moduleBaseAddress == nullptr) {
        std::cerr << "Could not get base address of " << moduleName
            << std::endl;
        std::abort();
    }
    return reinterpret_cast<T>(
        reinterpret_cast<DWORD_PTR>(moduleBaseAddress) + offset);
}

IServerTools* GetServerTools() {

    constexpr auto globalServerToolsOffset{ 0x3FC400 };
    static GetServerToolsFnc getServerToolsFnc{ GetFunctionPointer<GetServerToolsFnc>(
        "server.dll", globalServerToolsOffset) };

    return getServerToolsFnc();
}

Here we take the base address that is loaded

server.dll

add an offset to get to where the singleton can be accessed from

CServerTools

, and return it as a pointer to the calling function. Thanks to this, we will be able to call the functions we need in the interface and the game will react accordingly. We are interested in two functions:

GetPlayerPosition

and

SnapPlayerToPosition

.

Inside GetPlayerPosition by calling UTIL_GetLocalPlayer the class of the local player is obtained, and also called EyePosition and EyeAngles; inside SnapPlayerToPosition with help SnapEyeAngles the player’s viewing angles are adjusted. Together, this gives us what we need to get the positions and view angles of entities, so that we can perform the appropriate calculations for a new vector and view angle that bind to the eyes of enemies.

Let’s take it in order, let’s start with GetPlayerPosition. Since we can get a pointer to IServerTools and we have an interface definition, we can explicitly call GetPlayerPosition and step through the call using the debugger. This will take us here:

7C08BEF0 | 55                       | push ebp                                |
7C08BEF1 | 8BEC                     | mov ebp,esp                             |
7C08BEF3 | 8B01                     | mov eax,dword ptr ds:[ecx]              |
7C08BEF5 | 83EC 0C                  | sub esp,C                               |
7C08BEF8 | 56                       | push esi                                |
7C08BEF9 | FF75 10                  | push dword ptr ss:[ebp+10]              |
7C08BEFC | FF50 04                  | call dword ptr ds:[eax+4]               |
7C08BEFF | 8BF0                     | mov esi,eax                             |
7C08BF01 | 85F6                     | test esi,esi                            |
7C08BF03 | 75 14                    | jne server.7C08BF19                     |
7C08BF05 | E8 E616E7FF              | call server.7BEFD5F0                    |
7C08BF0A | 8BF0                     | mov esi,eax                             |
7C08BF0C | 85F6                     | test esi,esi                            |
7C08BF0E | 75 09                    | jne server.7C08BF19                     |
7C08BF10 | 32C0                     | xor al,al                               |
7C08BF12 | 5E                       | pop esi                                 |
7C08BF13 | 8BE5                     | mov esp,ebp                             |
7C08BF15 | 5D                       | pop ebp                                 |
7C08BF16 | C2 0C00                  | ret C                                   |
7C08BF19 | 8B06                     | mov eax,dword ptr ds:[esi]              |
7C08BF1B | 8D4D F4                  | lea ecx,dword ptr ss:[ebp-C]            |
7C08BF1E | 51                       | push ecx                                |
7C08BF1F | 8BCE                     | mov ecx,esi                             |
7C08BF21 | FF90 08020000            | call dword ptr ds:[eax+208]             |
7C08BF27 | 8B4D 08                  | mov ecx,dword ptr ss:[ebp+8]            |
7C08BF2A | D900                     | fld st(0),dword ptr ds:[eax]            |
7C08BF2C | D919                     | fstp dword ptr ds:[ecx],st(0)           |
7C08BF2E | D940 04                  | fld st(0),dword ptr ds:[eax+4]          |
7C08BF31 | D959 04                  | fstp dword ptr ds:[ecx+4],st(0)         |
7C08BF34 | D940 08                  | fld st(0),dword ptr ds:[eax+8]          |
7C08BF37 | 8B06                     | mov eax,dword ptr ds:[esi]              |
7C08BF39 | D959 08                  | fstp dword ptr ds:[ecx+8],st(0)         |
7C08BF3C | 8BCE                     | mov ecx,esi                             |
7C08BF3E | FF90 0C020000            | call dword ptr ds:[eax+20C]             |
7C08BF44 | 8B4D 0C                  | mov ecx,dword ptr ss:[ebp+C]            |
7C08BF47 | 5E                       | pop esi                                 |
7C08BF48 | D900                     | fld st(0),dword ptr ds:[eax]            |
7C08BF4A | D919                     | fstp dword ptr ds:[ecx],st(0)           |
7C08BF4C | D940 04                  | fld st(0),dword ptr ds:[eax+4]          |
7C08BF4F | D959 04                  | fstp dword ptr ds:[ecx+4],st(0)         |
7C08BF52 | D940 08                  | fld st(0),dword ptr ds:[eax+8]          |
7C08BF55 | B0 01                    | mov al,1                                |
7C08BF57 | D959 08                  | fstp dword ptr ds:[ecx+8],st(0)         |
7C08BF5A | 8BE5                     | mov esp,ebp                             |
7C08BF5C | 5D                       | pop ebp                                 |
7C08BF5D | C2 0C00                  | ret C                                   |

It’s quite a long time to understand this, but in the form of a control flow graph, everything looks quite simple:

If we compare the disassembled program line by line with the code, then we will quickly find what we need. The code calls the function

UTIL_GetLocalPlayer

only when the passed parameter

pClientEntity

equals

null

. This logic is tested in the block of the first function of the graph. If a valid client entity exists, the code proceeds to get the position and eye angles for it, and otherwise gets the local player entity. This call occurs when executing the command

call server.7BEFD5F0

by the address

server.7C08BF05

. As before, we can create a function pointer to

UTIL_GetLocalPlayer

and call it directly.

CBasePlayer* GetLocalPlayer() {

    constexpr auto globalGetLocalPlayerOffset{ 0x26D5F0 };
    static GetLocalPlayerFnc getLocalPlayerFnc{ GetFunctionPointer<GetLocalPlayerFnc>(
        "server.dll", globalGetLocalPlayerOffset) };

    return getLocalPlayerFnc();
}

Function calls come next in the disassembled code

EyePosition

and

EyeAngles

. We are only interested in getting the positions of the eye, so only the first call is important. To get the address of a function, we can step through the call until we call the address located in [

EAX+0x208

]. After executing this command, we will switch to

server.dll+0x119D00

thus knowing where the function is located.

Vector GetEyePosition(CBaseEntity* entity) {
    
    constexpr auto globalGetEyePositionOffset{ 0x119D00 };
    static GetEyePositionFnc getEyePositionFnc{ GetFunctionPointer<GetEyePositionFnc>(
        "server.dll", globalGetEyePositionOffset) };

    return getEyePositionFnc(entity);
}

And that’s all we need from

GetPlayerPosition

; we now have the ability to get the player’s local entity pointer and get the entity’s eye position. The last thing we need is the ability to set the player’s viewing angle. As mentioned above, this can be done by calling the function

SnapPlayerToPosition

and looking where the function is

SnapEyeAngles

. Disassembled code

SnapEyeAngles

as follows:

7C08C360 | 55                       | push ebp                                |
7C08C361 | 8BEC                     | mov ebp,esp                             |
7C08C363 | 8B01                     | mov eax,dword ptr ds:[ecx]              |
7C08C365 | 83EC 0C                  | sub esp,C                               |
7C08C368 | 56                       | push esi                                |
7C08C369 | FF75 10                  | push dword ptr ss:[ebp+10]              |
7C08C36C | FF50 04                  | call dword ptr ds:[eax+4]               |
7C08C36F | 8BF0                     | mov esi,eax                             |
7C08C371 | 85F6                     | test esi,esi                            |
7C08C373 | 75 14                    | jne server.7C08C389                     |
7C08C375 | E8 7612E7FF              | call server.7BEFD5F0                    |
7C08C37A | 8BF0                     | mov esi,eax                             |
7C08C37C | 85F6                     | test esi,esi                            |
7C08C37E | 75 09                    | jne server.7C08C389                     |
7C08C380 | 32C0                     | xor al,al                               |
7C08C382 | 5E                       | pop esi                                 |
7C08C383 | 8BE5                     | mov esp,ebp                             |
7C08C385 | 5D                       | pop ebp                                 |
7C08C386 | C2 0C00                  | ret C                                   |
7C08C389 | 8B06                     | mov eax,dword ptr ds:[esi]              |
7C08C38B | 8BCE                     | mov ecx,esi                             |
7C08C38D | FF90 24020000            | call dword ptr ds:[eax+224]             |
7C08C393 | 8B4D 08                  | mov ecx,dword ptr ss:[ebp+8]            |
7C08C396 | F3:0F1001                | movss xmm0,dword ptr ds:[ecx]           |
7C08C39A | F3:0F5C00                | subss xmm0,dword ptr ds:[eax]           |
7C08C39E | F3:0F1145 F4             | movss dword ptr ss:[ebp-C],xmm0         |
7C08C3A3 | F3:0F1041 04             | movss xmm0,dword ptr ds:[ecx+4]         |
7C08C3A8 | F3:0F5C40 04             | subss xmm0,dword ptr ds:[eax+4]         |
7C08C3AD | F3:0F1145 F8             | movss dword ptr ss:[ebp-8],xmm0         |
7C08C3B2 | F3:0F1041 08             | movss xmm0,dword ptr ds:[ecx+8]         |
7C08C3B7 | 8BCE                     | mov ecx,esi                             |
7C08C3B9 | F3:0F5C40 08             | subss xmm0,dword ptr ds:[eax+8]         |
7C08C3BE | 8D45 F4                  | lea eax,dword ptr ss:[ebp-C]            |
7C08C3C1 | 50                       | push eax                                |
7C08C3C2 | F3:0F1145 FC             | movss dword ptr ss:[ebp-4],xmm0         |
7C08C3C7 | E8 14CFD0FF              | call server.7BD992E0                    |
7C08C3CC | FF75 0C                  | push dword ptr ss:[ebp+C]               |
7C08C3CF | 8BCE                     | mov ecx,esi                             |
7C08C3D1 | E8 4A0FE0FF              | call server.7BE8D320                    |
7C08C3D6 | 8B06                     | mov eax,dword ptr ds:[esi]              |
7C08C3D8 | 8BCE                     | mov ecx,esi                             |
7C08C3DA | 6A FF                    | push FFFFFFFF                           |
7C08C3DC | 6A 00                    | push 0                                  |
7C08C3DE | FF90 88000000            | call dword ptr ds:[eax+88]              |
7C08C3E4 | B0 01                    | mov al,1                                |
7C08C3E6 | 5E                       | pop esi                                 |
7C08C3E7 | 8BE5                     | mov esp,ebp                             |
7C08C3E9 | 5D                       | pop ebp                                 |
7C08C3EA | C2 0C00                  | ret C                                   |

By repeating the process already described above, we find that the command

call server.7BE8D320

is a challenge

SnapEyeAngles

. We can define the following function:

void SnapEyeAngles(CBasePlayer* player, const QAngle& angles)
{
    constexpr auto globalSnapEyeAnglesOffset{ 0x1FD320 };
    static SnapEyeAnglesFnc snapEyeAnglesFnc{ GetFunctionPointer<SnapEyeAnglesFnc>(
        "server.dll", globalSnapEyeAnglesOffset) };

    return snapEyeAnglesFnc(player, angles);
}

So now we have everything we need.

▍ Create an aimbot

To create an aimbot we need the following:

  • Iteratively traverse entities

    • If the entity is an enemy, then find the distance between the entity and the player
    • Track nearest entity
  • Calculate the eye-to-eye vector between the player and the nearest enemy
  • Adjust the angles of the player’s eye to follow this vector

Earlier we learned that to get the player’s position, you can call

GetPlayerPosition

. To loop through a list of entities, you can call

FirstEntity

and

NextEntity

which return a pointer to an instance

CBaseEntity

. To understand whether an entity is an enemy, you can compare the name of the entity with many names of hostile

NPC entities

. If we got the essence of the enemy, then we calculate the distance between the player and the essence and save the position of the essence if it is the closest of all that we have found so far.

After iterating through the entire list of entities, we get the nearest enemy, calculate the eye-to-eye vector, and adjust the angles of the player’s eye using the function VectorAngles.

In code form, we get the following:

auto* serverEntity{ reinterpret_cast<IServerEntity*>(
    GetServerTools()->FirstEntity()) };

if (serverEntity != nullptr) {
    do {
        if (serverEntity == GetServerTools()->FirstEntity()) {

            SetPlayerEyeAnglesToPosition(closestEnemyVector);
            closestEnemyDistance = std::numeric_limits<float>::max();
            closestEnemyVector = GetFurthestVector();
        }

        auto* modelName{ serverEntity->GetModelName().ToCStr() };
        if (modelName != nullptr) {
            auto entityName{ std::string{GetEntityName(serverEntity)} };

            if (IsEntityEnemy(entityName)) {
                Vector eyePosition{};
                QAngle eyeAngles{};

                GetServerTools()->GetPlayerPosition(eyePosition, eyeAngles);

                auto enemyEyePosition{ GetEyePosition(serverEntity) };

                auto distance{ VectorDistance(enemyEyePosition, eyePosition) };
                if (distance <= closestEnemyDistance) {
                    closestEnemyDistance = distance;
                    closestEnemyVector = enemyEyePosition;
                }
            }
        }

        serverEntity = reinterpret_cast<IServerEntity*>(
            GetServerTools()->NextEntity(serverEntity));

    } while (serverEntity != nullptr);
}

There are several helper functions in the code that we have not considered before: the function

GetFurthestVector

returns a vector with the maximum float values ​​in the x, y, and z fields;

GetEntityName

returns the name of the entity as a string, getting a member

m_iName

instance

CBaseEntity

; a

IsEntityEnemy

it just checks the name of the entity against multiple hostile NPCs.

Vector calculations and the calculation of the new viewing angle occur in the following SetPlayerEyeAnglesToPosition:

void SetPlayerEyeAnglesToPosition(const Vector& enemyEyePosition) {

    Vector eyePosition{};
    QAngle eyeAngles{};
    GetServerTools()->GetPlayerPosition(eyePosition, eyeAngles);

    Vector forwardVector{ enemyEyePosition.x - eyePosition.x,
        enemyEyePosition.y - eyePosition.y,
        enemyEyePosition.z - eyePosition.z
    };

    VectorNormalize(forwardVector);

    QAngle newEyeAngles{};
    VectorAngles(forwardVector, newEyeAngles);

    SnapEyeAngles(GetLocalPlayer(), newEyeAngles);
}

This function calculates the eye-to-eye vector by subtracting the player’s eye position vector from the enemy’s eye position vector. This new vector is then normalized and passed to the function

VectorAngles

to calculate new viewing angles. The player’s eye angles are then adjusted to these new angles, which should create an entity-tracking effect.

What does it look like in action?

You see an almost transparent scope following the head of an NPC walking across the room. When the NPC is far enough away, the code binds to the closer NPC. Everything is working!

▍ Conclusion

The techniques described in the article are generally applicable to any FPS game. The way in which positions and angles are obtained can differ between game engines, but vector calculations to create a viewing angle from a distance vector are applicable in any case.

The reverse engineering of Half-Life 2 has been greatly simplified by the openness of the Source SDK. Being able to map code and data structures to assembly code has made debugging much easier, but you don’t usually get that lucky! I hope this article helped you understand how aimbots work and showed you how easy it is to create them.

Full source aimbot is available on GitHub, you can freely experiment with it.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *