Reversing Devil Daggers

Index | Up


Table of Contents

1. About the game

Devil Daggers is a very fun and simple first-person shooter with a 1990s visual style. The game uses it’s own engine, which makes the reversing task very interesting. It also uses the verse 2.0 protocol.

I will be using the linux version of the steam game, currently v3.2.

For reversing, I will use cutter.

2. Getting general information about the game

If we open the devildaggers binary in cutter, and go through the function list, we can immediately get some useful information. The game uses ImGui for the menus, and we can see a lot of functions with enemy names inside the verse2 namespace. We also see some hx2 namespace which contains some utility functions.

Fortunately for us, cutter is able to display a lot of RTTI data. We can display the “Classes” window from “Windows > Info… > Classes”. If we scroll down a bit, we can start to see these verse2 classes:

devildaggers1.png

We can asume that there is going to be some information related to the player inside verse2::hero::*. Some functions that look interesting are hero::step(), hero::take_hit() and hero::change_state_dead().

3. Making a simple godmode with detour hooking

3.1. Getting the function pointer

Let’s start by getting a pointer to the function we are interested in. Since these symbols are exported, we can use dlsym() to get a pointer.

First, we need to get a handler to our program. Since these function are inside the main program, can get our handle by passing NULL to dlopen().

dlopen(3)

The function dlopen() loads the dynamic shared object (shared library) file named by the null-terminated string filename and returns an opaque “handle” for the loaded object. […]

If filename is NULL, then the returned handle is for the main program. […]

The first parameter will be RTLD_LAZY | RTLD_NOLOAD because we want lazy binding (resolve symbols as the code that references them is executed) and we don’t want to load the shared object (since it’s already loaded).

To get the pointer to a symbol, we can use dlsym() with the handler we just got and the symbol name.

If we want to avoid dying, verse2::hero::change_state_dead() looks like a good place to start. I already had a look at this function, and it’s not ideal for our purpose, so we will use verse2::hero::take_hit() instead (since a single hit kills us anyway).

To get the real symbol name, we can either search for “take_hit” inside the “Symbols” window (“Windows > Info… > Symbols”) or search using readelf and grep:

readelf --wide --syms devildaggers | grep "hero.*take_hit"

We find that the symbol name is “_ZN6verse24hero8take_hitEbN5verse10death_typeEb”. In there, we can also see the type of the parameters.

devildaggers2.png

The only thing we are missing is the return type. Since the returned value is stored in rax (or eax, for 32-bits) for integer numbers, we have to look at the function itself to determine what kind of value is being returned. We can see that the value inside the ebp register is always being moved into eax before returning.

devildaggers3.png

If we scroll up a bit, we can see that the ebp register is either set to zero just before returning, or it’s set to the result of some kind of or operation before continuing with the execution.

devildaggers4.png

We can safely asume that the function is not only returning a boolean, but also returning false when something goes wrong (hit was not valid).

The final code for finding verse2::hero::take_hit() looks like this:

#define SYM_HERO_TAKE_HIT "_ZN6verse24hero8take_hitEbN5verse10death_typeEb"

bool globals_init(void) {
    void* handle_self = dlopen(NULL, RTLD_LAZY | RTLD_NOLOAD);
    if (!handle_self) {
        fprintf(stderr, "dlopen() returned NULL\n");
        return false;
    }

    typedef void (*hero_take_hit_t)(Hero*, bool, int, bool);
    hero_take_hit_t hero_take_hit = dlsym(handle_self, SYM_HERO_TAKE_HIT);
    if (!hero_take_hit) {
        fprintf(stderr, "dlsym() returned NULL\n");
        return false;
    }
}

3.2. Hooking verse2::hero::take_hit()

We will be using the detour hooking method I explained in my detour hooking article, specifically we will be using my own detour hooking library.

Hooking using my library is very simple, we just need to declare the function type using a macro, declare and initialize the detour context and enable the hook.

