Table of Contents:
This post will go over what is required to build an aimbot, and show how to reverse engineer our target executable to find what we need. In order to make something that automatically aims at a target, there are a few things that need to be known:
- The player’s eye position
- The nearest enemies’ eye position
- The vector from the player’s eye to the enemies’ eye (derived from the previous two points)
- How to adjust the player’s view such that the player’s eye is looking down the vector to the enemies’ eye
All but the third step require getting information from the running state of the game. This information is obtained by reverse engineering the game to find the classes that contain the relevant fields. Usually, this would be an extremely time consuming effort full of trial an error; however, in this case, we have access to the Source SDK, which we will use to guide the reverse engineering effort.
We will start the search by looking for references to eye position from within their repository. After clicking through a few pages of search results, we land on the gigantic CBaseEntity class. Inside this class, there are two functions:
virtual Vector EyePosition(void);
virtual const QAngle &EyeAngles(void);
Since CBaseEntity is the base class that all entities derive from, and it contains members for eye position and eye viewing angles, it seems like the class that we want to work with. The next step is to see where these functions are referenced from. Again, after searching in the Source SDK GitHub for a bit, we come across the IServerTools interface, which has a handful of very promising functions:
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 is really nice about this interface is that it provides access to the local player’s position, allows for snapping the player to another position and viewing angle, and also provides the ability to iterate over entities. Add to the fact that it is globally instantiated and tied to a hardcoded string, and it doesn’t get much better.
#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 begin the aimbot development by finding this class in memory. After launching Half-Life 2 and attaching a debugger, we can search for string references to VSERVERTOOLS.
We can see where these are referenced:
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 |
From the assembly listing, we see that a member function at server.7C0A7B30 is being invoked on server.7C332AB0 and server.7C332ABC. This function takes in two arguments, one of which is the string name of the interface. The other parameter, after browsing in the debugger, is a static instance to something.
After seeing what the EXPOSE_SINGLE_INTERFACE_GLOBALVAR macro does in the code, it is more clear that this is the CServerTools singleton, which is being exposed as a global interface. Knowing this, we can easily obtain a pointer to this singleton at runtime: we simply take the address of this pseudo-function that is moving the pointer into EAX and invoke it directly. We can write the following generic code to achieve this, which we will continue to re-use for 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 server.dll loaded at, add in the offset to get to where the CServerTools singleton can be accessed, and return it as a pointer to the caller. From there, we can call whatever functions we want in the interface and the game will act accordingly. The two functions of interest are going to be GetPlayerPosition and SnapPlayerToPosition.
Inside of GetPlayerPosition, the local player’s class is retrieved via a call to UTIL_GetLocalPlayer, and the EyePosition and EyeAngles functions are also called; inside of SnapPlayerToPosition, the player’s view angles are adjusted with a call to SnapEyeAngles. Together, this will give us everything we need to be able to retrieve entity positions and view angles, thus making it possible to make the appropriate calculations for the new vector and view angle that aligns to an enemies’ eyes.
Taking these one at a time, we can begin with GetPlayerPosition. Since we can obtain a pointer to IServerTools, and we have the interface definition, we can make an explicit call to GetPlayerPosition, and step through the call with a debugger. Doing so leads us to 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 |
There’s a bit to go through here, though it looks more straightforward as a control flow graph:
If we line up the disassembly side-by-side with the code, we can find what we want rather quickly. The code only calls the UTIL_GetLocalPlayer function if the passed in pClientEntity parameter is null. This logic is checked in the first function block in the graph. If there is a valid client entity, the code continues to retrieve the eye position and eye angles for it, otherwise it gets the local player’s entity. This call happens with the execution of the call server.7BEFD5F0 instruction at server.7C08BF05. Like before, we can create a function pointer to UTIL_GetLocalPlayer and invoke it directly.
CBasePlayer* GetLocalPlayer() {
constexpr auto globalGetLocalPlayerOffset{ 0x26D5F0 };
static GetLocalPlayerFnc getLocalPlayerFnc{ GetFunctionPointer<GetLocalPlayerFnc>(
"server.dll", globalGetLocalPlayerOffset) };
return getLocalPlayerFnc();
}
Next in the disassembly are calls to the EyePosition and EyeAngles functions. We are only interested in retrieving the eye positions, so only the first call is relevant. To get the address of the function we can step through the call until we end up calling the address that is in [EAX+0x208]. After executing that instruction, we will be at server.dll+0x119D00, and thus know where the function is located.
Vector GetEyePosition(CBaseEntity* entity) {
constexpr auto globalGetEyePositionOffset{ 0x119D00 };
static GetEyePositionFnc getEyePositionFnc{ GetFunctionPointer<GetEyePositionFnc>(
"server.dll", globalGetEyePositionOffset) };
return getEyePositionFnc(entity);
}
That is all that we need from GetPlayerPosition; we now have the ability to get the player’s local entity pointer, and have the ability to get the eye position of an entity. The last thing that we need is the ability to set the player’s eye angle. As mentioned before, we can do this by calling the SnapPlayerToPosition function and seeing where the SnapEyeAngles function is located at. The disassembly of SnapEyeAngles looks like this:
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 |
Following the same process as before, we find that the call server.7BE8D320 instruction is the call to SnapEyeAngles. We can define a function around it as follows:
void SnapEyeAngles(CBasePlayer* player, const QAngle& angles)
{
constexpr auto globalSnapEyeAnglesOffset{ 0x1FD320 };
static SnapEyeAnglesFnc snapEyeAnglesFnc{ GetFunctionPointer<SnapEyeAnglesFnc>(
"server.dll", globalSnapEyeAnglesOffset) };
return snapEyeAnglesFnc(player, angles);
}
At this point, we have everything needed to build the aimbot. The next post will put it all together and end with a working proof of concept.