hacking enemy territory

I’ve recently downloaded enemy territory: legacy it is based on the source code of one of my favorite games growing up wolfenstein: enemy territory and it’s arguably the best FPS game out there, Legacy is bascially an updated version of the original game which released in 2003 and they aim to improve and fix any bugs and exploits while making the game more modern. It never ceases to amaze me that even after almost 20 years the community is alive and active and people still playing anyways while I was playing the game I realized how rusty I was playing with rifle grenade when picking engineer (specifically throwing nades) so I searched for any console command to view the grenade trajectory (draw a grenade path when pulling the pin) to practice offline and was suprised to discover the game doesn’t have any, so I thought since legacy is open sourced on github it makes our process of reversing a game & creating a hack much easier and it would be a great opportunity to show you my approach on implementing such feature, I’ll be using IDA for the static analysis but any kind of disassembly tool (even a debugger) is fine. In addition to ‘reversing’ the game I will write a DLL in C++ to read and manipulate the memory internally, this method will require a loader(also referred as injector) to load our DLL into the game process, personally I’ll be using my own but feel free to use any out of the dozens released on various forums. This can be done externally meaning we can manipulate the memory using .exe but I prefer creating a DLL since its more efficient, easier when hooking/calling game functions and generally has better performance.

This post is aimed towards folks who have basic understanding of game hacking & C/C++, I’d also like to point out that I’ve never messed with quake games prior this so any feedback/critism is welcomed!

0x00 game engine

Before we start diving into reversing it’s important to understand the layout of the game, ET has various addons(mods) such as ETJump, Jaymod, ETPro, ETPub, Legacy & more.
Each mod has its own pros and cons and it basically controls the client-side modification.
When connecting to any server the main executable will load cgame_mp_x86.dll which is the mod located in Users\Daniel\Documents\ETLegacy\*modname*\cgame_mp_x86.dll.
cgame_mp_x86.dll exports 2 important functions for the main executable to call vmMain() acts as a dispatcher it will redirect any commands and parameters given from the main executable and dllEntry() that stores syscall pointer into a global variable which is used for the main executable to call for example to read files or indicate a critical error. I’d also like to point out that CG_ prefix stands for client game.

//https://github.com/etlegacy/etlegacy/blob/eef4d2bdb6abaa2716e993b86636084af226520a/src/cgame/cg_main.c#L64
__declspec(dllexport) intptr_t vmMain(intptr_t command, intptr_t arg0, intptr_t arg1, intptr_t arg2, intptr_t arg3, intptr_t arg4, intptr_t arg5, intptr_t arg6, intptr_t arg7, intptr_t arg8, intptr_t arg9, intptr_t arg10, intptr_t arg11)
{
	switch (command)
	{
	case CG_INIT:
		CG_Init(arg0, arg1, arg2, (qboolean)arg3, arg4, (demoPlayInfo_t *)arg5, arg6);
		cgs.initing = qfalse;
		return 0;
	case CG_SHUTDOWN:
		CG_Shutdown();
		return 0;
	case CG_DRAW_ACTIVE_FRAME:
		CG_DrawActiveFrame(arg0, (qboolean)arg2); // arg1 removed, order kept for vanilla client compatibility
		return 0;
	//...
	default:
		CG_Error("vmMain: unknown command %li", (long)command);
		break;
	}
	return -1;
}

static intptr_t(QDECL * syscall)(intptr_t arg, ...) = (intptr_t(QDECL *)(intptr_t, ...)) - 1;

//https://github.com/etlegacy/etlegacy/blob/00b35a7e14db6f0b73dd6fbf5fdfe7436a8ed776/src/game/g_syscalls.c#L42
__declspec(dllexport) void dllEntry(intptr_t(QDECL * syscallptr)(intptr_t arg, ...))
{
	syscall = syscallptr;
}

here we can see list of commands(syscalls) that the main executable can invoke, for example if ET.exe wants to shutdown it will call vmMain with CG_SHUTDOWN command to clean/flush any open files or CG_DRAW_ACTIVE_FRAME to draw stuff on the screen. CG_INIT invoked when connecting, reconnecting, loading new map etc which will call CG_Init()

