以下の内容はhttps://yocchin.hatenablog.com/entry/2025/03/06/080901より取得しました。


PwnMe CTF Quals 2025 Writeup

この大会は2025/3/1 0:30(JST)~2025/3/3 0:30(JST)に開催されました。
今回もチームで参戦。結果は300点で955チーム中202位でした。
自分で解けた問題をWriteupとして書いておきます。

RTFM : Infrastructure (Welcome)

Discordに入り、#informationsチャネルのメッセージを見ると、pwntoolsのコード例にフラグが書いてあった。

PWNME{Plz_r34d_tH3_d0c!!!}

ProfileEditor (Web)

パストラバーサルでflag.txtを読み取れる。
まず、以下のアカウントを作成し、ログインする。

Username: ../flag.txt
Password: hogeFuga

Show Profileをクリックすると、フラグが表示された。

PWNME{afb7daa3a595522882f6a9efae30c4ec}

Easy Diffy (Crypto)

g が p - 1 になっているため、剰余環p上のgの奇数乗は必ずgとなる。つまり以下のようになる。

A = pow(g, a, p) = g
B = pow(g, b, p) = g
C = pow(B, a, p) = pow(g, a, p) = g

これでkeyは割り出せるので、AES暗号の暗号文を復号できる。

#!/usr/bin/env python3
from Crypto.Util.number import *
from Crypto.Util.Padding import unpad
from Crypto.Cipher import AES
from hashlib import sha256

with open('output.txt', 'r') as f:
    params = f.read().splitlines()

p = int(params[0].split(' ')[-1])
g = int(params[1].split(' ')[-1])
ciphertext = bytes.fromhex(params[2].split(' ')[-1])

C = g
key = long_to_bytes(C)
key = sha256(key).digest()[:16]

cipher = AES.new(key, AES.MODE_ECB)
flag = unpad(cipher.decrypt(ciphertext), AES.block_size).decode()
print(flag)
PWNME{411_my_h0m13s_h4t35_sm411_Gs}

Square Power (Crypto)

暗号化処理の概要は以下の通り。

・n, g, k = generate_public_key()
 ・n: 素数の積
 ・k: 2以上n-1以下ランダム整数
 ・k, nのGCDが1以外の間、以下を実行
  ・k: 2以上n-1以下ランダム整数
 ・g = 1 + k * n
 ・n, g, kを返却
・a: 2以上n-1以下のランダム整数
・b: 2以上n-1以下のランダム整数
・A = pow(g, a, n * n)
・B = pow(g, b, n * n)
・secret_key = pow(B, a, n * n)
・n, g, k, A, Bを出力
・encrypt(flag, secret_key)を表示
 ・hash_secret_key: secret_keyを文字列としたsha256ダイジェスト
 ・hash_secret_keyを鍵としてflagをAES暗号化して16進数文字列で表示

Aは以下のように式を変形できる。

A = pow(g, a, n * n) = pow(1 + k * n, a, n * n)
  = (1 + a * k * n) % (n * n)

このことから以下のようにしてaを割り出すことができる。

