解析第九届 XCTF-Tch3s
好的,我现在需要帮用户总结一篇关于CTF密码学题目的文章,控制在100字以内。首先,我得通读整篇文章,理解其主要内容。 文章主要分析了一道CTF题目,涉及时序攻击和PRNG的弱点。题目提供了两个文件:ELF可执行文件和输出文件。作者通过分析发现,程序使用srand(time(NULL))生成随机数,导致种子可预测。利用output中的测试数据,作者编写程序爆破种子,成功恢复密钥,并通过GDB解密flag。 接下来,我需要将这些关键点浓缩到100字以内。要突出题目类型、漏洞、攻击方法和结果。确保语言简洁明了。 可能的结构是:介绍题目类型和漏洞,说明攻击方法(爆破种子),以及最终结果(解密flag)。 最后,检查字数是否符合要求,并确保内容准确无误。 </think> 本文分析了一道CTF密码学题目,揭示了随机数生成器的弱点。程序使用`srand(time(NULL))`生成密钥,导致种子可预测。通过爆破时间戳种子并利用输出的测试数据,成功恢复密钥并解密flag。最终flag为`flag{tim1ng_a7t@ck_1s_dangerous}`。 2025-11-2 13:16:12 Author: www.freebuf.com(查看原文) 阅读量:1 收藏

题目概述

本文将深入分析一道CTF密码学题目,该题目涉及时序攻击(Timing Attack)伪随机数生成器(PRNG)的弱点。题目提供了两个文件:

  • Tch3s:一个ELF可执行文件

  • output:程序的输出文件,包含加密的flag和测试数据

一、初步分析

1.1 查看文件类型

首先,我们查看提供的文件:

file Tch3s

输出显示:

Tch3s: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, stripped

这是一个64位的Linux可执行文件,且符号表已被移除(stripped),这增加了逆向分析的难度。

1.2 运行程序观察行为

让我们先运行程序看看它的行为:

export FLAG="flag{test_flag_for_analysis}"
./Tch3s | head -n 20

程序输出如下:

encrypted flag: BCB00E4069532EC83DA17A2F407858D83A4E557C1E8E2A332AB14B0C637470CE

Test 1 plaintext: 8EE8CCC42881517FA1100B36B29E547A
Test 1 encrypted: 1D2EE01F5596812A6A39FAEAFA745293, costs: 27800.000000 ns
Test 1 decrypted: 8EE8CCC42881517FA1100B36B29E547A, costs: 28936.000000 ns

Test 2 plaintext: 5E2C0A8B0A5188C71F3F487FF85E1C6A
Test 2 encrypted: 7DC18F60A6E5413DB22A3E179B374E7F, costs: 29187.000000 ns
Test 2 decrypted: 5E2C0A8B0A5188C71F3F487FF85E1C6A, costs: 29861.000000 ns
...

从输出可以观察到:

  1. 程序首先输出加密后的flag(十六进制格式)

  2. 然后进行多轮测试,每次:

    • 生成随机的16字节明文

    • 加密该明文并输出密文

    • 解密密文验证正确性

    • 记录加密和解密所需的时间(纳秒)

1.3 查看output文件内容

head -n 20 output

output文件包含了程序某次运行的完整输出:

encrypted flag: B789EB607A91D08888E2C4C4E4D9573FF5333E4783390B91EDB9D2B4FD2A5FC0

Test 1 plaintext: 720B4455C91B6A024135343386B6679D
Test 1 encrypted: C9BBC42200F90090C6A51602B63D0979, costs: 24497.000000 ns
Test 1 decrypted: 720B4455C91B6A024135343386B6679D, costs: 22873.000000 ns
...

这里的加密flag就是我们的目标:B789EB607A91D08888E2C4C4E4D9573FF5333E4783390B91EDB9D2B4FD2A5FC0

二、漏洞分析

2.1 程序逻辑推断

通过动态分析和字符串提取,我们可以推断程序的主要逻辑:

  1. 初始化随机数生成器:使用 srand(time(NULL))

  2. 生成加密密钥:通过16次 rand() % 256调用生成16字节的密钥

  3. 加密flag:使用生成的密钥对flag进行加密并输出

  4. 测试循环:进行多次测试,每次生成随机明文进行加密解密测试

2.2 核心漏洞:弱随机数种子

问题的关键在于 srand(time(NULL))的使用:

为什么这是一个漏洞?

  1. 种子空间有限time(NULL)返回的是Unix时间戳(自1970年1月1日以来的秒数),虽然数值很大,但在特定时间范围内的可能值是有限的。

  2. 可预测性:如果我们知道程序大致的运行时间,就可以在一个很小的时间窗口内暴力搜索所有可能的种子。

  3. 确定性:相同的种子会产生完全相同的随机数序列。如果我们找到了正确的种子,就能重现所有的随机数,包括用于生成密钥的随机数。

如何利用这个漏洞?

观察output文件中的第一个测试明文:720B4455C91B6A024135343386B6679D

这个明文实际上是由16个随机数(每个 rand() % 256)生成的。由于程序在生成这个明文之前已经调用了16次rand()来生成密钥,我们可以利用这个特性:

攻击思路

  1. 遍历可能的时间戳作为种子

  2. 对每个种子:

    • 调用 srand(seed)

    • 跳过前16次rand()调用(密钥生成)

    • 生成16个随机数

    • 检查是否与第一个测试明文匹配

  3. 找到匹配的种子后,就能重现密钥

三、攻击实现

3.1 步骤一:编写种子爆破程序

我们编写一个C程序来暴力搜索正确的种子:

#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>

// 从output文件中获取的第一个test plaintext
uint8_t seq[] = {0x72, 0x0B, 0x44, 0x55, 0xC9, 0x1B, 0x6A, 0x02,
                 0x41, 0x35, 0x34, 0x33, 0x86, 0xB6, 0x67, 0x9D};

int main() {
  // 时间戳应该是最近的,大约在2024-2025年之间
  // 1730000000 对应 2024年10月27日
  // 1770000000 对应 2026年2月3日
  for (int seed = 1730000000; seed < 1770000000; seed++) {
    if (seed % 1000000 == 0) {
      printf("Progress: %d\\n", seed);
    }
    srand(seed);
    bool good = true;

    // 跳过key生成(前16次rand调用)
    for (int i = 0; i < 16; i++)
      rand();

    // 检查生成的随机数序列是否匹配第一个test plaintext
    for (int i = 0; i < sizeof(seq) / sizeof(seq[0]); i++) {
      unsigned int temp = (unsigned int)rand() % 256;
      if (temp != seq[i]) {
        good = false;
        break;
      }
    }

    if (good) {
      printf("Found seed: %d\\n", seed);
      // 验证并输出key
      printf("Key bytes: ");
      srand(seed);
      for (int i = 0; i < 16; i++) {
        printf("%02X ", (unsigned int)rand() % 256);
      }
      printf("\\n");
      break;
    }
  }
  return 0;
}

程序原理解析

  • 我们遍历可能的时间戳范围(约4000万个值)

  • 对于每个候选种子,我们模拟程序的行为:先跳过16次rand()调用,然后生成16个随机数

  • 将生成的随机数与output文件中的第一个测试明文进行比对

  • 如果完全匹配,说明找到了正确的种子

编译并运行:

gcc crack_seed.c -o crack_seed
./crack_seed

输出:

Progress: 1730000000
Progress: 1731000000
...
Progress: 1753000000
Found seed: 1753843495
Key bytes: 17 A7 C5 6D 2D DE 47 FD BD CE B6 80 60 B9 16 9C

成功找到种子:1753843495

这对应的时间是:2025年5月30日 10:44:55 GMT+8

3.2 步骤二:验证种子的正确性

为了验证我们找到的种子是否正确,我们编写一个动态库来劫持 srand函数调用:

#include <dlfcn.h>
#include <stdio.h>
#include <stdlib.h>

typedef void (*srand_t)(unsigned int seed);
srand_t real_srand;

void srand(unsigned int seed) {
  fprintf(stderr, "called srand(%d)\\n", seed);
  if (!real_srand) {
    real_srand = dlsym(RTLD_NEXT, "srand");
  }
  char *s = getenv("SRAND");
  int real_seed = 0;
  if (s) {
    sscanf(s, "%d", &real_seed);
    fprintf(stderr, "real seed is(%d)\\n", real_seed);
    real_srand(real_seed);
  } else {
    real_srand(seed);
  }
}

原理说明

  • 这个动态库使用 LD_PRELOAD技术来劫持程序对 srand的调用

  • 当程序调用 srand(time(NULL))时,我们的函数会被调用

  • 我们从环境变量 SRAND中读取预设的种子值,并用它替换原始的种子

  • 这样就能让程序使用我们找到的种子,而不是当前时间

编译动态库:

gcc -shared -fPIC hook_srand.c -o hook_srand.so -ldl

使用劫持库运行程序:

export FLAG="flag{test}"
export SRAND=1753843495
LD_PRELOAD=./hook_srand.so ./Tch3s | head -n 10

输出:

called srand(1761836786)
real seed is(1753843495)

encrypted flag: D355DF17F209B90CFFFEA6DA1692780D

Test 1 plaintext: 720B4455C91B6A024135343386B6679D
Test 1 encrypted: C9BBC42200F90090C6A51602B63D0979, costs: 27116.000000 ns
Test 1 decrypted: 720B4455C91B6A024135343386B6679D, costs: 25238.000000 ns

验证成功!注意 Test 1 plaintext完全匹配output文件中的值,这证明我们找到了正确的种子!

四、解密flag

4.1 解密方案

现在我们有了正确的种子,意味着程序内存中已经有了正确的密钥。但是我们不想重新实现加密算法(因为二进制文件中的算法可能比较复杂),所以我们采用一个巧妙的方法:利用调试器直接调用程序中的解密函数

4.2 使用GDB进行解密

找到关键函数地址

通过反汇编分析,我们找到了两个关键函数:

  • 加密函数地址:0x30ba(相对地址)

  • 解密函数地址:0x31a1(相对地址)

由于程序是PIE(Position Independent Executable),实际运行时的地址会加上一个基址,通常是 0x555555554000

GDB脚本

我们编写GDB脚本来解密flag:

# 设置环境变量
set environment FLAG=flag{test_flag_for_testing_12345}
set environment SRAND=1753843495
set environment LD_PRELOAD=./hook_srand.so

file ./Tch3s

# 在加密函数入口打断点
b *0x5555555570ba

run

# 解密第一部分: B789EB607A91D08888E2C4C4E4D9573F
# 注意字节序转换(小端序)
set *((int *)$rdi) = 0x60eb89b7
set *((int *)$rdi+1) = 0x88d0917a
set *((int *)$rdi+2) = 0xc4c4e288
set *((int *)$rdi+3) = 0x3f57d9e4

# 调用解密函数
p ((void (*)(void*,void*,void*))0x5555555571a1)($rdi,$rsi,$rdx)
x/s $rsi

run

# 解密第二部分: F5333E4783390B91EDB9D2B4FD2A5FC0
set *((int *)$rdi) = 0x473e33f5
set *((int *)$rdi+1) = 0x910b3983
set *((int *)$rdi+2) = 0xb4d2b9ed
set *((int *)$rdi+3) = 0xc05f2afd

# 调用解密函数
p ((void (*)(void*,void*,void*))0x5555555571a1)($rdi,$rsi,$rdx)
x/s $rsi

quit

关键步骤解析

  1. 环境准备:设置环境变量,使用我们找到的种子

  2. 断点设置:在加密函数入口处设置断点

  3. 内存修改

    • 当程序执行到加密函数时,$rdi指向明文缓冲区,$rsi指向密文缓冲区,$rdx指向密钥

    • 我们将output中的加密flag写入 $rdi(作为"假明文")

    • 注意字节序:十六进制字符串 B789EB60在内存中以小端序存储为 60 EB 89 B7,对应整数 0x60eb89b7

  4. 调用解密函数:直接调用程序中的解密函数,传入相同的三个参数

  5. 查看结果:解密后的明文(即flag)会写入 $rsi指向的缓冲区

执行解密

gdb -batch -x decrypt_exact.gdb

输出:

Breakpoint 1, 0x00005555555570ba in ?? ()
0x55555555b2e0:	"flag{tim1ng_a7t@"

Breakpoint 1, 0x00005555555570ba in ?? ()
0x55555555b2e0:	"ck_1s_dangerous}"

成功解密!完整的flag是:flag{tim1ng_a7t@ck_1s_dangerous}

五、技术总结

5.1 攻击原理回顾

这道题目展示了一个经典的密码学安全问题:弱随机数生成。整个攻击链包括:

  1. 识别弱PRNG:程序使用 srand(time(NULL))作为随机数种子

  2. 种子空间分析:时间戳的可能范围有限,可以暴力搜索

  3. 侧信道信息泄露:程序输出的测试数据泄露了随机数序列

  4. 种子恢复:通过匹配已知的随机数序列来找到种子

  5. 密钥重建:使用恢复的种子重新生成密钥

  6. 密文解密:利用程序自身的解密函数完成解密

5.2 为什么这种攻击有效?

C语言rand()函数的特性

C标准库的 rand()函数使用线性同余生成器(LCG),其通用公式为:

X[n+1] = (a * X[n] + c) mod m

特点:

  • 完全确定性:给定相同的种子,产生完全相同的序列

  • 可预测性:知道几个连续的输出值,理论上可以推断出种子

  • 周期性:序列会周期性重复