//https://github.com/etlegacy/etlegacy/blob/eef4d2bdb6abaa2716e993b86636084af226520a/src/cgame/cg_main.c#L2614
void CG_Init(int serverMessageNum, int serverCommandSequence, int clientNum, qboolean demoPlayback, int etLegacyClient, demoPlayInfo_t *info, int clientVersion)
{
	const char *s;
	int        i;
	char       versionString[128];
	DEBUG_INITPROFILE_INIT

	//int startat = trap_Milliseconds();

	Com_Printf(S_COLOR_MDGREY "Initializing %s cgame " S_COLOR_GREEN ETLEGACY_VERSION "\n", MODNAME);

	// clean up the config backup if one exists
	CG_RestoreProfile();

	// clear everything
	Com_Memset(&cgs, 0, sizeof(cgs));
	Com_Memset(&cg, 0, sizeof(cg));
	Com_Memset(cg_entities, 0, sizeof(cg_entities));
	Com_Memset(cg_weapons, 0, sizeof(cg_weapons));

	cgs.initing = qtrue;
	//...
}

this function essentially initialises stuff related to the client but most importantly it clears these cg_ structures using Com_Memset these 4 structures contain all the information we need, cg_entities is a global array containing all entities, cg holds info related to the client while cgs related to the server and lastly cg_weapons might be relevant if we want to achieve wallhack for weapons. most of these structures are defined in src/cgame/cg_local.h

// from cg_local.h
extern cgs_t        cgs;
extern cg_t         cg;
extern centity_t    cg_entities[MAX_GENTITIES];
extern weaponInfo_t cg_weapons[MAX_WEAPONS];
0x01 wallhack

the idea of wallhack is quite simple- obtain all players position in game world and transform them to our screen position and use any kind of API rendering (direct3d or opengl) to render boxes and names. as we’ve seen previously CG_Init() function clears cg_entities structure which might be our entitylist offset (refers to an array or list in the memory of the game which contains all the entities/players in the game), finding it in any disassembly is fairly easy- go through the exports specifically vmMain export and inside the function we want to jump to our first call that is the CG_Init function

 // CG_Init(..)
 //...
 push    offset aLegacy  ; "legacy"
 push    offset a9initializingS ; "^9Initializing %s cgame ^2v2.77.1-261-g"...
 call    sub_100481C0		; Com_Printf(..)
 call    sub_10047790		; CG_RestoreProfile()
 push    2059414h		; sizeof(cgs)
 push    0
 push    offset dword_11B6C880	; cgs
 call    sub_100A0C10		; Com_Memset(&cgs, 0, sizeof(cgs));
 push    99F3Ch			; sizeof(cg)
 push    0
 push    offset dword_11AD2940	; cg
 call    sub_100A0C10		; Com_Memset(&cg, 0, sizeof(cg));
 push    2D5000h		; sizeof(cg_entities)
 push    0
 >> push    offset unk_117FD940	; cg_entities
 call    sub_100A0C10		; Com_Memset(cg_entities, 0, sizeof(cg_entities));
 push    41000h			; sizeof(cg_weapons)
 push    0
 push    offset dword_117BC940	; cg_weapons
 call    sub_100A0C10		; Com_Memset(cg_weapons, 0, sizeof(cg_weapons));
 //...

From here we could calculate the RVA offset by subtracting the base address from 0x117FD940 but I prefer creating and searching for a pattern of bytes as its more reliable between any game updates (cg_entities pattern 68 ?? ?? ?? ?? E8 ?? ?? ?? ?? 68 00 10 04 00 6A 00).
Using ReClass or Cheat Engine we can view the memory of these structures (even reconstruct these data structures if needed) but seems like when viewing cg_entities in memory it appears to have big chunks of null memory, cross-referencing cg_entities in IDA we can see where and how its being used

 movzx   eax, byte ptr [eax+26Ch] ; eax acts as an index
 imul    edx, eax, 0B54h	  ; multiply eax with 0xB54 and store it in edx
 add     edx, offset unk_117FD940 ; add cg_entities to edx

I was under the impression it contains list of pointers pointing to each entity but now it makes more sense unk_117FD940 is used as ‘base address’ and each centity_t has a size of 0xB54 bytes so accessing each entity would look something like this

for (int i = 0; i < 64; i++)
	auto entity = (centity_t*)(cg_entities + (0xB54 * i));

Once we obtained a valid entitylist offset we need a way of converting each player position (x,y,z) into screen position (x,y) that function is known as world to screen the implementation might differ from game to game but the concept stays the same- it takes 4x4 matrix (16 floats) called viewmatrix that represents 4 vectors: forward, right, up (they define the orientation ot the camera) and camera position to calculate the screen position. CG_WorldCoordToScreenCoordFloat() does exactly what we need, the viewmatrix is inside refdef structure which is located in cg, viewaxis in redef represents the camera while vieworg represents the camera position, we can easily rebuild these structures by copying them from tr_types.h and cg_local.h.