#include "detour.h"

/* Make sure these are accesible by the hook and the init function */
DETOUR_DECL_TYPE(bool, hero_take_hit, Hero*, bool, int, bool);
detour_ctx_t ctx_hero_take_hit;

static bool hook_hero_take_hit(Hero* thisptr, bool rsi, int death_type, bool rcx) {
    /* TODO: Implement our hook */
}

/* Call when our shared object is injected */
void hooks_init(void) {
    detour_init(&ctx_hero_take_hit, hero_take_hit, hook_hero_take_hit);
    detour_add(&ctx_hero_take_hit);
}

/* Call when unloading our shared object */
void hooks_restore(void) {
    detour_del(&ctx_hero_take_hit);
}

When the hook is enabled, all the calls to verse2::hero::take_hit() will instead call our hook_hero_take_hit() function. From there, we can simply return false without doing anything else to create a simple godmode. If we want to disable the godmode, we can simply call the original function and return its value:

/* TODO: Toggle this setting from some kind of menu */
bool godmode = true;

static bool hook_hero_take_hit(Hero* thisptr, bool rsi, int death_type, bool rcx) {
    /* Return false without calling the original */
    if (godmode)
        return false;

    /* Call original hero::take_hit(), save the result and return it */
    bool ret;
    DETOUR_ORIG_GET(&ctx_hero_take_hit, ret, hero_take_hit, thisptr, rsi,
                    death_type, rcx);
    return ret;
}

The only thing left is creating our constructor and destructor functions:

__attribute__((constructor)) /* Entry point when injected */
void load(void) {
    /* Find function address */
    globals_init();

    /* Hook it */
    hooks_init();

    loaded = true;
}

__attribute__((destructor)) /* Entry point when unloaded */
void unload() {
    if (!loaded)
        return;

    /* Unhook it */
    hooks_restore();
}

When compiling, remember to link with the detour.c source and to use -fPIC to avoid relocation errors.

4. Enabling the hidden particle and level editors

While searching for some strings (specifically “Score submitted”, which appears when you die), I found a verse2::VERSE::step_ui() function which seems to be used for rendering most of the menus. In there, I found some interesting code related to some “PARTICLES” and “PROGRESSION” windows.

devildaggers5.png

The only thing that stops the program from rendering these menus is some value located at [rax + 0x488] and [rax + 0x489]. We can see that the value that was loaded into rax is verse2::globals.

devildaggers6.png

4.1. Getting a pointer to verse2::globals

At this point, I already hooked another function called verse2::hero::step(), which is useful since it’s executed every tick and contains a pointer to the hero. That might be a good place to enable and disable these verse2::globals[0x488] values.

To get the pointer, I had to use a different method than usual. I looked at the functions I was using, and I saw that hero::step() also uses verse2::globals internally.

devildaggers7.png

Before trying to understand the next part, it’s important to note that verse2::globals is not the data structure itself, but a pointer to that structure. Therefore we are not interested in the address of verse2::globals, but the address of the structure that verse2::globals is pointing to. We will obviously need to get the address of the verse2::globals pointer before accessing its contents.

As you can see, the address of verse2::globals is relative to the instruction (to rip). To get the absolute address, we need to:

  1. Get the verse2::globals offset. We are interested in the last 4 bytes of the highlighted instruction (0x003A7BE3).
    • Get the offset of these 4 bytes relative to the function start. We can skip the first 3 bytes of the instruction to get the last 4, and since the instruction is at func_start+0x3E, we know that the final offset is 0x41.
    • Add the offset to the function pointer. Since we have a pointer to the start of hero::step(), we can simply add 0x41 to get the address of these 4 bytes.
    • Dereference this pointer to get the 4 bytes we want.
  2. Get the address of the next instruction. Since the relative offset we just got is 32 bits, we just need to add 4 (sizeof(uint32_t)) to the pointer from the previous step.
  3. Add the relative verse2::globals offset (0x003A7BE3) to the address of this next instruction, to get the absolute address of verse2::globals.

