Pragyan CTF 2023 Writeup

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