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

Alright, that is enough Q3VM for now…

Friday, April 16, 2021

Reading C++'s std::string with Frida

 Something that is very interesting to us is the ability to read strings, however this is not always possible by simply calling FRIDA’s readUtf8String/readCString built-ins due to the different ways a string can be represented. For example, Window’s UNICODE_STRING is defined in a struct as follows:

typedef struct _UNICODE_STRING {

  USHORT Length;

  USHORT MaximumLength;

  PWSTR  Buffer;


A common string type to parse is a C++ std::string. A similar concept will be seen in Swift.String’s datatype later on. For std::string’s the LSB (Least Significant Bit) will store 0 if the it is a short string (< 22 bytes) or 1 for long strings. If it is a long string, the pointer to the string we want to get will be stored at two times the Process.pointerSize of the process we are attached to. To test this knowledge out and see how to obtain the string, let’s see this simple program:

#include <iostream>

void print_std_string(std::string arg_1)


std::cout << arg_1 << std::endl;





std::string my_string = "FRIDA is great, you should check it out at";


return 0;


This program simply calls the print_std_string(std::string arg_1) function and prints it to screen. This way it is easy to get the std::string parameter and inspect it. Once we fire up this program in FRIDA’s REPL and run Module.enumerateExportsSync() on our binary we notice that names are mangled, but due to the name we have chosen for the test function we can spot a mangled function named _Z16print_std_stringNSt3__112basic_stringIcNS_11char_traitsIcEENS_9allocatorIcEEEE. This is the function we want to use Interceptor.attach on.

Interceptor.attach(Module.getExportByName(null, '_Z16print_std_stringNSt3__112basic_stringIcNS_11char_traitsIcEENS_9allocatorIcEEEE'), {

onEnter (args) {

const LSB = args[0].readU8() & 1;

console.log('LSB: ' + LSB);

const stdString = args[0].add(Process.pointerSize*2).readPointer().readUtf8String();

console.log("std::string: " + stdString);



Then, we can run this small script and get the following output:

LSB: 1

std::string: FRIDA is great, you should check it out at

[Local::a.out]-> Process terminated

It is important to address that this was tested using clang++ 12.0.0, the memory layout may differ within compilers such as GCC which implements unions to store small strings


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...