この大会は2022/7/10 1:00(JST)~2022/7/11 1:00(JST)に開催されました。
今回もチームで参戦。結果は2372点で635チーム中60位でした。
自分で解けた問題をWriteupとして書いておきます。
Sanity Check (Web)
HTMLソースのコメントにフラグが書いてあった。
vsctf{v1ew_s0urc3_a_f1rst_y3ar_t3am!}
Discord (Miscellaneous)
Discordに入り、#announcementsチャネルのメッセージを見ると、フラグが書いてあった。
vsctf{w3lc0m3_t0_vsctf_2022!}
Recovery (Cryptography)
スクリプトの処理概要は以下の通り。
・validate(passwd)がTrueの場合、パスワードがフラグとなる。 ・passwdの長さは49バイトでない場合、Falseを返す。 ・key: passwdの末尾から1バイト飛びで、各文字のASCIIコード分"[7-9]vs"という形式の文字列を構成した配列 ・gate: 固定の25個の整数配列 ・gateとkeyについて、各index(=i)について以下が成り立つ。 ・a = i + 1 ・len(key[i]) == 3 * (gate[i] + 7 * a) // a ・hammer: passwdの2バイト目から1バイト飛びで、12個目まで以下のような構成になる。 {1: passwd[1] + passwd[25], 3: passwd[3] + passwd[27], ..., } ・hammerの各要素でpasswd[i] + passwd[i+24]を"."で結合したものをbase64エンコードしたものが b'c3MxLnRkMy57XzUuaE83LjVfOS5faDExLkxfMTMuR0gxNS5fTDE3LjNfMTkuMzEyMS5pMzIz'と一致しない場合 Falseを返す。 ・Trueを返す。
まず末尾から1バイト飛びの文字については、3 * (gate[i] + 7 * a) // aを計算し、3で割ったものがASCIIコードになることを使って、復元する。次に先頭2バイトから1バイト飛びの文字については、base64デコードしたものから"."区切りで順に文字列を取り出し、復元する。あとは復元したものを交互に結合し、フラグを復元する。
#!/usr/bin/env python3 from base64 import b64decode gate = [118, 140, 231, 176, 205, 480, 308, 872, 702, 820, 1034, 1176, 1339, 1232, 1605, 1792, 782, 810, 1197, 880, 924, 1694, 2185, 2208, 2775] flag2 = '' for i in range(25): a = i + 1 code = (3 * (gate[i] + 7 * a) // a) // 3 flag2 = chr(code) + flag2 block = b'c3MxLnRkMy57XzUuaE83LjVfOS5faDExLkxfMTMuR0gxNS5fTDE3LjNfMTkuMzEyMS5pMzIz' flag1 = [''] * 24 s = b64decode(block).split(b'.') for i in range(len(s)): flag1[i] = chr(s[i][0]) flag1[i+12] = chr(s[i][1]) flag1 = ''.join(flag1) flag = '' for i in range(len(flag1)): flag += flag2[i] flag += flag1[i] flag += flag2[-1] print(flag)
vsctf{Th353_FL4G5_w3r3_inside_YOU_th3_WH0L3_T1M3}
Baby RSA (Cryptography)
公開鍵を読み取ると、以下のようになっている。
n = 52419317100235286358057114349639882093779997394202082664044401328860087685103 e = 101
nをyafuで素因数分解する。
>yafu-x64.exe "factor(52419317100235286358057114349639882093779997394202082664044401328860087685103)" -v -threads 4 07/10/22 07:20:04 v1.34.5 @ XXXX-XXXX, System/Build Info: Using GMP-ECM 6.3, Powered by GMP 5.1.1 detected Intel(R) Core(TM) i7-10700 CPU @ 2.90GHz detected L1 = 32768 bytes, L2 = 16777216 bytes, CL = 64 bytes measured cpu frequency ~= 2894.441030 using 20 random witnesses for Rabin-Miller PRP checks =============================================================== ======= Welcome to YAFU (Yet Another Factoring Utility) ======= ======= bbuhrow@gmail.com ======= ======= Type help at any time, or quit to quit ======= =============================================================== cached 78498 primes. pmax = 999983 >> fac: factoring 52419317100235286358057114349639882093779997394202082664044401328860087685103 fac: using pretesting plan: normal fac: no tune info: using qs/gnfs crossover of 95 digits div: primes less than 10000 fmt: 1000000 iterations rho: x^2 + 3, starting 1000 iterations on C77 rho: x^2 + 2, starting 1000 iterations on C77 rho: x^2 + 1, starting 1000 iterations on C77 pm1: starting B1 = 150K, B2 = gmp-ecm default on C77 fac: setting target pretesting digits to 23.69 fac: sum of completed work is t0.00 fac: work done at B1=2000: 0 curves, max work = 30 curves fac: 30 more curves at B1=2000 needed to get to t23.69 ecm: 30/30 curves on C77, B1=2K, B2=gmp-ecm default fac: setting target pretesting digits to 23.69 fac: t15: 1.00 fac: t20: 0.04 fac: sum of completed work is t15.18 fac: work done at B1=11000: 0 curves, max work = 74 curves fac: 74 more curves at B1=11000 needed to get to t23.69 ecm: 74/74 curves on C77, B1=11K, B2=gmp-ecm default fac: setting target pretesting digits to 23.69 fac: t15: 7.17 fac: t20: 1.04 fac: t25: 0.05 fac: sum of completed work is t20.24 fac: work done at B1=50000: 0 curves, max work = 214 curves fac: 149 more curves at B1=50000 needed to get to t23.69 ecm: 149/149 curves on C77, B1=50K, B2=gmp-ecm default, ETA: 0 sec fac: setting target pretesting digits to 23.69 fac: t15: 28.45 fac: t20: 8.13 fac: t25: 0.74 fac: t30: 0.05 fac: sum of completed work is t23.72 starting SIQS on c77: 52419317100235286358057114349639882093779997394202082664044401328860087685103 ==== sieve params ==== n = 79 digits, 260 bits factor base: 34944 primes (max prime = 882967) single large prime cutoff: 75052195 (85 * pmax) allocating 7 large prime slices of factor base buckets hold 2048 elements using SSE4.1 enabled 32k sieve core sieve interval: 10 blocks of size 32768 polynomial A has ~ 10 factors using multiplier of 23 using SPV correction of 22 bits, starting at offset 41 using SSE2 for x64 sieve scanning using SSE2 for resieving 13-16 bit primes using SSE2 for 8x trial divison to 13 bits using SSE4.1 and inline ASM for small prime sieving using SSE2 for poly updating up to 15 bits using SSE4.1 for medium prime poly updating using SSE4.1 and inline ASM for large prime poly updating trial factoring cutoff at 88 bits ==== sieving in progress ( 4 threads): 35008 relations needed ==== ==== Press ctrl-c to abort and save state ==== 35775 rels found: 18827 full + 16948 from 179344 partial, (10962.98 rels/sec) sieving required 60180 total polynomials trial division touched 2821739 sieve locations out of 39439564800 QS elapsed time = 18.1073 seconds. ==== post processing stage (msieve-1.38) ==== begin with 198171 relations reduce to 50546 relations in 2 passes attempting to read 50546 relations recovered 50546 relations recovered 34267 polynomials freed 24 duplicate relations attempting to build 35751 cycles found 35751 cycles in 1 passes distribution of cycle lengths: length 1 : 18824 length 2 : 16927 largest cycle: 2 relations matrix is 34944 x 35751 (5.1 MB) with weight 1050811 (29.39/col) sparse part has weight 1050811 (29.39/col) filtering completed in 3 passes matrix is 24604 x 24668 (3.9 MB) with weight 816278 (33.09/col) sparse part has weight 816278 (33.09/col) saving the first 48 matrix rows for later matrix is 24556 x 24668 (3.2 MB) with weight 660080 (26.76/col) sparse part has weight 586096 (23.76/col) matrix includes 64 packed rows using block size 9867 for processor cache size 16384 kB commencing Lanczos iteration memory use: 2.9 MB lanczos halted after 390 iterations (dim = 24548) recovered 13 nontrivial dependencies Lanczos elapsed time = 0.8640 seconds. Sqrt elapsed time = 0.0230 seconds. SIQS elapsed time = 18.9947 seconds. pretesting / qs ratio was 0.59 Total factoring time = 30.2311 seconds ***factors found*** P39 = 283378097758180413812138939650885549231 P39 = 184980129074643957218827272858529362113 ans = 1
p - 1もq - 1もeと互いに素ではないため、通常の復号方法で復号できない。mod p, qの場合の式で、このことを前提にCRTを使いながら、復号する。
#!/usr/bin/env python3 from Crypto.PublicKey import RSA from Crypto.Util.number import * from sympy.ntheory.modular import crt c = 0x459cc234f24a2fb115ff10e272130048d996f5b562964ee6138442a4429af847 with open('pubkey.pem', 'r') as f: pub_data = f.read() pubkey = RSA.importKey(pub_data) n = pubkey.n e = pubkey.e print('[+] n =', n) print('[+] e =', e) p = 283378097758180413812138939650885549231 q = 184980129074643957218827272858529362113 assert p * q == n assert (p - 1) % e == 0 assert (q - 1) % e == 0 _lambda = p - 1 assert _lambda % e == 0 assert _lambda // e % e != 0 L = pow(2, _lambda // e, p) assert L > 1 d = inverse(e, _lambda // e) m1s = [] for i in range(e): m = pow(c % p, d, p) * pow(L, i, p) % p m1s.append(m) _lambda = q - 1 assert _lambda % e == 0 assert _lambda // e % e != 0 L = pow(2, _lambda // e, q) assert L > 1 d = inverse(e, _lambda // e) m2s = [] for i in range(e): m = pow(c % q, d, q) * pow(L, i, q) % q m2s.append(m) for m1 in m1s: for m2 in m2s: m, _ = crt([p, q], [m1, m2]) flag = long_to_bytes(m) if flag.startswith(b'vsctf{'): flag = flag.decode() print('[*] flag:', flag) break
実行結果は以下の通り。
[+] n = 52419317100235286358057114349639882093779997394202082664044401328860087685103 [+] e = 101 [*] flag: vsctf{5m411_Pr1m3_15_Un54f3!}
vsctf{5m411_Pr1m3_15_Un54f3!}
Art Final (Cryptography)
暗号処理の概要は以下の通り。
・boring_pix: Art_Final_2022.pngのデータ ・spicy_pix: 新イメージデータ初期化 ・rgba: ランダム32ビット整数を8ビットずつリトルエンディアンで分離 ・spicy_pix: boring_pixの各ピクセルのRGBAデータとrgbaとのXOR ・spicy_pixをENHANCED_Final_2022.pngに保存 ・key: ランダム16バイト文字列 ・iv: ランダム16バイト文字列 ・iv + flagをパディングしてAES-CBC暗号化し、その後base64エンコードしたものを表示
32bitランダム整数のデータがたくさん得られそうなので、Mersenne Twisterの特徴から状態を復元し、ランダム値を得られるようにすれば、AESの鍵が得られ、復号できる。
#!/usr/bin/env python3 import random from PIL import Image from Crypto.Cipher import AES from Crypto.Util.Padding import unpad from base64 import b64decode def four_to_one_int(ns): ret = 0 for n in ns[::-1]: ret *= 256 ret += n return ret def untemper(rand): rand ^= rand >> 18; rand ^= (rand << 15) & 0xefc60000; a = rand ^ ((rand << 7) & 0x9d2c5680); b = rand ^ ((a << 7) & 0x9d2c5680); c = rand ^ ((b << 7) & 0x9d2c5680); d = rand ^ ((c << 7) & 0x9d2c5680); rand = rand ^ ((d << 7) & 0x9d2c5680); rand ^= ((rand ^ (rand >> 11)) >> 11); return rand boring = Image.open('Art_Final_2022.png', 'r').convert('RGBA') boring_pix = boring.load() spicy = Image.open('ENHANCED_Final_2022.png', 'r').convert('RGBA') spicy_pix = spicy.load() N = 624 state = [] collect = True for i in range(boring.size[0] * boring.size[1]): x = i % boring.size[0] y = i // boring.size[0] rgba = tuple([bore ^ spice for bore, spice \ in zip(boring_pix[x, y], spicy_pix[x, y])]) num = four_to_one_int(rgba) if collect: state.append(untemper(num)) else: rgba2 = tuple(random.randbytes(4)) assert rgba == rgba2 if len(state) == 624: collect = False state.append(N) random.setstate([3, tuple(state), None]) enc_flag = b64decode('Tl5nK8L2KYZRCJCqLF7TbgKLgy1vIkH+KIAJv5/ILFoC+llemcmoLmCQYkiOrJ/orOOV+lwX+cVh+pwE5mtx6w==') key = bytes(random.sample(random.randbytes(16), 16)) iv = enc_flag[:AES.block_size] enc = AES.new(key, AES.MODE_CBC, iv) flag = unpad(enc.decrypt(enc_flag[AES.block_size:]), AES.block_size).decode() print(flag)
vsctf{1_gu355_R4ND0m_i5nt_tH4T_5p1cy}
Feedback Survey (Miscellaneous)
アンケートに答えたら、フラグが表示された。
vsctf{surv3y_c0mpl3t3r}