๋ฌธ์ : DreamHack โ Unbreakable ๋ถ๋ฅ: crypto ๋์ด๋: ๐ Platinum 3 FLAG:
DH{now_you_love_Mersenne_twister_dont_ya?}
๋ฌธ์ ์ค๋ช ์ด ๋ฑ ํ ์ค์ด๋ค. "Python random module is safe." ๊ทธ ์๋ ํ๋๊ทธ ํฌ๋งท๋ง ์ ํ ์๋ค.

๋ฐ์ ํ์ผ์ 39์ค์ง๋ฆฌ test.py ์ 136KB ์ง๋ฆฌ output ๋ ๊ฐ. ์๋ฒ๋ ์๋ค. ์ถ์ ์๊ฐ ๋์๋ฅผ ๋๋๊ณ ๋ฟ๋ ค ๋๊ณ "์์ ํ๋ค"๊ณ ์จ ๋์ผ๋, ์ ๋นํธ ๋๋ฏธ์์ ์ํ๋ฅผ ๋์ฐพ๋ ๊ฒ ์ ๋ถ๋ค.
์ฐธ๊ณ ๋ก ์ถ์ ์ ๋๋ค์์ด RBTree ๋ค. ๋์ค์ ์์ค์์ ๋ง์ฃผ์น iloveredblacktree / iluvredblacktree ๊ฐ ๊ฑฐ๊ธฐ์ ์๋ค.
๋ฌธ์ ๊ฐ์
| ํญ๋ชฉ | ๋ด์ฉ |
|---|---|
| ๋ฌธ์ ๋ช | Unbreakable |
| ๋์ด๋ | ๐ Platinum 3 |
| ๋ถ๋ฅ | crypto |
| ์ ๊ณต ํ์ผ | test.py (์์ฑ ์คํฌ๋ฆฝํธ), output (12์ค, 136KB) |
| ์๋ฒ | ์์ โ ์์ ์คํ๋ผ์ธ |
| ํต์ฌ ๊ธฐ๋ฒ | MT19937 ๋ถ๋ถ๋นํธ ์ํ๋ณต์ (GF(2) ์ ํ๊ณ 19968 ๋ฏธ์ง์) |
| ๋ณด์กฐ ๊ธฐ๋ฒ | tempering ์ญํจ์, sha256 ํค์ฒด์ธ ์ฌํ, AES-256-CBC ๋ณตํธ |
ํ๋ฆ์ ์ด๋ ๋ค. challenge() ๊ฐ ์ฌ์ฏ ๋ฒ ๋ถ๋ฆฌ๊ณ , ๊ฐ ํธ์ถ์ ์ ์๋๋ก MT19937 ์ ์ด๊ธฐํํ ๋ค 23088๋นํธ๋ฅผ k๋นํธ์ฉ ์๋ผ ๊ทธ๋๋ก ์ถ๋ ฅํ๋ค. ์ด์ด์ ์ถ๋ ฅ ์์ด 100๋ฒ ๋ ๋์๋ฅผ ๋ฝ์ ๊ทธ๊ฑธ๋ก AES ํค๋ฅผ ๋ง๋ค๊ณ , ๋ค์ ๋จ๊ณ์ ํ๋ฌธ์ ์ํธํํด hex ๋ก ์ฐ๋๋ค. k ๋ 32 โ 16 โ 8 โ 4 โ 2 โ 1 ๋ก ์ค์ด๋ค๊ณ , ๋ง์ง๋ง ๋จ๊ณ์ ํ๋ฌธ์ด ํ๋๊ทธ๋ค.
์ฌ์ฏ ๋จ๊ณ๋ ๋
๋ฆฝ์ด ์๋๋ค. i๋ฒ์งธ ๋จ๊ณ์ ํค ์๋๊ฐ i-1๋ฒ์งธ ๋จ๊ณ์ ํ๋ฌธ์ด๋ผ, ์์ ๋ชป ํ๋ฉด ๋ค๋ ์๋ ๋ชป ๋๋ค.
๐ฌ ์ ์ฐฐ โ ๋ฐ์ ๊ฒ ๋ญ์ง๋ถํฐ
๋จผ์ ํ์ผ ๋ ๊ฐ์ ์ ์ฒด๋ฅผ ๋ณธ๋ค.
file extracted/test.py extracted/output && ls -l extracted && wc -l extracted/output
136KB ๊ฐ 12์ค. ํ ์ค์ด ์์ฒญ๋๊ฒ ๊ธธ๋ค๋ ๋ป์ด๋ค. ์์ฑ ์คํฌ๋ฆฝํธ๋ฅผ ํต์งธ๋ก ์ฝ๋๋ค.
cat extracted/test.py
ํต์ฌ๋ง ์ฎ๊ฒจ ์ ์ผ๋ฉด ์ด๋ ๋ค.
TOTAL_BITS = 624 * 37
def challenge(num_bit, previous, current):
random.seed(os.urandom(16))
num_task = (TOTAL_BITS + num_bit - 1) // num_bit
output = ""
for _ in range(num_task):
output += format(random.getrandbits(num_bit), f"0{num_bit}b")
print(output)
key = previous
for _ in range(100):
key += format(random.getrandbits(num_bit), f"0{num_bit}b").encode()
key = sha256(key).digest()
cipher = AES.new(key, AES.MODE_CBC, iv=b'iluvredblacktree')
print(cipher.encrypt(pad(current, 16)).hex())
challenge(32, b'iloveredblacktree', iflags[0])
challenge(16, iflags[0], iflags[1])
challenge(8, iflags[1], iflags[2])
challenge(4, iflags[2], iflags[3])
challenge(2, iflags[3], iflags[4])
challenge(1, iflags[4], final_flag)TOTAL_BITS ๊ฐ 624 * 37 = 23088. 624 ๋ผ๋ ์ซ์๊ฐ ๋๋๊ณ ๋ฐํ ์์ผ๋ ๋ฉ๋ฅด์ผ ํธ์์คํฐ๋ฅผ ๊ฒจ๋ฅํ ๋ฌธ์ ๋ผ๋ ๊ฑด ์ฌ๊ธฐ์ ํ์ ๋๋ค. ์๋๋ os.urandom(16) โ 128๋นํธ๋ผ ์ ์์กฐ์ฌ๋ ๋
ผ์ธ๋ค.
ํ ๋จ๊ณ์ ๊ตฌ์กฐ๋ฅผ ๊ทธ๋ฆผ์ผ๋ก ์ ๋ฆฌํ๋ฉด ์ด๋ ๊ฒ ๋๋ค.

