本题是一个基于 C 语言随机数生成器的挑战。程序首先生成一个 24 位的随机种子,然后允许用户进行两种操作:
附件:
#include <assert.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main() {
setvbuf(stdout, NULL, _IONBF, 0);
unsigned int seed = 0;
FILE *fp = fopen("/dev/urandom", "rb");
fread(&seed, 3, 1, fp);
srand(seed);
while (1) {
printf("Actions: (1) play a random guess game (2) guess the flag\n");
printf("Please input action:");
int action = 0;
scanf("%d", &action);
if (action == 1) {
int number = rand() % 65536;
while (1) {
printf("Enter your guess:");
int guess = 0;
scanf("%d", &guess);
if (guess > number) {
printf("Bigger\n");
} else if (guess < number) {
printf("Smaller\n");
} else {
printf("You win!\n");
break;
}
}
} else if (action == 2) {
char *flag = getenv("GZCTF_FLAG");
if (!flag) {
flag = "flag{fake_flag_for_testing}";
}
unsigned long len = strlen(flag);
printf("Guess the flag:\n");
for (unsigned long i = 0; i < len; i++) {
printf("%02X", (uint8_t)flag[i] ^ (uint8_t)rand());
}
printf("\n");
break;
}
}
return 0;
}
攻击目标是 C 标准库(glibc)的 rand() 函数。该随机数生成器的弱点在于种子空间较小,而本题为了简化进一步将种子限制为 24 位(3 字节),使得暴力枚举成为可能。
rand() % 65536 的结果使用二分法快速猜中数字,收集 10 个随机数样本:
# pip3 install pwntools tqdm
from pwn import *
import ctypes
import tqdm
p = process(["./main"])
context(log_level="DEBUG")
numbers = []
for i in range(10):
p.recvuntil(b"action:")
p.sendline(b"1")
left = 0
right = 65536 - 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
numbers.append(number)
使用 ctypes 调用 glibc 的 rand() 函数,枚举所有可能的种子:
# bruteforce random seed
libc = ctypes.CDLL("libc.so.6")
for seed in tqdm.trange(256**3):
libc.srand(seed)
good = True
for i in range(len(numbers)):
if libc.rand() % 65536 != numbers[i]:
good = False
break
if good:
print("Found seed", seed)
break
使用找到的种子预测后续随机数,解密加密的 Flag:
p.recvuntil(b"action:")
p.sendline(b"2")
p.recvline()
encoded = p.recvline().decode()
# recover flag
libc.srand(seed)
for i in range(len(numbers)):
libc.rand()
flag = bytes([a ^ (libc.rand() % 256) for a in bytes.fromhex(encoded)])
print(flag)
# pip3 install pwntools tqdm
from pwn import *
import ctypes
import tqdm
p = process(["./main"])
context(log_level="DEBUG")
numbers = []
for i in range(10):
p.recvuntil(b"action:")
p.sendline(b"1")
left = 0
right = 65536 - 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
numbers.append(number)
# bruteforce random seed
libc = ctypes.CDLL("libc.so.6")
for seed in tqdm.trange(256**3):
libc.srand(seed)
good = True
for i in range(len(numbers)):
if libc.rand() % 65536 != numbers[i]:
good = False
break
if good:
print("Found seed", seed)
break
p.recvuntil(b"action:")
p.sendline(b"2")
p.recvline()
encoded = p.recvline().decode()
# recover flag
libc.srand(seed)
for i in range(len(numbers)):
libc.rand()
flag = bytes([a ^ (libc.rand() % 256) for a in bytes.fromhex(encoded)])
print(flag)
本题展示了当随机数生成器的状态空间足够小时,无论其计算过程多么复杂,都可以通过枚举状态空间来进行攻击。在实际的 CTF 比赛中,类似的随机数攻击经常出现,关键在于识别随机数生成器的弱点并收集足够的样本进行攻击。