squ1rrel CTF 2024 Writeup

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

jadx-guiデコンパイルする。

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!}