虚数は好きですか?
リモートで虚数を追加、表示できるアプリが動いています
各虚数はRe + Imi
のようなキー名で保存されます
また、虚数のリストを暗号化してエクスポートしたりそのデータをインポートしたりすることもできます
これらの機能を上手く使って、キー名が1337i
のデータを作成して_secret()
を呼べばflagが得られるようです
問題ファイル
import json
import os
from socketserver import ThreadingTCPServer, BaseRequestHandler
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad
from secret import flag, key
class ImaginaryService(BaseRequestHandler):
def handle(self):
try:
self.request.sendall(b'Welcome to Secret IMAGINARY NUMBER Store!\n')
self.numbers = {}
while True:
num = self._menu()
if num == 1:
self._save()
elif num == 2:
self._show()
elif num == 3:
self._import()
elif num == 4:
self._export()
elif num == 5:
self._secret()
else:
break
except Exception as e:
try:
self.request.sendall(f'ERR: {e}\n'.encode())
except Exception:
pass
def _menu(self):
self.request.sendall(b'1. Save a number\n')
self.request.sendall(b'2. Show numbers\n')
self.request.sendall(b'3. Import numbers\n')
self.request.sendall(b'4. Export numbers\n')
self.request.sendall(b'0. Exit\n')
self.request.sendall(b'> ')
try:
return int(self.request.recv(128).strip())
except ValueError:
return 0
def _save(self):
try:
self.request.sendall(b'Real part> ')
re = int(self.request.recv(128).strip())
self.request.sendall(b'Imaginary part> ')
im = int(self.request.recv(128).strip())
name = f'{re} + {im}i'
self.numbers[name] = [re, im]
except ValueError:
pass
def _show(self):
self.request.sendall(b'-' * 50 + b'\n')
for name in self.numbers:
re, im = self.numbers[name]
self.request.sendall(f'{name}: ({re}, {im})\n'.encode())
self.request.sendall(b'-' * 50 + b'\n')
def _import(self):
self.request.sendall(b'Exported String> ')
data = self.request.recv(1024).strip().decode()
enc = bytes.fromhex(data)
cipher = AES.new(key, AES.MODE_ECB)
plaintext = unpad(cipher.decrypt(enc), AES.block_size)
self.numbers = json.loads(plaintext.decode())
self.request.sendall(b'Imported.\n')
self._show()
def _export(self):
cipher = AES.new(key, AES.MODE_ECB)
dump = pad(json.dumps(self.numbers).encode(), AES.block_size)
self.request.sendall(dump + b'\n')
enc = cipher.encrypt(dump)
self.request.sendall(b'Exported:\n')
self.request.sendall(enc.hex().encode() + b'\n')
def _secret(self):
if '1337i' in self.numbers:
self.request.sendall(b'Congratulations!\n')
self.request.sendall(f'The flag is {flag}\n'.encode())
if __name__ == '__main__':
host = os.getenv('CTF4B_HOST')
port = os.getenv('CTF4B_PORT')
if not host:
host = 'localhost'
if not port:
port = '1337'
ThreadingTCPServer.allow_reuse_address = True
server = ThreadingTCPServer((host, int(port)), ImaginaryService)
print(f'Start server at {host}:{port}')
server.serve_forever()
_import()
及び_export()
関数の暗号化処理ではAESのECBモードを利用しています
Wikipediaにも書いてある通りECBモードは「各ブロックの内容と鍵が一致すれば常に暗号文は同じになる(前のデータや位置に左右されない)」という特徴があります
つまり、「ブロックの最後が"
で終わる暗号文」と「ブロックの最初が1337i"
で始まる暗号文」を用意してつぎはぎしたものをimportさせればキー名が1337i
のデータを作成することが可能です
pycryptodomeのAESのブロックサイズは16Bらしいので、16nB目が"
になるようなデータと16n+1Bからの6Bが1337i"
のデータをexportしてもらいます
前者は{Re: 1111111, Im: 1}
の虚数を追加後{Re: 1, Im: 1}
の虚数を追加してexport、後者は{Re: 11111, Im: 1}
の虚数を追加後{Re: 1, Im: 1337}
の虚数を追加してexportすることで得られます
それぞれのデータは以下のようになります
{"1111111 + 1i": [1111111, 1], "1 + 1i": [1, 1]}
33c7461caec455639a2c78889df87b2b787ed5c63954b411c12f2306190bb676450ebe4d6d0ea85378c0212781ca5cdd8db4341b6d2b363abdc9d13de3042f42
{"11111 + 1i": [11111, 1], "1 + 1337i": [1, 1337]}
8de3745294d8770f84b8c692029f1cc452280c588bc07a960f5fdd9f619008dd063254e779270e657f7ffba29dcd4af2f36e1309837f85a8ef0b81de1e9a6bb2
これらのデータは16進数表記なので2文字で1Bを表します。
前者では32B目が"
になっているので先頭64文字、後者では33B目以降が1337i"
なので先頭64文字を落としたものを組み合わせた暗号文を作成します
33c7461caec455639a2c78889df87b2b787ed5c63954b411c12f2306190bb676063254e779270e657f7ffba29dcd4af2f36e1309837f85a8ef0b81de1e9a6bb2
後は作成した暗号文をimportさせて_secret()
を呼べばflag
ctf4b{yeah_you_are_a_member_of_imaginary_number_club}