ctf-writeups

Easy Random 1 Writeup

题目描述

本题是一个基于 C 语言随机数生成器的挑战。程序首先生成一个 24 位的随机种子,然后允许用户进行两种操作:

  1. 玩一个猜数字游戏(数字范围 0-65535)
  2. 猜测 Flag(Flag 会与随机数进行异或加密后输出)

附件:

#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 字节),使得暴力枚举成为可能。

攻击思路

  1. 收集随机数样本:通过二分法玩猜数字游戏,收集多个 rand() % 65536 的结果
  2. 暴力枚举种子:枚举所有可能的 24 位种子(0 到 2^24-1),找到与收集到的随机数序列匹配的种子
  3. 恢复 Flag:使用找到的种子预测后续随机数,解密加密的 Flag

解题步骤

1. 收集随机数样本

使用二分法快速猜中数字,收集 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)

2. 暴力枚举种子

使用 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

3. 恢复 Flag

使用找到的种子预测后续随机数,解密加密的 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 比赛中,类似的随机数攻击经常出现,关键在于识别随机数生成器的弱点并收集足够的样本进行攻击。