[๐Ÿ’  Platinum 3] getrandbits ๊ฐ€ ํ˜๋ฆฐ ๋น„ํŠธ๋กœ MT19937 ์ƒํƒœ๋ฅผ ๋˜์ฐพ๋‹ค โ€” DreamHack Unbreakable ํ’€์ด

2026-08-18ยท1๋ถ„ ์ฝ๊ธฐยท

[๐Ÿ’  Platinum 3] getrandbits ๊ฐ€ ํ˜๋ฆฐ ๋น„ํŠธ๋กœ MT19937 ์ƒํƒœ๋ฅผ ๋˜์ฐพ๋‹ค โ€” DreamHack Unbreakable ํ’€์ด

๊ฐ™์€ 23088๋น„ํŠธ๋ฅผ 32๋น„ํŠธ์”ฉ, 16๋น„ํŠธ์”ฉ, 8ยท4ยท2๋น„ํŠธ์”ฉ, ๋งˆ์ง€๋ง‰์—” 1๋น„ํŠธ์”ฉ ํ˜๋ฆฌ๋Š” ์—ฌ์„ฏ ๊ฐœ์˜ MT19937. twist ๋„ tempering ๋„ GF(2) ์œ„์—์„œ ์„ ํ˜•์ด๋ผ ๋ˆ„์ถœ ๋น„ํŠธ ํ•˜๋‚˜๊ฐ€ ๊ณง 1์ฐจ์‹ ํ•œ ์ค„์ด ๋œ๋‹ค. 19968๊ฐœ ๋ฏธ์ง€์ˆ˜๋ฅผ numpy ๋น„ํŠธํŒฉ ์†Œ๊ฑฐ๋กœ ํ’€์–ด ์ƒํƒœ๋ฅผ ๋ณต์›ํ•˜๊ณ , ์˜ˆ์ธกํ•œ ๋‚œ์ˆ˜๋กœ AES ํ‚ค ์ฒด์ธ์„ ๋˜๊ฐ์•„ ํ”Œ๋ž˜๊ทธ๊นŒ์ง€ ๊ฐ”๋‹ค.

๋ฌธ์ œ: DreamHack โ€” Unbreakable ๋ถ„๋ฅ˜: crypto ๋‚œ์ด๋„: ๐Ÿ’  Platinum 3 FLAG: DH{now_you_love_Mersenne_twister_dont_ya?}

๋ฌธ์ œ ์„ค๋ช…์ด ๋”ฑ ํ•œ ์ค„์ด๋‹ค. "Python random module is safe." ๊ทธ ์•„๋ž˜ ํ”Œ๋ž˜๊ทธ ํฌ๋งท๋งŒ ์ ํ˜€ ์žˆ๋‹ค.

๋“œ๋ฆผํ•ต Unbreakable ๋ฌธ์ œ ํŽ˜์ด์ง€ - ๋ฌธ์ œ ์„ค๋ช…์ด Python random module is safe ํ•œ ์ค„๊ณผ ํ”Œ๋ž˜๊ทธ ํฌ๋งท๋ฟ์ด๊ณ  ์ถœ์ œ์ž๋Š” RBTree, ํ’€์ด 13๊ฐœ๊ฐ€ ์˜ฌ๋ผ์™€ ์žˆ๋‹ค
๋“œ๋ฆผํ•ต Unbreakable ๋ฌธ์ œ ํŽ˜์ด์ง€ - ๋ฌธ์ œ ์„ค๋ช…์ด Python random module is safe ํ•œ ์ค„๊ณผ ํ”Œ๋ž˜๊ทธ ํฌ๋งท๋ฟ์ด๊ณ  ์ถœ์ œ์ž๋Š” RBTree, ํ’€์ด 13๊ฐœ๊ฐ€ ์˜ฌ๋ผ์™€ ์žˆ๋‹ค

๋ฐ›์€ ํŒŒ์ผ์€ 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

file ๊ณผ ls ์ถœ๋ ฅ - test.py ๋Š” Python ์Šคํฌ๋ฆฝํŠธ 967๋ฐ”์ดํŠธ, output ์€ ASCII ํ…์ŠคํŠธ 139068๋ฐ”์ดํŠธ์ด๊ณ  ์ค„ ์ˆ˜๋Š” 12์ค„์ด๋‹ค
file ๊ณผ ls ์ถœ๋ ฅ - test.py ๋Š” Python ์Šคํฌ๋ฆฝํŠธ 967๋ฐ”์ดํŠธ, output ์€ ASCII ํ…์ŠคํŠธ 139068๋ฐ”์ดํŠธ์ด๊ณ  ์ค„ ์ˆ˜๋Š” 12์ค„์ด๋‹ค