์ด์ output ์ 12์ค์ด ์ ๋ง ์ ๊ตฌ์กฐ์ ๋ง๋์ง ์ผ๋ค. ํ์ ์ค์ด ๋นํธ์ด, ์ง์ ์ค์ด hex ์ํธ๋ฌธ์ด์ด์ผ ํ๊ณ , ๋นํธ์ด ๊ธธ์ด๋ num_task * k ์ฌ์ผ ํ๋ค.
#!/usr/bin/env python3
"""output ํ์ผ์ ๊ตฌ์กฐ๋ฅผ ํ์ธํ๋ค - ์ด๋ ์ค์ด ์ด๋ challenge() ํธ์ถ์ธ์ง."""
TOTAL_BITS = 624 * 37
lines = open('extracted/output').read().split('\n')
print(f"TOTAL_BITS = 624 * 37 = {TOTAL_BITS}")
for idx, k in enumerate([32, 16, 8, 4, 2, 1]):
bits, ct = lines[idx * 2], lines[idx * 2 + 1]
num_task = (TOTAL_BITS + k - 1) // k
print(f"k={k:2d} num_task={num_task:5d} expect={num_task*k:6d} "
f"got={len(bits):6d} {'OK' if len(bits)==num_task*k else 'MISMATCH'}"
f" ct={len(ct)//2:2d}B = {len(ct)//32} AES block(s)")python3 inspect_output.py
์ฌ์ฏ ์ค ์ ๋ถ OK. k=32 ๋ง 23088 / 32 = 721.5 ๋ผ ์ฌ๋ฆผ ๋๋ฌธ์ 722ํ โ 23104๋นํธ๋ก ์กฐ๊ธ ๊ธธ๋ค. ๋๋จธ์ง๋ ๋ฑ 23088๋นํธ๋ค.
์ฌ๊ธฐ์ ์ด๋ฏธ ๋ต์ ์ค๊ณฝ์ด ๋ณด์ธ๋ค. MT19937 ์ ๋ด๋ถ ์ํ๋ 624์๋ ร 32๋นํธ = 19968๋นํธ. ์ถ์ ์๋ k ์ ๋ฌด๊ดํ๊ฒ ํญ์ 23088๋นํธ๋ฅผ ํ๋ฆฌ๋๋ก TOTAL_BITS ๋ฅผ ์ก์ ๋๋ค. 19968๋ณด๋ค ํฌ๋ค. ์ฐ์ฐ์ด ์๋๋ค.
๐งฉ ๋ฐฐ๊ฒฝ โ ๋ฉ๋ฅด์ผ ํธ์์คํฐ์ getrandbits
MT19937 ์ 624๊ฐ์ 32๋นํธ ์๋๋ฅผ ์ํ๋ก ๋ค๊ณ ๋ค๋๋ฉด์, ๋ค ์ฐ๋ฉด ํ๊บผ๋ฒ์ ๋ค์ 624๊ฐ๋ฅผ ๋ง๋ค์ด ๋ด๋(twist) ๊ตฌ์กฐ๋ค. ์ถ๋ ฅ์ ์ํ ์๋๋ฅผ ๊ทธ๋๋ก ์ฃผ๋ ๊ฒ ์๋๋ผ ์ํํธยท๋ง์คํฌยทXOR ์ ๋ค ๋ฒ ๊ฑฐ์น tempering ๊ฒฐ๊ณผ๋ค.

