TL;DR
- AES CTR 송신 루프가 counter를 block 수만큼 증가시키지 않아 연속 keystream 재사용이 발생
- 두 암호문을 한 블록씩 어긋나게 XOR하여 plaintext XOR 연쇄를 구성
- printable scoring 탐색
Analysis
#!/usr/bin/env python3import sysimport timefrom binascii import hexlifyfrom Crypto.Cipher import AESfrom Crypto.Util import Counterfrom Crypto.Random import get_random_bytesfrom ctf_secrets import MESSAGE
KEY = get_random_bytes(16)NONCE = get_random_bytes(8)
WELCOME = ''' ,-. / \ : \ ....* | . .- \-----00'' : . ..' \''// \ . . \/ \ . ' . NASA Deep Space Listening Posts , . \ \ ~ Est. 1969 ~,|,. -.\ \ '.|| `-...__..- | | "We're always listening to you!" |__| /||\\ //||\\ // || \\\__//__||__\\__'--------------''''
def main():
# Print ASCII art and intro sys.stdout.write(WELCOME) sys.stdout.flush() time.sleep(0.5)
sys.stdout.write("\nConnecting to remote station") sys.stdout.flush()
for i in range(5): sys.stdout.write(".") sys.stdout.flush() time.sleep(0.5)
sys.stdout.write("\n\n== BEGINNING TRANSMISSION ==\n\n") sys.stdout.flush()
C = 0 while True: ctr = Counter.new(64, prefix=NONCE, initial_value=C, little_endian=False) cipher = AES.new(KEY, AES.MODE_CTR, counter=ctr) ct = cipher.encrypt(MESSAGE) sys.stdout.write("%s\n" % hexlify(ct).decode()) sys.stdout.flush() C += 1 time.sleep(0.5)
if __name__ == "__main__": main()Counter.new(64, prefix=NONCE, initial_value=C, little_endian=False)는 상위 64비트를 8바이트NONCE, 하위 64비트를 big endian counter로 사용하는 AES CTR counter 객체를 생성- 루프 변수
C는 메시지를 한 번 보낼 때마다 1씩 증가 cipher.encrypt(MESSAGE)는 길이 인 plaintext를 암호화하고, 결과 ciphertext는hexlify후 표준 출력으로 전송MESSAGE길이는shell로그 기준 0x343바이트이므로 최소 21개의 16바이트 블록을 포함
CTR 모드에서 keystream block 는 nonce와 counter의 조합으로 결정된다. 첫 번째 전송에서 사용되는 counter는 이다. 두 번째 전송에서는 초기 값이 이므로 counter 집합은 이다. 따라서 block 인덱스 관계는 다음과 같다.
두 식을 XOR하면 keystream이 제거되어
plaintext XOR 연쇄가 얻어진다. 가 block 수 만큼 증가하지 않는 버그 때문에 keystream이 한 block만큼 이동하여 재사용된다.
Exploit
from pwn import *import string
context.log_level = 'debug'
PRINTABLE_SET = set(range(0x20, 0x7F)) | {0x0a, 0x0d, 0x09}
def char_score(b): if b in (0x0a, 0x0d): return 6 if 0x30 <= b <= 0x39: return 5 if 0x41 <= b <= 0x5A or 0x61 <= b <= 0x7A: return 8 if b == 0x20: return 7 if b in map(ord, ".,:;!?()[]{}<>-_'\"/@\\"): return 4 if b in PRINTABLE_SET: return 1 return -50
def score_chain(bytes_seq): return sum(char_score(x) for x in bytes_seq)
def recv_cipher_lines(p, n=2): p.recvuntil(b'==\n') while True: line = p.recvline(timeout=5) if not line: continue if line.strip(): cts = [] try: cts.append(bytes.fromhex(line.strip().decode())) except Exception: continue break while len(cts) < n: line = p.recvline(timeout=5) if not line: continue line = line.strip() if not line: continue try: ct = bytes.fromhex(line.decode()) except Exception: continue cts.append(ct)
lengths = {len(c) for c in cts} if len(lengths) != 1: raise ValueError('ciphertext lengths differ') return cts
def xor_bytes(a, b): return bytes(x ^ y for x, y in zip(a, b))
def recover_message(ct0, ct1): L = len(ct0) if L != len(ct1) or L < 32: raise ValueError('need two equal-length ciphertexts, >=32 bytes')
delta = xor_bytes(ct0[16:], ct1[:-16])
M = bytearray(L)
for r in range(16): best_seed = None best_score = -10**9 best_chain = None
for seed in PRINTABLE_SET: chain = [] v = seed i = r ok = True chain.append(v) while i <= L - 16 - 1: v = v ^ delta[i] chain.append(v) if v not in PRINTABLE_SET: ok = False break i += 16 if not ok: continue sc = score_chain(chain) if sc > best_score: best_score = sc best_seed = seed best_chain = chain
if best_seed is None: best_seed = 0x20 best_chain = [best_seed] v = best_seed i = r while i <= L - 16 - 1: v = v ^ delta[i] best_chain.append(v) i += 16
M[r] = best_chain[0] i = r idx = 1 while i <= L - 16 - 1: M[i + 16] = best_chain[idx] i += 16 idx += 1
return bytes(M)
def main(): p = remote('chal.sunshinectf.games', 25403) ct0, ct1 = recv_cipher_lines(p, n=2) msg = recover_message(ct0, ct1)
try: s = msg.decode('utf-8') except UnicodeDecodeError: try: s = msg.decode('latin-1') except UnicodeDecodeError: s = ''.join(chr(b) if 32 <= b < 127 else '.' for b in msg)
print(s)
if __name__ == '__main__': main()delta = xor_bytes(ct0[16:], ct1[:-16])는 첫 번째 암호문의 block 1 이후와 두 번째 암호문의 마지막 block 이전을 XOR하여 체인을 만든다- 각 열에 대해 모든 초기 seed 후보를 탐색
b'== BEGINNING TRANSMISSION ==\n' b'\n'[DEBUG] Received 0x343 bytes: b'497e8eba7052109da931829016a3255e63104b814893976216ee6ca86ad7fdd4f4c00b69f3af51250951b4e653a6b727b6a56d07a7f2f74be92d13c4dafc1b02dcb4856706727c51b1fa7031db854ab0413008d5ebe0ba4e715fb23b0abddddf5f5a1182284b3e82a7006653450020c608208eaf1100f1720449a547da2d5797cc698018c0a579e06a45a30df79d03030e4af1ef76d1c6cc1929f2eb66f36dda5da3cfb81c8b6dc8cc653ba3394258c15fc12e5215c25fc48a7dc495c2e7688ae9397107012bc67488057e612cb070552c2a8c4018d1dee92b818b83c81a868e4b312912dbba9017dcabe7d9a40b1d9eed2c696f73d5f56409dec4c72fd2e9b9841bcdef93cc1f7266e030620d961939615209d21d7f020c6a850d0ef10df35f6a94e00f2b6a67d7fda433e2c40f1583ed1431a8ac4e9f020bebd01cd0a82420f27cfc825cafb03465b678cae14087298b0b263c1c10f208dcd5facbc0b719be10f03af600467e0ba656fdd9044f6482e1b2a26ddd4bf160af6e419d020837a6af6bf5890b5076a5bf11e969f912103db5a906f73c59d73eda00d5e87144b99e4a\n'[DEBUG] Received 0x343 bytes: b'480b40834fd4d94c11e2248c7885ead3f5cc457aefa11e190f51bdf354e3ba3cb2af2353bcbaf94abb741dc58ef11400d0fb882f1d69784bb1f770649f825ca843620ad9e1b3b055230bb07209badec818581c87230e25d7a14f2b4e4a0d74cc5c3196bb0c45ea345001e144d33a11c39628b808c7a57ff26a00a25afd810a534e4ac9e263858fd9553fffa478fa6a8d44e69bb10f9a6ddacd703fa0600544894d845e5f12d310d98e7dc6d99db77d81e9394f0a0226896d88087c6131af65163a7e834f0a8897fa2e94c7c1df5593c05c65291fdcff890790a1b2d8b50b1899fa7e6d7f60d5e664078de1c82293feb5d70ac7f297c81f7268b5132e1c984a38284205c91a6d410c22955e5ae010be482388e55a1d7835d7fde660b6d2080a82a81533ed9b0f8f1305a3d15983aa3372f660ea8e58eaf2386df96bdfb1458d71831435210f19ab4183c5bbc2d5a404b557bd0ef1000a7343fe15fdd4134f7882a2fb846dd104eb28a572058c4a492aa0ac79e8c9140c6adabf4ef862b76d533daf8d1cac7935d550cc4490e86c2fe79843e8eaa86c9eb65b6011c0fb42b11be380\n'Greerings, Earthlingu. It has come ti our attention rhat you have chisen to downgradc our mighty plahet to the statuu of 'dwarf'. Ler it be known thgt although we ate small, the Plstonian space napy will not stanb idly by and aceept such disresvect. Please conract us to discuus the terms of Carth's surrendet before we arripe in approximatcly 10 years. Usc this transmissoon code: sun{n3p3r_c0unt_0ut_th5_p1ut0ni4ns}[*] Closed connection to chal.sunshinectf.games port 25403키 바이트 후보를 잘못 잡아 플래그가 다르게 나오는데, sun{n3v3r_c0unt_0ut_th3_p1ut0ni4ns}로 잡으면 풀린다.