136KB ๊ฐ€ 12์ค„. ํ•œ ์ค„์ด ์—„์ฒญ๋‚˜๊ฒŒ ๊ธธ๋‹ค๋Š” ๋œป์ด๋‹ค. ์ƒ์„ฑ ์Šคํฌ๋ฆฝํŠธ๋ฅผ ํ†ต์งธ๋กœ ์ฝ๋Š”๋‹ค.

cat extracted/test.py

cat ์œผ๋กœ ์—ฐ test.py ์ „๋ฌธ - challenge ํ•จ์ˆ˜๊ฐ€ random.seed ํ›„ getrandbits ๋ฅผ num_task ํšŒ ๋Œ๋ ค ์ถœ๋ ฅํ•˜๊ณ  100ํšŒ ๋” ๋ฝ‘์•„ sha256 ์ฒด์ธ์œผ๋กœ AES ํ‚ค๋ฅผ ๋งŒ๋“ ๋‹ค
cat ์œผ๋กœ ์—ฐ test.py ์ „๋ฌธ - challenge ํ•จ์ˆ˜๊ฐ€ random.seed ํ›„ getrandbits ๋ฅผ num_task ํšŒ ๋Œ๋ ค ์ถœ๋ ฅํ•˜๊ณ  100ํšŒ ๋” ๋ฝ‘์•„ sha256 ์ฒด์ธ์œผ๋กœ AES ํ‚ค๋ฅผ ๋งŒ๋“ ๋‹ค

ํ•ต์‹ฌ๋งŒ ์˜ฎ๊ฒจ ์ ์œผ๋ฉด ์ด๋ ‡๋‹ค.

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๋น„ํŠธ๋ผ ์ „์ˆ˜์กฐ์‚ฌ๋Š” ๋…ผ์™ธ๋‹ค.

ํ•œ ๋‹จ๊ณ„์˜ ๊ตฌ์กฐ๋ฅผ ๊ทธ๋ฆผ์œผ๋กœ ์ •๋ฆฌํ•˜๋ฉด ์ด๋ ‡๊ฒŒ ๋œ๋‹ค.

challenge ํ•จ์ˆ˜ ํ•œ ๋ฒˆ์˜ ํ•ด๋ถ€๋„ - 128๋น„ํŠธ ์‹œ๋“œ๋กœ ์ดˆ๊ธฐํ™”ํ•œ 624์›Œ๋“œ ์ƒํƒœ์—์„œ 23088๋น„ํŠธ๋ฅผ ๊ณต๊ฐœํ•˜๊ณ  100ํšŒ๋Š” ์ˆจ๊ธด ๋’ค sha256 ์ฒด์ธ์œผ๋กœ AES-256-CBC ํ‚ค๋ฅผ ๋งŒ๋“ ๋‹ค
challenge ํ•จ์ˆ˜ ํ•œ ๋ฒˆ์˜ ํ•ด๋ถ€๋„ - 128๋น„ํŠธ ์‹œ๋“œ๋กœ ์ดˆ๊ธฐํ™”ํ•œ 624์›Œ๋“œ ์ƒํƒœ์—์„œ 23088๋น„ํŠธ๋ฅผ ๊ณต๊ฐœํ•˜๊ณ  100ํšŒ๋Š” ์ˆจ๊ธด ๋’ค sha256 ์ฒด์ธ์œผ๋กœ AES-256-CBC ํ‚ค๋ฅผ ๋งŒ๋“ ๋‹ค

์ด์ œ 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

inspect_output.py ์‹คํ–‰ ๊ฒฐ๊ณผ - ์—ฌ์„ฏ ๋‹จ๊ณ„ ๋ชจ๋‘ num_task ๊ณฑํ•˜๊ธฐ k ๊ฐ€ ์‹ค์ œ ์ค„ ๊ธธ์ด์™€ ์ผ์น˜ํ•ด OK ๋กœ ์ฐํžˆ๊ณ  ์•”ํ˜ธ๋ฌธ์€ 32๋ฐ”์ดํŠธ ๋˜๋Š” 48๋ฐ”์ดํŠธ๋‹ค
inspect_output.py ์‹คํ–‰ ๊ฒฐ๊ณผ - ์—ฌ์„ฏ ๋‹จ๊ณ„ ๋ชจ๋‘ num_task ๊ณฑํ•˜๊ธฐ k ๊ฐ€ ์‹ค์ œ ์ค„ ๊ธธ์ด์™€ ์ผ์น˜ํ•ด OK ๋กœ ์ฐํžˆ๊ณ  ์•”ํ˜ธ๋ฌธ์€ 32๋ฐ”์ดํŠธ ๋˜๋Š” 48๋ฐ”์ดํŠธ๋‹ค

