CSAW CTF 2023: writeups
These are my notes for CSAW CTF 2023 (held in September 2023). All the challenges here are reverse engineering: rebug1, rebug2, Impossibrawler and rox. The first three have flags. For rox I recovered the input the binary wants, but its output is a decoy string, so I list it as unsolved.
Rev - rebug1
Files: rebug1.zip
The challenge gives an ELF binary, test.out, that asks for a string.
Reading the decompiled code, the program checks that a loop counter i equals 12 after the loop ends.
So any input string v8 that is 12 characters long makes the loop end with i = 12:
1
2
3
4
⚡halston ❯❯ ./test.out
Enter the String: AAAAAAAAAAAA
that's correct!
csawctf{c20ad4d76fe97759aa27a0c99bff6710}
The other way is to patch the program while debugging it so the check always passes.
Flag: csawctf{c20ad4d76fe97759aa27a0c99bff6710}
Rev - rebug2
Files: rebug2.zip
The challenge gives a binary called bin.
Looking at the control flow, the program builds the flag in a function named xoring. So I debug the program and read the data at that point.
Debugging it caused no problems.
Flag: csawctf{01011100010001110000}
Rev - Impossibrawler
Files: impossibrawler.zip
The challenge is a Godot game. I got Impossibrawler.exe and Impossibrawler.pck. The icon of the exe looks like a Godot export, so I guessed the pck was built with Godot and decompiled it with the GDRE tools.
The decompile gives many files, including audio, graphics and game data.
Going through the scripts, the flag is given after you pass level 1, reach level 2 and win it. Playing is hard because shots do not kill the target, which looks like it was done on purpose. Instead I rewrote the flag code in GDScript so it matches the program. The game reseeds the random number generator with a fixed seed, so the first randf() value is deterministic, and the flag is its hex-encoded string.
1
2
3
4
5
6
7
8
9
10
extends Node
var rng = RandomNumberGenerator.new()
func _ready():
rng.seed = 0
var fbytes1 = str(rng.randf())
var flg = fbytes1.to_ascii().hex_encode()
print("csawctf{" + flg + "}")
pass
Flag: csawctf{302e323032323732}
Rev - rox
Files: rox.zip
Status: unsolved (no csawctf{...} flag). The source writeup for this challenge was empty, so I analyzed the food binary statically with file, strings, objdump and a Python script.
food is a FreeBSD x86-64 ELF, not stripped. It takes one command line argument and passes it to verify. What I found:
verifyholds a 74-byte array on the stack and a globalvector<int>of 3497 values (copied from.rodata).- Stage 1 XORs the first
len(arg)bytes of the array with the argument. - Stage 2 sets
v[i] = G[(G[(10*i+12) % n] + arg[i % len]) % n] ^ v[i]. - Stage 3 runs a 300-round loop per byte from index 5 on, mixing in
j << 5and whetherv[i-5] == 0x6e. - The result is compared with a 74-byte string in
.rodata. A mismatch prints a random decoy such asflag{trying random strings isn't going to help}.
Since stage 3 and stage 2 are invertible per byte, I inverted them from the target string and tried every key length. Only length 29 is consistent, and the readable key is aN0ther_HeRRing_or_iS_tHis_iT. I checked it with a forward simulation in Python, and it reproduces the target string exactly:
1
2
3
4
5
6
7
8
key = b"aN0ther_HeRRing_or_iS_tHis_iT"
v = v0[:]
for i in range(len(key)): v[i] ^= key[i]
for i in range(N): v[i] = (G[(G[(10*i+12) % n] + key[i % len(key)]) % n] ^ v[i]) & 0xff
for i in range(5, N):
for j in range(300):
v[i] = (v[i] ^ ((j << 5) & 0xff) ^ (1 if v[i-5] == 0x6e else 0)) & 0xff
# bytes(v) == b"flag{ph3w...u finaLly g0t it! jump into cell wHen U g3t t0 the next cha11}"
The string the binary prints for the correct key is flag{ph3w...u finaLly g0t it! jump into cell wHen U g3t t0 the next cha11}. It is not in the csawctf{...} format, and it reads like a message to move on to the next challenge, so I do not claim it as the flag. I could not run the FreeBSD binary here to confirm the exact output, and I did not find any other csawctf{...} value in it.







