Github project : https://github.com/0xUKN/libeggnogg
[+] Presentation
During an EggNogg+ game with a friend, we realized that it would be convenient to have the possibility to play against an AI. As my friend is passionate about deep learning, he suggested to develop a reinforcement learning based AI for this game. In this purpose, I reverse-engineered the game and developped an instrumentation library with easy-to-use bindings in Python3, so that he could collect live informations and script the game to train his AI (we actually streamed on Twitch some AI training sessions).
[+] Reversing the game
To train the AI, we need to be able to collect live informations about the game (for example : each player’s position, current map, each player’s weapon, health bars…). As the game is not open source, I had to reverse-engineer it. I mainly used IDA and GDB to reverse the essentials parts of the game :
- main game loop
- most importants in-memory structures such as players, maps …
- most importants in-memory structures offsets
- understand how the game globally works
Fortunately, the game binary is not stripped so we have most of function names and global variable names, which made the reversing part easier.
\
[+] Instrumenting the game
I decided to instrument the game through a shared library mapped in the game’s memory space. I use two different techniques to inject the library in the game’s process :
- if the game is already running, I inject the library with my LibInjector, which basically uses
ptrace
syscall to pause the game, and modify its execution flow to calldlopen
on our library. - if the game is not running, we just need to use the LD_PRELOAD trick : run the game with
LD_PRELOAD=LIBEGGNOGG_PATH
in the environment.
The library executes its _init
function at loading, which basically starts everything we need in a new background thread.
namespace LibEggnogg
{
__attribute__((constructor))void _init(void)
{
void * libSDL_handle;
std::string LIBSDL;
puts("[+] Library loaded !");
std::thread background_task(init_libeggnogg_rpc_serv);
background_task.detach();
atexit(exit_libeggnogg_rpc_serv);
atexit(RemoveSharedMemory);
...
}
}
1- Live-information extraction from the game
After reversing the game, we now know where are located in memory the informations we need. So, I made a GameState
C structure which contains all these informations (actual leader, player’s position, key pressed …).
enum PlayerAction {DEFAULT, DEAD, DUCK, EGGNOGG, JUMP, KICK, NOTHING, PUNCH, RUN, STAB, STANCE, STUN, THROW};
typedef enum PlayerAction PlayerAction;
struct Player
{
bool isAlive;
unsigned char life;
float last_pos_x;
float last_pos_y;
float pos_x;
float pos_y;
bool hasSword;
float sword_pos_x;
float sword_pos_y;
signed char direction;
unsigned char bounce_ctr;
unsigned char contact_point;
unsigned char keys_pressed;
PlayerAction action;
};
typedef struct Player Player;
struct Sword
{
float pos_x;
float pos_y;
};
typedef struct Sword Sword;
struct GameState
{
Player player1;
Player player2;
unsigned char leader;
unsigned char room_number;
unsigned char total_room_number;
unsigned char winner;
unsigned char nb_swords;
Sword swords[MAX_SWORD_NUMBER];
};
typedef struct GameState GameState;
This structure is mapped into a shared memory, so that we can read this structure from another process (for example the AI process !).
GameState* InitGameState()
{
return (GameState*)CreateSharedMemory(SHMEM, sizeof(LibEggnogg::GameState));
}
Finally, the GameState
structure is regularly updated by the UpdateGameState
function.
//Shared Memory Functions
void UpdateGameState()
{
gs->player1.isAlive = !(*(bool *)(PLAYER1_ADDRESS + ISDEAD_OFFSET));
gs->player2.isAlive = !(*(bool *)(PLAYER2_ADDRESS + ISDEAD_OFFSET));
if(gs->player1.isAlive)
{
gs->player1.life = *(unsigned char *)(PLAYER1_ADDRESS + LIFE_OFFSET);
gs->player1.pos_x = *(float *)(PLAYER1_ADDRESS + POSX_OFFSET);
gs->player1.pos_y = *(float *)(PLAYER1_ADDRESS + POSY_OFFSET);
gs->player1.last_pos_x = gs->player1.pos_x;
gs->player1.last_pos_y = gs->player1.pos_y;
gs->player1.hasSword = !(*(bool *)(PLAYER1_ADDRESS + ISDISARMED_OFFSET));
...
}
...
}
This function is called after each frame thanks to a hook placed in the main game loop. This hook is applied by overwriting the GOT entry for SDL_NumJoysticks
function (this function is only called in this loop) by our custom SDL_NumJoysticks_hook
function.
__attribute__((constructor))void _init(void)
{
...
SDL_NumJoysticks_real_GOT = (void**)SDL_NumJoysticks_GOT;
*SDL_NumJoysticks_real_GOT = (void*)&SDL_NumJoysticks_hook;
...
}
...
int SDL_NumJoysticks_hook(void)
{
#ifdef DEBUG
puts("[+] Main loop hook called !");
#endif
UpdateGameState();
return ((int (*)(void))SDL_NumJoysticks_real)();
}
So, we now have a way to extract needed informations !
2- Scripting the game
When training the AI, it can be useful to have the possibility to send “commands” to the game. For example, if the game is over and you want to restart a new game without using the GUI, a neat game.restart()
function can be pretty handy. Another example, a game.setSpeed()
function capable of setting the game speed can be pretty handy too.
For this, I used an RPC server : when our library is loaded, it starts the RPC server, and it exposes functions we need.
//RPC Functions
void * set_speed_3_svc(u_long *argp, struct svc_req *rqstp) //set game speed
{
static char * result;
*logic_rate = *argp;
return (void *) &result;
}
u_long * get_speed_3_svc(void *argp, struct svc_req *rqstp) //get current game speed
{
static u_long result;
result = *logic_rate;
return &result;
}
static char * roomdef_result;
char ** get_roomdef_3_svc(void *argp, struct svc_req *rqstp) //get room definition
{
unsigned char current_room_number = (*(unsigned char *)(ROOM_NUMBER_ADDRESS)); //scaled from 0 (player1 win) to x (player2 win)
unsigned char total_room_number = (*(unsigned char *)(TOTAL_ROOM_NUMBER_ADDRESS)) - 1; //between 1 and x, rescaled from 0 to x-1
long room_number_in_mapdef = current_room_number - total_room_number; //scaled from 0 (center) to x (win maps for both player)
room_number_in_mapdef = (room_number_in_mapdef < 0) ? -room_number_in_mapdef : room_number_in_mapdef;
roomdef_result = *((char **)(ROOM_TEMPLATES_ADDRESS + room_number_in_mapdef * ROOM_TEMPLATE_SIZE));
return &roomdef_result;
}
static char * mapname_result;
char ** get_mapname_3_svc(void *argp, struct svc_req *rqstp) //get map name
{
mapname_result = *((char **)(MAP_NAME_ADDRESS));
return &mapname_result;
}
void * game_reset_3_svc(void *argp, struct svc_req *rqstp) //reset game
{
static char * result;
((void (*)())GAME_RESET_FUNCTION)();
return (void *) &result;
}
3- Python bindings
As the AI is Python based (and because I like Python), I made some C bindings for Python3. So, you can simply import the pyeggnogg
module and start to play !
import pyeggnogg as EggNogg
lib_path = "../bin/libeggnogg.so"
executable_path = "../../eggnoggplus"
EggNogg.init(lib_path, executable_path)
EggNogg.setSpeed(15)
gs = EggNogg.getGameState()
print(gs)
{
"player1": {
"isAlive": True,
"life": 100,
"pos_x": 2872.0,
"pos_y": 111.8499984741211,
"last_pos_x": 2872.0,
"last_pos_y": 111.8499984741211,
"hasSword": True,
"sword_pos_x": 2878.0,
"sword_pos_y": 111.8499984741211,
"direction": 1,
"bounce_ctr": 0,
"contact_point": 1,
"keys_pressed": 0,
"action": 10,
},
"player2": {
"isAlive": True,
"life": 100,
"pos_x": 2995.28955078125,
"pos_y": 111.8499984741211,
"last_pos_x": 2995.28955078125,
"last_pos_y": 111.8499984741211,
"hasSword": True,
"sword_pos_x": 3001.28955078125,
"sword_pos_y": 111.8499984741211,
"direction": 1,
"bounce_ctr": 0,
"contact_point": 1,
"keys_pressed": 0,
"action": 10,
},
"leader": 0,
"room_number": 5,
"total_room_number": 11,
"nb_swords": 0,
"swords": {},
}
It is also possible to dump the actual map from memory and monitor it from Python.
[+] Bye
Feel free to tell me what you think about this post :)