์—ฌ์„ฏ ์ค„ ์ „๋ถ€ 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 ๊ฒฐ๊ณผ๋‹ค.

๋ฉ”๋ฅด์„ผ ํŠธ์œ„์Šคํ„ฐ ์‹œ๊ฐํ™” - 624์›Œ๋“œ ์ƒํƒœ ๋ฐฐ์—ด๊ณผ twist ๋กœ ๋‹ค์Œ ๋ธ”๋ก์„ ๋งŒ๋“œ๋Š” ๊ตฌ์กฐ๋ฅผ ๋‚˜ํƒ€๋‚ธ ์œ„ํ‚ค๋ฏธ๋””์–ด ๋„์‹
๋ฉ”๋ฅด์„ผ ํŠธ์œ„์Šคํ„ฐ ์‹œ๊ฐํ™” - 624์›Œ๋“œ ์ƒํƒœ ๋ฐฐ์—ด๊ณผ twist ๋กœ ๋‹ค์Œ ๋ธ”๋ก์„ ๋งŒ๋“œ๋Š” ๊ตฌ์กฐ๋ฅผ ๋‚˜ํƒ€๋‚ธ ์œ„ํ‚ค๋ฏธ๋””์–ด ๋„์‹

์ถœ์ฒ˜: 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

probe_getrandbits.py ์‹คํ–‰ ๊ฒฐ๊ณผ - k ๊ฐ€ 16, 8, 4, 2, 1 ์ธ ๋‹ค์„ฏ ๊ฒฝ์šฐ ๋ชจ๋‘ getrandbits(k) ๊ฐ’์ด 32๋น„ํŠธ ์ถœ๋ ฅ์˜ ์ƒ์œ„ k๋น„ํŠธ์™€ ์ •ํ™•ํžˆ ์ผ์น˜ํ•ด match True ๋กœ ์ฐํžŒ๋‹ค
probe_getrandbits.py ์‹คํ–‰ ๊ฒฐ๊ณผ - k ๊ฐ€ 16, 8, 4, 2, 1 ์ธ ๋‹ค์„ฏ ๊ฒฝ์šฐ ๋ชจ๋‘ getrandbits(k) ๊ฐ’์ด 32๋น„ํŠธ ์ถœ๋ ฅ์˜ ์ƒ์œ„ k๋น„ํŠธ์™€ ์ •ํ™•ํžˆ ์ผ์น˜ํ•ด match True ๋กœ ์ฐํžŒ๋‹ค

๋‹ค์„ฏ ๊ฒฝ์šฐ ๋ชจ๋‘ ์ผ์น˜. 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

check_linear.py ์‹คํ–‰ ๊ฒฐ๊ณผ - ๋ฌด์ž‘์œ„ ์ž…๋ ฅ 20000๊ฐœ์—์„œ ์—ญํ•จ์ˆ˜ ๋ถˆ์ผ์น˜ 0๊ฑด, XOR ๊ธฐ์ € ์žฌ์กฐ๋ฆฝ ๋ถˆ์ผ์น˜ 0๊ฑด์ด๊ณ  temper ๊ฐ€ XOR ์— ๋Œ€ํ•ด ๋ถ„๋ฐฐ๋˜๋ฉฐ ์ตœ์ƒ์œ„ ๋น„ํŠธ๋Š” ์›๋ณธ 16, 24, 27, 31๋ฒˆ ๋น„ํŠธ์˜ XOR ์ด๋‹ค
check_linear.py ์‹คํ–‰ ๊ฒฐ๊ณผ - ๋ฌด์ž‘์œ„ ์ž…๋ ฅ 20000๊ฐœ์—์„œ ์—ญํ•จ์ˆ˜ ๋ถˆ์ผ์น˜ 0๊ฑด, XOR ๊ธฐ์ € ์žฌ์กฐ๋ฆฝ ๋ถˆ์ผ์น˜ 0๊ฑด์ด๊ณ  temper ๊ฐ€ XOR ์— ๋Œ€ํ•ด ๋ถ„๋ฐฐ๋˜๋ฉฐ ์ตœ์ƒ์œ„ ๋น„ํŠธ๋Š” ์›๋ณธ 16, 24, 27, 31๋ฒˆ ๋น„ํŠธ์˜ XOR ์ด๋‹ค