时间戳作为种子的问题

  1. 熵不足time(NULL)的精度是秒级,1秒内的所有调用得到相同的值

  2. 可预测范围:我们通常能估计程序的运行时间范围

  3. 搜索空间小:即使搜索几年的时间跨度,也只有约1亿个可能值,现代计算机可以在几分钟内完成

5.3 时序攻击的意义

题目名为"Tch3s",暗示了"Timing Attack"。虽然本题的主要漏洞是弱PRNG,但程序确实输出了加密/解密时间:

Test 1 encrypted: ..., costs: 24497.000000 ns
Test 1 decrypted: ..., costs: 22873.000000 ns

在现实场景中,这些时序信息可能被用于:

  • 差分功耗分析(DPA)

  • 缓存时序攻击

  • 推断密钥信息

因此,密码学实现应该:

  • 使用恒定时间算法(Constant-time algorithms)

  • 避免分支依赖于密钥数据

  • 小心缓存和内存访问模式

5.4 实战中的类似案例

  1. Debian OpenSSL漏洞(2008):由于错误的补丁,OpenSSL的PRNG熵源被严重削弱,导致只有32768种可能的密钥。

  2. Android Bitcoin钱包漏洞(2013):Android的 SecureRandom实现有缺陷,导致生成的ECDSA签名可预测,比特币被盗。

  3. PlayStation 3签名密钥泄露(2010):Sony在ECDSA签名时重复使用了相同的随机数k,导致私钥可被计算出来。

六、总结

这道CTF题目是一个优秀的教学案例,它展示了:

  1. 密码学实现的重要性:即使加密算法本身是安全的,糟糕的实现也会导致系统完全崩溃。

  2. 随机性的关键作用:密码学系统的安全性高度依赖于高质量的随机数。

  3. 侧信道攻击的威胁:看似无害的信息泄露(如测试数据输出)可能成为攻击的突破口。

  4. 实战技能的综合运用

    • 二进制分析与逆向工程

    • C编程与系统调用

    • 动态分析与调试技术

    • 密码学原理与攻击技术

  5. "时序攻击"的警示:虽然本题主要利用的是PRNG弱点,但题目名称提醒我们时序信息也可能是攻击向量。

最终答案flag{tim1ng_a7t@ck_1s_dangerous}

这个flag本身也是一个警告:"时序攻击是危险的(timing attack is dangerous)",提醒开发者在实现密码学系统时必须格外小心。

附录:完整攻击代码

种子爆破程序(crack_seed.c)

#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>

uint8_t seq[] = {0x72, 0x0B, 0x44, 0x55, 0xC9, 0x1B, 0x6A, 0x02,
                 0x41, 0x35, 0x34, 0x33, 0x86, 0xB6, 0x67, 0x9D};

int main() {
  for (int seed = 1730000000; seed < 1770000000; seed++) {
    if (seed % 1000000 == 0) {
      printf("Progress: %d\\n", seed);
    }
    srand(seed);
    bool good = true;
    for (int i = 0; i < 16; i++) rand();
    for (int i = 0; i < sizeof(seq) / sizeof(seq[0]); i++) {
      unsigned int temp = (unsigned int)rand() % 256;
      if (temp != seq[i]) {
        good = false;
        break;
      }
    }
    if (good) {
      printf("Found seed: %d\\n", seed);
      srand(seed);
      printf("Key: ");
      for (int i = 0; i < 16; i++) {
        printf("%02X ", (unsigned int)rand() % 256);
      }
      printf("\\n");
      break;
    }
  }
  return 0;
}

srand劫持库(hook_srand.c)

#include <dlfcn.h>
#include <stdio.h>
#include <stdlib.h>

typedef void (*srand_t)(unsigned int seed);
srand_t real_srand;

void srand(unsigned int seed) {
  fprintf(stderr, "called srand(%d)\\n", seed);
  if (!real_srand) {
    real_srand = dlsym(RTLD_NEXT, "srand");
  }
  char *s = getenv("SRAND");
  int real_seed = 0;
  if (s) {
    sscanf(s, "%d", &real_seed);
    fprintf(stderr, "real seed is(%d)\\n", real_seed);
    real_srand(real_seed);
  } else {
    real_srand(seed);
  }
}

编译命令:

gcc crack_seed.c -o crack_seed
gcc -shared -fPIC hook_srand.c -o hook_srand.so -ldl

作者注:本文仅用于技术学习和CTF竞赛交流,请勿将相关技术用于非法用途。


文章来源: https://www.freebuf.com/articles/others-articles/455332.html
如有侵权请联系:admin#unsafe.sh