Warsow, first assault (part 2)

Warsow, first assault (part 2)

Introduction

In this post, we will continue to play with Warsow (see the first article) and exposes some basic knowledge about pointers paths and /proc/mem.

Pointers paths?

I don’t know if pointers paths is the real name but I’ll use it in the rest of the article. Last day, we saw where is the struct of the first opponent in our Warsow memory. However, due to the ASLR, the struct location changes between two executions. Thus we need a little trick to retrieve its address between each running.

In fact, we have to find a path of pointers from a static one to the struct address. A static pointer is a pointer whose its address didn’t change between two executions. For example:

#include <stdio.h>
#include <stdlib.h>

static int* opponent = NULL;

int main(int argc, char** argv)
{
    opponent  = malloc(sizeof(*opponent));
    *opponent = 5;

    printf("opponent addr:   %p\n", &opponent);
    printf("opponent value:  %p\n", opponent);
    printf("*opponent value: %d\n", *opponent);
    return 0;
}

In this small code, opponents is a static variable. It resides into a segment of the executable (ELF file) and is loaded into the process memory at the execution. Because the segments of the executable are not randomly loaded into the memory, their addresses remain the same (the ASLR only works on position independent executables, besides I recommended you to read the first answer of this StackExchange question). Here are the result of two executions:

$ ./a.out 
opponent addr:   0x404038
opponent value:  0x1ba12a0
*opponent value: 5

$ ./a.out 
opponent addr:   0x404038
opponent value:  0x14582a0
*opponent value: 5

No big surprise, the opponent address is constant (0x404038) whereas its value is variable (0x1ba12a0 then 0x14582a0). In this case, how to find the *opponent value at each execution? Simply by finding a static pointer on it. In this case, it’s simply opponent. Below is a simple memory representation of the process. In green, the static variable ; its address never change. In red, the dynamically allocated variable whose address changes.

In a more complex executable, a path may be more intricate, for example:

#include <stdio.h>
#include <stdlib.h>

static int*** opponent = NULL;

int main(int argc, char** argv)
{
    opponent    = malloc(sizeof(*opponent));
    *opponent   = malloc(sizeof(**opponent));
    **opponent  = malloc(sizeof(***opponent));
    ***opponent = 5;

    printf("opponent addr:    %p\n", &opponent);
    printf("*opponent addr:   %p\n", opponent);
    printf("**opponent addr:  %p\n", *opponent);
    printf("***opponent addr: %p\n", **opponent);
    printf("***opponent:      %d\n", ***opponent);

    return 0;
}

In this code, the path between the static variable opponent and the value is longer. Two executions of this code gives:

$ ./a.out 
opponent addr:    0x404038
*opponent addr:   0x13162a0
**opponent addr:  0x13162c0
***opponent addr: 0x13162e0
***opponent:      5

$ ./a.out 
opponent addr:    0x404038
*opponent addr:   0x23ca2a0
**opponent addr:  0x23ca2c0
***opponent addr: 0x23ca2e0
***opponent:      5

In order to grasp the concept, here is the memory representation of the last execution:

To draw a parallel between this example and our Warsow game, the opponent struct we found in the last article is the value 5 at 0x23ca2e0 in this article. Because its address changes at each execution, we have to find a static address (the green one in the example) to follow the pointers path until the struct.

To make this path, we can use a basic strategy. In our example, imagine that we scanned the process memory and found the address of the value 5 (0x23ca2e0). Now:

  • Scan the memory to find a pointer whose the value is 0x23ca2e0. We find 0x23ca2c0.
  • Scan the memory to find a pointer whose the value is 0x23ca2c0. We find 0x23ca2a0.
  • Scan the memory to find a pointer whose the value is 0x23ca2a0. We find 0x404038.

These steps allowed us to make the complete path from the static pointer to our value. A question may arise: how do you know when to stop the algorithm? When the found address is in a mapping of our executable or one of its dynamically loaded library.

Pointers path in Warsow

By chance, the method outlined above is not tedious on Warsow. In fact, the path is very short:

libcgame_x86_64.so+4670C0 -> opponent struct

Thus, the value at the address libcgame_x86_64.so (the start address of the library mapping) plus the offset 0x4670C0 is the address of the opponent struct.

Now, we have all the information to code a simple POC.

Useless POC

Step 0: init