Now with drawing the easiest approach is to git clone any of these direct3d overlays but since the game is open-sourced we can call the game functions to draw. CG_FillRect is the function used to draw filled rectangles the params are pretty self explanitory, locating it in IDA can be done by searching for specific strings near the call for example “Ping %d” here displayed in the corner of the screen

 // snippet from IDA
 mov     eax, cg_snap
 mov     ecx, 3E7h
 mov     eax, [eax+4]
 cmp     eax, ecx
 cmovl   ecx, eax
 push    ecx
 push    offset aPingD   ; "Ping %d"
 call    sub_10093E50    ; va func
 add     esp, 8
 mov     [ebp+var_C], eax
 push    offset unk_11B7DDF0
 push    0
 push    ecx
 mov     [esp+2Ch+var_2C], 3E428F5Ch
 push    eax
 call    sub_1001AC50    ; CG_Text_Width_Ext func
 cmp     eax, 34h
 mov     [ebp+var_14], eax
 mov     esi, eax
 mov     [esp+30h+var_24], 441E8000h
 cmovl   esi, edi
 add     esp, 0Ch
 mov     [ebp+var_8], esi
 call    sub_10023FD0    ; Ccg_WideX func
 movss   xmm0, [ebp+var_4]
 add     esp, 4
 fstp    [ebp+var_10]
 cvttss2si ebx, [ebp+var_10]
 push    offset unk_100DFDD8
 sub     esp, 10h
 mov     [esp+34h+var_28], 41600000h
 sub     ebx, esi
 add     esi, 5
 movd    xmm1, esi
 lea     edi, [ebx-2]
 cvtdq2ps xmm1, xmm1
 movd    xmm2, edi
 cvtdq2ps xmm2, xmm2
 movss   [esp+34h+var_2C], xmm1
 movss   [esp+34h+var_30], xmm0
 movss   [esp+34h+var_34], xmm2
 call    sub_100232F0    ; CG_FillRect func

55 8B EC FF 75 18 E8 ?? ?? ?? ?? F3 0F 10 05 ?? ?? ?? ?? 83 C4 04
is the pattern to find CG_FillRect() address dynamically (aka sub_100232F0) but simply calling it will crash our game because its not thread-safe, one solution for this is to hook vmMain and draw stuff when CG_DRAW_ACTIVE_FRAME syscall is invoked, I won’t be diving into hooking I feel like this topic is widely documented, I’ll be using minhook library (its simple and minimalstic open-sourced library for hooking on windows x86 & x64)