๋‘˜ ๋‹ค 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
161515
82323
42727
22929
13030

์ •ํ™•ํžˆ 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

nullspace.py ์‹คํ–‰ ๊ฒฐ๊ณผ - ๋‹ค์„ฏ ๊ฒฝ์šฐ ๋ชจ๋‘ ์ž์œ ๋ณ€์ˆ˜๊ฐ€ 0๋ฒˆ ์›Œ๋“œ ํ•œ ๊ณณ์—๋งŒ ๊ฑธ์ณ ์žˆ๊ณ  ๋น„ํŠธ ๋ฒ”์œ„๋Š” 0์—์„œ 30 ์‚ฌ์ด์ด๋ฉฐ ์ž์œ ๋ณ€์ˆ˜ ๊ฐœ์ˆ˜๊ฐ€ 31์—์„œ k ๋ฅผ ๋บ€ ๊ฐ’๊ณผ ์ •ํ™•ํžˆ ๊ฐ™๋‹ค
nullspace.py ์‹คํ–‰ ๊ฒฐ๊ณผ - ๋‹ค์„ฏ ๊ฒฝ์šฐ ๋ชจ๋‘ ์ž์œ ๋ณ€์ˆ˜๊ฐ€ 0๋ฒˆ ์›Œ๋“œ ํ•œ ๊ณณ์—๋งŒ ๊ฑธ์ณ ์žˆ๊ณ  ๋น„ํŠธ ๋ฒ”์œ„๋Š” 0์—์„œ 30 ์‚ฌ์ด์ด๋ฉฐ ์ž์œ ๋ณ€์ˆ˜ ๊ฐœ์ˆ˜๊ฐ€ 31์—์„œ k ๋ฅผ ๋บ€ ๊ฐ’๊ณผ ์ •ํ™•ํžˆ ๊ฐ™๋‹ค

์ž์œ ๋ณ€์ˆ˜๊ฐ€ ์ „๋ถ€ 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, rhs

bitstr ์—์„œ ์–ด๋А ๊ธ€์ž๊ฐ€ ์–ด๋А ๋น„ํŠธ์ธ์ง€๋„ ํ—ท๊ฐˆ๋ฆฌ๊ธฐ ์‰ฌ์šด ๋ถ€๋ถ„์ด๋‹ค. 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

check_symbolic.py ์‹คํ–‰ ๊ฒฐ๊ณผ - k ๊ฐ€ 16, 8, 2, 1 ์ธ ๋„ค ๊ฒฝ์šฐ ๋ชจ๋‘ 23088๊ฐœ์˜ ๋ฐฉ์ •์‹์— ์ง„์งœ ์ƒํƒœ๋ฅผ ๋Œ€์ž…ํ–ˆ์„ ๋•Œ ๋ถˆ์ผ์น˜๊ฐ€ 0๊ฑด์œผ๋กœ ๋‚˜์˜จ๋‹ค
check_symbolic.py ์‹คํ–‰ ๊ฒฐ๊ณผ - k ๊ฐ€ 16, 8, 2, 1 ์ธ ๋„ค ๊ฒฝ์šฐ ๋ชจ๋‘ 23088๊ฐœ์˜ ๋ฐฉ์ •์‹์— ์ง„์งœ ์ƒํƒœ๋ฅผ ๋Œ€์ž…ํ–ˆ์„ ๋•Œ ๋ถˆ์ผ์น˜๊ฐ€ 0๊ฑด์œผ๋กœ ๋‚˜์˜จ๋‹ค

๋„ค ๊ฒฝ์šฐ ๋ชจ๋‘ 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

selftest.py ์‹คํ–‰ ๊ฒฐ๊ณผ - k ๊ฐ€ 16, 8, 4, 2, 1 ์ธ ๋‹ค์„ฏ ๊ฒฝ์šฐ ๋ชจ๋‘ replay True ์™€ next5 True ๋กœ ๋ณต์›ํ•œ ์ƒํƒœ๊ฐ€ ์ด๋ฏธ ๋ณธ ๋น„ํŠธ์—ด๊ณผ ์•ž์œผ๋กœ ๋‚˜์˜ฌ ๊ฐ’ ๋‹ค์„ฏ ๊ฐœ๋ฅผ ๋ชจ๋‘ ๋งžํžŒ๋‹ค
selftest.py ์‹คํ–‰ ๊ฒฐ๊ณผ - k ๊ฐ€ 16, 8, 4, 2, 1 ์ธ ๋‹ค์„ฏ ๊ฒฝ์šฐ ๋ชจ๋‘ replay True ์™€ next5 True ๋กœ ๋ณต์›ํ•œ ์ƒํƒœ๊ฐ€ ์ด๋ฏธ ๋ณธ ๋น„ํŠธ์—ด๊ณผ ์•ž์œผ๋กœ ๋‚˜์˜ฌ ๊ฐ’ ๋‹ค์„ฏ ๊ฐœ๋ฅผ ๋ชจ๋‘ ๋งžํžŒ๋‹ค

