この大会は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!}