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 :

Fortunately, the game binary is not stripped so we have most of function names and global variable names, which made the reversing part easier.

Function list



\

game_start function

[+] 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 :

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 :)