I participated in this year's UIUCTF as a member of zer0pts
. We got 5th place! The competition was well-designed and had a lot of good challenges. The other, there were a few of a guess or boring challenges and unfair operations. I hope it will be fixed in the next competition.
[web] nookstop
There was a quite simple web page. It seems easy to get a flag. Just launch a click event for the flag
button by this script: $("#flag").click()
. It set a cookie clicked_flag_button=true
and reloaded the page.
<html> <head> <script src="/static/cookie.js"></script> <script src="/static/jquery.min.js"></script> </head> <body> <!-- im so sorry --> <div id="main"> <pre id="output" ></pre> <br><br> <div id="buttons"> <button type="button" id="status">Network Status</button> <div> <button type="button" id="flag" style="margin-left: 300px">Flag</button> </div> </div> <pre>Not that easy c: but here's a crumb: uiuctf{w</pre> </div> <script> async function print(m, ms) { $("#output").append("\n"); for (var i=0; i < m.length; i++) { var sel = $("#output"); sel.append(m[i]); await sleep(ms); } $("#output").append("\n"); } var msg1 = "Welcome to the Nook Stop, a magical multimedia terminal from Nook Inc.\nPlease select one of the following services:\n1) Network status\n2) Flag"; var msg2 = "Ah, sorry!! Our banking systems run on FORTRAN and they're a bit \non the slow side today.............................................\nyou know how this game's netcode can be sometimes, ahahahahaha!\nAlthought, come to think of it, they did say they were rolling out \nsome kind of new system the other day.......anyway, seems like that flag button isn't working right now." $(document).ready(function(){ $("#flag").click(function(){ console.log("TODO: シークレット_バックエンド_サービスを可能にする"); document.cookie = "clicked_flag_button=true"; location.reload(); }); f = $("#flag"); f.mousemove(function(){ var r = Math.random()*100; if (r < 25) f.css("margin-left", "+=100px"); else if (r >= 25 && r < 50) f.css("margin-left", "-=30px"); else if (r >= 50 && r < 75) f.css("margin-top", "+=50px"); else if (r >= 75 && r < 97) f.css("margin-top", "-=50px"); else f.remove(); }); $("#status").click(function(){ print(msg2,100); }); print(msg1,10); }); </script> </body> </html>
Then the following line is appended to HTML.
Not that easy c: but here's a crumb: uiuctf{w
To get full of the flag, we should set additionally cookie secret_backend_service=true
according to the log of the console. Then got a flag! uiuctf{wait_its_all_cookies?}
[crypto] isabelles_file_encryption
The following script was a challenge. Also blackmail_encrypted
was given.
#!/usr/bin/python # password-protect your files with this super powerful encryption! def super_secret_encryption(file_name, password): with open(file_name, "rb") as f: plaintext = f.read() assert(len(password) == 8) # I heard 8 character long passwords are super strong! assert(password.decode("utf-8").isalpha()) # The numbers on my keyboard don't work... assert(b"Isabelle" in plaintext) # Only encrypt files Isabelle has been mentioned in add_spice = lambda b: 0xff & ((b << 1) | (b >> 7)) ciphertext = bytearray(add_spice(c) ^ password[i % len(password)] for i, c in enumerate(plaintext)) with open(file_name + "_encrypted", "wb") as f: f.write(ciphertext) # use this to decrypt the file with the same password! def super_secret_decryption(file_name, password): with open(file_name + "_encrypted", "rb") as f: ciphertext = f.read() remove_spice = lambda b: 0xff & ((b >> 1) | (b << 7)) plaintext = bytearray(remove_spice(c ^ password[i % len(password)]) for i, c in enumerate(ciphertext)) with open(file_name + "_decrypted", "wb") as f: f.write(plaintext) with open("password", "rb") as f: # I got too lazy typing it in each time password = f.read() # Make sure to encrypt the text in the middle!!! super_secret_encryption("blackmail", password) super_secret_decryption("blackmail", password)
It must be included in the plaintext that the spice added (= bit rotated) Isabelle
and the key must be alphabetical 8 bytes. So using like the following script, we can enumerate list of (some rotation of) the key. And easily decrypt the blackmail by the given super_secret_decryption
function.
from ptrlib import xor add_spice = lambda b: 0xff & ((b << 1) | (b >> 7)) plaintext = bytes(add_spice(b) for b in b"Isabelle") ciphertext = open("blackmail_encrypted", "rb").read() for i in range(0, len(ciphertext) - 8): key = xor(plaintext, ciphertext[i:]) try: if key.decode().isalpha(): print(repr(key)) except UnicodeDecodeError: pass
The key was iSaBelLE
and the flag was in the mail: uiuctf{winner_winner_raccoon_dinner}
[crypto] coelacanth_vault
coelacanth_vault.py
#!/usr/bin/python import random from Crypto.Util import number from functools import reduce TOTAL = 15 THRESHOLD = 10 MAX_COELACANTH = 9 NUM_LOCKS = 5 NUM_TRIES = 250 # substitute for math.prod prod = lambda n: reduce(lambda x, y: x*y, n) def create_key(t, n, size=8): while True: seq = sorted([number.getPrime(size) for _ in range(TOTAL)]) if len(set(seq)) != len(seq): continue alpha = prod(seq[:t]) beta = prod(seq[-t + 1:]) if alpha > beta: secret = random.randint(beta, alpha) shares = [(secret % num, num) for num in seq] return secret, shares def construct_key(shares): glue = lambda A, n, s=1, t=0, N=0: (n < 2 and t % N or glue(n, A % n, t, s - A//n * t, N or n), -1)[n < 1] mod = prod([m for s, m in shares]) secret = sum([s * glue(mod//m, m) * mod//m for s, m in shares]) % mod return secret if __name__ == "__main__": print("Hi, and welcome to the virtual Animal Crossing: New Horizon coelacanth vault!") print("There are {} different cryptolocks that must be unlocked in order to open the vault.".format(NUM_LOCKS)) print("You get one portion of each code for each coelacanth you have caught, and will need to use them to reconstruct the key for each lock.") print("Unfortunately, it appears you have not caught enough coelacanths, and thus will need to find another way into the vault.") print("Be warned, these locks have protection against brute force; too many wrong attempts and you will have to start over!") print("") num_shares = abs(int(input("How many coelacanth have you caught? "))) if num_shares > MAX_COELACANTH: print("Don't be silly! You definitely haven't caught more than {} coelacanth!".format(MAX_COELACANTH)) print("Come back when you decide to stop lying.") quit() for lock_num in range(NUM_LOCKS): print("Generating key for lock {}, please wait...".format(lock_num)) secret, shares = create_key(THRESHOLD, TOTAL) r_secret = construct_key(random.sample(shares, THRESHOLD)) assert(secret == r_secret) print("Generated!") print("Here are your key portions:") print(random.sample(shares, num_shares)) for num_attempts in range(NUM_TRIES): n_secret = abs(int(input("Please input the key: "))) if secret == n_secret: print("Lock {} unlocked with {} failed attempts!".format(lock_num, num_attempts)) break else: print("Key incorrect. You have {} tries remaining for this lock.".format(NUM_TRIES - num_attempts)) else: print("BRUTE FORCE DETECTED. LOCKS SHUT DOWN.") print("Get out. You don't deserve what's in this vault.") quit() print("Opening vault...") with open("flag.txt", "r") as f: print(f.read())
It makes some of the shares by getting the remainder by the prime modulus of the secret. So it is recoverable by the Chinese Remainder Theorem. It seems impossible for the lack of shares, however, the 8-bit primes is at most 251 and we have 250 times to challenge. Thus we can pick a one of the large (and not in the given share) prime and bruteforce to hit the correct remainder.
The solution script:
from ptrlib import Socket, crt NUM_LOCKS = 5 NUM_TRIES = 250 sock = Socket("chal.uiuc.tf", 2004) sock.sendlineafter("caught? ", "9") primes = [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97, 101, 103, 107, 109, 113, 127, 131, 137, 139, 149, 151, 157, 163, 167, 173, 179, 181, 191, 193, 197, 199, 211, 223, 227, 229, 233, 239, 241, 251][::-1] for i in range(NUM_LOCKS): print(f"stage {i+1}") shares = eval(sock.recvlineafter("portions:\n").decode()) for p in primes: flag = True for s in shares: if s[1] == p: flag = False break if flag: prime = p break for x in range(251): v, _ = crt(shares + [(x, prime)]) sock.sendlineafter("key: ", str(v)) if b"unlocked" in sock.recvline(): break sock.interactive()
the flag was uiuctf{small_oysters_expire_quick}
[rev] Redd's Art
We're given the 64bit ELF binary. The point was sub_A5A
. It does xor bytes in address 973
by the key: the result of sub_91A
which was sum of string uiuctf{v3Ry_r341_@rTT}
. However the modification of .text
causes a segmentation fault. So I wrote the modification script
data = [0x4B,0x56,0x97,0xFB,0x4D,0x56,0x9D,0xF2,0x36,0x56,0xD9,0x5B,0xF6,0x17,0x1E,0x1E,0x1E,0x56,0x95,0x5B,0xF6,0x11,0xA8,0x1E,0x56,0x11,0xA0,0xDE,0x56,0x1F,0x5B,0xF6,0x56,0x95,0x5B,0xF6,0x11,0xA8,0x1E,0x96,0x5B,0xC5,0xD9,0x5B,0xC2,0x1E,0x1E,0x1E,0x1E,0xF5,0x2C,0x56,0x95,0x0B,0x65,0x08,0x3E,0x1E,0x95,0x5B,0xC2,0x56,0x86,0x56,0x1F,0xCE,0x11,0xA8,0x1E,0x97,0xDC,0x11,0xA8,0x5B,0xC5,0x93,0x12,0x1C,0x56,0x95,0x0B,0x7E,0x08,0x3E,0x1E,0x95,0x5B,0xC2,0x56,0x86,0x56,0x1F,0xCE,0x97,0xD4,0x96,0x0E,0x9D,0x5B,0xC2,0x1F,0x95,0x5B,0xC2,0x56,0x7D,0xC6,0x56,0x95,0x1B,0x5D,0x08,0x3E,0x1E,0x56,0x97,0xD9,0xF6,0xAD,0xE3,0xE1,0xE1,0x56,0x27,0xDD,0x6C,0xAA,0xA6,0x1E,0x1E,0x1E,0x1E,0xF6,0x00,0xE1,0xE1,0xE1,0x97,0x5B,0xFA,0xD9,0x5B,0xFE,0x1E,0x1E,0x1E,0x1E,0xF5,0x2E,0x56,0x95,0x0B,0x07,0x08,0x3E,0x1E,0x95,0x5B,0xFE,0x56,0x86,0x56,0x1F,0xCE,0x11,0xA8,0x16,0x95,0x5B,0xFA,0x97,0xD8,0x56,0x95,0x0B,0x1C,0x08,0x3E,0x1E,0x95,0x5B,0xFE,0x56,0x86,0x56,0x1F,0xCE,0x2F,0xEF,0x97,0xD4,0x96,0x0E,0x9D,0x5B,0xFE,0x1F,0x95,0x5B,0xFE,0x56,0x7D,0xC6,0x56,0x95,0x1B,0xFD,0x0B,0x3E,0x1E,0x56,0x97,0xD9,0xF6,0x4D,0xE3,0xE1,0xE1,0x56,0x27,0xDD,0x6C,0xA8,0x8E,0x56,0x9D,0xDA,0x36,0x45,0x43] key = sum(ord(c) for c in "uiuctf{v3Ry_r341_@rTT}") xored = [(d^key)&0xff for d in data] data = bytearray(open("ReddsArt", "rb").read()) data = data[:0x973] + bytes(xored) + data[0x973 + len(xored):] open("modified", "wb").write(data)
Then It tuned out that the sub_973
does add some constant x
to the each byte of string at 0x202028
, and after that, xor them by the key which calculated by sub_91A
.
The unknown x
was only 1 byte. So I brute-forced all candidates.
key = sum(ord(c) for c in "uiuctf{v3Ry_r341_@rTT}") xs = [ord(c) for c in "hthzgubI>*ww7>z+Ha,m>W,7z+hmG`"] for k in range(256): ys = "".join(chr((((k+x) % 256) ^ key) & 0xff) for x in xs) print(repr(ys))
the flag was: uiuctf{R_3dd$_c0Uz1n_D1$c0unT}