As I said earlier, this is the address of verse2::globals, not the address of the structure it’s pointing to. To get the address of the structure, we simply dereference this pointer once more. Now we have a “clone” of verse2::globals that can be used to access the actual structure.

From the code:

/* Add 0x41 to the function start. Point to 0x003A7BE3 */
uint32_t* relative_offset_address = (uintptr_t)hero_step + 0x41;

/* Dereference to get 0x003A7BE3 */
uint32_t verse_globals_relative = *offset_address;

/* Store address of the next instruction (func_start+0x45) */
void* next_instruction = (void*)offset_address + sizeof(uint32_t);

/* Calculate pointer TO verse2::globals */
void* verse_globals_absolute = next_instruction + verse_globals_relative;

/* Dereference once to get the value stored in verse2::globals */
void* verse_globals = *(void**)verse_globals_absolute;

/* Now we can access the members */
verse_globals->my_member = true;

Or in a more compact way using a RELATIVE2ABSOLUTE() macro:

/* Location of address + Size of offset + Offset */
#define RELATIVE2ABSOLUTE(ADDR) (void*)((void*)(ADDR) + 4 + *(uint32_t*)(ADDR))

void* verse_globals = *(void**)RELATIVE2ABSOLUTE((uintptr_t)hero_step + 0x41);

4.2. Declaring a verse_globals_t struct

Since we want to change two booleans for the level and particle editors, we should declare our own verse_globals_t structure with the offsets we got before (0x488 and 0x489):

typedef struct verse_globals_t verse_globals_t;
struct verse_globals_t {
    uint8_t pad[0x488];
    bool particle_editor; /* 0x488 */
    bool level_editor;    /* 0x489 */
} __attribute__((packed));

It’s not a bad idea to use __attribute__((packed)) since paddings in more complex structures with different data types can be counter-intuitive, since gcc itself adds paddings to smaller types.

We just need to change the declaration of verse_globals to match our new structure:

verse_globals_t* verse_globals;

4.3. Enabling and disabling the editors

Now we can simply set these two members to true, if the user wants to enable the editors. I set them inside my verse2::hero::step() hook:

static bool hook_hero_step(Hero* thisptr, float rsi) {
    if (LEVEL_EDITOR)
        verse_globals->level_editor = true;
    if (PARTICLE_EDITOR)
        verse_globals->particle_editor = true;

    /* ... */
}

Note that the game detects if they are enabled, and will mark your score as invalid. This could be easily bypassed, but I am not going to do it here (Hint: have a look at verse2::VERSE::step_ui())

devildaggers8.png

devildaggers9.png

Don’t forget to set them to false when unloading the library (in unload()).

5. Enabling the top-down camera outside of replays

There is a top-down camera mode available when watching a replay, so I thought it would be interesting to enable it while playing.

To get the offset of this boolean, I simply scanned the process using GameConqueror to find that there were two values being set to zero when the top-down camera was disabled, and to one when it was enabled (in a replay). These two values correspond to the members of the verse2::hero and verse2::globals structures. I decided to get the offset inside the globals by subtracting the address I got in GameConqueror by the base address of verse2::globals.

The final offset was 0x2BC, so I simply updated my verse_globals_t struct:

typedef struct verse_globals_t verse_globals_t;
struct verse_globals_t {
    uint8_t pad0[0x2BC];
    bool top_down_cam;    /* 0x2BC */
    uint8_t pad1[0x1CB];
    bool particle_editor; /* 0x488 */
    bool level_editor;    /* 0x489 */
} __attribute__((packed));

I am not sure where this value is set from the game’s code, but if you have a suggestion feel free to contribute to this article.

devildaggers10.png

6. Complete source code

The complete source code with some extra features can be found in 8dcc/devildaggers-re.