replay=True ๋Š” ๋ณต์›ํ•œ ์ƒํƒœ๊ฐ€ ์ด๋ฏธ ๋ณธ 23088๋น„ํŠธ๋ฅผ ํ•œ ๋น„ํŠธ๋„ ์•ˆ ํ‹€๋ฆฌ๊ณ  ๋‹ค์‹œ ๋งŒ๋“ค์–ด ๋‚ธ๋‹ค๋Š” ๋œป์ด๊ณ , next5=True ๋Š” ์•„์ง ์•ˆ ๋ณธ ๊ฐ’๊นŒ์ง€ ๋งžํžŒ๋‹ค๋Š” ๋œป์ด๋‹ค. k=1 ๋„ ํ†ต๊ณผํ–ˆ๋‹ค. ์›Œ๋“œ๋งˆ๋‹ค ์ตœ์ƒ์œ„ 1๋น„ํŠธ์”ฉ๋งŒ ๋ด๋„ ์ƒํƒœ๊ฐ€ ์™„์ „ํžˆ ๊ฒฐ์ •๋œ๋‹ค.

์‹œ๊ฐ„์€ ๋‹จ๊ณ„๋‹น 3~12์ดˆ. k=2 ๊ฐ€ ์ œ์ผ ์˜ค๋ž˜ ๊ฑธ๋ฆฌ๋Š”๋ฐ, ๋ฏธ์ง€์ˆ˜ ์ˆ˜๋Š” ๊ฐ™์€๋ฐ ์ž์œ ๋ณ€์ˆ˜๊ฐ€ ๋Š˜์–ด ํ”ผ๋ฒ—์„ ์ฐพ๋А๋ผ ํ—›๋„๋Š” ์—ด์ด ๋งŽ์•„์„œ๋‹ค.


๐Ÿš€ Full Exploit

์ด์ œ ์‹ค์ œ output ์— ๋ถ™์ธ๋‹ค. ๊ฐ ๋‹จ๊ณ„์—์„œ ์ƒํƒœ๋ฅผ ๋ณต์›ํ•˜๊ณ , 23088๋น„ํŠธ๋ฅผ ์žฌ์ƒํ•ด ํ•œ ๊ธ€์ž๋„ ๋‹ค๋ฅด์ง€ ์•Š์€์ง€ assert ๋กœ ํ™•์ธํ•œ ๋’ค, ์ด์–ด์„œ 100๋ฒˆ์„ ๋” ๋ฝ‘์•„ sha256 ์ฒด์ธ์„ ๊ทธ๋Œ€๋กœ ์žฌํ˜„ํ•˜๊ณ , AES-CBC ๋กœ ํ‰๋ฌธ์„ ์–ป๋Š”๋‹ค. ๊ทธ ํ‰๋ฌธ์ด ๋‹ค์Œ ๋‹จ๊ณ„์˜ ํ‚ค ์‹œ๋“œ๊ฐ€ ๋œ๋‹ค.

์—ฌ์„ฏ ๋‹จ๊ณ„ ์ฒด์ธ ๋‹ค์ด์–ด๊ทธ๋žจ - k ๊ฐ€ 32์—์„œ 1๊นŒ์ง€ ์ค„์–ด๋“ค๋ฉฐ ๊ฐ ๋‹จ๊ณ„์˜ ํ‰๋ฌธ iflags ๊ฐ€ ๋‹ค์Œ ๋‹จ๊ณ„ AES ํ‚ค์˜ ์‹œ๋“œ๋กœ ๋“ค์–ด๊ฐ€๊ณ  ๋งˆ์ง€๋ง‰ ์นธ์— final_flag ๊ฐ€ ์žˆ๋‹ค
์—ฌ์„ฏ ๋‹จ๊ณ„ ์ฒด์ธ ๋‹ค์ด์–ด๊ทธ๋žจ - k ๊ฐ€ 32์—์„œ 1๊นŒ์ง€ ์ค„์–ด๋“ค๋ฉฐ ๊ฐ ๋‹จ๊ณ„์˜ ํ‰๋ฌธ iflags ๊ฐ€ ๋‹ค์Œ ๋‹จ๊ณ„ AES ํ‚ค์˜ ์‹œ๋“œ๋กœ ๋“ค์–ด๊ฐ€๊ณ  ๋งˆ์ง€๋ง‰ ์นธ์— final_flag ๊ฐ€ ์žˆ๋‹ค

