Tools used: radare2 for disassembly, Frida for instrumentation.
Note: This is post is written from my understanding of the q3vm and is not meant to be a throughout explanation of it. If you want a more detailed, accurate description of is internals I encourage you to read about it in Fabien’s blog, which helped me fill some gaps.
What we are going to see in this post:
- A very basic understanding of how the Q3VM loads the game
- How to make small instrumentation script with Frida to detect new players connecting and userinfo changes without doing any engine modifications or loading server-side mods.
In essence, when the engine starts:
- Searches for PK3s (PK3 are the packed files that quake-based games parse) that are in the desired fs_game folder + the base game’s folder. E.g: If we use the JA’s JA+(japlus) mod, the engine will search for PK3 files in base/ and japlus/ folders.
- Initializes dynamic memory (com_hunkMegs, com_zoneMegs) and open sockets.
And it stops there (there are more things happening but it is not the point of this post) until a valid map is provided, which starts the game. An example of a JA valid map command would be: map mp/duel1:
- Loads the BSP file matching the levelname provided.
If it is not a base BSP, it tries to find it in any of the loaded PK3s
- Loads the QVM
- Jumps to vmMain()
- Does whatever syscall has received
- Jumps to vmMain() again and again and again.
Some of these vmMain arguments that can be received:
- GAME_INIT: Game startup
- GAME_SHUTDOWN: Map change or server shutdown.
- GAME_CLIENT_CONNECT: Client, bot or real connects.
- GAME_CLIENT_USERINFO_CHANGED: Client info changes, forcestrings, playermodels, name…
- … and more
For a more detailed list refer to gameExport_t struct in game/g_public.h https://github.com/jedis/jediacademy/blob/master/codemp/game/g_public.h#L734-L799
Alright, that is enough Q3VM for now…
Firsts things first, we want to where the vmMain export is in the original binary (we are using the 2003 bins ;) ), so we will chage for jampgamex86.dll:
[0x200ddd31]> iE
[Exports]
nth paddr vaddr bind type size lib name
----------------------------------------------------------------
0 0x000a5e70 0x200a5e70 GLOBAL FUNC 0 jampgamex86.dll dllEntry
1 0x00092a10 0x20092a10 GLOBAL FUNC 0 jampgamex86.dll vmMain
[0x200ddd31]> pdf @ 0x20092a10
/ 1489: sym.jampgamex86.dll_vmMain (int32_t arg_4h, int32_t arg_8h, int32_t arg_ch, int32_t arg_10h, int32_t arg_14h, int32_t arg_18h, int32_t arg_1ch, int32_t arg_20h, int32_t arg_24h, int32_t arg_28h);
| ; var int32_t var_3dch @ esp+0x14
| ; var int32_t var_3d4h @ esp+0x1c
| ; var int32_t var_3d0h @ esp+0x20
| ; var int32_t var_3cch @ esp+0x24
| ; var int32_t var_3a0h @ esp+0x50
| ; var int32_t var_394h @ esp+0x5c
| ; var int32_t var_390h @ esp+0x60
| ; var int32_t var_38ch @ esp+0x64
| ; var int32_t var_388h @ esp+0x68
| ; var int32_t var_384h @ esp+0x6c
| ; var int32_t var_380h @ esp+0x70
| ; var int32_t var_37ch @ esp+0x74
| ; var int32_t var_378h @ esp+0x78
| ; var int32_t var_370h @ esp+0x80
| ; var int32_t var_36ch @ esp+0x84
| ; arg int32_t arg_4h @ esp+0x3f4
| ; arg int32_t arg_8h @ esp+0x3f8
| ; arg int32_t arg_ch @ esp+0x3fc
| ; arg int32_t arg_10h @ esp+0x400
| ; arg int32_t arg_14h @ esp+0x404
| ; arg int32_t arg_18h @ esp+0x408
| ; arg int32_t arg_1ch @ esp+0x40c
| ; arg int32_t arg_20h @ esp+0x410
| ; arg int32_t arg_24h @ esp+0x414
| ; arg int32_t arg_28h @ esp+0x418
| 0x20092a10 8b442404 eax = dword [arg_4h]
| 0x20092a14 83f827 var = eax - 0x27 ; 39
| ,=< 0x20092a17 0f8713040000 if (((unsigned) var) > 0) goto case.default.0x20092a1d
| | ;-- switch
| | 0x20092a1d ff2485342e09. goto dword [eax*4 + 0x20092e34] ; switch table (40 cases) at 0x20092e34
| | ; CODE XREF from sym.jampgamex86.dll_vmMain @ 0x20092a1d
| | ;-- case 0: ; from 0x20092a1d
| | 0x20092a24 8b442410 eax = dword [arg_10h]
| | 0x20092a28 8b4c240c ecx = dword [arg_ch]
| | 0x20092a2c 8b542408 edx = dword [arg_8h]
| | 0x20092a30 50 push eax ; int32_t arg_ch
| | 0x20092a31 51 push ecx ; int32_t arg_8h
| | 0x20092a32 52 push edx ; int32_t arg_4h
| | 0x20092a33 e8d8d8ffff fcn.20090310 ()
| | 0x20092a38 83c40c esp += 0xc
| | 0x20092a3b 33c0 eax = 0
| | 0x20092a3d c3 return
| | ; CODE XREF from sym.jampgamex86.dll_vmMain @ 0x20092a1d
| | ;-- case 1: ; from 0x20092a1d
| | 0x20092a3e 8b442408 eax = dword [arg_ch]
| | 0x20092a42 50 push eax ; int32_t arg_4h
We can see it is a huge jump table (Switch) with lots of cases, one for each member of the aforementioned struct. Our interesting use case scenario is the third switch which matches GAME_CLIENT_CONNECT:
| | ; CODE XREF from sym.jampgamex86.dll_vmMain @ 0x20092a1d
| | ;-- case 2: ; from 0x20092a1d
| | 0x20092a4e 8b4c2410 ecx = dword [arg_18h]
| | 0x20092a52 8b54240c edx = dword [arg_14h]
| | 0x20092a56 8b442408 eax = dword [arg_10h]
| | 0x20092a5a 51 push ecx ; int32_t arg_ch
| | 0x20092a5b 52 push edx ; int32_t arg_8h
| | 0x20092a5c 50 push eax ; int32_t arg_4h
| | 0x20092a5d e87e61feff fcn.20078be0 ()
| | 0x20092a62 83c40c esp += 0xc
| | 0x20092a65 c3 return
This code will then call fcn.20078be0 that in this case is the client validation function. This includes a valid ping, a valid user infostring, a valid password (if server is password protected) and whether the user is banned or not:
[0x200ddd31]> s fcn.20078be0
[0x20078be0]> pdf
; CALL XREF from fcn.20075770 @ 0x20075b9e
; CALL XREF from sym.jampgamex86.dll_vmMain @ 0x20092a5d
/ 720: fcn.20078be0 (int32_t arg_4h, int32_t arg_8h, int32_t arg_ch);
| ; var int32_t var_400h @ esp+0x2c
| ; arg int32_t arg_4h @ esp+0x430
| ; arg int32_t arg_8h @ esp+0x434
| ; arg int32_t arg_ch @ esp+0x438
| 0x20078be0 81ec00040000 esp -= 0x400
| 0x20078be6 55 push ebp
| 0x20078be7 57 push edi
| 0x20078be8 8bbc240c0400. edi = dword [arg_4h]
| 0x20078bef 8bef ebp = edi
| 0x20078bf1 69edec050000 ebp = ebp * 0x5ec
| 0x20078bf7 6800040000 push 0x400 ; 1024 ; int32_t arg_ch
| 0x20078bfc 8d44240c eax = [var_400h]
| 0x20078c00 50 push eax ; int32_t arg_8h
| 0x20078c01 57 push edi ; int32_t arg_4h
| 0x20078c02 81c5e00f6320 ebp += 0x20630fe0
| 0x20078c08 e8d3d40200 fcn.200a60e0 ()
| 0x20078c0d 8d4c2414 ecx = [var_400h]
| 0x20078c11 6830b10f20 push 0x200fb130 ; "ip" ; int32_t arg_2004h
| 0x20078c16 51 push ecx ; int32_t arg_0h
| 0x20078c17 e864740400 fcn.200c0080 ()
| 0x20078c1c 50 push eax ; int32_t arg_4h
| 0x20078c1d e80ecb0200 fcn.200a5730 ()
| 0x20078c22 83c418 esp += 0x18
| 0x20078c25 85c0 var = eax & eax
| ,=< 0x20078c27 740e if (!var) goto 0x20078c37
| | 0x20078c29 5f pop edi
| | 0x20078c2a b8b0b10f20 eax = str.Banned. ; 0x200fb1b0 ; "Banned."
| | 0x20078c2f 5d
| | 0x20078c30 81c400040000 esp += 0x400
| | 0x20078c36 c3 return
| | ; CODE XREF from fcn.20078be0 @ 0x20078c27
| `-> 0x20078c37 f68538020000. var = byte [ebp + 0x238] & 8
| 0x20078c3e 53 push ebx
| 0x20078c3f 56 push esi
| ,=< 0x20078c40 0f85c0000000 if (var) goto 0x20078d06
| | 0x20078c46 8b84241c0400. eax = dword [arg_ch]
| | 0x20078c4d 85c0 var = eax & eax
| ,==< 0x20078c4f 0f85b1000000 if (var) goto 0x20078d06
| || 0x20078c55 a1ac257b20 eax = dword [0x207b25ac] ; [0x207b25ac:4]=0
| || 0x20078c5a 85c0 var = eax & eax
| ,===< 0x20078c5c 0f84a4000000 if (!var) goto 0x20078d06
| ||| 0x20078c62 8d542410 edx = [var_400h]
| ||| 0x20078c66 68a4b10f20 push str.password ; 0x200fb1a4 ; "password" ; int32_t arg_2004h
| ||| 0x20078c6b 52 push edx ; int32_t arg_0h
| ||| 0x20078c6c e80f740400 fcn.200c0080 ()
| ||| 0x20078c71 8bf0 esi = eax
| ||| 0x20078c73 a070026320 al = byte [0x20630270] ; [0x20630270:1]=0
| ||| 0x20078c78 83c408 esp += 8
| ||| 0x20078c7b 84c0 var = al & al
| ,====< 0x20078c7d 0f8483000000 if (!var) goto 0x20078d06
| |||| 0x20078c83 68c09d0e20 push 0x200e9dc0 ; "none" ; int32_t arg_8h
| |||| 0x20078c88 6870026320 push 0x20630270 ; int32_t arg_4h
In this case what we are able to inspect, is the three arguments sent to vmMain: clientId, firstTime and isBot.
- clientId: Client ID to be assigned.
- firstTime: Already in server or connected from change map.
- isBot: Client is player or bot.
So let’s write our instrumentation script. To test it, we will be using a server with 5 bots so our client ID is not 0:
status
map: mp/ffa1
num score ping name lastmsg address qport rate
--- ----- ---- --------------- ------- --------------------- ----- -----
0 0 0 Jedi Trainer 50 bot 0 16384
1 0 0 Mercenary 50 bot 0 16384
2 0 0 Desann 50 bot 0 16384
3 0 0 Reborn 50 bot 0 16384
4 0 0 Imperial Sabote 50 bot 0 16384
import { log } from "./logger";
// gameExport_t struct members.
const GAME_INIT = 0;
const GAME_SHUTDOWN = 1;
const GAME_CLIENT_CONNECT = 2;
const GAME_CLIENT_BEGIN = 3;
const GAME_CLIENT_USERINFO_CHANGED = 4;
// dllEntry and vmMain pointers.
const dllEntry = Module.getExportByName("jampgamex86.dll", "dllEntry");
const vmMainPtr = Module.getExportByName("jampgamex86.dll", "vmMain");
log("dllEntry: " + dllEntry);
log("vmMainPtr: " + vmMainPtr);
class vmMain {
onEnter (args:NativePointer[]) {
// First argument a.k.a args[0] is the one interacting with
// our switch case. Therefore, we will use it ourselves to find the player
// connect call.
switch (args[0].toInt32()) {
case GAME_INIT:
log('[+] Game init.');
break;
case GAME_SHUTDOWN:
log('[*] Game shutdown.');
break;
case GAME_CLIENT_CONNECT:
log('[+] Client connected.')
log("\tclientId: " + args[1].toInt32());
log("\tfirstTime: " + args[2].toInt32());
log("\tisBot: " + args[3].toInt32());
break;
case GAME_CLIENT_BEGIN:
log('[^] Client begin.');
log("\tclientId: " + args[1].toInt32());
break;
case GAME_CLIENT_USERINFO_CHANGED:
log('[~] Userinfo changed.');
log('\tclientId: ' + args[1].toInt32());
break
}
}
}
Interceptor.attach(vmMainPtr, new vmMain);
So in this case, we are getting the pointer to vmMain from jampgamex86.dll that the engine has loaded and intercept it to filter out connected clients.
And our script output with two new clients is:
____
/ _ | Frida 12.10.4 - A world-class dynamic instrumentation toolkit
| (_| |
> _ | Commands:
/_/ |_| help -> Displays the help system
. . . . object? -> Display information about 'object'
. . . . exit/quit -> Exit
. . . .
. . . . More info at https://www.frida.re/docs/home/
Attaching...
dllEntry: 0x200a5e70
vmMainPtr: 0x20092a10
[Remote::jampded.exe]-> 0x2
[+] Client connected.
clientId: 5
firstTime: 1
isBot: 0
[~] Userinfo changed.
clientId: 5
[^] Client begin.
clientId: 5
[+] Client connected.
clientId: 0
firstTime: 1
isBot: 0
[~] Userinfo changed.
clientId: 0
[^] Client begin.
clientId: 0
[*] Game shutdown.
[Remote::jampded.exe]->
Isn’t it weird that we were told that the second clientId is 0? But let’s check the server status client list :)
map: mp/ffa1
num score ping name lastmsg address qport rate
--- ----- ---- --------------- ------- --------------------- ----- -----
0 0 0 Padawan 0 192.168.56.1:29072 60970 25000
2 1 0 Desann 50 bot 0 16384
3 1 0 Reborn 50 bot 0 16384
4 0 0 Imperial Sabote 50 bot 0 16384
5 0 1 [DARK]S 0 192.168.56.1:29071 12395 25000
I will expand this blogpost further with the aim of proxying engine calls. :)
Have a good day!
No comments:
Post a Comment