文章总结: 本文解析SECCONCTF密码学题,逆向发现程序rnd因使用弱随机数种子导致密钥可预测。作者利用文件元数据重建随机序列,通过XOR还原PNG图片,并应用整数分解与中国剩余定理破解图片中的二次同余方程,最终获取FlagSECCON{Ra_b1_N}。文章深入讲解了流密码原理与逆向技巧,警示了元数据泄露风险及弱随机数的危害。 综合评分: 98 文章分类: CTF,逆向分析,漏洞分析,二进制安全
二、逆向分析加密程序
2.1 使用Ghidra进行静态分析
由于rnd程序已被strip,没有符号信息,我们需要使用反编译工具进行分析。这里使用Ghidra打开rnd程序。
通过分析程序入口点,找到主函数(Ghidra自动识别为FUN_08048514)。以下是反编译后的C代码:
undefined4 FUN_08048514(int param_1, int param_2)
{
undefined4 uVar1;
uint __seed;
FILE *__stream;
FILE *__stream_00;
size_t sVar2;
int iVar3;
byte local_11[13];
if (param_1 < 3) {
uVar1 = 1;
}
else {
__seed = time((time_t *)0x0);
srand(__seed);
__stream = fopen(*(char **)(param_2 + 4), "rb");
__stream_00 = fopen(*(char **)(param_2 + 8), "wb");
while(true) {
sVar2 = fread(local_11, 1, 1, __stream);
if (sVar2 != 1) break;
iVar3 = rand();
local_11[0] = local_11[0] ^ (byte)iVar3;
fwrite(local_11, 1, 1, __stream_00);
}
fclose(__stream);
fclose(__stream_00);
uVar1 = 0;
}
return uVar1;
}
2.2 加密算法逐行分析
让我们逐行分析这个加密算法的工作流程:
第1步:参数检查
if (param_1 < 3) {
uVar1 = 1;
}
param_1是命令行参数个数(argc)- 如果参数少于3个(程序名、输入文件、输出文件),返回错误
第2步:初始化随机数生成器
__seed = time((time_t *)0x0);
srand(__seed);
time(NULL)获取当前Unix时间戳(自1970年1月1日以来的秒数)- 使用这个时间戳作为随机数种子初始化
srand()
这是算法的关键漏洞!时间戳是可以预测或获取的。
第3步:打开文件
__stream = fopen(*(char **)(param_2 + 4), "rb");
__stream_00 = fopen(*(char **)(param_2 + 8), "wb");
param_2是命令行参数数组(argv)param_2 + 4指向第一个参数(输入文件名)param_2 + 8指向第二个参数(输出文件名)- 以二进制模式打开输入和输出文件
第4步:逐字节加密
while(true) {
sVar2 = fread(local_11, 1, 1, __stream);
if (sVar2 != 1) break;
iVar3 = rand();
local_11[0] = local_11[0] ^ (byte)iVar3;
fwrite(local_11, 1, 1, __stream_00);
}
这是核心加密逻辑:
- 从输入文件读取1个字节到
local_11[0] - 如果读取失败(文件结束),退出循环
- 调用
rand()生成一个随机数 - 将读取的字节与随机数的低8位进行XOR运算
- 将结果写入输出文件
2.3 加密算法总结
通过逆向分析,我们得出加密算法的本质:
加密公式:
密文字节 = 明文字节 XOR (rand() & 0xFF)
算法特点:
- 这是一个流密码(Stream Cipher)
- 使用伪随机数生成器(PRNG)生成密钥流
- 通过XOR运算加密每个字节
关键漏洞:
- 使用
time()作为种子 – 时间戳可以从文件元数据获取 - 使用标准库的
rand()– 这不是密码学安全的随机数生成器 - 相同的种子会产生完全相同的随机数序列
2.4 XOR运算的密码学性质
XOR(异或)运算在密码学中有重要的性质:
自反性(Self-inverse):
A XOR B XOR B = A
这意味着:
- 如果
密文 = 明文 XOR 密钥 - 那么
明文 = 密文 XOR 密钥
加密和解密使用完全相同的操作!
实例演示:
明文字节: 0x89 (PNG文件头的第一个字节)
随机数: 0x1234 (假设rand()返回这个值)
密钥字节: 0x34 (取低8位:0x1234 & 0xFF)
加密:0x89 XOR 0x34 = 0xBD
解密:0xBD XOR 0x34 = 0x89 (恢复原始字节)
三、获取加密时间戳
3.1 文件系统元数据
Linux文件系统为每个文件保存三个时间戳:
- atime(Access Time):最后访问时间
- mtime(Modification Time):最后修改时间
- ctime(Change Time):最后状态改变时间
我们使用stat命令查看ecrypt1.bin的详细信息:
$ stat ecrypt1.bin
File: ecrypt1.bin
Size: 45989 Blocks: 96 IO Block: 4096 regular file
Device: 801h/2049d Inode: 49544 Links: 1
Access: (0664/-rw-rw-r--) Uid: ( 1000/ user) Gid: ( 1000/ user)
Access: 2014-11-22 15:46:36.000000000 +0100
Modify: 2014-11-22 15:46:30.000000000 +0100
Change: 2014-12-07 00:53:58.000000000 +0100
3.2 确定关键时间戳
关键信息是Modify时间:2014-11-22 15:46:30
为什么是Modify时间?因为:
rnd程序在开始时调用time(NULL)获取种子- 然后立即开始写入加密文件
- 文件的Modify时间记录的是文件内容最后被修改的时刻
- 这两个操作几乎同时发生,时间戳相同(或最多相差1秒)
3.3 转换为Unix时间戳
使用stat命令的格式化输出获取Unix时间戳:
$ stat --printf=%Y ecrypt1.bin
1416667590
这个数字1416667590就是:
- 自1970年1月1日00:00:00 UTC以来的秒数
- 对应时间:2014-11-22 15:46:30 +0100
- 这就是
rnd程序调用time(NULL)时获得的值
四、编写解密程序
4.1 解密原理
基于我们的分析,解密过程非常直接:
- 使用相同的种子
1416667590初始化随机数生成器 - 逐字节读取加密文件
- 对每个字节,生成相同的随机数并进行XOR运算
- 由于XOR的自反性,我们可以恢复原始数据
数学原理:
加密:C = P XOR K
解密:P = C XOR K
验证:C XOR K = (P XOR K) XOR K = P XOR (K XOR K) = P XOR 0 = P
4.2 解密代码实现
我们编写C++程序来实现解密:
#include<iostream>
#include<cstdlib>
#include <time.h>
using namespace std;
int main() {
FILE *__stream;
FILE *__stream_00;
int sVar2;
int local_11[11];
int iVar3;
// 使用文件的修改时间作为随机数种子
srand(1416667590);
__stream = fopen("ecrypt1.bin","rb");
__stream_00 = fopen("crypt1.png","wb");
while(1) {
sVar2 = fread(local_11, 1, 1, __stream);
if (sVar2 != 1) break;
iVar3 = rand();
local_11[0] = local_11[0] ^ (iVar3 & 0xff);
fwrite(local_11, 1, 1, __stream_00);
}
fclose(__stream);
fclose(__stream_00);
return 0;
}
4.3 代码关键点解析
让我们详细分析解密代码的关键部分:
关键点1:种子设置
srand(1416667590);
这是整个解密的核心。我们使用从文件元数据提取的时间戳初始化随机数生成器,确保生成的随机数序列与加密时完全相同。
关键点2:随机数的低8位
local_11[0] = local_11[0] ^ (iVar3 & 0xff);
rand()返回int类型(32位)& 0xff提取最低的8位(一个字节)- 这与加密程序中的
(byte)iVar3效果相同
关键点3:相同的循环结构解密程序的循环结构与加密程序完全相同,确保每个字节都使用序列中对应位置的随机数。
4.4 编译和运行
编译解密程序:
$ g++ -o decrypt decrypt.cpp
运行解密程序:
$ ./decrypt
验证解密结果:
$ file crypt1.png
crypt1.png: PNG image data, 676 x 517, 8-bit/color RGB, non-interlaced
$ ls -lh crypt1.png
-rwxrwxrwx 1 root root 45K Jan 8 13:02 crypt1.png
成功!我们恢复了原始的PNG图片文件(45989字节)。
五、分析PNG图片中的密码学问题
5.1 PNG图片内容
打开恢复的PNG图片后,发现图片中包含了一个密码学问题:
N = pq
C = M(M + B) mod N
N = B8AE199365 B = FFEEE C = 8D5051562B
N = B86E78C811 B = FFFEE C = 5FFA0AC1A2
N = 7BD4071E55 B = FEFEF C = 6008DDF867
这是三组独立的加密数据,我们需要求解每组的明文M。
5.2 问题分析
加密方程:
C ≡ M(M + B) (mod N)
C ≡ M² + MB (mod N)
这是一个关于M的二次同余方程。
关键观察:
- N是两个质数p和q的乘积(类似RSA)
- 我们有三组独立的数据
- N的值相对较小(约40位十进制数)
解题策略:
- 分解N得到p和q
- 在模p和模q下分别求解
- 使用中国剩余定理(CRT)组合结果
5.3 分解N
使用在线工具factordb.com对三个N值进行分解:
N₁ = 0xB8AE199365 = 793194894181 = 868019 × 913799
N₂ = 0xB86E78C811 = 792127391761 = 875543 × 904727
N₃ = 0x7BD4071E55 = 531838213717 = 597263 × 890459
这些N值相对较小,可以被快速分解。在实际的RSA系统中,N通常是2048位或更长,无法被分解。
5.4 中国剩余定理(CRT)的应用
为什么使用CRT?
由于 N = p × q,我们可以利用中国剩余定理简化问题:
如果我们知道:
- M mod p
- M mod q
那么我们就可以唯一确定 M mod N。
CRT的数学原理:
对于同余方程组:
x ≡ a₁ (mod m₁)
x ≡ a₂ (mod m₂)
当m₁和m₂互质时,存在唯一解 x (mod m₁×m₂)。
5.5 在模p和模q下求解
由于p和q的值不是很大(约80-90万),我们可以使用暴力搜索:
求解方法: 对于每组数据,分别在模p和模q下搜索满足条件的M:
# 在模p下搜索
for M in range(p):
if C % p == (M * (M + B)) % p:
print(f"M ≡ {M} (mod {p})")
# 在模q下搜索
for M in range(q):
if C % q == (M * (M + B)) % q:
print(f"M ≡ {M} (mod {q})")
运行结果:
第1组数据:
M ≡ 158558 (mod 868019)
M ≡ 529178 (mod 868019)
M ≡ 24100 (mod 913799)
M ≡ 755196 (mod 913799)
第2组数据:
M ≡ 270422 (mod 875543)
M ≡ 432106 (mod 875543)
M ≡ 263205 (mod 904727)
M ≡ 497691 (mod 904727)
第3组数据:
M ≡ 194177 (mod 597263)
M ≡ 553149 (mod 597263)
M ≡ 351135 (mod 890459)
M ≡ 385320 (mod 890459)
为什么有多个解?
对于二次方程 M² + BM - C ≡ 0 (mod p),在模质数p的情况下,根据二次剩余理论,如果存在解,通常会有两个解。
这是因为如果M₀是一个解,那么 -M₀ - B 也是一个解。
5.6 使用CRT组合解
由于每个模数下有2个解,对于每组数据,我们有 2 × 2 = 4 种可能的组合。
使用Python的sympy库实现CRT:
from sympy.ntheory.modular import crt
from Crypto.Util.number import long_to_bytes
# 对第1组数据的一个组合求解
result = crt([868019, 913799], [529178, 755196])
M = result[0]
print(long_to_bytes(M)) # 输出: b'SECCO'
完整求解脚本:
我们需要遍历所有可能的组合,找出能转换为有效ASCII字符的解:
from sympy.ntheory.modular import crt
from Crypto.Util.number import long_to_bytes
# 三组数据的模数解
modular_solutions = [
{'p': 868019, 'q': 913799,
'solutions_p': [158558, 529178],
'solutions_q': [24100, 755196]},
{'p': 875543, 'q': 904727,
'solutions_p': [270422, 432106],
'solutions_q': [263205, 497691]},
{'p': 597263, 'q': 890459,
'solutions_p': [194177, 553149],
'solutions_q': [351135, 385320]}
]
flag_parts = []
for sol in modular_solutions:
p, q = sol['p'], sol['q']
solutions_p = sol['solutions_p']
solutions_q = sol['solutions_q']
for mp in solutions_p:
for mq in solutions_q:
result = crt([p, q], [mp, mq])
if result:
M = result[0]
try:
bytes_result = long_to_bytes(M)
if all(32 <= b < 127 for b in bytes_result):
flag_parts.append(bytes_result.decode('ascii'))
print(f"找到有效解: {bytes_result}")
except:
pass
运行结果:
找到有效解: b'SECCO'
找到有效解: b'N{Ra_'
找到有效解: b'b1_N}'
5.7 组合得到最终flag
观察这三个字符串:
- 第一个:
SECCO– 看起来像是”SECCON”的一部分 - 第二个:
N{Ra_– 包含花括号,符合flag格式 - 第三个:
b1_N}– 以花括号结尾,符合flag格式
将它们按顺序组合:
SECCO + N{Ra_ + b1_N} = SECCON{Ra_b1_N}
最终答案:
SECCON{Ra_b1_N}
这个flag的含义可能是”Rabin”的变形,因为我们使用的加密方程与Rabin密码系统有相似之处。
六、技术知识点总结
6.1 密码学知识点
1. 流密码(Stream Cipher)
流密码是一种对称加密算法,逐位或逐字节地加密数据。本题使用的就是流密码:
- 使用伪随机数生成器(PRNG)生成密钥流
- 通过XOR运算将密钥流与明文结合
- 加密和解密使用相同的操作
流密码的安全性要求:
- 密钥流必须是真随机的(不可预测)
- 密钥流不能重复使用
- 密钥流长度必须至少与明文相同
本题的漏洞在于使用了可预测的种子,导致密钥流可以被重现。
2. XOR运算的密码学性质
XOR(异或)运算具有以下重要性质:
- 自反性:
A XOR B XOR B = A - 交换律:
A XOR B = B XOR A - 结合律:
(A XOR B) XOR C = A XOR (B XOR C) - 零元素:
A XOR 0 = A
这些性质使得XOR成为密码学中常用的操作。
3. 二次同余方程
形如 ax² + bx + c ≡ 0 (mod n) 的方程称为二次同余方程。
在模质数p的情况下:
- 如果存在解,通常有0个或2个解
- 解的个数与二次剩余理论相关
- 可以通过暴力搜索或使用Tonelli-Shanks算法求解
4. 中国剩余定理(CRT)
中国剩余定理用于求解同余方程组:
x ≡ a₁ (mod m₁)
x ≡ a₂ (mod m₂)
当m₁和m₂互质时,存在唯一解 x (mod m₁×m₂)。
CRT在密码学中的应用:
- RSA加密/解密的加速
- 将大模数问题分解为小模数问题
- 秘密共享方案
5. 大整数分解
RSA安全性基于大整数分解的困难性:
- 本题中的N值约为40位十进制数,可以被快速分解
- 实际应用中,RSA使用2048位或更长的密钥
- 随着计算能力提升,密钥长度需要不断增加
6.2 逆向工程技巧
1. 使用反编译工具
- Ghidra:开源的反编译工具,支持多种架构
- IDA Pro:商业反编译工具,功能强大
- 理解反编译代码的局限性(变量名丢失、类型推断不准确)
2. 识别标准库函数
time()、srand()、rand()等函数的特征- 文件操作函数:
fopen()、fread()、fwrite() - 通过函数调用模式推断程序行为
3. 分析程序控制流
- 识别循环结构(while、for)
- 识别条件判断(if-else)
- 理解程序的整体逻辑
6.3 取证分析技巧
1. 文件元数据分析
- 使用
stat命令查看文件时间戳 - 理解atime、mtime、ctime的区别
- 时间戳可能泄露重要信息
2. 文件格式识别
- 使用
file命令识别文件类型 - 了解常见文件格式的魔数(Magic Number)
- PNG文件头:
89 50 4E 47 0D 0A 1A 0A
6.4 安全启示
1. 不要使用可预测的随机数种子
- 永远不要使用
time()作为加密的随机数种子 - 应该使用密码学安全的随机数生成器(CSPRNG)
- 在Linux上可以使用
/dev/urandom或getrandom()系统调用
2. XOR加密需要真正的随机密钥
- XOR本身可以是完美的加密算法(一次性密码本)
- 但前提是密钥必须是真随机的、与明文等长、且只使用一次
- 使用PRNG生成的密钥流不满足这些条件
3. 元数据也是重要的信息
- 文件的时间戳、大小、权限等元数据可能泄露敏感信息
- 在安全场景中,需要考虑清除或混淆元数据
- 攻击者可以从元数据中获取有价值的线索
4. 密钥长度的重要性
- 本题中的N值只有约40位,可以轻易分解
- 密钥长度直接影响安全性
- 随着计算能力的提升,密钥长度需要不断增加
5. 使用成熟的加密库
- 不要自己实现加密算法
- 使用经过验证的加密库(如OpenSSL、libsodium)
- 密码学实现中的细微错误可能导致严重的安全问题
七、全文总结
本题是一道综合性很强的CTF密码学题目,涉及了逆向工程、密码分析、数论等多个领域的知识。
7.1 解题流程回顾
第一阶段:逆向分析
- 使用Ghidra反编译
rnd程序 - 识别出流密码加密算法
- 发现使用
time()作为随机数种子的关键漏洞
第二阶段:获取时间戳
- 使用
stat命令查看文件元数据 - 提取文件修改时间戳:1416667590
- 理解时间戳与加密种子的关系
第三阶段:编写解密程序
- 使用相同的种子初始化随机数生成器
- 实现与加密程序相同的XOR操作
- 成功恢复PNG图片文件
第四阶段:求解密码学问题
- 使用factordb分解N得到p和q
- 在模p和模q下暴力搜索求解
- 使用中国剩余定理组合结果
- 得到最终flag:SECCON{Ra_b1_N}
7.2 核心要点
密码学的”链条最薄弱环节”原理
这道题目充分展示了密码学中的一个重要原理:系统的安全性取决于最薄弱的环节。即使使用了复杂的数学问题(二次同余方程),如果实现中存在漏洞(可预测的随机数种子),整个系统的安全性就会崩溃。
多层次的安全考虑
一个安全的密码系统需要考虑多个层面:
- 算法层面:使用经过验证的加密算法
- 实现层面:正确实现算法,避免实现漏洞
- 密钥管理:使用真随机数生成密钥,安全存储密钥
- 元数据保护:防止元数据泄露敏感信息
7.3 学习价值
对于初学者来说,这道题目是很好的学习材料,它涵盖了:
技术技能:
- 二进制程序逆向分析
- 流密码的工作原理和弱点
- XOR运算的密码学应用
- 文件系统取证技术
- 二次同余方程的求解
- 中国剩余定理的实际应用
- RSA相关密码系统的攻击方法
思维方式:
- 系统化的问题分析方法
- 从多个角度寻找突破口
- 将复杂问题分解为可管理的小问题
- 利用数学工具简化问题
通过完整地复现这道题目,我们不仅学会了具体的解题技巧,更重要的是理解了密码学系统设计中需要注意的安全要点。这些知识和经验对于从事信息安全工作具有重要的实践价值。
最终答案:SECCON{Ra_b1_N}
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:破镜安全 破镜安全《CTF密码学题目深度解析:Decrypt-It-easy》
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。








评论