๋ณตํ˜ธ๋Š” CBC ๋ผ ์•ž ๋ธ”๋ก์ด ํ‰๋ฌธ์— ๊ทธ๋Œ€๋กœ XOR ๋กœ ์„ž์ธ๋‹ค. IV ๋Š” ์†Œ์Šค์— iluvredblacktree ๋กœ ๋ฐ•ํ˜€ ์žˆ์œผ๋‹ˆ ์šฐ๋ฆฌ๊ฐ€ ์•Œ์•„๋‚ผ ๊ฑด ํ‚ค๋ฟ์ด๋‹ค.

CBC ๋ณตํ˜ธ ๋„์‹ - ๊ฐ ๋ธ”๋ก์„ ๋ธ”๋ก์•”ํ˜ธ๋กœ ๋ณตํ˜ธํ•œ ๋’ค ์ง์ „ ์•”ํ˜ธ๋ฌธ ๋ธ”๋ก๊ณผ XOR ํ•˜๋ฉฐ ์ฒซ ๋ธ”๋ก์€ IV ์™€ XOR ํ•œ๋‹ค
CBC ๋ณตํ˜ธ ๋„์‹ - ๊ฐ ๋ธ”๋ก์„ ๋ธ”๋ก์•”ํ˜ธ๋กœ ๋ณตํ˜ธํ•œ ๋’ค ์ง์ „ ์•”ํ˜ธ๋ฌธ ๋ธ”๋ก๊ณผ XOR ํ•˜๋ฉฐ ์ฒซ ๋ธ”๋ก์€ IV ์™€ XOR ํ•œ๋‹ค

์ถœ์ฒ˜: 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

solve.py ์‹คํ–‰ ๊ฒฐ๊ณผ - ์—ฌ์„ฏ ๋‹จ๊ณ„๊ฐ€ ์ˆœ์„œ๋Œ€๋กœ ๋ณต์›๋˜๋ฉฐ ์ค‘๊ฐ„ ํ‰๋ฌธ์ด Congratz This is basic right ๋ถ€ํ„ฐ You are very close ๊นŒ์ง€ ์ด์–ด์ง€๊ณ  ๋งˆ์ง€๋ง‰์— DH ๋กœ ์‹œ์ž‘ํ•˜๋Š” ํ”Œ๋ž˜๊ทธ๊ฐ€ ๋‚˜์˜จ๋‹ค
solve.py ์‹คํ–‰ ๊ฒฐ๊ณผ - ์—ฌ์„ฏ ๋‹จ๊ณ„๊ฐ€ ์ˆœ์„œ๋Œ€๋กœ ๋ณต์›๋˜๋ฉฐ ์ค‘๊ฐ„ ํ‰๋ฌธ์ด Congratz This is basic right ๋ถ€ํ„ฐ You are very close ๊นŒ์ง€ ์ด์–ด์ง€๊ณ  ๋งˆ์ง€๋ง‰์— DH ๋กœ ์‹œ์ž‘ํ•˜๋Š” ํ”Œ๋ž˜๊ทธ๊ฐ€ ๋‚˜์˜จ๋‹ค

์ „ ๋‹จ๊ณ„ 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_check.sh ์‹คํ–‰ ๊ฒฐ๊ณผ - ๋ณต์›ํ•œ 32๋ฐ”์ดํŠธ ํ‚ค hex ์™€ IV hex ๋ฅผ openssl enc -d -aes-256-cbc ์— ๋„ฃ์–ด pycryptodome ๊ณผ ๋™์ผํ•œ ํ”Œ๋ž˜๊ทธ ๋ฌธ์ž์—ด์ด ๋‚˜์˜จ๋‹ค
openssl_check.sh ์‹คํ–‰ ๊ฒฐ๊ณผ - ๋ณต์›ํ•œ 32๋ฐ”์ดํŠธ ํ‚ค hex ์™€ IV hex ๋ฅผ openssl enc -d -aes-256-cbc ์— ๋„ฃ์–ด pycryptodome ๊ณผ ๋™์ผํ•œ ํ”Œ๋ž˜๊ทธ ๋ฌธ์ž์—ด์ด ๋‚˜์˜จ๋‹ค

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