a = ((A - 1) // n) * inverse(k, n) % n

あとはsecret_keyが算出できるので、AES復号をすればよい。

#!/usr/bin/env python3
from Crypto.Util.number import *
from Crypto.Cipher import AES
from hashlib import sha256

with open('output.txt', 'r') as f:
    params = f.read().splitlines()

n = int(params[0].split(' ')[-1])
g = int(params[1].split(' ')[-1])
k = int(params[2].split(' ')[-1])
A = int(params[3].split(' ')[-1])
B = int(params[4].split(' ')[-1])
enc = bytes.fromhex(eval(params[5].split(' ')[-1]))

a = ((A - 1) // n) * inverse(k, n) % n
assert pow(g, a, n * n) == A

secret_key = pow(B, a, n * n)
hash_secret_key = sha256(str(secret_key).encode()).digest()
cipher = AES.new(hash_secret_key, AES.MODE_ECB)
flag = cipher.decrypt(enc).decode()
print(flag)
PWNME{Thi5_1s_H0w_pAl1ier_WorKs}

My Zed (Crypto)

暗号化処理の概要は以下の通り。

・file = openzed.Openzed(b'zed', os.urandom(16), 'flag.txt', len(FLAG))
 ・file.user = b'zed'
 ・file.password = ランダム16バイト文字列
 ・file.filename = 'flag.txt'
 ・file.size = FLAGの長さ
 ・file.generate_metadata()
  ・metadata = {}
  ・metadata["user"] = 'zed'
  ・metadata["password_hash"] = sha256(file.password).hexdigest()
  ・metadata["filename"] = 'flag.txt'
  ・metadata["size"] = FLAGの長さ
  ・file.metadata = metadataのjson文字列
  ・file.padding_len = 300 - len(file.metadata)
  ・file.metadataに300バイトになるようb"\x00"をパディング
  ・file.metadataを返却
・file.encrypt(FLAG)
 ・cipher = AES_CBC_ZED(file.user, file.password)
  ・cipher.user = b'zed'
  ・cipher.password = file.password
  ・cipher.derive_password()
   ・100回繰り返し
    ・cipher.key = sha256(cipher.password).digest()[:16]
  ・cipher.generate_iv()
   ・cipher.iv = (cipher.user + cipher.password)[:16]
 ・file.encrypted = cipher.encrypt(FLAG)
  ・iv = cipher.iv
  ・ciphertext = b""
  ・ecb_cipher = AES.new(key=cipher.key, mode=AES.MODE_ECB)
  ・FLAGを16バイトごとに以下の処理を実行
   ・chunk: FLAGのブロック
   ・最後のブロックの場合
    ・plaintextの長さが16以下の場合
     ・prev = iv
    ・plaintextの長さが16より大きい場合
     ・prev = ciphertext[pos-16:pos]
    ・prev = ecb_cipher.encrypt(prev)
    ・ciphertextにxor(chunk, prev)を結合
   ・最初のブロックの場合
    ・xored = bytes(xor(plaintext, iv))
    ・ciphertext += ecb_cipher.encrypt(xored)
   ・最初でも最後のブロックでもない場合
    ・xored = bytes(xor(chunk, ciphertext[pos-16:pos]))
    ・ciphertext += ecb_cipher.encrypt(xored)
  ・iv + ciphertextを返却
 ・file.encrypted = zlib.compress(file.encrypted)
 ・file.encryptedを返却
・file.generate_container()
 ・file.secure_container = b'OZED' + file.metadata + file.encrypted
 ・file.secure_containerを返却
・file.secure_containerを出力

metadata["password_hash"]がわかっているので、AES暗号のkeyも算出できる。あとは逆算して復号すればフラグを割り出せる。

#!/usr/bin/env python3
import json
import zlib
from Crypto.Cipher import AES

def xor(a: bytes, b: bytes) -> bytes:
    return bytes(x ^ y for x, y in zip(a, b))

with open('flag.txt.ozed', 'rb') as f:
    secure_container = f.read()

assert secure_container[:4] == b'OZED'
metadata = json.loads(secure_container[4:304].rstrip(b'\x00'))
encrypted = secure_container[304:]

key = bytes.fromhex(metadata['password_hash'])[:16]

encrypted = zlib.decompress(encrypted)
iv = encrypted[:16]
ciphertext = encrypted[16:]

ecb_cipher = AES.new(key=key, mode=AES.MODE_ECB)

FLAG = b''
for pos in range(0, len(ciphertext), 16):
    chunk = ciphertext[pos:pos+16]
    if len(ciphertext[pos+16:pos+32]) == 0:
        if len(ciphertext) <= 16:
            prev = iv
        else:
            prev = ciphertext[pos-16:pos]
        prev = ecb_cipher.encrypt(prev)
        FLAG += xor(prev, chunk)
    elif not FLAG:
        xored = ecb_cipher.decrypt(chunk)
        FLAG += bytes(xor(xored, iv))
    else:
        xored = ecb_cipher.decrypt(chunk)
        FLAG += bytes(xor(xored, ciphertext[pos-16:pos]))

FLAG = FLAG.decode()
print(FLAG)
PWNME{49e531f28d1cedef03103af6cec79669_th4t_v3Ct0r_k1nd4_l3aky}



以上の内容はhttps://yocchin.hatenablog.com/entry/2025/03/06/080901より取得しました。
このページはhttp://font.textar.tv/のウェブフォントを使用してます

不具合報告/要望等はこちらへお願いします。
モバイルやる夫Viewer Ver0.14