intptr_t __cdecl vmMain_hooked(intptr_t command, intptr_t arg0, intptr_t arg1, intptr_t arg2, intptr_t arg3, intptr_t arg4, intptr_t arg5, intptr_t arg6, intptr_t arg7, intptr_t arg8, intptr_t arg9, intptr_t arg10, intptr_t arg11)
{
	intptr_t result = 0;

	static float color_white[4] = { 1, 1, 1, 0.35 };

	switch (command)
	{	
	case CG_DRAW_ACTIVE_FRAME:
		for (int i = 0; i < 64; i++)
		{
			auto entity = (centity_t*)(cg_entities + (0xB54 * i));
			if (!entity)
				continue;
		
			float x, y;
			if (CG_WorldCoordToScreenCoordFloat(entity->currentState.pos.trBase, &x, &y))
			{	
				CG_FillRect(x, y, 5, 5, color_white);
			}
		}

		return vmMain_original(command, arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11);;
		
	return vmMain_original(command, arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11);
}

here is the wallhack in action

0x02 nade trajectory

Calculating our weapon’s grenade trajectory requires some sort of engine prediction to predict the path it should follow, using BG_EvaluateTrajectory() we can predict the position of objects, trType describes how an object should move by using specific equations for example TR_LINEAR should move in a straight linear line while TR_GRAVITY moves in a gravity affected line, by reading the function implementation we can guess what to initialise in trajectory_t, we already know that trType_t is a set of constants describing how an object should move and we’re interested in TR_GRAVITY, trTime is the start time of an object, trDuration is not used in TR_GRAVITY so its irrelevant, trBase is the start position of an object and trDelta I’m not sure about it so I dug deeper in the source and stumbled upon this structured array called weapFireTable_t (in g_weapon.c) basically its a lookup table for various flags and properties for the weapon fire etc, the fire member function is invoked by FireWeapon() when firing any weapon, the firing function used in our table for WP_GPG40 and WP_M7 (rifle-nades that we’re interested at) is weapon_gpg40_fire(), inside the function it calls AngleVector() (function that takes our viewangles and calculates vectors that point in directions relative to our viewangles) to obtain forward vector (vector that points in the forward facing direction) and multiplies the forward vector by 2000, afterwards it will pass that vector to G_PreFilledMissileEntity() which initialises trDelta with it.

But still running BG_EvaluateTrajectory() in a loop isn’t enough because our rifle-grenade can hit a wall and bounce or just have an impact and explode, CG_Trace() allows us to trace a ray/line between 2 points in our game world and check if it hits something solid.
first parameter is the function result that expects trace_t

typedef struct
{
	qboolean allsolid;      ///< if true, plane is not valid
	qboolean startsolid;    ///< if true, the initial point was in a solid area
	float fraction;         ///< time completed, 1.0 = didn't hit anything
	vec3_t endpos;          ///< final position
	cplane_t plane;         ///< surface normal at impact, transformed to world space
	int surfaceFlags;       ///< surface hit
	int contents;           ///< contents on other side of surface hit
	int entityNum;          ///< entity the contacted surface is a part of
} trace_t;

so if startsolid returns true or if fraction is not equal to 1.f our predicted position hits something solid, the second param is the start position of the ray, third and fourth params are the box boundies (in our weapon fire table mins and maxs are { -4.f, -4.f, 0.f } and { 4.f, 4.f, 6.f }), fifth param is the end position of the ray, sixth param is entity id to skip when enumerating all entities (we can skip our own) and lastly MASK_MISSILESHOT is our mask. using those two functions we can predict the grenade trajectory till impact.

void draw_riflenade_traj()
{
	trajectory_t rifle_traj;
	rifle_traj.trType = TR_GRAVITY;
	rifle_traj.trTime = 0;
	rifle_traj.trDuration = 0;
	VectorCopy(cg->snap->ps.origin, rifle_traj.trBase);
	rifle_traj.trBase[2] += cg->snap->ps.viewheight; // adjust the start pos
	
	vec3_t forward;
	angles_vectors(cg->snap->ps.viewangles, forward, 0, 0);
	VectorScale(forward, 2000, forward);
	VectorCopy(forward, rifle_traj.trDelta);
	
	int max_time = 3250;
	for (int cur_time = 0; cur_time < max_time; cur_time += 10)
	{
		vec3_t predicted_pos;
		BG_EvaluateTrajectory(&rifle_traj, cur_time, predicted_pos, qfalse, 0);

		trace_t rifle_trace;
		auto mins = vec3_t{ -4.f, -4.f, 0.f };
		auto maxs = vec3_t{ 4.f, 4.f, 6.f };
		CG_Trace(&rifle_trace, start_pos, mins, maxs, predicted_pos, cg->snap->ps.clientNum, MASK_MISSILESHOT);

		if (rifle_trace.startsolid || rifle_trace.fraction != 1.0)
		{
			// reflect angle and velocity
		}
		else
		{
			float x, y;
			if (CG_WorldCoordToScreenCoordFloat(predicted_pos, &x, &y))
			{
				static float color_white[4] = { 1, 0, 0, 1 };
				CG_FillRect(x - 1, y - 1, 2, 2, color_white);
			}
		}
	}
}

Now we’re left with calculating the reflected angle and velocity, if we cross reference EF_BOUNCE_HALF constant used in eFlag (in our weapon fire table for WP_GPG40 and WP_M7) we can see its being used only in G_BounceMissile(), this function first checks if 750 milliseconds has passed since firing, if true then call G_ExplodeMissile() which means if our grenade travels over 750ms it will explode on impact instead of bouncing otherwise calculate the hitTime and velocity using BG_EvaluateTrajectoryDelta() and then multiply the velocity to make sort of third bounce.

hitTime = (int)(level.previousTime + (level.time - level.previousTime) * trace->fraction);
BG_EvaluateTrajectoryDelta(&ent->s.pos, hitTime, velocity, qfalse, ent->s.effect2Time);
dot = DotProduct(velocity, trace->plane.normal);
VectorMA(velocity, -2 * dot, trace->plane.normal, ent->s.pos.trDelta);

Using these calculations we can predict how it should bounce, here is the full code

0x03 triggerbot

Lastly triggerbot, finding offset that corresponds to our aim can be really easy and its usually done by scanning changed memory using cheat engine alternatively there’s a member in cg structure called crosshairClientNum but I prefer leaving this as an excerise for the reader :)


references:

https://guidedhacking.com/threads/how-to-hack-call-of-duty-games-quake-engine-games.11155/ https://www.quakewiki.net/archives/code3arena/tutorials/tutorial38.shtml