reproduce.sh ์‹คํ–‰ ๊ฒฐ๊ณผ - ์—ฌ์„ฏ ๋‹จ๊ณ„๋ฅผ ๋‹ค์‹œ ํ’€์–ด ์–ป์€ ํ”Œ๋ž˜๊ทธ๊ฐ€ ๊ธฐ๋Œ€๊ฐ’๊ณผ ์ผ์น˜ํ•ด PASS ๋กœ ๋๋‚œ๋‹ค
reproduce.sh ์‹คํ–‰ ๊ฒฐ๊ณผ - ์—ฌ์„ฏ ๋‹จ๊ณ„๋ฅผ ๋‹ค์‹œ ํ’€์–ด ์–ป์€ ํ”Œ๋ž˜๊ทธ๊ฐ€ ๊ธฐ๋Œ€๊ฐ’๊ณผ ์ผ์น˜ํ•ด PASS ๋กœ ๋๋‚œ๋‹ค

๋กœ์ปฌ ํ™˜๊ฒฝ์€ 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

๋Œ“๊ธ€

0๊ฐœ

๋Œ“๊ธ€์„ ๋‚จ๊ธฐ๋ ค๋ฉด ๋กœ๊ทธ์ธ์ด ํ•„์š”ํ•ด์š”. (๋„ค์ด๋ฒ„ ยท ๊ตฌ๊ธ€ ๊ณ„์ •)

๋Œ“๊ธ€ ๋ถˆ๋Ÿฌ์˜ค๋Š” ์ค‘โ€ฆ

Related

๊ด€๋ จ ๊ธ€

3๊ฐœ
[๐Ÿฅ‡ Gold 3] 42๋งŒ ์ค„์งœ๋ฆฌ XOR ์ง€์˜ฅ์ด ์‚ฌ์‹ค์€ ํ–‰๋ ฌ ํ•œ ์žฅ โ€” DreamHack XOR Disaster ํ’€์ด
blog

[๐Ÿฅ‡ Gold 3] 42๋งŒ ์ค„์งœ๋ฆฌ XOR ์ง€์˜ฅ์ด ์‚ฌ์‹ค์€ ํ–‰๋ ฌ ํ•œ ์žฅ โ€” DreamHack XOR Disaster ํ’€์ด

15MB์งœ๋ฆฌ disaster.py ์•ˆ์— ์‹œํ”„ํŠธยทXOR ์—ฐ์‚ฐ์ด 320,000์ค„ ํฉ๋ฟŒ๋ ค์ ธ ์žˆ๊ณ  ํ•จ์ˆ˜ 32๊ฐœ๊ฐ€ ๊ฐ๊ฐ 8๋น„ํŠธ๋งŒ ๋Œ๋ ค์ค€๋‹ค. ์„ธ ์—ฐ์‚ฐ ๋ชจ๋‘ ์ž๋ฆฌ์˜ฌ๋ฆผ์ด ์—†์–ด GF(2) ์œ„์—์„œ ์„ ํ˜•์ด๋ผ, ์ „์ฒด๊ฐ€ f(v) = Aยทv + c ๋ผ๋Š” ์•„ํ•€์‚ฌ์ƒ ํ•˜๋‚˜๋กœ ์ ‘ํžŒ๋‹ค. f(0)์œผ๋กœ ์ƒ์ˆ˜๋ฅผ, f(e_i)๋กœ ์—ด๋ฒกํ„ฐ๋ฅผ ๋ฝ‘์•„ 257๋ฒˆ ํ˜ธ์ถœ๋งŒ์— 256ร—256 ํ–‰๋ ฌ์„ ๋ณต์›ํ•˜๊ณ  ๊ฐ€์šฐ์Šค ์†Œ๊ฑฐ ํ•œ ๋ฒˆ์œผ๋กœ ๋๋‚ฌ๋‹ค.
#dreamhack#ctf#crypto+7
2026-08-22#dreamhack +5
[๐Ÿฅ‰ Bronze 4] "๋กœ๋ด‡ ์ธ์ฆ"์ด ์˜คํžˆ๋ ค ๋กœ๋ด‡์—๊ฒŒ ํŒ์„ ๊น”์•„์คฌ๋‹ค โ€” DreamHack Robot Only ํ’€์ด
blog

