この大会は2023/3/6 17:30(JST)~2023/3/7 23:30(JST)に開催されました。
今回もチームで参戦。結果は1463点で398チーム中27位でした。
自分で解けた問題をWriteupとして書いておきます。
Sanity Check (Main)
Discordに入り、#rulesチャネルのルールを見ると、フラグが書いてあった。
p_ctf{pl34s3_f0ll0w_th3_rul3s}
Bits and Pieces (Binary)
Ghidraでデコンパイルする。
undefined8 main(void) { long in_FS_OFFSET; undefined local_78 [104]; long local_10; local_10 = *(long *)(in_FS_OFFSET + 0x28); printf("Enter the password:"); __isoc99_scanf(&DAT_0010207e,local_78); getchar(); encode1(local_78); encode2(local_78,10); flagcheck1(local_78); flagcheck2(local_78); if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) { /* WARNING: Subroutine does not return */ __stack_chk_fail(); } return 0; } int reverse(char param_1) { int local_10; int local_c; local_10 = 0; for (local_c = 0; local_c < 8; local_c = local_c + 1) { local_10 = local_10 * 2; if (((int)param_1 >> ((byte)local_c & 0x1f) & 1U) != 0) { local_10 = local_10 + 1; } } return local_10; } uint myfunc(uint param_1,uint param_2) { byte bVar1; bool bVar2; bool bVar3; uint local_10; int local_c; local_10 = 0; for (local_c = 0x1f; -1 < local_c; local_c = local_c + -1) { bVar2 = (1 << ((byte)local_c & 0x1f) & param_1) != 0; bVar3 = (1 << ((byte)local_c & 0x1f) & param_2) != 0; if ((bVar3 && bVar2) || (!bVar2 && !bVar3)) { bVar1 = 0; } else { bVar1 = 1; } local_10 = local_10 << 1 | (uint)bVar1; } return local_10; } void encode1(long param_1) { undefined uVar1; int local_18; for (local_18 = 0; local_18 < 10; local_18 = local_18 + 1) { uVar1 = reverse((int)*(char *)(param_1 + local_18)); *(undefined *)(local_18 + param_1) = uVar1; uVar1 = myfunc(0x20,(int)*(char *)(param_1 + local_18)); *(undefined *)(param_1 + local_18) = uVar1; } return; } void encode2(long param_1,int param_2) { undefined uVar1; int local_c; for (local_c = 10; local_c <= param_2 + 8; local_c = local_c + 1) { uVar1 = myfunc((int)*(char *)(param_1 + (long)local_c + 1),(int)*(char *)(param_1 + local_c)); *(undefined *)(param_1 + local_c) = uVar1; } return; } void flagcheck1(long param_1) { int local_c; local_c = 0; while( true ) { if (9 < local_c) { puts("Access Granted - 1st half"); return; } if (flag1[local_c] != *(char *)(param_1 + local_c)) break; local_c = local_c + 1; } puts("Access Denied - 1st half"); return; } void flagcheck2(long param_1) { int local_c; local_c = 10; while( true ) { if (0x13 < local_c) { puts("Access Granted - 2nd half"); return; } if (flag2[local_c + -10] != *(char *)(param_1 + local_c)) break; local_c = local_c + 1; } puts("Access Denied - 2nd half"); return; } flag1 XREF[3]: Entry Point(*), flagcheck1:001013c1(*), flagcheck1:001013c8(R) 00104048 3a 2c 6e undefine da ac ee da 2e 2c ce 00104048 3a undefined13Ah [0] XREF[3]: Entry Point(*), flagcheck1:001013c1(*), flagcheck1:001013c8(R) 00104049 2c undefined12Ch [1] 0010404a 6e undefined16Eh [2] 0010404b da undefined1DAh [3] 0010404c ac undefined1ACh [4] 0010404d ee undefined1EEh [5] 0010404e da undefined1DAh [6] 0010404f 2e undefined12Eh [7] 00104050 2c undefined12Ch [8] 00104051 ce undefined1CEh [9] flag2 XREF[3]: Entry Point(*), flagcheck2:0010142d(*), flagcheck2:00101434(R) 00104058 41 14 13 undefine 19 33 2d 41 73 71 31 00104058 41 undefined141h [0] XREF[3]: Entry Point(*), flagcheck2:0010142d(*), flagcheck2:00101434(R) 00104059 14 undefined114h [1] 0010405a 13 undefined113h [2] 0010405b 19 undefined119h [3] 0010405c 33 undefined133h [4] 0010405d 2d undefined12Dh [5] 0010405e 41 undefined141h [6] 0010405f 73 undefined173h [7] 00104060 71 undefined171h [8] 00104061 31 undefined131h [9]
reverse関数は1文字ごとにbit単位で逆転する。またmyfunc関数はXOR関数と同じ。
encode1は先頭10バイトに対して、1文字ごとにbit単位で逆転し、0x20とXORを取る。
encode2は先頭10バイト目以降18バイト目までに対して、隣通しでXORをとる。
この結果先頭10バイトがflag1と同じであれば、flagcheck1はクリア。
残り10バイトがflag2と同じであれば、flagcheck2はクリア。
この条件を満たすパスワードがフラグになる。
#!/usr/bin/env python3 enc_flag1 = [0x3a, 0x2c, 0x6e, 0xda, 0xac, 0xee, 0xda, 0x2e, 0x2c, 0xce] enc_flag2 = [0x41, 0x14, 0x13, 0x19, 0x33, 0x2d, 0x41, 0x73, 0x71, 0x31] flag1 = '' for c in enc_flag1: code = c ^ 0x20 code = int(bin(code)[2:].zfill(8)[::-1], 2) flag1 += chr(code) flag2 = chr(enc_flag2[-1]) for i in range(len(enc_flag2) - 1): code = ord(flag2[0]) ^ enc_flag2[-i-2] flag2 = chr(code) + flag2 flag = flag1 + flag2 print(flag)
実行結果は以下の通り。
X0r_1s_p0w3rful_r3@1
p_ctf{X0r_1s_p0w3rful_r3@1}
The Kingpin (Forensics)
No.501と543のパケットからzipを抽出する。それぞれ解凍すると、00フォルダにfilter.pngで赤い25×25の画像がある。あとは文字が赤い25×25の画像でマスクされたような画像がある。
フォルダ内のファイルは結合する必要がありそう。結合した画像で白い部分だけマージすると画像になるのかもしれない。
結果偶数フォルダのある画像ファイルを02~20.pngの後に01.pngを結合するよう変更したところ、フラグ画像が表示された。
#!/usr/bin/env python3 from PIL import Image from string import * CELL_SIZE = 25 COUNT = 20 SIZE = CELL_SIZE * COUNT FILE_FORMAT = 'images/%02d/%s.png' for num in range(1, 11, 2): output_img = Image.new('RGB', (SIZE, SIZE), (255, 255, 255)) for i, c in enumerate(ascii_uppercase[:20]): fname = FILE_FORMAT % (num, c) img = Image.open(fname).convert('RGB') output_img.paste(img, (0, i * CELL_SIZE)) output_img.save('images/%02d.png' % num) for num in range(2, 11, 2): output_img = Image.new('RGB', (SIZE, SIZE), (255, 255, 255)) for i in range(1, 21): c = '%02d' % (i % 20 + 1) fname = FILE_FORMAT % (num, c) img = Image.open(fname).convert('RGB') output_img.paste(img, (i * CELL_SIZE, 0)) output_img.save('images/%02d.png' % num) output_img = Image.new('RGB', (SIZE, SIZE), (255, 255, 255)) for num in range(1, 11): fname = 'images/%02d.png' % num img = Image.open(fname).convert('RGB') for y in range(0, SIZE, CELL_SIZE): for x in range(0, SIZE, CELL_SIZE): r, g, b = img.getpixel((x, y)) if r != 255 or g != 0 or b != 0: img_crop = img.crop((x, y, x + CELL_SIZE, y + CELL_SIZE)) output_img.paste(img_crop, (x, y)) output_img.save('flag.png')
p_ctf{TH3_D3V1L_0F_H3LL5_K1TCH3N_15_B4CK}
Leaky RSA (Crypto)
RSA暗号でn, e, dがわかっているので、p, qがわかる。pとqのXORをXOR鍵として、暗号を復号する。
Looks like someone mutiplied "password" with the flag before encrypting. message=0b100010111110011111010011001110001111000010000100000111100100101100111110011110001010000111010100110000101001000111011100010000010111110010011001001111111111101110000111110111110010110010010000111000110011000010010100010000111100111010100010111100111111101100001111000100101001001000001111010111111101110011010011111111111010011010101100001011001000110101110010010101110011001000010111101100110101110101001001110111110010111110000101110000000110010110110110001111110110111100011000100101100101001001001011010101101000000010011111011110000001101100110000101001011101001011100111111101101111100111110101001010111001001011001100101101110110101010011011011110010000100011111111100010001101000101110000100110000000110110001001100000111011111101011011011000000101010110100011101011000001000000100010100000011000010000011001001110001011000101100110011000001110110010010000011110001110111000001100010011111110111101100111111010100011000011001110010000000000100111010010000011101100000001001110010100001100011010001011101110001001111
"mutiplied"は"multiplied"の誤記かもしれない。messageを数値としてRSA暗号のパラメータを使って、復号する。復号した値を"password"を数値に変換したもので割り、文字列化する。
#!/usr/bin/env python3 from Crypto.PublicKey import RSA from Crypto.Util.number import * def decrypt(ct, key): pt = '' for i in range(len(ct)): pt += chr(ct[i] ^ key[i % len(key)]) return pt with open('cipher.txt', 'r') as f: ct = bytes.fromhex(f.read().rstrip()) with open('keys.txt', 'r') as f: params = f.read().splitlines() n = int(params[0].split('=')[1]) e = int(params[1].split('=')[1]) d = int(params[2].split('=')[1]) rsa = RSA.construct((n, e, d)) p = rsa.p q = rsa.q key = p ^ q key = long_to_bytes(key) msg = decrypt(ct, key) print(msg) print() c = int(msg.split('\n')[1].split('=')[1], 2) m = pow(c, d, n) key = bytes_to_long(b'password') assert m % key == 0 flag = long_to_bytes(m // key).decode() print(flag)
実行結果は以下の通り。
Looks like someone mutiplied "password" with the flag before encrypting. message=0b100010111110011111010011001110001111000010000100000111100100101100111110011110001010000111010100110000101001000111011100010000010111110010011001001111111111101110000111110111110010110010010000111000110011000010010100010000111100111010100010111100111111101100001111000100101001001000001111010111111101110011010011111111111010011010101100001011001000110101110010010101110011001000010111101100110101110101001001110111110010111110000101110000000110010110110110001111110110111100011000100101100101001001001011010101101000000010011111011110000001101100110000101001011101001011100111111101101111100111110101001010111001001011001100101101110110101010011011011110010000100011111111100010001101000101110000100110000000110110001001100000111011111101011011011000000101010110100011101011000001000000100010100000011000010000011001001110001011000101100110011000001110110010010000011110001110111000001100010011111110111101100111111010100011000011001110010000000000100111010010000011101100000001001110010100001100011010001011101110001001111 p_ctf{0n3_1eak_15_en0u6h_70_4774ck_rsa}
p_ctf{0n3_1eak_15_en0u6h_70_4774ck_rsa}
Compromised (Crypto)
サーバの処理概要は以下の通り。
・p: 既知の固定の素数 ・g = 65537 ・o = p - 1 ・eve: 入力(16進数表記数値) ・is_too_much_evil(o, eve)がTrueの場合、例外発生 ・eveが0以下の場合、Trueを返却 ・z = o // eve ・zが2のべき乗の場合、Trueを返却。そうでない場合Falseを返却 ・alice_secret: 2以上o未満のランダム整数 ・recv_alice = pow(g, alice_secret, p) ・recv_aliceを16進数表記で表示 ・send_bob = pow(recv_alice, eve, p) ・send_bobを16進数表記で表示 ・bob_scret: 2以上o未満のランダム整数 ・recv_bob = pow(g, bob_scret, p) ・recv_bobを16進数表記で表示 ・send_alice = pow(recv_bob, eve, p) ・send_aliceを16進数表記で表示 ・key = pow(send_alice, alice_secret, p) ・keyがpow(send_bob, bob_scret, p)と一致しない場合終了 ・magic(key).hex()を表示 ・key: keyの文字列化したもののsha256ダイジェスト ・iv: ランダム16バイト文字列 ・ct: flagのパディングしたもののAES-CBC暗号化 ・ctを返却
oをfactordbで素因数分解する。
o = 2**4 * 4335281 * 2070678721765822593665771188103088096219791020706517153290392386787776514445331961971978575310183373092521978673650294701725639224173214693498019049238262629682329713473376071829454117139158057824371818171224862872959461186186904911993659565500520150465830609918173958630318077973573212768091681907453
eveに以下の値を指定すれば、iが4335281未満の値のどれかでpow(send_alice, i, p)がkeyになる。
2**4 * 2070678721765822593665771188103088096219791020706517153290392386787776514445331961971978575310183373092521978673650294701725639224173214693498019049238262629682329713473376071829454117139158057824371818171224862872959461186186904911993659565500520150465830609918173958630318077973573212768091681907453
そのkeyを元に復号し、フラグの形式になるものを探す。
#!/usr/bin/env python3 import socket from Crypto.Util.number import long_to_bytes from Crypto.Util.Padding import unpad from Crypto.Cipher import AES from hashlib import sha256 def recvuntil(s, tail): data = b'' while True: if tail in data: return data.decode() data += s.recv(1) p = 143631585913210514235039010914091901837885309376633126253342809551771176885137171094877459999188913342142748419620501172236669295062606053914284568348811271223549440680905140640909882790482660545326407684050654315851945053611416821020364550956522567974906505478346737880716863798325607222759444397302795988689 g = 65537 o = p-1 eve = 2**4 * 2070678721765822593665771188103088096219791020706517153290392386787776514445331961971978575310183373092521978673650294701725639224173214693498019049238262629682329713473376071829454117139158057824371818171224862872959461186186904911993659565500520150465830609918173958630318077973573212768091681907453 eve = hex(eve)[2:] s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect(('compromised.ctf.pragyan.org', 56931)) data = recvuntil(s, b': ') print(data + eve) s.sendall(eve.encode() + b'\n') data = recvuntil(s, b'\n').rstrip() print(data) data = recvuntil(s, b'\n').rstrip() print(data) data = recvuntil(s, b'\n').rstrip() print(data) data = recvuntil(s, b'\n').rstrip() print(data) send_alice = int(data.split(' ')[-1], 16) data = recvuntil(s, b'\n').rstrip() print(data) ct = bytes.fromhex(data.split(' ')[-1]) iv = ct[:AES.block_size] ct = ct[AES.block_size:] for i in range(4335281): key = pow(send_alice, i, p) key = sha256(long_to_bytes(key)).digest() aes = AES.new(key, AES.MODE_CBC, iv) flag = aes.decrypt(ct) if flag.startswith(b'p_ctf'): flag = unpad(flag, AES.block_size).decode() print(flag) break
実行結果は以下の通り。
Eve's evil number: 3178c2bb94d51e8a50fea9f198d59ae788dd11255ae2a669806033637e865418167d45da1bad072f1643895e411055d0946c539e729355735e4d59dc8648f420b917d287b4d886908b3274680493d3c6a157371e8085da1c60ba1f3666a178a856398aed51e6c95a38c7ac5efe49be5790d977ebeeb4173e77aa30e2fd0 Received from Alice: b236ed7592ba6e3e04eb5505fd40d2200f0bf034d9898347b86b1281555017acba1390250faa3835baf24117479fcb0a73deea251ad4330882f6547e8afe1dada3244a477174f472fa825a40dfd8cca71cda7fb1c1bd66861a40725a64fe15547a24dd4667598c028fcbf481c769456935c321fe6ce4344bf836a2bd263ffdaa Sent to Bob: 24ffbe18ceaca32616f2bf7c1ad7e8176e86572b10cbb3f4300284ba222d91eab73c4cba650a6c87677027be3a94c0454a45556c20485d602ba1611d657bb973364eec34de95c630a1a6a32a1cffb356088b8babcea6f887b419ab14ee130ccee9aa841681ff61416c368a82faf681f0a2d377c9720ca8b73f2615b019c2856 Received from Bob: 740af0c6185fe0bdf8c1a2dcdfebf0faa7763878bb0531ae852122d643971c480bcda190c82570ede9d3e5fd32ed755c05f72352ba3acd8ca0f705a4cf822b18de6bf4b2572173a7aa26edbc3474a1a3d5cd4a0ec29c8f307fe141669d282960b22d92075ee797f73894a9316b2bebd313c7b2738c2cc27f739e7676ebed87fd Sent to Alice: aa9ceaaa09e4134b7062ef8bcb69e4cbd5ae3f2a378d97e6bb773b2e0550dda50b4055f1c866709b1c320170646245e954725e118ea349e56ffd36edbd59b483712ab331beb0307d79d9e54a7cbc156a8b44dc90b63afef7fadb2b56628e7be6ac3fef4ece4a78f27881372178f0ce243bd5f8f28f1747a1f5fb32054c0fc2aa Ciphertext: 80ad4ff16e8897a089b9e8db0601cb76354a97102f7d7f3c7d02ba41350e3ccec3e93bff27c5135f8dcfcb058fc01f85b158d4dcc5f39e76444f92b65430d7e6d17d4ffb5896b1be7d91179d2778374d p_ctf{7hi5_1s_su86r0up_6on51nem3n7_a774ck_on_DH}
p_ctf{7hi5_1s_su86r0up_6on51nem3n7_a774ck_on_DH}