Hacking ETPro qagame for fun and (no) profit

Few weeks back, I came with an idea of extending ETPro's voting capabilities. We lived under an impression that it's not possible without giving it too much thought. Recently, I spent quite some time reading ET source code and I realised that the whole voting mechanism isn't special from anything else the game does.

  1. Client sends callvote, so we can intercept that.
  2. Server then updates config strings, namely CS_VOTE_TIME, CS_VOTE_STRING and few others.
  3. Client, based on what's in the config strings displays the voting interface.
  4. Depending on what button player presses, client sends vote yes or vote no. Again, we can intercept that.

ETPro's Lua implementation not only offers altering the actual config strings, it can also send server commands to individual clients, one of which is cs - yes, we can easily set config strings per client. That's all you need for implementing team-specific votes (or, more generally, votes that only some can see).

Vote++ was born, deployed and we were happy (or at least I was), because we could implement our own voting handlers, like kick-vote that results in a PunkBuster kick (ensuring the player can't return instantly by changing his net_port) or PUTSPEC. But then one day, x0rnn (who helped me a lot with testing the silly thing) came and forwarded me a message from Liquid (who apparently does bug hunting for living) pointing out that the yellow voting interface doesn't reflect the fact it was casted (after receiving the vote command).

So this is what you could see after calling a vote:

And this is what you were supposed to see:

Well, this isn't a big deal as it's only an interface problem (actual vote was reflected in the result), but I was annoyed that:

  1. I didn't notice this at all.
  2. I couldn't really fix it.

Why couldn't I fix it, you ask?

Let's take a look on how client decides what to render (cg_draw.c):

if ( !( cg.snap->ps.eFlags & EF_VOTED ) ) {
	s = va( CG_TranslateString( "VOTE(%i): %s" ), sec, cgs.voteString );
	CG_DrawStringExt( 8, 200, s, color, qtrue, qtrue, TINYCHAR_WIDTH, TINYCHAR_HEIGHT, 80 );

	if ( cgs.clientinfo[cg.clientNum].team != TEAM_AXIS && cgs.clientinfo[cg.clientNum].team != TEAM_ALLIES ) {
		s = CG_TranslateString( "Cannot vote as Spectator" );
	} else {
		s = va( CG_TranslateString( "YES(%s):%i, NO(%s):%i" ), str1, cgs.voteYes, str2, cgs.voteNo );
	}
	CG_DrawStringExt( 8, 214, s, color, qtrue, qtrue, TINYCHAR_WIDTH, TINYCHAR_HEIGHT, 60 );
	return;
} else {
	s = va( CG_TranslateString( "YOU VOTED ON: %s" ), cgs.voteString );
	CG_DrawStringExt( 8, 200, s, color, qtrue, qtrue, TINYCHAR_WIDTH, TINYCHAR_HEIGHT, 80 );

	s = va( CG_TranslateString( "Y:%i, N:%i" ), cgs.voteYes, cgs.voteNo );
	CG_DrawStringExt( 8, 214, s, color, qtrue, qtrue, TINYCHAR_WIDTH, TINYCHAR_HEIGHT, 20 );
	return;
}

The first line is the important one: If EF_VOTED flag is set in ps.eFlags, the desired interface (You voted on...) is displayed.

ETPro has et.gentity_get() function that you can use to read various entity state fields. I couldn't believe it for a while, but it was a sad fact that ps.eFlags field simply wasn't exposed. ETPro enumerates all the fields somewhere in its source code, it doesn't automagically expose everything. That's kinda obvious, as during compilation, these fields become just numbers (offsets).

Let's hack this for fun and (no) profit!

Let's deep dive into this. The et.gentity_get() offers just a limited set of fields we can work with and there's no way around that. Or is there? Take a look at et.gentity_get()'s third argument arrayindex. This is supposed to be used for reading array fields like ps.ammo:

local bulletsInClip = et.gentity_get(clientNum, "ps.ammo", 9)

This might doesn't seem too exciting, but stay with me. What exactly is it that ETPro does when this code is executed? We can assume it's something like this:

// Let's assume we already know the field name is "ps.ammo" at this point.
int GetPSAmmo(lua_State *l) {

	int       clientNum, arrayindex, value;
	gentity_t *ent;

	// 1st argument is clientNum (entityNum, effectively the same),
	// 3rd argument is array index.
	clientNum  = luaL_checkinteger(l, 1);
	arrayindex = luaL_checkinteger(l, 3);

	ent   = &g_entities[clientNum];
	value = ent->client->ps.ammo[arrayindex];

	// Put the value on the stack.
	lua_pushinteger(l, value);

	// This tells the Lua how many arguments are being returned.
	return 1;

}

This is C, right? All the names like ps or ammo are lost during compilation, they have no meaning to the computer. Computer works with numerical addresses. Compiler knows:

  1. what the address of g_entities is,
  2. how long gentity_t is,
  3. how far client (gclient_t) pointer is from the beginning of gentity_t and where the actual struct is placed in memory,
  4. how far ps is from the beginning of the gclient_t,
  5. how far ammo is from the beginning of the playerState_s,
  6. how many bytes each element of ammo takes (it's 4, because that's how long integer is on x86).

So, when the Lua code above executes, this is how the exact memory location of the value we're interested in is computed:

  1. Read the starting address of g_entities array (it's a pointer to an array, but that's not that important).
  2. Add sizeof(gentity_t) * clientNum to it, so we're now at the beginning of the entity we're interested in.
  3. Get the address stored in the client field (it's another pointer to gclient_t).
  4. Add distance of ps.ammo from the beginning of gentity_t.
  5. Add sizeof(int) * arrayindex.

You just computed the exact address in memory of the value we're trying to access.

Now this is where the fun begins. Notice the 5. step: sizeof(int) * arrayindex. And take a look at the playerState_s:

int damageCount;
int stats[MAX_STATS];
int persistant[MAX_PERSISTANT];
int powerups[MAX_POWERUPS];
int ammo[MAX_WEAPONS];          // <- ps.ammo
int ammoclip[MAX_WEAPONS];

Let's just assume for a while that all the MAX constants equal to 3, this is how memory would look like:

Address Description
0x3039 damageCount
0x303D stats[0]
0x303D stats[1]
0x3045 stats[2]
0x3049 persistant[0]
0x304D persistant[1]
0x3051 persistant[2]

...and so on.

So, tell me, what would the following code do?

local x = et.gentity_get(clientNum, "ps.stats", -1)

Notice it's ps.stats (which is at our hypothetical address 0x303D) and we're trying to read minus 1st element. To C, arrayindex is just an integer it adds to the address at which the supposed value should be.

0 would return value at address 0x303D, 1 would return address at 0x303D. And -1 would return 0x3039 (damageCount), which is pretty cool, because we were never supposed to have access to that field! It also means that we can access arbitrary field in the whole gclient_t struct. We can now break the game in a completely different manner!

Not so fast. ETPro has a boundary check preventing us from reading outside of the array field. So to make this work, we need to patch it.

We need an already exposed array field. Those are:

The ps is playerState_t struct, sess is clientSession_t. Both are part of gclient_t and we can access any of its fields by hacking any of the listed fields. The gentity_t is elsewhere in the memory and the client (the gclient_t) field is actually a pointer, so we aren't going to get access to the gentity_t struct itself (not that easily, at least).

The ps.stats is closest to the beginning of gclient_t, so let's use that. The string itself must be hardcoded somewhere in ETPro (we'll work with Windows binary):

.rdata:200E7264 aPsStats        db 'ps.stats',0

And this is where it's referenced:

.data:200DAD00                 dd offset aSessAweaponsta ; "sess.aWeaponStats"
.data:200DAD04                 db  50h ; P
.data:200DAD05                 db    1
.data:200DAD06                 db    0
.data:200DAD07                 db    0
.data:200DAD08                 dd offset sub_200173B0
.data:200DAD0C                 dd offset sub_20017450
.data:200DAD10                 dd offset aPsStats      ; "ps.stats"
.data:200DAD14                 db 0D0h ; Đ
.data:200DAD15                 db    0
.data:200DAD16                 db    0
.data:200DAD17                 db    0
.data:200DAD18                 dd offset sub_200163A0
.data:200DAD1C                 dd offset sub_20016950
.data:200DAD20                 dd offset aPsPersistant ; "ps.persistant"

In the source code, there's probably enumeration of fields and their handlers (getters, setters):

{
	...
	{"sess.aWeaponStats", FLAGS, GetSessWeaponStats, SetSessWeaponStats},
	{"ps.stats",          FLAGS, GetPSStats,         SetPSStats        },
	{"ps.persistant",     FLAGS, GetPSPersistent,    SetPSPersistent   },
	...
}

The FLAGS could be whatever... And since several fields refer the same function, it's probably an offset or something like that. We don't really care.

All the ps getters lead to sub_200163A0:

The 0Fh (decimal 15) and the ps array index 0-%d shows we're on the right track. There are two branches, the left one gets the value from given index and pushes it into the stack, the right one yields an error. We always want to take the left one, so we're going to simply NOP (90) the JNZ (75 60) instruction.

All it takes are two bytes changed:

Instead of conditionally jumping to the error branch, we just do nothing twice and continue execution.

...and there we go!

This is basically everything we need to accomplish our goal. The boring stuff remains, such as computing the offset of every accessible field and of course patching the setter in the same manner.

Note that the struct layout of gclient_t could be a little bit different from etmain. However, at least the distance of ps.eFlags and ps.stats seems to be the same:

And here's the patch for Vote++. Using pcall, it checks whether the unlocked functions are available and if so, it uses them to reflect the state in the voting interface.

In the next lesson, we're going to teach our server fly.

Resources