[๐Ÿฅ‰ Bronze 4] "๋กœ๋ด‡ ์ธ์ฆ"์ด ์˜คํžˆ๋ ค ๋กœ๋ด‡์—๊ฒŒ ํŒ์„ ๊น”์•„์คฌ๋‹ค โ€” DreamHack Robot Only ํ’€์ด

์ˆซ์ž ํ•˜๋‚˜๋ฅผ 3์ดˆ ์•ˆ์— ๊ทธ๋Œ€๋กœ ๋˜๋ฐ›์•„ ํƒ€์ดํ•‘ํ•˜๋ฉด "๋กœ๋ด‡ ์ธ์ฆ"์ด ์™„๋ฃŒ๋œ๋‹ค. ๊ทธ ์ˆซ์ž๋Š” Python random ๋ชจ๋“ˆ์˜ ์›์‹œ ์ถœ๋ ฅ 6๊ฐœ๋ฅผ ์ด์–ด๋ถ™์—ฌ ๊ณ ์ •๋œ ์ƒ์ˆ˜๋กœ XORํ•œ ๊ฐ’์ด๋ผ, ์šฐ๋ฆฌ๊ฐ€ ํ™”๋ฉด์—์„œ ์ฝ๋Š” ์ˆœ๊ฐ„ ๊ทธ 6๊ฐœ ์ˆซ์ž๋ฅผ ๊ทธ๋Œ€๋กœ ์—ญ์‚ฐํ•  ์ˆ˜ ์žˆ๋‹ค. ๊ทธ๋ฆฌ๊ณ  ์ด ์ธ์ฆ์€ ์‹คํŒจํ•ด๋„ ๋ช‡ ๋ฒˆ์ด๋“  ๋‹ค์‹œ ์‹œ๋„ํ•  ์ˆ˜ ์žˆ๋‹ค โ€” 104๋ฒˆ๋งŒ ๋ฐ˜๋ณตํ•˜๋ฉด Mersenne Twister์˜ ๋‚ด๋ถ€ ์ƒํƒœ๋ฅผ ์™„์ „ํžˆ ๋ณต์›ํ•˜๋Š” ๋ฐ ํ•„์š”ํ•œ 624๊ฐœ์˜ ์—ฐ์†๋œ ์›์‹œ๊ฐ’์„ ์ „๋ถ€ ๋ชจ์„ ์ˆ˜ ์žˆ๋‹ค๋Š” ๋œป์ด๋‹ค. ๊ทธ ๋’ค๋กœ๋Š” ์„œ๋ฒ„๊ฐ€ ๋ฝ‘์„ ๋„๋ฐ• ๊ฒŒ์ž„์˜ ์ •๋‹ต์„ ๋ฒ ํŒ…ํ•˜๊ธฐ๋„ ์ „์— ์•Œ ์ˆ˜ ์žˆ๊ฒŒ ๋œ๋‹ค.
#dreamhack#ctf#misc+5
2026-07-20#dreamhack +5
[๐Ÿฅ‡ Gold 3] CSP nonce๋ฅผ ์˜ˆ์ธกํ•ด์„œ ๋šซ๋Š” stored XSS โ€” DreamHack Mini Social Media ํ’€์ด
blog

[๐Ÿฅ‡ Gold 3] CSP nonce๋ฅผ ์˜ˆ์ธกํ•ด์„œ ๋šซ๋Š” stored XSS โ€” DreamHack Mini Social Media ํ’€์ด

CSP๊ฐ€ inline script๋ฅผ ๋ง‰์ง€๋งŒ nonce๋ฅผ Python random()์œผ๋กœ ๋งŒ๋“ ๋‹ค. ์‘๋‹ต ํ—ค๋”๋กœ ์ƒˆ๋Š” nonce๋ฅผ 624์›Œ๋“œ ๋ชจ์•„ MT19937 ์ƒํƒœ๋ฅผ ๋ณต์›ํ•˜๋ฉด ๋‹ค์Œ nonce๊ฐ€ ์˜ˆ์ธก๋œ๋‹ค. reporter username์˜ stored XSS์— ์˜ˆ์ธกํ•œ nonce๋ฅผ ๋ฐ•์•„ admin ๋ด‡ ๋ธŒ๋ผ์šฐ์ €์—์„œ /admin ํ”Œ๋ž˜๊ทธ๋ฅผ ๋นผ๋ƒˆ๋‹ค.
#dreamhack#ctf#web+6
2026-06-05#dreamhack +5