I used my proc library for this POC. In the next excerpts the warsow variable is just a Process instance with the plugin readmem.proc_mem_read. This plugin reads the memory of the process only by accessing the /proc/pid/mem file. Thus, when you’ll see something like warsow.read_mem(offset, size), the plugin just opens the mem file, seeks at offset and reads size bytes.

warsow = Process(args.pid, [readmem.proc_mem_read()])

Step 1: get the base address of libcgame_x86_64.so

A simple parsing of the /proc/pid/maps is enough:

def libcgame_filter(m):
    '''A filter used by `get_libcgame`.'''
    return 'libcgame_x86_64.so' in m.pathname and 'x' in m.perms

def get_libcgame(warsow):
    '''Get the libcgame_x86_64.so address.'''
    mem_path = f'/proc/{warsow.pid}/mem'
    mappings = warsow.get_maps(libcgame_filter)
    if len(mappings) != 1:
        raise RuntimeError('Invalid mappings number found')
    return mappings[0]

Step 2: compute the opponent struct address

BASE_PTR_OFFSET = 0x4670C0

#[...]

def compute_opponent_addr(warsow, libcgame):
    '''Compute the opponent struct address.'''
    base_ptr = libcgame.start_address + BASE_PTR_OFFSET
    raw      = warsow.read_mem(base_ptr, 8)
    opp_addr = struct.unpack('<Q', raw)[0]
    return opp_addr

Step 3: read the /proc/pid/mem file

The /proc/pid/mem file allows to read the memory of any process (only readable mappings). Thus, we have all the data to retrieve the opponent position:

  • Its structure address.
  • The offset of each field (x, y and z) in this structure (found in the first article).

Here is the code:

FIELDS_OFFSETS  = {
    'x'      : 0x1C,
    'y'      : 0x20,
    'z'      : 0x24,
}

# [...]

for field, offset in FIELDS_OFFSETS.items():
        raw   = warsow.read_mem(opp_addr + offset, 4)
        value = struct.unpack('<f', raw)[0]
        print(f'{field}: {value}')

Final result

Warning, it’s just a ugly, useless and (really) inefficient POC:

import argparse
import struct
import time

from deedee.proc         import Process
from deedee.proc.plugins import readmem


BASE_PTR_OFFSET = 0x4670C0

FIELDS_OFFSETS  = {
    'x'      : 0x1C,
    'y'      : 0x20,
    'z'      : 0x24,
}


def libcgame_filter(m):
    '''A filter used by `get_libcgame`.'''
    return 'libcgame_x86_64.so' in m.pathname and 'x' in m.perms

def get_libcgame(warsow):
    '''Get the libcgame_x86_64.so address.'''
    mem_path = f'/proc/{warsow.pid}/mem'
    mappings = warsow.get_maps(libcgame_filter)
    if len(mappings) != 1:
        raise RuntimeError('Invalid mappings number found')
    return mappings[0]

def compute_opponent_addr(warsow, libcgame):
    '''Compute the opponent struct address.'''
    base_ptr = libcgame.start_address + BASE_PTR_OFFSET
    raw      = warsow.read_mem(base_ptr, 8)
    opp_addr = struct.unpack('<Q', raw)[0]
    return opp_addr

def compute_fields_addr(opp_addr):
    addrs = dict((k, v + opp_addr) for k, v in FIELDS_OFFSETS.items())
    return addrs

def display_fields(warsow, fields):
    for field, offset in fields.items():
        raw   = warsow.read_mem(offset, 4)
        value = struct.unpack('<f', raw)[0]
        print(f'{field}: {value}')


if __name__ == '__main__':
    parser = argparse.ArgumentParser(description='A simple POC about Warsow')
    parser.add_argument('pid', type=int, help='Warsow pid')
    args = parser.parse_args()

    warsow   = Process(args.pid, [readmem.proc_mem_read()])
    libcgame  = get_libcgame(warsow)
    opp_addr = compute_opponent_addr(warsow, libcgame)
    fields   = compute_fields_addr(opp_addr)

    while True:
        display_fields(warsow, fields)
        time.sleep(0.1)
        print('\033c', end='')

Demonstration

I already show a demo into the first article. But here is another one:

The end

Perhaps I will write a bonus article showing how to achieve the same result but by using a shared library injection.

Edit: you can find the bonus article here.

If you have any comments, be free to share them with me.

Related articles

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur comment les données de vos commentaires sont utilisées.