์ถ์ฒ: Wikimedia Commons, Cmglee, CC BY-SA 3.0
์ด ๋ฌธ์ ๋ฅผ ํ๋ ค๋ฉด random.getrandbits(k) ๊ฐ ์ ํํ ๋ฌด์จ ์ผ์ ํ๋์ง ์์์ผ ํ๋ค. CPython ๊ตฌํ์ k <= 32 ์ผ ๋ genrand_uint32() >> (32 - k) ๋ฅผ ๋๋ ค์ค๋ค. ์ฆ ํธ์ถ ํ ๋ฒ์ 32๋นํธ ์๋ ํ๋๋ฅผ ํต์งธ๋ก ์๋นํ๊ณ , ๊ทธ ์ค ์์ k๋นํธ๋ง ์ค๋ค. ๋๋จธ์ง 32-k ๋นํธ๋ ๋ฒ๋ ค์ง๋ค.
๋ฌธ์๋ก๋ง ๋ฏฟ์ง ๋ง๊ณ ์ฌ ๋ณธ๋ค. ๊ฐ์ ์๋๋ก ๋ ๊ฐ์ Random ์ ๋ง๋ค์ด ํ๋๋ getrandbits(32), ํ๋๋ getrandbits(k) ๋ก ๋ฝ์ ๋น๊ตํ๋ฉด ๋์ด๋ค.
#!/usr/bin/env python3
"""getrandbits(k) (k<=32) ๊ฐ 32๋นํธ ์๋ ํ๋๋ฅผ ํต์งธ๋ก ์๋นํ๊ณ ์์ k๋นํธ๋ง ์ค๋ค๋ ๊ฒ์
๊ฐ์ ์ํ์์ ๋ ๊ฐ๋๋ก ๋ฝ์ ๋์กฐํด ํ์ธํ๋ค."""
import random, os
seed = os.urandom(16)
for k in (16, 8, 4, 2, 1):
a = random.Random(); a.seed(seed)
b = random.Random(); b.seed(seed)
full = [a.getrandbits(32) for _ in range(8)]
part = [b.getrandbits(k) for _ in range(8)]
top = [w >> (32 - k) for w in full]
print(f"k={k:2d} getrandbits(k)={part}")
print(f" genrand>>{32-k:2d} ={top} match={part == top}")python3 probe_getrandbits.py
๋ค์ฏ ๊ฒฝ์ฐ ๋ชจ๋ ์ผ์น. k=1 ์ด๋ฉด ์๋๋น ์ต์์ 1๋นํธ๋ง ๋ณด์ธ๋ค๋ ๋ป์ด๊ณ , ๊ทธ๋์ 23088๋นํธ๋ฅผ ์ป์ผ๋ ค๋ฉด 23088๋ฒ ํธ์ถํด์ผ ํ๋ค. ๊ทธ๋์ ์ํ๋ ๊ณ์ twist ๋๋ค. 23088 / 624 = 37 โ ์ด๊ฑด "624์๋์ง๋ฆฌ ์ฐฝ์ ํ ๋ฒ ๋ค์ฌ๋ค๋ณด๋" ๊ฒ ์๋๋ผ "37๋ธ๋ก๋งํผ ๊ตด๋ฌ๊ฐ ์์ด์ ์ต์์ ๋นํธ๋ฅผ ์ ๋ถ ๋ณด๋" ์ํฉ์ด๋ค.
๐งฑ ์ ์ด๊ฒ ์ ํ๋์ ๋ฌธ์ ๊ฐ ๋๋
MT19937 ์๋ ์ฌ๋์ด ๋ฃ์ ๋น์ ํ ์ฐ์ฐ์ด ์๋ค. twist ๋, tempering ๋ ์ ๋ถ ์ํํธยทAND ๋ง์คํฌยทXOR ๋ฟ์ด๋ค. ์ด ์ ์ GF(2)(๋นํธ ํ๋๋ฅผ ์์๋ก ๋ณด๋ 2์์ฒด) ์์์ ์ ํ์ด๋ค.
tempering ๋ถํฐ ๋ณด์.
y ^= y >> 11;
y ^= (y << 7) & 0x9d2c5680;
y ^= (y << 15) & 0xefc60000;
y ^= y >> 18;์ถ๋ ฅ์ ๊ฐ ๋นํธ๋ ์ ๋ ฅ 32๋นํธ ์ค ๋ช ๊ฐ๋ฅผ XOR ํ ๊ฒ์ ์ง๋์ง ์๋๋ค. ๊ทธ๋ฌ๋ฉด ๊ตณ์ด ๋ง์คํฌ๋ฅผ ์์ผ๋ก ์ฎ๊ฒจ ์ ์ด ํ๋ฅผ ๋ง๋ค ํ์๊ฐ ์๋ค. ๋จ์๋ฒกํฐ๋ฅผ ํ๋์ฉ tempering ์ ํต๊ณผ์ํค๋ฉด ๊ทธ ํ๊ฐ ๊ทธ๋๋ก ๋์จ๋ค.
TEMPER_BITS = [[j for j in range(32) if (temper(1 << j) >> b) & 1] for b in range(32)]์ด๊ฒ ๋ง๋์ง ๋ ๊ฐ์ง๋ก ํ์ธํ๋ค. ํ๋๋ untemper(temper(x)) == x ์๋ณต, ๋ค๋ฅธ ํ๋๋ ์ ํ๋ก ์ฌ์กฐ๋ฆฝํ ๊ฐ์ด ์ง์ง temper(x) ์ ๊ฐ์์ง๋ค.
#!/usr/bin/env python3
"""tempering ์ด (1) ๊ฐ์ญ์ด๊ณ (2) GF(2) ์์์ ์ ํ์ด๋ผ๋ ๊ฒ์ ๋ฌด์์ ์
๋ ฅ์ผ๋ก ํ์ธํ๋ค."""
import os
import mt
n = 20000
bad_inv = bad_lin = 0
for _ in range(n):
v = int.from_bytes(os.urandom(4), 'little')
if mt.untemper(mt.temper(v)) != v:
bad_inv += 1
t = 0
for b in range(32): # TEMPER_BITS ๋ก ์ฌ์กฐ๋ฆฝ
acc = 0
for j in mt.TEMPER_BITS[b]:
acc ^= (v >> j) & 1
t |= acc << b
if t != mt.temper(v):
bad_lin += 1
print(f"random inputs : {n}")
print(f"untemper(temper(x))!=x : {bad_inv}")
print(f"XOR-basis != temper(x) : {bad_lin}")
x, y = int.from_bytes(os.urandom(4), 'little'), int.from_bytes(os.urandom(4), 'little')
print(f"temper(x^y) == temper(x)^temper(y) : {mt.temper(x ^ y) == mt.temper(x) ^ mt.temper(y)}")
print(f"TEMPER_BITS[31] = {mt.TEMPER_BITS[31]} (MSB ๋ ์๋ณธ 4๋นํธ์ XOR)")python3 check_linear.py
๋ ๋ค 0๊ฑด. ๋ค์ผ๋ก temper(x ^ y) == temper(x) ^ temper(y) ๋ ์ฐธ์ด๋ค. ์ ํ์ด๋ผ๋ ๊ฒ ์ด ํ ์ค๋ก ๋๋๋ค.
๊ทธ๋ฆฌ๊ณ ๋ง์ง๋ง ์ค์ด k=1 ์ง๋ฆฌ ๋จ๊ณ์์ ์ฐ๋ฆฌ๊ฐ ์ค์ ๋ก ์ฐ๊ฒ ๋ ์์ด๋ค. ์ถ๋ ฅ์ ์ต์์ ๋นํธ๋ ์๋ณธ ์๋์ 16ยท24ยท27ยท31๋ฒ ๋นํธ๋ฅผ XOR ํ ๊ฐ. ์๋ ํ๋๊ฐ ํต์งธ๋ก ์จ์ด ์์ด๋ ์ ๋ค ๋นํธ์ XOR ๊ฐ ํ๋๋ ์๋ ค์ง๋ ์
์ด๋ค.
twist ์ชฝ๋ ๋ง์ฐฌ๊ฐ์ง๋ค. ์์ด ์ ์๋
x[i+624] = x[i+397] ^ ((x[i] & 0x80000000 | x[i+1] & 0x7fffffff) >> 1)
^ (0x9908b0df if x[i+1] & 1 else 0)์ธ๋ฐ, ๋ง์ง๋ง ํญ์ด ์กฐ๊ฑด๋ฌธ์ฒ๋ผ ๋ณด์ฌ๋ ์ค์ x[i+1] ์ ์ตํ์ ๋นํธ๋ฅผ ์์ ๋ง์คํฌ์ ๊ณฑํ ๊ฒ์ด๋ผ ์ญ์ ์ ํ์ด๋ค. ๊ทธ๋์ ์ด๊ธฐ 624์๋์ 19968๋นํธ๋ฅผ ๋ฏธ์ง์๋ก ๋์ผ๋ฉด ์ดํ ๋ชจ๋ ์ถ๋ ฅ ๋นํธ๊ฐ ๊ทธ ๋ฏธ์ง์๋ค์ 1์ฐจ์์ด ๋๋ค.
์ ๋ฆฌํ๋ฉด ์ด๋ ๋ค.
- ๋ฏธ์ง์: 19968๊ฐ (์ฒซ ์ถ๋ ฅ์ ๋ง๋๋ ์์ ์ ์ํ ์๋ 624๊ฐ ร 32๋นํธ)
- ์: 23088๊ฐ (
k์ ๋ฌด๊ดํ๊ฒ ํญ์ ์ด ๊ฐ์) - ๋จ๋ ์ฌ์ : 3120๊ฐ
์์ด ๋ฏธ์ง์๋ณด๋ค ๋ง๋ค. ํธ๋ ๋ฌธ์ ๋ค.
๐ ์ฝ์ง
โถ๐ ์ฝ์ง 1 โ '๊ฐ์ฐ์ค ์๊ฑฐ๋ ๋ช ์๊ฐ ๊ฑธ๋ฆฐ๋ค'๊ณ ์ง๋ ํฌ๊ธฐํ ๋ปํ๋ค
์ฒ์ ๋น์ฉ์ ์ด๋ฆผ์ก์ ๋ ์ด๋ ๊ฒ ์
๋ค. ๋ฏธ์ง์ 19968๊ฐ, ์ 23088๊ฐ๋๊น ์๊ฑฐ์ ํ์ํ ํ XOR ์ด ๋๋ต 23088 ร 19968 / 2 โ 2.3์ต ๋ฒ. ํ ํ์ด 19969๋นํธ(์ฝ 2.5KB)์ด๋ ํ์ด์ฌ ์ ์ XOR ํ ๋ฒ์ 1๋ง์ดํฌ๋ก์ด๋ง ์ก์๋ 230์ด, ์ค์ ๋ก๋ ๋ฃจํ ์ค๋ฒํค๋๊น์ง ๋ถ์ด ์์ญ ๋ถ. ๊ทธ๊ฒ ๋ค์ฏ ๋ฒ(k ๊ฐ 16ยท8ยท4ยท2ยท1)์ด๋ฉด ์ธ์
ํ๋๊ฐ ํต์งธ๋ก ๋ ์๊ฐ๋ค.
๊ทธ๋์ ํ์ฐธ์ "์ ํธ๋ ๋ฐฉ๋ฒ"๋ง ์ฐพ์๋ค. ๋ฐฉ์ ์์ ํฌ์์ฑ์ ์ธ๊น(์ถ๋ ฅ i๋ ์๋ ์ธ ๊ฐ๋ง ๊ฑด๋๋ฆฐ๋ค), ์๋ ๋จ์๋ก ์ฌ๋งค๊ฐํํด์ ๋ฏธ์ง์๋ฅผ 9984๊ฐ๋ก ์ค์ผ๊น, ๋ธ๋ก ๋๊ฐ ๋ถ๋ถ๋ง ๋จผ์ ์๊ฑฐํ ๊น.
์ ๋ถ ํ์๊ณ ์๋ค. numpy ๋ก ํ์ uint64 313๊ฐ์ ํฉํนํ๊ณ , ํผ๋ฒ ์ด์ด ์ํ ์๋๋ถํฐ ๋๊น์ง๋ง XOR ํ๋๋ก ์ฌ๋ผ์ด์คํ๋ฉด ๊ทธ๋ง์ด์๋ค.
tgt = r + nz[1:]
if tgt.size:
A[tgt, w:] ^= A[r, w:]ํผ๋ฒ์ด ๋ค๋ก ๊ฐ์๋ก w ๊ฐ ์ปค์ ธ์ ์ค์ XOR ํญ์ด ๊ณ์ ์ค์ด๋ ๋ค. ํ๊ท ์ ์ผ๋ก ํ์ ์ ๋ฐ๋ง ๋ง์ง๋ ์
์ด๋ค. ์ฌ๊ธฐ์ RREF ๋์ ์ ๋ฐฉ์๊ฑฐ + ํ๋ฐฉ๋์
์ผ๋ก ๋ฐ๊ฟ ํ ๋ฒ ๋ ๋ฐ์ผ๋ก ์ค์๋ค. ํ๋ฐฉ๋์
์ ํ ํ๋๋น popcount(A[i] & x) & 1 ํ ๋ฒ์ด๋ผ 19968ํ์ ๋ค ๋์๋ ์์ญ ๋ฐ๋ฆฌ์ด๋ค.
๊ฒฐ๊ณผ๋ ๋จ๊ณ๋น 3~12์ด. ์ฒ์ ์ด๋ฆผ๋ณด๋ค ๋ ์๋ฆฟ์ ๋นจ๋๋ค. ๋น์ฉ ๊ณ์ฐ์ด ํ๋ฆฐ ๊ฒ ์๋๋ผ, ํ์ด์ฌ ์ ์ XOR ์ ์ ์ ๋ก ๊ณ์ฐํ ๊ฒ ํ๋ ธ๋ค.
โถ๐ ์ฝ์ง 2 โ ํด๊ฐ ์ ์ผํ์ง ์์์ ๋ณต์์ ์คํจํ ์ค ์์๋ค
k=16 ์ ์ฒ์ ๋๋ ธ์ ๋ ์๊ฑฐ ๊ฒฐ๊ณผ์ ๋ญํฌ๊ฐ 19968์ด ์๋๋ผ 19953์ด์๋ค. ์์ ๋ณ์๊ฐ 15๊ฐ ๋จ๋๋ค๋ ๋ป์ด๋ค. ํด๊ฐ ํ๋๋ก ์ ๋จ์ด์ง๋ ๋น์ฐํ "์์ด ๋ชจ์๋๋ค = ๋ชป ํผ๋ค"๋ก ์ฝ์๋ค.
๊ทธ๋ฐ๋ฐ k ๋ฅผ ๋ฎ์ถ๋ฉด์ ์ฌ ๋ณด๋ ์์ ๋ณ์ ๊ฐ์๊ฐ ๊ท์น์ ์ด์๋ค.
k | ์์ ๋ณ์ | 31 - k |
|---|---|---|
| 16 | 15 | 15 |
| 8 | 23 | 23 |
| 4 | 27 | 27 |
| 2 | 29 | 29 |
| 1 | 30 | 30 |
์ ํํ 31 - k. ์ฐ์ฐ์ผ ๋ฆฌ๊ฐ ์์ด์ ์์ ๋ณ์๊ฐ ์ด๋ ๋ฏธ์ง์์ธ์ง๋ฅผ ์ง์ ์ฐ์ด ๋ดค๋ค.
#!/usr/bin/env python3
"""ํด๊ฐ ํ๋๋ก ์ ๋จ์ด์ง๋๋ฐ ์ ์์ธก์ ๋๋๊ฐ - ์์ ๋ณ์์ ์์น๋ฅผ ์ง์ ๋ณธ๋ค.
solve_gf2 ๊ฐ ๋๋ ค์ฃผ๋ free column ๋ฒํธ๋ฅผ (์๋, ๋นํธ) ๋ก ํ์ด์ ์ฐ๋๋ค.
์์ ๋ณ์๊ฐ ์ ๋ถ word 0 ์ ํ์ 31๋นํธ ์์ ์์ผ๋ฉด, twist ๋ word 0 ์ ์์ 1๋นํธ๋ก๋ง
์ฝ์ผ๋ฏ๋ก ๊ทธ ๋ชจํธํจ์ o_0 ํ ๊ณณ์๋ง ๋จ๊ณ ๋ฏธ๋ ์ถ๋ ฅ์๋ ์ํฅ์ด ์๋ค."""
import mt
TOTAL_BITS = 624 * 37
lines = open('extracted/output').read().split('\n')
for idx, k in enumerate([16, 8, 4, 2, 1]):
bits = lines[idx * 2 + 2]
nout = (TOTAL_BITS + k - 1) // k
rows, rhs = mt.build_equations(k, bits, nout)
sol, free = mt.solve_gf2(rows, rhs)
words = sorted({c // 32 for c in free})
bits_in_w0 = [c % 32 for c in free if c // 32 == 0]
print(f"k={k:2d} nullity={len(free):2d} free ๊ฐ ๊ฑธ์น ์๋={words} "
f"word0 ๋นํธ={min(bits_in_w0)}..{max(bits_in_w0)} "
f"(31-k={31-k})")python3 nullspace.py
์์ ๋ณ์๊ฐ ์ ๋ถ 0๋ฒ ์๋์ ํ์ ๋นํธ์๋ค. ์ด์ ๊ฐ ์๋ค. twist ๋ x[i] ๋ฅผ ์ต์์ 1๋นํธ๋ก๋ง ์ฝ๊ณ (& 0x80000000), ํ์ 31๋นํธ๋ x[i+1] ์ชฝ์์๋ง ์ด๋ค. ๊ทธ๋ฌ๋ 0๋ฒ ์๋์ ํ์ 31๋นํธ๋ ์๊ธฐ ์์ ์ ์ฒซ ์ถ๋ ฅ์๋ง ๋ํ๋๊ณ ์ดํ ์ด๋ค ์๋์๋ ์ ํ๋์ง ์๋๋ค. ๊ทธ ์ฒซ ์ถ๋ ฅ์์ ์ฐ๋ฆฌ๊ฐ ๋ณด๋ ๊ฑด ์์ k๋นํธ๋ฟ์ด๋, ์ ํํ 31 - k ๊ฐ๊ฐ ๋๊น์ง ๊ฒฐ์ ๋์ง ์๊ณ ๋จ๋๋ค.
๊ทธ๋ฆฌ๊ณ ์ด ๋ชจํธํจ์ ์ฐ๋ฆฌ๊ฐ ํ์ํ ๊ฒ๊ณผ ๋ฌด๊ดํ๋ค. ์ฐ๋ฆฌ๊ฐ ์์ธกํ๋ ค๋ ๊ฑด 23088๋ฒ์งธ ์ดํ์ ์ถ๋ ฅ์ด๊ณ , ๊ฑฐ๊ธฐ์ 0๋ฒ ์๋์ ํ์ ๋นํธ๊ฐ ํ ํจ๋ ์ ๋ค์ด๊ฐ๋ค. ์์ ๋ณ์๋ฅผ ์ ๋ถ 0์ผ๋ก ๋ฐ์๋ ์๋ฌด ๋ฌธ์ ๊ฐ ์๋ค.
์ฐธ๊ณ ๋ก 624 ร 32 - 31 = 19937. MT19937 ์ ์ด๋ฆ์ ๋ถ์ ๊ทธ 19937์ด๋ค. ์ํ ๊ณต๊ฐ์ด 19968๋นํธ๊ฐ ์๋๋ผ 19937๋นํธ์ธ ์ด์ ๊ฐ ๋ฐ๋ก ์ด "์ฐ์ด์ง ์๋ 31๋นํธ"๋ค. ๋ญํฌ ๋ถ์กฑ์ ์คํจ๋ก ์ฝ์๋๋ฐ, ์ค์ ๊ทธ ์ซ์๊ฐ ์๊ณ ๋ฆฌ์ฆ ์ด๋ฆ์ ๊ทธ๋๋ก ์ค๋ช
ํ๊ณ ์์๋ค.
๐ฃ ํต์ฌ โ ๋์ถ ๋นํธ ํ๋๋ฅผ 1์ฐจ์ ํ ์ค๋ก
๋ฐฉ์นจ์ด ์ ํด์ก์ผ๋ ๊ตฌํ์ ๋จ์ํ๋ค. ๋ฏธ์ง์๋ฅผ 19968๊ฐ์ ์ฌ๋ณผ๋ก ๋๊ณ , MT19937 ์ ์ฌ๋ณผ๋ฆญ์ผ๋ก ๋๋ฆฐ๋ค. ํ์ด์ฌ ์ ์ ํ๋๋ฅผ 19968๋นํธ์ง๋ฆฌ GF(2) ํ๋ฒกํฐ๋ก ์ฐ๋ฉด XOR ์ฐ์ฐ์๊ฐ ๊ทธ๋๋ก ๋ฒกํฐ ๋ง์ ์ด ๋๋ค.
๋ฏธ์ง์ ๋ฒํธ๋ฅผ ์ก๋ ๊ธฐ์ค์ด ์ค์ํ๋ค. random.setstate ๋ 624์๋ + ์ธ๋ฑ์ค๋ฅผ ๋ฐ๋๋ฐ, ์ธ๋ฑ์ค๋ฅผ 0์ผ๋ก ์ฃผ๋ฉด ๋ค์ ์ถ๋ ฅ์ด ๊ณง state[0] ์ tempering ์ด๋ค. ๊ทธ๋์ "์ฒซ ์ถ๋ ฅ์ ๋ง๋๋ ์์ ์ 624์๋"๋ฅผ ๋ฏธ์ง์๋ก ์ก์ผ๋ฉด, ๋ณต์ ๊ฒฐ๊ณผ๋ฅผ ๊ทธ๋๋ก setstate ์ ๋ฐ์ด ๋ฃ์ด ์ ๋๋ ์ดํฐ๋ฅผ ๋ณต์ ํ ์ ์๋ค.
๊ทธ ๋ค์ ์๋๋ค์ ์ฌ๊ท๋ก ์ป๋๋ค. ์ฐฝ(window) ์ 624๊ฐ์ง๋ฆฌ deque ๋ก ๊ตด๋ฆฌ๋ฉด win[0], win[1], win[397] ์ด ๊ฐ๊ฐ x[i-624], x[i-623], x[i-227] ์ด ๋๋ค.
def build_equations(k, bitstr, nout):
need = list(range(31, 31 - k, -1)) # top k output bit indices, MSB first
rows, rhs = [], []
win = deque(maxlen=N)
def emit(i, w):
base = i * k
for p, b in enumerate(need):
acc = 0
for j in TEMPER_BITS[b]:
acc ^= w[j]
rows.append(acc)
rhs.append(bitstr[base + p] == '1')
for i in range(N):
w = [1 << (i * 32 + j) for j in range(32)]
if i < nout:
emit(i, w)
win.append(w)
for i in range(N, nout):
w = twist_sym(win[0], win[1], win[N - 227])
emit(i, w)
win.append(w)
return rows, rhsbitstr ์์ ์ด๋ ๊ธ์๊ฐ ์ด๋ ๋นํธ์ธ์ง๋ ํท๊ฐ๋ฆฌ๊ธฐ ์ฌ์ด ๋ถ๋ถ์ด๋ค. format(v, f"0{k}b") ๋ MSB ๋ถํฐ ์ฐ์ผ๋, ๊ทธ๋ฃน i ์ p๋ฒ์งธ ๊ธ์๋ tempering ์ถ๋ ฅ์ 31-p ๋ฒ ๋นํธ๋ค. ๊ทธ๋์ need ๋ฅผ 31๋ถํฐ ๋ด๋ ค๊ฐ๊ฒ ์ก์๋ค.
์ ์ฒด ๋ชจ๋์ ์ด๋ ๋ค. ์ด๊ฒ ์ํ ๋ณต์์ ์ ๋ถ๋ค.
#!/usr/bin/env python3
"""MT19937 partial-bit state recovery over GF(2).
getrandbits(k) (k<=32) in CPython returns genrand_uint32() >> (32-k), i.e. the
TOP k bits of one tempered 32-bit output. Both the twist and the tempering are
GF(2)-linear in the 624*32 = 19968 state bits, so every leaked bit is one linear
equation. Collect enough of them and solve.
"""
import numpy as np
N = 624
M = 397
MATRIX_A = 0x9908B0DF
NUNK = N * 32 # 19968 unknowns
def temper(y):
y ^= y >> 11
y ^= (y << 7) & 0x9D2C5680
y ^= (y << 15) & 0xEFC60000
y ^= y >> 18
return y & 0xFFFFFFFF
# TEMPER_BITS[b] = which source bits of the raw state word feed output bit b.
# Derived by tempering the unit vectors, so no hand-transcribed shift masks.
TEMPER_BITS = [[j for j in range(32) if (temper(1 << j) >> b) & 1] for b in range(32)]
def _unshift_right(y, s):
x = y
for _ in range(32 // s + 1):
x = y ^ (x >> s)
return x & 0xFFFFFFFF
def _unshift_left(y, s, mask):
x = y
for _ in range(32 // s + 1):
x = y ^ ((x << s) & mask)
return x & 0xFFFFFFFF
def untemper(y):
y = _unshift_right(y, 18)
y = _unshift_left(y, 15, 0xEFC60000)
y = _unshift_left(y, 7, 0x9D2C5680)
return _unshift_right(y, 11)
def twist_sym(u, v, z):
"""Symbolic x[i] = x[i-227] ^ (((x[i-624]&0x80000000)|(x[i-623]&0x7fffffff))>>1)
^ (0x9908b0df if x[i-623]&1 else 0)
u = x[i-624], v = x[i-623], z = x[i-227]; each is a list of 32 GF(2) row vectors."""
out = [0] * 32
for j in range(32):
t = z[j]
if j <= 29:
t ^= v[j + 1] # (concat >> 1) bit j == concat bit j+1 == v[j+1]
elif j == 30:
t ^= u[31] # concat bit 31 is the UPPER bit of u
if (MATRIX_A >> j) & 1:
t ^= v[0] # mag01[v & 1]
out[j] = t
return out
def build_equations(k, bitstr, nout):
"""Return (rows, rhs): one GF(2) equation per leaked bit.
Unknown j*32+b is bit b of state word j, where word 0 is the word that
produces the very first output (i.e. random.setstate index == 0)."""
from collections import deque
need = list(range(31, 31 - k, -1)) # top k output bit indices, MSB first
rows, rhs = [], []
win = deque(maxlen=N)
def emit(i, w):
base = i * k
for p, b in enumerate(need):
acc = 0
for j in TEMPER_BITS[b]:
acc ^= w[j]
rows.append(acc)
rhs.append(bitstr[base + p] == '1')
for i in range(N):
w = [1 << (i * 32 + j) for j in range(32)]
if i < nout:
emit(i, w)
win.append(w)
for i in range(N, nout):
w = twist_sym(win[0], win[1], win[N - 227])
emit(i, w)
win.append(w)
return rows, rhs
def solve_gf2(rows, rhs, nunk=NUNK):
"""Forward elimination to row echelon form, then back substitution.
Free variables are pinned to 0. Returns (solution_bits, free_columns)."""
R = len(rows)
W = (nunk + 1 + 63) // 64
A = np.zeros((R, W), dtype=np.uint64)
for i, (v, b) in enumerate(zip(rows, rhs)):
if b:
v |= 1 << nunk
A[i] = np.frombuffer(v.to_bytes(W * 8, 'little'), dtype=np.uint64)
piv = []
r = 0
for c in range(nunk):
w = c >> 6
msk = np.uint64(1 << (c & 63))
nz = np.flatnonzero(A[r:, w] & msk)
if nz.size == 0:
continue
p = r + int(nz[0])
if p != r:
A[[r, p]] = A[[p, r]]
nz = np.flatnonzero(A[r:, w] & msk)
tgt = r + nz[1:]
if tgt.size:
A[tgt, w:] ^= A[r, w:]
piv.append(c)
r += 1
last = nunk >> 6
lastbit = np.uint64(1 << (nunk & 63))
if r < R and np.any(A[r:, last] & lastbit):
raise ValueError("inconsistent system")
x = np.zeros(W, dtype=np.uint64)
for i in range(len(piv) - 1, -1, -1):
c = piv[i]
dot = int.from_bytes((A[i] & x).tobytes(), 'little').bit_count() & 1
val = int(A[i][last] >> np.uint64(nunk & 63)) & 1
if dot ^ val:
x[c >> 6] |= np.uint64(1 << (c & 63))
free = sorted(set(range(nunk)) - set(piv))
return int.from_bytes(x.tobytes(), 'little'), free
def state_from_bits(sol):
return [(sol >> (i * 32)) & 0xFFFFFFFF for i in range(N)]k=32 ๋ง์ ์ ํ๊ณ๋ฅผ ์ธ์ธ ํ์๊ฐ ์๋ค. ์๋๊ฐ ํต์งธ๋ก ๋ณด์ด๋ ์ 624๊ฐ๋ฅผ untemper ํ๋ฉด ๊ทธ๊ฒ ๊ณง ์ํ๋ค. ์ ๋ชจ๋์ untemper ๊ฐ ๊ทธ ์ฉ๋๋ค.
์ฌ๋ณผ๋ฆญ ์ ๊ฐ๊ฐ ์ง์ง ๋ง๋์ง๋ถํฐ
์ด๋ฐ ์ฝ๋๋ ๋ถํธ ํ๋๋ง ํ๋ ค๋ "์์ ์ธ์์ง๋๋ฐ ๋ต๋ง ์ ๋์ค๋" ์ํ๊ฐ ๋๋ค. ๊ทธ๋์ ๋ฌธ์ ๋ฐ์ดํฐ๋ฅผ ๊ฑด๋๋ฆฌ๊ธฐ ์ ์, ์ ๋ต์ ์๋ ์ํฉ์ ๋ง๋ค์ด ๊ฒ์ฆํ๋ค. ๋ก์ปฌ์์ ์ง์ ์๋ํ Random ์ ์ง์ง ์ํ๋ฅผ ๋ฝ์ ๋๊ณ , ์ธ์ด 1์ฐจ์์ ๊ทธ ์ํ๋ฅผ ๋์
ํด ์ค์ ์ถ๋ ฅ ๋นํธ์ ํ ๊ฐ๋ผ๋ ์ด๊ธ๋๋์ง ์ผ๋ค.
#!/usr/bin/env python3
"""์ฌ๋ณผ๋ฆญ ์ ๊ฐ๊ฐ ๋ง๋์ง ๊ฒ์ฆํ๋ค.
๋ก์ปฌ์์ ์ง์ seed ํ MT19937 ์ ์ง์ง ์ํ๋ฅผ ์๊ณ ์์ผ๋ฏ๋ก, build_equations ๊ฐ ๋ง๋
1์ฐจ์์ ๊ทธ ์ํ๋ฅผ ๋์
ํด ์ค์ ์ถ๋ ฅ ๋นํธ์ ํ ๊ฐ๋ ์ด๊ธ๋์ง ์๋์ง ์ผ๋ค."""
import os
import random
import mt
r = random.Random(); r.seed(os.urandom(16))
clone = random.Random(); clone.setstate(r.getstate())
U = [mt.untemper(clone.getrandbits(32)) for _ in range(624)] # ์ง์ง ๋ฏธ์ง์ ๋ฒกํฐ
truth = 0
for i, w in enumerate(U):
truth |= w << (i * 32)
for k, nout in ((16, 1443), (8, 2886), (2, 11544), (1, 23088)):
rr = random.Random(); rr.setstate(r.getstate())
bits = ''.join(format(rr.getrandbits(k), f'0{k}b') for _ in range(nout))
rows, rhs = mt.build_equations(k, bits, nout)
bad = sum(1 for row, b in zip(rows, rhs) if ((row & truth).bit_count() & 1) != b)
print(f"k={k:2d} nout={nout:5d} equations={len(rows):5d} unknowns={mt.NUNK} "
f"mismatch={bad}")python3 check_symbolic.py
๋ค ๊ฒฝ์ฐ ๋ชจ๋ mismatch=0. twist ์ ๋นํธ ์ธ๋ฑ์ค๋, TEMPER_BITS ๋, bitstr ํ์ฑ ๋ฐฉํฅ๋ ๋ค ๋ง๋ค๋ ๋ป์ด๋ค.
๐ฏ ๋ก์ปฌ ์์ฒด๊ฒ์ฆ โ ๋ฌธ์ ๋ฐ์ดํฐ๋ฅผ ๊ฑด๋๋ฆฌ๊ธฐ ์ ์
์์ด ๋ง๋ ๊ฒ๊ณผ ์ค์ ๋ก ํ๋ฆฌ๋ ๊ฒ์ ๋ค๋ฅธ ์๊ธฐ๋ค. ๋ญํฌ๊ฐ ๋ชจ์๋ผ๊ฑฐ๋ ์์ ๋ณ์๊ฐ ์๋ฑํ ๊ณณ์ ๊ฑธ๋ฆฌ๋ฉด ์ํ๋ฅผ ๋ณต์ํด๋ ๋ค์ ๊ฐ์ ๋ชป ๋งํ๋ค. ๊ทธ๋์ ๋๊ฐ์ ์๋๋ฆฌ์ค๋ฅผ ๋ก์ปฌ์์ ํ ๋ฒ ํต์งธ๋ก ๋๋ ค ๋ดค๋ค. os.urandom(16) ์ผ๋ก ์๋ํ๊ณ , k๋นํธ์ฉ 23088๋นํธ๋ฅผ ๋ฐ๊ณ , ์ํ๋ฅผ ๋ณต์ํ๊ณ , ๊ทธ ๋ค์ ๋์ฌ ๊ฐ 5๊ฐ๋ฅผ ์ค์ ๊ฐ๊ณผ ๋์กฐํ๋ค.
#!/usr/bin/env python3
"""๋ฌธ์ ๋ฐ์ดํฐ๋ฅผ ๊ฑด๋๋ฆฌ๊ธฐ ์ ์, ๋ด๊ฐ ๋ง๋ ์ํ ๋ณต์์ด ์ ๋ง ๋๋์ง ๋ก์ปฌ์์ ๋จผ์ ๋ณธ๋ค.
os.urandom(16) ์ผ๋ก seed ํ MT19937 ์์ k๋นํธ์ฉ 23088๋นํธ๋ฅผ ๋ฐ์ ์ํ๋ฅผ ๋ณต์ํ๊ณ ,
๊ทธ ๋ค์ ๋์ฌ ๊ฐ 5๊ฐ๋ฅผ ์ค์ ๊ฐ๊ณผ ๋์กฐํ๋ค."""
import os
import random
import time
import mt
TOTAL_BITS = 624 * 37
for k in (16, 8, 4, 2, 1):
nout = (TOTAL_BITS + k - 1) // k
src = random.Random(); src.seed(os.urandom(16))
outs = [src.getrandbits(k) for _ in range(nout)]
bits = ''.join(format(v, f'0{k}b') for v in outs)
t0 = time.time()
rows, rhs = mt.build_equations(k, bits, nout)
sol, free = mt.solve_gf2(rows, rhs)
el = time.time() - t0
rng = random.Random()
rng.setstate((3, tuple(mt.state_from_bits(sol)) + (0,), None))
replay = [rng.getrandbits(k) for _ in range(nout)]
pred = [rng.getrandbits(k) for _ in range(5)]
real = [src.getrandbits(k) for _ in range(5)]
print(f"k={k:2d} nout={nout:5d} nullity={len(free):2d} {el:5.1f}s "
f"replay={replay == outs} next5={pred == real} {pred}")python3 selftest.py
replay=True ๋ ๋ณต์ํ ์ํ๊ฐ ์ด๋ฏธ ๋ณธ 23088๋นํธ๋ฅผ ํ ๋นํธ๋ ์ ํ๋ฆฌ๊ณ ๋ค์ ๋ง๋ค์ด ๋ธ๋ค๋ ๋ป์ด๊ณ , next5=True ๋ ์์ง ์ ๋ณธ ๊ฐ๊น์ง ๋งํ๋ค๋ ๋ป์ด๋ค. k=1 ๋ ํต๊ณผํ๋ค. ์๋๋ง๋ค ์ต์์ 1๋นํธ์ฉ๋ง ๋ด๋ ์ํ๊ฐ ์์ ํ ๊ฒฐ์ ๋๋ค.
์๊ฐ์ ๋จ๊ณ๋น 3~12์ด. k=2 ๊ฐ ์ ์ผ ์ค๋ ๊ฑธ๋ฆฌ๋๋ฐ, ๋ฏธ์ง์ ์๋ ๊ฐ์๋ฐ ์์ ๋ณ์๊ฐ ๋์ด ํผ๋ฒ์ ์ฐพ๋๋ผ ํ๋๋ ์ด์ด ๋ง์์๋ค.
๐ Full Exploit
์ด์ ์ค์ output ์ ๋ถ์ธ๋ค. ๊ฐ ๋จ๊ณ์์ ์ํ๋ฅผ ๋ณต์ํ๊ณ , 23088๋นํธ๋ฅผ ์ฌ์ํด ํ ๊ธ์๋ ๋ค๋ฅด์ง ์์์ง assert ๋ก ํ์ธํ ๋ค, ์ด์ด์ 100๋ฒ์ ๋ ๋ฝ์ sha256 ์ฒด์ธ์ ๊ทธ๋๋ก ์ฌํํ๊ณ , AES-CBC ๋ก ํ๋ฌธ์ ์ป๋๋ค. ๊ทธ ํ๋ฌธ์ด ๋ค์ ๋จ๊ณ์ ํค ์๋๊ฐ ๋๋ค.

๋ณตํธ๋ CBC ๋ผ ์ ๋ธ๋ก์ด ํ๋ฌธ์ ๊ทธ๋๋ก XOR ๋ก ์์ธ๋ค. IV ๋ ์์ค์ iluvredblacktree ๋ก ๋ฐํ ์์ผ๋ ์ฐ๋ฆฌ๊ฐ ์์๋ผ ๊ฑด ํค๋ฟ์ด๋ค.

์ถ์ฒ: Wikimedia Commons, public domain
์ ์ฒด ์คํฌ๋ฆฝํธ๋ ์๋๊ฐ ์ ๋ถ๋ค.
#!/usr/bin/env python3
"""Unbreakable (DreamHack, Platinum 3, crypto) - full solve.
Six independent MT19937 instances leak 23088 bits each, k bits at a time
(k = 32, 16, 8, 4, 2, 1). Recover every state, replay the 100 draws that build
the AES key, and unwind the flag chain.
python3 solve.py extracted/output
"""
import random
import sys
import time
from hashlib import sha256
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad
import mt
TOTAL_BITS = 624 * 37
IV = b'iluvredblacktree'
STAGES = [32, 16, 8, 4, 2, 1]
def recover_state(k, bitstr):
"""Return (624 state words that reproduce bitstr with index 0, nout, free cols)."""
nout = (TOTAL_BITS + k - 1) // k
if k == 32:
# every output word is fully visible: untempering alone rebuilds the state
words = [int(bitstr[i * 32:(i + 1) * 32], 2) for i in range(624)]
return [mt.untemper(w) for w in words], nout, []
rows, rhs = mt.build_equations(k, bitstr, nout)
sol, free = mt.solve_gf2(rows, rhs)
return mt.state_from_bits(sol), nout, free
def chain(path, log=None):
"""Walk the six challenge() calls in order. Returns [(k, key, ct, plaintext)]."""
lines = open(path).read().split('\n')
prev = b'iloveredblacktree'
result = []
for idx, k in enumerate(STAGES):
bitstr = lines[idx * 2]
ct = bytes.fromhex(lines[idx * 2 + 1])
t0 = time.time()
state, nout, free = recover_state(k, bitstr)
rng = random.Random()
rng.setstate((3, tuple(state) + (0,), None))
replay = ''.join(format(rng.getrandbits(k), f'0{k}b') for _ in range(nout))
assert replay == bitstr, f'k={k}: replay mismatch'
key = prev
for _ in range(100):
key += format(rng.getrandbits(k), f'0{k}b').encode()
key = sha256(key).digest()
pt = unpad(AES.new(key, AES.MODE_CBC, iv=IV).decrypt(ct), 16)
if log:
log(f'[k={k:2d}] {nout} outputs, nullity={len(free):2d}, '
f'{time.time() - t0:5.1f}s -> {pt!r}')
result.append((k, key, ct, pt))
prev = pt
return result
if __name__ == '__main__':
path = sys.argv[1] if len(sys.argv) > 1 else 'extracted/output'
stages = chain(path, log=lambda s: print(s, flush=True))
print()
print('FLAG:', stages[-1][3].decode())python3 solve.py extracted/output
์ ๋จ๊ณ 30์ด ๋จ์ง. ์ค๊ฐ ํ๋ฌธ๋ค์ด ์ถ์ ์์ ์ฝ๋ฉํธ๋ผ์ ์ ๊ฐ๊ณ ์๋์ง ๋จ๊ณ๋ง๋ค ๋ฐ๋ก ํ์ธ์ด ๋๋ค.
[k=32] Congratz! This is basic, right?
[k=16] Wow, possible to break with only 16 bits?
[k= 8] Mersenne Twister, 1-byte edition.
[k= 4] Nah, still possible to break RNGs.
[k= 2] You're very close! Go go go!
[k= 1] DH{now_you_love_Mersenne_twister_dont_ya?}ํค๊ฐ ์ง์ง ๋ง๋์ง openssl ๋ก ํ ๋ฒ ๋
๋ง์ง๋ง ๋จ๊ณ ํค๋ฅผ ๋ณต์ํ๋ค๋ ๊ทผ๊ฑฐ๊ฐ pycryptodome ์ unpad ๊ฐ ์ ํฐ์ก๋ค๋ ๊ฒ๋ฟ์ด๋ฉด ์กฐ๊ธ ํ์ ํ๋ค. ๊ตฌํ์ด ์์ ํ ๋ค๋ฅธ openssl ๋ก ๊ฐ์ ํคยทIV ๋ฅผ ๋ฃ์ด ๋ณตํธํด ๋ดค๋ค.
#!/usr/bin/env python3
"""๋ง์ง๋ง(k=1) ๋จ๊ณ์ AES ํค์ ์ํธ๋ฌธ์ ํ์ผ๋ก ๋จ๊ถ, openssl ๋ก ๋ฐ๋ก ๋ณตํธํด ๋ณผ ์ ์๊ฒ ํ๋ค."""
import solve
stages = solve.chain('extracted/output')
k, key, ct, pt = stages[-1]
open('key6.hex', 'w').write(key.hex())
open('ct6.bin', 'wb').write(ct)
print(f'stage k={k}')
print(f'key = {key.hex()}')
print(f'ct = {len(ct)} bytes -> ct6.bin')
print(f'pycryptodome ๋ณตํธ ๊ฒฐ๊ณผ = {pt!r}')#!/usr/bin/env bash
# ๋ณต์ํ ํค๊ฐ ์ง์ง์ธ์ง pycryptodome ์ด ์๋ openssl ๋ก ํ ๋ฒ ๋ ๋ณตํธํด ํ์ธํ๋ค.
set -eu
cd "$(dirname "$(readlink -f "$0")")"
python3 dump_key.py
IVHEX=$(printf 'iluvredblacktree' | xxd -p)
echo "iv = $IVHEX (= 'iluvredblacktree')"
echo "--- openssl enc -d -aes-256-cbc ---"
openssl enc -d -aes-256-cbc -K "$(cat key6.hex)" -iv "$IVHEX" -in ct6.bin
echo./openssl_check.sh
openssl enc -d -aes-256-cbc ๊ฐ ๊ฐ์ ๋ฌธ์์ด์ ๋ฑ๋๋ค. ํค๊ฐ ๋ง๋ค.
๐ ์ฌํ
์์
ํด๋๋ง ์์ผ๋ฉด ํ ๋ฐฉ์ ๋์๊ฐ๊ฒ reproduce.sh ๋ฅผ ๋์๋ค. ํ์ํ ๊ฑด numpy ์ pycryptodome ๋ฟ์ด๊ณ , ์๋ฒ๊ฐ ์๋ ๋ฌธ์ ๋ผ ๋คํธ์ํฌ๋ ์ ์ด๋ค.
#!/usr/bin/env bash
# Unbreakable (DreamHack, Platinum 3, crypto) โ ํ ๋ฐฉ ์ฌํ.
# MT19937 ์ํ๋ฅผ 6๋ฒ ๋ณต์ํด AES ํค ์ฒด์ธ์ ๋๊ฐ๊ณ flag ๋ฅผ ๋ฝ๋๋ค. ์คํ๋ผ์ธ ๋ฌธ์ ๋ผ ์๋ฒ ๋ถํ์.
# ํ์ ํจํค์ง: numpy, pycryptodome (pip install numpy pycryptodome)
# ์์: ์ฝ 30์ด (19968 ๋ฏธ์ง์ ร 23088 ์ GF(2) ์๊ฑฐ๋ฅผ 5๋ฒ)
set -eu
cd "$(dirname "$(readlink -f "$0")")"
EXPECT='DH{now_you_love_Mersenne_twister_dont_ya?}'
OUT=$(timeout 600 python3 solve.py extracted/output 2>&1) || true
echo "$OUT" | tail -10
FLAG=$(printf '%s' "$OUT" | grep -aoE 'DH\{[^}]+\}' | head -1)
if [ -n "$FLAG" ] && [ "$FLAG" = "$EXPECT" ]; then
echo; echo "โ
PASS $FLAG"; exit 0
fi
echo; echo "โ FAIL (์ป์ ๊ฐ: '${FLAG:-์์}' / ๊ธฐ๋: '$EXPECT')"; exit 1./reproduce.sh
๋ก์ปฌ ํ๊ฒฝ์ Python 3.13.7 / numpy 2.2.4 / pycryptodome 3.23.0 ์ด๋ค. ์๊ฑฐ๋ฅผ uint64 ๋ฐฐ์ด๋ก ํ๊ธฐ ๋๋ฌธ์ numpy ๋ฒ์ ์ ํฌ๊ฒ ์ ํ๋ค.
๐ ๊ฒฐ๋ก
"๋์๋ฅผ ๋ช ๋นํธ๋ง ํ๋ ธ์ผ๋ ๊ด์ฐฎ๋ค"๋ ๋ฐฉ์ด๊ฐ ์๋๋ค.
์ด ๋ฌธ์ ๊ฐ k ๋ฅผ 32์์ 1๊น์ง ๋ฎ์ถฐ ๊ฐ๋ฉฐ ๋ฌผ์ด๋ณด๋ ๊ฒ ์ ํํ ๊ทธ ์ง์ ์ด๋ค. 32๋นํธ๋ฅผ ๋ค ๋ณด์ฌ ์ฃผ๋ฉด 624๊ฐ๋ง ๋ชจ์๋ ์ํ๊ฐ ๊ทธ๋๋ก ๋์ค๊ณ , 1๋นํธ๋ง ๋ณด์ฌ ์ค๋ 23088๊ฐ๋ฅผ ๋ชจ์ผ๋ฉด ๋๊ฐ์ด ๋์จ๋ค. ์ฐจ์ด๋ "๋ช ๊ฐ๋ฅผ ๋ชจ์์ผ ํ๋"๋ฟ์ด๊ณ , ๊ทธ ๊ฐ์๋ ์ถ๋ ฅ ํ๋๊ฐ ์ฃผ๋ ์ ๋ณด๋์ผ๋ก ๋๋ ๊ฐ์ ์ง๋์ง ์๋๋ค. ๋
ธ์ถ ๋นํธ ํญ์ ์ค์ด๋ ๊ฑด ๊ณต๊ฒฉ ๋น์ฉ์ ์ ํ์ผ๋ก ๋๋ฆด ๋ฟ ์ฑ์ง์ ๋ฐ๊พธ์ง ๋ชปํ๋ค.
์ ํ์ด๋ฉด ๋ถ๋ถ ์ ๋ณด๋ ๋ค ์์ธ๋ค.
๋ฉ๋ฅด์ผ ํธ์์คํฐ์ ๋น์ ํ ์ฐ์ฐ์ด ํ๋๋ผ๋ ์์์ผ๋ฉด ์ด ํ์ด๋ ์ฑ๋ฆฝํ์ง ์๋๋ค. ์๋๋ง๋ค ์ต์์ 1๋นํธ์ฉ ํฉ์ด์ ธ ์๋ ์ ๋ณด๋ฅผ ๊ทธ๋ฌ๋ชจ์ ํ๋์ ํด๋ก ํฉ์น๋ ๊ฒ ๊ฐ๋ฅํ ๊ฑด, ๊ทธ ๋นํธ๋ค์ด ์ ๋ถ ๊ฐ์ ๋ฏธ์ง์ ์งํฉ์ ๋ํ 1์ฐจ์์ด๊ธฐ ๋๋ฌธ์ด๋ค. ์ ๋ณด๊ฐ ์ผ๋ง๋ ์๊ฒ ์ชผ๊ฐ์ ธ ์๋๋๋ ์๊ด์ด ์๋ค. ์ด๋๋ง ๋์ผ๋ฉด ๋๋ค.
๋ญํฌ๊ฐ ๋ชจ์๋ ๊ฒ ํญ์ ์คํจ๋ ์๋๋ค.
์์ ๋ณ์๊ฐ ๋จ๋๋ค๊ณ ์์ ๋๊ธฐ ์ ์ ๊ทธ๊ฒ ์ด๋ ๋ฏธ์ง์์ธ์ง ๋ด์ผ ํ๋ค. ์ด ๋ฌธ์ ์์๋ ๊ทธ๊ฒ ์ ๋ถ 0๋ฒ ์๋์ ํ์ ๋นํธ์๊ณ , ๊ทธ ๋นํธ๋ค์ twist ๋ฅผ ํ์ง ๋ชปํด ๋ฏธ๋ ์ถ๋ ฅ์ ์๋ฌด ์ํฅ์ด ์์๋ค. 624 ร 32 - 31 = 19937 ์ด๋ผ๋ ๊ณ์ฐ์ด ์๊ณ ๋ฆฌ์ฆ ์ด๋ฆ ๊ทธ๋๋ก์๋ค๋ ๊ฑธ ๊ทธ๋ ์์๋ค.
๊ฒ์ ๋ฐ์ ์๊ธฐ.
random ๋ชจ๋์ ๋ฌธ์์ "์ํธํ์ ์ฉ๋๋ก ์ฐ์ง ๋ง๋ผ"๊ณ ์ ํ ์๋ค. ์ด ๋ฌธ์ ๋ ๊ทธ ๋ฌธ์ฅ์ด ์ ๋ถ์ด ์๋์ง๋ฅผ ์์ฃผ ๊ตฌ์ฒด์ ์ผ๋ก ๋ณด์ฌ ์ค๋ค. ์ธ์
ํ ํฐ์ด๋ ๋น๋ฐ๋ฒํธ ์ฌ์ค์ ๋งํฌ๋ , ์๋ค๋ก ๋ช ๊ฐ๋ง ๊ด์ธก๋๋ฉด ๋๋จธ์ง๊ฐ ํต์งธ๋ก ๋ฐ๋ผ์จ๋ค. ๊ทธ๋ฐ ์ฉ๋์๋ secrets ๋ os.urandom ์ ์ด๋ค.
Comments
๋๊ธ
๋๊ธ์ ๋จ๊ธฐ๋ ค๋ฉด ๋ก๊ทธ์ธ์ด ํ์ํด์. (๋ค์ด๋ฒ ยท ๊ตฌ๊ธ ๊ณ์ )
๋๊ธ ๋ถ๋ฌ์ค๋ ์คโฆ