LINE CTF 2021 writeup
LINE CTF 2021 にチーム Tomatosalad で参加しました。結果は 200 ポイント、 73 位でした。 僕は crypto カテゴリの問題を 2 問解きました。
babycrypto1
#!/usr/bin/env python from base64 import b64decode from base64 import b64encode import socket import multiprocessing from Crypto.Cipher import AES from Crypto.Random import get_random_bytes from Crypto.Util.Padding import pad, unpad import hashlib import sys class AESCipher: def __init__(self, key): self.key = key def encrypt(self, data): iv = get_random_bytes(AES.block_size) self.cipher = AES.new(self.key, AES.MODE_CBC, iv) return b64encode(iv + self.cipher.encrypt(pad(data, AES.block_size))) def encrypt_iv(self, data, iv): self.cipher = AES.new(self.key, AES.MODE_CBC, iv) return b64encode(iv + self.cipher.encrypt(pad(data, AES.block_size))) def decrypt(self, data): raw = b64decode(data) self.cipher = AES.new(self.key, AES.MODE_CBC, raw[:AES.block_size]) return unpad(self.cipher.decrypt(raw[AES.block_size:]), AES.block_size) flag = open("flag", "rb").read().strip() COMMAND = [b'test',b'show'] def run_server(client, aes_key, token): client.send(b'test Command: ' + AESCipher(aes_key).encrypt(token+COMMAND[0]) + b'\n') client.send(b'**Cipher oracle**\n') client.send(b'IV...: ') iv = b64decode(client.recv(1024).decode().strip()) client.send(b'Message...: ') msg = b64decode(client.recv(1024).decode().strip()) client.send(b'Ciphertext:' + AESCipher(aes_key).encrypt_iv(msg,iv) + b'\n\n') while(True): client.send(b'Enter your command: ') tt = client.recv(1024).strip() tt2 = AESCipher(aes_key).decrypt(tt) client.send(tt2 + b'\n') if tt2 == token+COMMAND[1]: client.send(b'The flag is: ' + flag) client.close() break if __name__ == '__main__': server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) server.bind(('0.0.0.0', 16001)) server.listen(1) while True: client, address = server.accept() aes_key = get_random_bytes(AES.block_size) token = b64encode(get_random_bytes(AES.block_size*10))[:AES.block_size*10] process = multiprocessing.Process(target=run_server, args=(client, aes_key, token)) process.daemon = True process.start()
攻撃者ができることは
- iv + AES.enc(token + "test", iv) を手に入れられる
- 一度だけ攻撃者が指定した iv', msg を使って iv' + AWS.enc(msg, iv') を手に入れられる
- テキトーな暗号文を復号してもらえる
目標は iv + AES.enc(token + "show", iv) をサーバに渡して復号してもらうことです。iv はレスポンスの先頭に引っ付いてるので既知です。問題はどのようにして AES.enc(token + "test", iv) から AES.enc(token + "show", iv) を求めるかです。
ここでポイントとなるのは、token はちょうど 10 ブロック分でというところです。CBC モードの暗号化処理を見てみましょう。(Wikipedia より引用)
token がちょうど 10 ブロック分ということは、暗号化した後の token の最後の 1 ブロックを iv 、msg を "show" に設定してサーバに暗号化して貰えれば、 "show" の暗号ブロックのみを手に入れられます。
これを暗号化した token と結合すれば AES.enc(token + "show", iv) の出来上がりです。
ソルバは以下の通り
from pwn import * from base64 import b64decode from base64 import b64encode AES_BLOCK_SIZE = 16 # bytes p = remote("35.200.115.41", 16001) def get_test_command(p) -> bytes: p.recvuntil("test Command: ") return b64decode(p.recvline(keepends=False)) def enc(p, iv: bytes, msg: bytes) -> bytes: p.recvuntil("IV...: ") p.sendline(b64encode(iv)) p.recvuntil("Message...: ") p.sendline(b64encode(msg)) p.recvuntil("Ciphertext:") return b64decode(p.recvline(keepends=False)) def send_command(p, msg: bytes) -> bytes: p.recvuntil("Enter your command: ") p.sendline(b64encode(msg)) return b64decode(p.recvline(keepends=False)) iv_token_test = get_test_command(p) iv_token = iv_token_test[:AES_BLOCK_SIZE*11] next_iv = iv_token[-AES_BLOCK_SIZE:] # feed to oracle as iv next_iv_show = enc(p, next_iv, b"show") show = next_iv_show[AES_BLOCK_SIZE:] iv_token_show = iv_token + show result = p.recvline(keepends=False) send_command(p, iv_token_show) print(p.recv()) p.close()
babycrypto2
ぶっちゃけ babycrypto1 より簡単です。ソースコードは babycrypto1 とほぼ同じで、差分だけ取り出してみると
AES_KEY = get_random_bytes(AES.block_size) TOKEN = b64encode(get_random_bytes(AES.block_size*10-1)) COMMAND = [b'test',b'show'] PREFIX = b'Command: ' def run_server(client): client.send(b'test Command: ' + AESCipher(AES_KEY).encrypt(PREFIX+COMMAND[0]+TOKEN) + b'\n') while(True): client.send(b'Enter your command: ') tt = client.recv(1024).strip() tt2 = AESCipher(AES_KEY).decrypt(tt) client.send(tt2 + b'\n') if tt2 == PREFIX+COMMAND[1]+TOKEN: client.send(b'The flag is: ' + flag) client.close() break
ポイントは
- 最初に手に入れられる暗号が iv + AES.enc(token + "test", iv) から iv + AES.enc("Command: " + "test" + token, iv) に変わった
- サーバに暗号化してもらえなくなった(できるのは暗号文の復号のみ)
このような状態で AES.enc("Command: " + "show" + token, iv) を求める必要があります。この問題で使えるのが iv です。
CBC モードの復号処理を見てみましょう。(Wikipedia より引用)
最初のブロックが平文に戻る直前に iv と xor を取っていますよね。xor の性質(同じ値で xor を計算すると 0 になる)を使って iv を改ざんすることで、復号したあとの平文を test ではなく show にすることができます。
ソルバは以下の通り
from pwn import * from base64 import b64decode from base64 import b64encode from Crypto.Util.number import bytes_to_long as b2l from Crypto.Util.number import long_to_bytes as l2b AES_BLOCK_SIZE = 16 # bytes p = remote("35.200.39.68", 16002) def get_test_command(p) -> bytes: p.recvuntil("test Command: ") return b64decode(p.recvline(keepends=False)) def send_command(p, msg: bytes) -> bytes: p.recvuntil("Enter your command: ") p.sendline(b64encode(msg)) return p.recvline(keepends=False) iv_command_test_token = get_test_command(p) iv = iv_command_test_token[:AES_BLOCK_SIZE] partial_iv = b2l(iv[9:9+4]) ^ b2l(b"test") ^ b2l(b"show") partial_iv = l2b(partial_iv) iv = iv[:9] + partial_iv + iv[9+4:] enc = iv + iv_command_test_token[AES_BLOCK_SIZE:] send_command(p, enc) print(p.recv()) p.close()
おわりに
死ぬほどやる気がでなかった