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 find0x23ca2c0
. - Scan the memory to find a pointer whose the value is
0x23ca2c0
. We find0x23ca2a0
. - Scan the memory to find a pointer whose the value is
0x23ca2a0
. We find0x404038
.
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
andz
) 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.