本题是 Easy Random 2 的进阶版本。程序使用两个独立的随机数生成器:
Flag 会与随机数进行异或加密后输出。
附件:
import os
import random
flag = os.getenv("GZCTF_FLAG") or "flag{fake_flag_for_testing}"
rng = random.Random()
while True:
print("Actions: (1) play a random guess game (2) guess the flag")
action = int(input("Please input action:"))
if action == 1:
nbits = rng.randrange(1, 64)
number = random.getrandbits(nbits)
print("The random number is of", nbits, "bits")
while True:
guess = int(input("Enter your guess:"))
if guess > number:
print("Bigger")
elif guess < number:
print("Smaller")
else:
print("You win!")
break
elif action == 2:
print("Guess the flag:")
print(bytes([a ^ random.getrandbits(8) for a in flag.encode()]).hex())
break
Python 的 random 模块使用 Mersenne Twister (MT19937) 算法。虽然本题中随机数的位数是变化的,但 MT19937 的内部状态仍然是确定的。攻击的关键在于建立关于 MT19937 内部状态的方程组并求解。
gf2bv 工具建立关于 MT19937 内部状态在 GF(2) 上的方程组收集足够多的随机数样本(约 1500 个):
# pip3 install git+https://github.com/maple3142/gf2bv pwntools
from pwn import *
from gf2bv import LinearSystem
from gf2bv.crypto.mt import MT19937
p = process(["python3", "main.py"])
known = []
for i in range(1500):
p.recvuntil(b"action:")
p.sendline(b"1")
nbits = int(p.recvline().decode().split()[5])
left = 0
right = 2**nbits - 1
number = None
while left < right:
middle = (left + right) // 2
p.recvuntil(b"guess:")
p.sendline(str(middle).encode())
resp = p.recvline().strip()
if resp == b"Bigger":
right = middle - 1
elif resp == b"Smaller":
left = middle + 1
else:
assert resp == b"You win!"
number = middle
break
if number is None:
p.recvuntil(b"guess:")
p.sendline(str(left).encode())
resp = p.recvline().strip()
assert resp == b"You win!"
number = left
known.append((nbits, number))
使用 gf2bv 建立关于 MT19937 内部状态的方程组:
# create equations
ls = LinearSystem([32] * 624)
gen = ls.gens()
rng = MT19937(gen)
zeros = [gen[0] ^ int(0x80000000)]
for nbits, value in known:
zeros.append(rng.getrandbits(nbits) ^ value)
求解方程组得到 MT19937 的内部状态并恢复出 Flag:
# found solutions
for sol in ls.solve_all(zeros):
print("Solved")
# recreate random number generator
RNG = MT19937(sol).to_python_random()
for nbits, value in known:
assert RNG.getrandbits(nbits) == value
p.recvuntil(b"action:")
p.sendline(b"2")
p.recvline()
encoded = p.recvline().decode()
flag = bytes([a ^ RNG.getrandbits(8) for a in bytes.fromhex(encoded)])
print(flag)
break
# pip3 install git+https://github.com/maple3142/gf2bv pwntools
from pwn import *
from gf2bv import LinearSystem
from gf2bv.crypto.mt import MT19937
p = process(["python3", "main.py"])
known = []
for i in range(1500):
p.recvuntil(b"action:")
p.sendline(b"1")
nbits = int(p.recvline().decode().split()[5])
left = 0
right = 2**nbits - 1
number = None
while left < right:
middle = (left + right) // 2
p.recvuntil(b"guess:")
p.sendline(str(middle).encode())
resp = p.recvline().strip()
if resp == b"Bigger":
right = middle - 1
elif resp == b"Smaller":
left = middle + 1
else:
assert resp == b"You win!"
number = middle
break
if number is None:
p.recvuntil(b"guess:")
p.sendline(str(left).encode())
resp = p.recvline().strip()
assert resp == b"You win!"
number = left
known.append((nbits, number))
# create equations
ls = LinearSystem([32] * 624)
gen = ls.gens()
rng = MT19937(gen)
zeros = [gen[0] ^ int(0x80000000)]
for nbits, value in known:
zeros.append(rng.getrandbits(nbits) ^ value)
# found solutions
for sol in ls.solve_all(zeros):
print("Solved")
# recreate random number generator
RNG = MT19937(sol).to_python_random()
for nbits, value in known:
assert RNG.getrandbits(nbits) == value
p.recvuntil(b"action:")
p.sendline(b"2")
p.recvline()
encoded = p.recvline().decode()
flag = bytes([a ^ RNG.getrandbits(8) for a in bytes.fromhex(encoded)])
print(flag)
break
gf2bv 工具的主要局限性在于:
本题展示了当随机数生成器的输出经过变换(如不同位数的截断)时,仍然可以通过建立方程组来恢复其内部状态。关键在于:
gf2bv 等工具可以自动化方程组的建立和求解过程对于涉及更复杂运算的随机数生成器,可能需要使用其他方法,如符号执行或约束求解。