Warsow, first assault (bonus)
Introduction
This article is the continuation of the last post about Warsow. In this one, we will see how to achieve the same result than the useless tool describes previously but, this time, with a shared library.
The theory
If you read the article series about shared library injection, the method exposed here will not be hard to understand. This one relies on the same few steps already described:
- Get the base address of
libcgame_x86_64.so
. - Compute the opponent position address.
- Read the memory.
- Use a thread
The practice
Step 1: get the base address of libcgame_x86_64.so
Like the previous Python tool, our shared library can parse the maps file but that’s not a very clever solution. The library has all the accesses to the process memory in which it is loaded so it is possible to use dlopen
(man page). If the library is already loaded, it just returns a handle on it. This handle contains some information including the lib base address. Here is the code:
#include <dlfcn.h>
#include <link.h>
static char* libcgame_path = "libcgame_x86_64.so";
static void* get_libcgame_addr()
{
void* handler = dlopen(libcgame_path, RTLD_NOW);
if (handler == NULL)
return NULL;
return (void*)((struct link_map*)handler)->l_addr;
}
Step 2/3: compute the opponent position address and read its values
We just have to use the previously found offsets:
static off_t base_ptr_off = 0x4670C0;
static off_t pos_off = 0x1C;
/* the static pointer into libcgame */
void* opp_ptr = (char*)libcgame + base_ptr_off;
/* the value of opp_ptr is the address of the opp struct */
void* opp_addr = *(char**)opp_ptr;
/* use the pos_off to get the position field (3 consecutive floats) */
float* opp_pos = (float*)((char*)opp_addr + pos_off);
To read the position:
opp_pos[0] /* x */
opp_pos[1] /* y */
opp_pos[2] /* z */
Step 4: use a thread
If you remember, when we inject a library, its constructors are run by one of the victim process thread. So we can not make a simple infinite loop in the constructor of our library. However, the constructor can just start a thread which does the infinite reading loop:
#include <pthread.h>
static pthread_t thread;
static bool is_running;
static void* pos_reader(void* _)
{
/* [...] */
while (1)
read_the_memory();
}
/* call when the library is loaded */
__attribute__((constructor))
static void run()
{
is_running = true;
pthread_create(&thread, NULL, pos_reader, NULL);
}
/* call when the library is unloaded */
__attribute__((destructor))
static void stop()
{
is_running = false;
pthread_join(thread, NULL);
}
Final result
Here is the final code:
#include <stdio.h>
#include <stdbool.h>
#include <pthread.h>
#include <dlfcn.h>
#include <link.h>
#include <unistd.h>
/*
* Some useful constants.
*/
static char* libcgame_path = "libcgame_x86_64.so";
static off_t base_ptr_off = 0x4670C0;
static off_t pos_off = 0x1C;
/*
* Thread related variables.
*/
static pthread_t thread;
static bool is_running;
static void* get_libcgame_addr()
{
void* handler = dlopen(libcgame_path, RTLD_NOW);
if (handler == NULL)
return NULL;
return (void*)((struct link_map*)handler)->l_addr;
}
static void* pos_reader(void* _)
{
void* libcgame = get_libcgame_addr();
if (libcgame == NULL)
return NULL;
void* opp_ptr = (char*)libcgame + base_ptr_off;
void* opp_addr = *(char**)opp_ptr;
float* opp_pos = (float*)((char*)opp_addr + pos_off);
while (is_running)
{
printf("%f, %f, %f\n", opp_pos[0], opp_pos[1], opp_pos[2]);
usleep(10000);
}
return NULL;
}
__attribute__((constructor))
static void run()
{
is_running = true;
pthread_create(&thread, NULL, pos_reader, NULL);
}
__attribute__((destructor))
static void stop()
{
is_running = false;
pthread_join(thread, NULL);
}
Warning: the path of the libcgame
changes between two executions. Thus it is necessary to run the game, get the libcgame
path, modify the libcgame_path
variable, compile the lib and inject it.
Demonstration
In this demonstration, I used my dummy injector to inject the library:
Improvements
The thread method is not necessarily the best one. For example, it may be smarter to hook a function into the Warsow process to call our code. A good candidate is a function called at each frame.