Saturday, April 17, 2021

Proxy Quake VM with Frida

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

2023

Every year I start writing about a wrap-up of my year but I never end up finishing it. Hope this year is different. I'm starting with th...