この大会は2024/5/5 0:00(JST)~2024/5/6 14:00(JST)に開催されました。
今回は個人で参戦。結果は1422点で378チーム中73位でした。
自分で解けた問題をWriteupとして書いておきます。
Join Discord! (misc)
Discordに入り、#announcementsチャネルのメッセージを見ると、フラグが書いてあった。
squ1rrel{hope_you_learn_something_:)}
Secret Coffee Nut Stash (rev)
package defpackage; import java.util.Scanner; /* renamed from: CoffeNutStash reason: default package */ /* loaded from: CoffeNutStash.class */ public class CoffeNutStash { private static final int[] expected = {578, 568, 588, 248, 573, 573, 508, 543, 618, 258, 553, 533, 243, 608, 478, 608, 243, 588, 573, 478, 533, 263, 593, 263, 478, 498, 243, 513, 513, 258, 258, 478, 273, 258, 288, 253, 278, 263, 628}; public static void main(String[] strArr) { System.out.println("Welcome to the Coffee Nut Stash!"); System.out.println("Enter the password? "); Scanner scanner = new Scanner(System.in); String next = scanner.next(); scanner.close(); char[] charArray = next.toCharArray(); if (charArray.length != expected.length) { System.out.println("Incorrect password!"); return; } for (int i = 0; i < charArray.length; i++) { if ((charArray[i] * 5) + 3 != expected[i]) { System.out.println("Incorrect password!"); return; } } System.out.println("Correct!"); } }
入力文字の各文字について、5を掛けて3足したものがexpectedの各要素になることがパスワードの条件になっている。逆算してパスワードを導き出す。
#!/usr/bin/env python3 expected = [578, 568, 588, 248, 573, 573, 508, 543, 618, 258, 553, 533, 243, 608, 478, 608, 243, 588, 573, 478, 533, 263, 593, 263, 478, 498, 243, 513, 513, 258, 258, 478, 273, 258, 288, 253, 278, 263, 628] flag = '' for c in expected: flag += chr((c - 3) // 5) print(flag)
squ1rrel{3nj0y_y0ur_j4v4_c0ff33_639274}
Lazy RSA (crypto)
RSA暗号。nをhttps://www.dcode.fr/prime-factors-decompositionで素因数分解する。
n = 136883787266364340043941875346794871076915042034415471498906549087728253259343034107810407965879553240797103876807324140752463772912574744029721362424045513479264912763274224483253555686223222977433620164528749150128078791978059487880374953312009335263406691102746179899587617728126307533778214066506682031517 * 173071049014527992115134608840044450224804187710129859708853805709176487316207010402251651554296674942983458628001825388092613984020357016543095854903752286499436288875811897772811421580394898931781960982007306544027009178109074133665714245347548210688178519450728052309689045110008994598784658702110905581693
あとは通常通り復号する。
#!/usr/bin/env python3 from Crypto.Util.number import * with open('lazyrsa.txt', 'r') as f: params = f.read().splitlines() n = int(params[0].split(' ')[-1]) e = int(params[1].split(' ')[-1]) ct = int(params[2].split(' ')[-1]) p = 136883787266364340043941875346794871076915042034415471498906549087728253259343034107810407965879553240797103876807324140752463772912574744029721362424045513479264912763274224483253555686223222977433620164528749150128078791978059487880374953312009335263406691102746179899587617728126307533778214066506682031517 q = 173071049014527992115134608840044450224804187710129859708853805709176487316207010402251651554296674942983458628001825388092613984020357016543095854903752286499436288875811897772811421580394898931781960982007306544027009178109074133665714245347548210688178519450728052309689045110008994598784658702110905581693 assert p * q == n phi = (p - 1) * (q - 1) d = inverse(e, phi) m = pow(ct, d, n) flag = long_to_bytes(m).decode() print(flag)
squ1rrel{laziness_will_be_the_answer_eventually}
RSA RSA RSA (crypto)
RSA暗号。eが3でn, cのペアが3つあるので、Hastad's Broadcast Attackで復号する。
#!/usr/bin/env python3 from Crypto.Util.number import * from sympy.ntheory.modular import crt from gmpy2 import iroot with open('rsarsarsa.txt', 'r') as f: params = f.read().splitlines() e = int(params[0].split(' ')[-1]) n1 = int(params[1].split(' ')[-1]) ct1 = int(params[2].split(' ')[-1]) n2 = int(params[3].split(' ')[-1]) ct2 = int(params[4].split(' ')[-1]) n3 = int(params[5].split(' ')[-1]) ct3 = int(params[6].split(' ')[-1]) ns = [n1, n2, n3] cs = [ct1, ct2, ct3] me, _ = crt(ns, cs) m, success = iroot(me, e) assert success flag = long_to_bytes(m).decode() print(flag)
squ1rrel{math_is_too_powerful_1q3y41t1s98u23rf8}
squ1rrel treasury (crypto)
サーバの処理概要は以下の通り。
・ACCOUNT_NAME_CHARS: 英大文字、英小文字からなる集合 ・FLAG_COST: 10**13以上10**14-1以下のランダム整数 ・以下繰り返し ・opt: 数値入力 ・optが0の場合、accountLogin()実行 ・account: 入力 ・account = Account.load(account) ・key_split: accountを':'区切りにした配列 ・iv: key_split[0]のhexデコード ・ct: key_split[1]のhexデコード ・pt: ctの復号したものの16バイトのブロックごとに配列にしたもの ・ct: ctの16バイトのブロックごとに配列にしたもの ・ptの各ブロックbについて以下を実行 ・最初のブロックの場合 ・pt[i] = strxor(p, iv) ・最初のブロック以外の場合 ・pt[i] = strxor(strxor(ct[i-1], pt[i-1]), p) ・pt: ptの結合文字列 ・pt_split: ptを':'区切りにした配列 ・name: pt_split[0] ・balance: pt_split[1]のアンパディングしたものの数値 ・Account(iv, name, balance)を返却 ・account.__nameを表示 ・以下繰り返し ・FLAG_COSTを表示 ・opt: 数値入力 ・optが0の場合 ・account.__balanceを表示 ・optが1の場合 ・account.__balanceがFLAG_COSTより小さい場合、不十分であることをメッセージで表示 ・account.__balanceがFLAG_COST以上の場合、フラグを表示 ・optが2の場合 ・account.getKey()を表示 ・optが1の場合、accountNew()実行 ・account_name: 入力 ・dif: account_nameに使用されている文字でACCOUNT_NAME_CHARSにない文字の数 ・difが0以外の場合、メッセージを表示し、optの入力からやり直し ・account_iv: ランダム16バイト文字列 ・account = Account(account_iv, account_name, 0) ・account.__iv = account_iv ・account.__name = account_name ・account.__balance = 0 ・account.__nameを表示 ・account.getKey()を表示 ・save: "{account.__name}:0" ・pblocks: saveを"\x00"でパディングし、16バイトのブロックごとに配列にしたもの ・ct = [] ・pblocksの各ブロックbについて以下を実行 ・最初のブロックの場合 ・tmp: bとaccount.__ivのXOR ・tmpをAES ECBモード暗号化したものをctに追加 ・最初のブロック以外の場合 ・tmp: 前のブロックの暗号文と前のブロックの平文とbのXOR ・tmpをAES ECBモード暗号化したものをctに追加 ・ct_str: {account.__ivの16進数表記}:{ctの結合文字列の16進数表記} ・ct_strを返却
暗号のイメージは以下の通り。
pt0 ^ iv --(AES暗号)--> ct0 pt1 ^ pt0 ^ ct0 --(AES暗号)--> ct1
XORで調整する。
pt[0][0] = 'b', pt[1][0] = '0'で、pt[1][0] = '1'にする場合、以下よりpt[0][0]は'c'になる。
>>> chr(ord('0') ^ ord('1') ^ ord('b')) 'c'
2バイト目以降は以下のように考えればよい。
pt[0][1] = 'b', pt[1][1] = '\x00'で、pt[1][1] = '1'にする場合、以下よりpt[0][1]は'S'になる。
>>> chr(ord('\x00') ^ ord('1') ^ ord('b')) 'S'
つまり以下の順で実行すれば、フラグが得られる。
1.accountNew()でaccount_nameに"bbbbbbbbbbbbbbb"を指定する。 →得られたkeyをctとする。 2.new_pt0を"cSSSSSSSSSSSSSS:"と考え、new_ivを計算する。 →new_iv = iv ^ pt0 ^ new_pt0 3.accountLogin()で、account = [new_ivの16進数表記]:[ct]を指定する。
#!/usr/bin/env python3 import socket from Crypto.Util.strxor import strxor def recvuntil(s, tail): data = b'' while True: if tail in data: return data.decode() data += s.recv(1) s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect(('treasury.squ1rrel-ctf-codelab.kctf.cloud', 1337)) data = recvuntil(s, b'\n> ') print(data + '1') s.sendall(b'1\n') account_name = 'b' * 15 data = recvuntil(s, b'\n> ') print(data + account_name) s.sendall(account_name.encode() + b'\n') data = recvuntil(s, b'\n').rstrip() print(data) data = recvuntil(s, b'\n').rstrip() print(data) iv = bytes.fromhex(data.split(': ')[1].split(':')[0]) ct = data.split(': ')[1].split(':')[1] data = recvuntil(s, b'\n> ') print(data + '0') s.sendall(b'0\n') pt0 = account_name + ':' new_pt0 = 'cSSSSSSSSSSSSSS:' new_iv = (strxor(strxor(pt0.encode(), iv), new_pt0.encode())).hex() account = new_iv + ':' + ct data = recvuntil(s, b'\n> ') print(data + account) s.sendall(account.encode() + b'\n') data = recvuntil(s, b'\n').rstrip() print(data) data = recvuntil(s, b'\n').rstrip() print(data) data = recvuntil(s, b'\n> ') print(data + '0') s.sendall(b'0\n') data = recvuntil(s, b'\n').rstrip() print(data) data = recvuntil(s, b'\n> ') print(data + '1') s.sendall(b'1\n') data = recvuntil(s, b'\n').rstrip() print(data) data = recvuntil(s, b'\n').rstrip() print(data)
実行結果は以下の通り。
== proof-of-work: disabled == ⠀⠀⠀⠀⠀⠀⠀ ⢀⣀⣤⣄⣀⠀⠀⠀ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⠀⢴⣶⠀⢶⣦⠀⢄⣀⠀⠠⢾⣿⠿⠿⠿⠿⢦⠀⠀ ___ __ _ _ _/ |_ __ _ __ ___| | ⠀⠀⠀⠀⠀⠀⠀⠀⠺⠿⠇⢸⣿⣇⠘⣿⣆⠘⣿⡆⠠⣄⡀⠀⠀⠀⠀⠀⠀⠀/ __|/ _` | | | | | '__| '__/ _ \ | ⠀⠀⠀⠀⠀⠀⢀⣴⣶⣶⣤⣄⡉⠛⠀⢹⣿⡄⢹⣿⡀⢻⣧⠀⡀⠀⠀⠀⠀⠀\__ \ (_| | |_| | | | | | | __/ | ⠀⠀⠀⠀⠀⣰⣿⣿⣿⣿⣿⣿⣿⣿⣶⣤⡈⠓⠀⣿⣧⠈⢿⡆⠸⡄⠀⠀⠀⠀|___/\__, |\__,_|_|_| |_| \___|_| ⠀⠀⠀⠀⣰⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⣦⣈⠙⢆⠘⣿⡀⢻⠀⠀⠀⠀ |_| ⠀⠀⠀⢀⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⣄⠀⠹⣧⠈⠀⠀⠀⠀ _____ ⠀⠀⠀⣸⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⣄⠈⠃⠀⠀⠀⠀/__ \_ __ ___ __ _ ___ _ _ _ __ _ _ ⠀⠀⠀⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠁⠀⠀⠀⠀⠀ / /\/ '__/ _ \/ _` / __| | | | '__/ | | | ⠀⠀⠀⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠃⠀⠀⠀⠀⠀⠀ / / | | | __/ (_| \__ \ |_| | | | |_| | ⠀⠀⠀⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡿⠃⠀⠀⠀⠀⠀⠀⠀ \/ |_| \___|\__,_|___/\__,_|_| \__, | ⠀⠀⠀⢹⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡿⠋⠀⠀⠀⠀⠀⠀⠀⠀⠀ |___/ ⠀⠀⠀⠈⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡿⠟⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ ⠀⠀⠀⢠⣿⣿⠿⠿⠿⠿⠿⠿⠟⠛⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ Welcome to squ1rrel Treasury! What would you like to do? 0 -> Login 1 -> Create new account > 1 What would you like the account to be named? > bbbbbbbbbbbbbbb Wecome to Squirrel Treasury bbbbbbbbbbbbbbb Here is your account key: 12d5d474efe7dc2fc73a393a82ad3dc4:e94bed5adc8e5f26115cf710bf5349595e871e5afcee649ac41b1970a6d98fba ⠀⠀⠀⠀⠀⠀⠀ ⢀⣀⣤⣄⣀⠀⠀⠀ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⠀⢴⣶⠀⢶⣦⠀⢄⣀⠀⠠⢾⣿⠿⠿⠿⠿⢦⠀⠀ ___ __ _ _ _/ |_ __ _ __ ___| | ⠀⠀⠀⠀⠀⠀⠀⠀⠺⠿⠇⢸⣿⣇⠘⣿⣆⠘⣿⡆⠠⣄⡀⠀⠀⠀⠀⠀⠀⠀/ __|/ _` | | | | | '__| '__/ _ \ | ⠀⠀⠀⠀⠀⠀⢀⣴⣶⣶⣤⣄⡉⠛⠀⢹⣿⡄⢹⣿⡀⢻⣧⠀⡀⠀⠀⠀⠀⠀\__ \ (_| | |_| | | | | | | __/ | ⠀⠀⠀⠀⠀⣰⣿⣿⣿⣿⣿⣿⣿⣿⣶⣤⡈⠓⠀⣿⣧⠈⢿⡆⠸⡄⠀⠀⠀⠀|___/\__, |\__,_|_|_| |_| \___|_| ⠀⠀⠀⠀⣰⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⣦⣈⠙⢆⠘⣿⡀⢻⠀⠀⠀⠀ |_| ⠀⠀⠀⢀⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⣄⠀⠹⣧⠈⠀⠀⠀⠀ _____ ⠀⠀⠀⣸⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⣄⠈⠃⠀⠀⠀⠀/__ \_ __ ___ __ _ ___ _ _ _ __ _ _ ⠀⠀⠀⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠁⠀⠀⠀⠀⠀ / /\/ '__/ _ \/ _` / __| | | | '__/ | | | ⠀⠀⠀⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠃⠀⠀⠀⠀⠀⠀ / / | | | __/ (_| \__ \ |_| | | | |_| | ⠀⠀⠀⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡿⠃⠀⠀⠀⠀⠀⠀⠀ \/ |_| \___|\__,_|___/\__,_|_| \__, | ⠀⠀⠀⢹⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡿⠋⠀⠀⠀⠀⠀⠀⠀⠀⠀ |___/ ⠀⠀⠀⠈⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡿⠟⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ ⠀⠀⠀⢠⣿⣿⠿⠿⠿⠿⠿⠿⠟⠛⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ Welcome to squ1rrel Treasury! What would you like to do? 0 -> Login 1 -> Create new account > 0 Please provide your account details. > 13e4e545ded6ed1ef60b080bb39c0cc4:e94bed5adc8e5f26115cf710bf5349595e871e5afcee649ac41b1970a6d98fba Welcome cSSSSSSSSSSSSSS! What would you like to do? 0 -> View balance 1 -> Buy flag (40732806758731 acorns) 2 -> Save > 0 Balance: 111111111111111 acorns What would you like to do? 0 -> View balance 1 -> Buy flag (40732806758731 acorns) 2 -> Save > 1 Flag: squ1rrel{7H3_4C0rN_3NCrYP710N_5CH3M3_15_14CK1N6}
squ1rrel{7H3_4C0rN_3NCrYP710N_5CH3M3_15_14CK1N6}
Partial RSA (crypto)
フラグの形式から、Coppersmithの定理を使って復号する。
#!/usr/bin/env sage from Crypto.Util.number import * with open('partialrsa.txt', 'r') as f: params = f.read().splitlines() n = int(params[0].split(' ')[-1]) e = int(params[1].split(' ')[-1]) ct = int(params[2].split(' ')[-1]) flag_head = b'squ1rrel{' flag_tail = b'}' for i in range(1, 64): mbar_str = flag_head + b'\x00' * i + flag_tail mbar = bytes_to_long(mbar_str) PR.<x> = PolynomialRing(Zmod(n)) f = (mbar + x * 256)^e - ct f = f.monic() x0 = f.small_roots(X=256^i, beta=1) if (len(x0)) > 0: m = mbar + int(x0[0]) * 256 break assert pow(m, e, n) == ct flag = long_to_bytes(m).decode() print(flag)
squ1rrel{wow_i_was_betrayed_by_my_own_friend}
Survey (misc)
アンケートの最後にフラグが書いてあった。
squ1rrel{thanks_for_playing!}