1. 题目背景与环境准备
这道CTF逆向题目来自2020年羊城杯比赛,题目名为"babyre"。题目主要考察了逆向工程中常见的加密算法分析和自修改代码(SMC)处理技巧。我们先从环境搭建开始讲起。
在Kali Linux环境下运行题目提供的二进制文件时,会遇到一个常见错误:
code复制./re: error while loading shared libraries: libcrypto.so.1.0.0: cannot open shared object file: No such file or directory
这是因为程序依赖了OpenSSL 1.0.0版本的动态链接库。解决方法很简单:
code复制sudo apt-get install libssl1.0.0
如果找不到这个包,也可以考虑使用以下替代方案:
- 从源码编译OpenSSL 1.0.0并安装
- 使用LD_LIBRARY_PATH指定库路径
- 在Docker容器中运行(推荐)
注意:在实际CTF比赛中,建议使用Docker容器来隔离不同版本的库依赖,避免污染主机环境。
2. 初步分析与DES解密
2.1 主函数逻辑分析
使用IDA Pro打开二进制文件,可以看到主函数结构相对清晰。程序主要使用了DES加密算法,采用CBC模式。关键数据如下:
- 密文存储在byte_6040C0数组
- 初始向量(IV)存储在v7变量
- 密钥长度为8字节(DES标准密钥长度)
通过动态调试(推荐使用gdb配合pwndbg插件),我们可以输入任意16个字符作为测试输入,然后观察内存中的密钥数据。
2.2 密钥提取与解密
在动态调试过程中,我们发现密钥实际上只使用了8位(虽然输入了16个字符)。通过内存查看,可以提取出完整的密钥:
python复制key = [0xAD, 0x52, 0xF2, 0x4C, 0xE3, 0x2C, 0x20, 0xD6]
使用Python的Crypto库进行DES解密:
python复制from Crypto.Cipher import DES
byte_6040C0 = [
0x0A, 0xF4, 0xEE, 0xC8, 0x42, 0x8A, 0x9B, 0xDB,
0xA2, 0x26, 0x6F, 0xEE, 0xEE, 0xE0, 0xD8, 0xA2
]
iv = [0] * 8 # CBC模式需要IV,这里全0
cipher = DES.new(bytes(key), DES.MODE_CBC, bytes(iv))
print(cipher.decrypt(bytes(byte_6040C0)))
解密结果得到字符串:th1s1sth3n1c3k3y
3. SMC自修改代码分析
3.1 自解密函数解析
程序中的sub_402563函数实现了SMC(Self-Modifying Code)功能,对byte_40272D区域进行了自解密。关键点:
- 解密密钥就是我们上一步得到的"th1s1sth3n1c3k3y"
- 解密后需要重新分析代码(在IDA中按C键将数据转换为代码)
- 解密后的函数包含主要的加密/混淆逻辑
3.2 逆向工程难点
解密后的sub_40272D函数包含三段主要逻辑,其中最复杂的是以下表达式:
code复制if ( (2 * (v5 ^ 0x13) + 7) ^ (v5 % 9 + v6 + 2) != *v4 )
这个表达式存在以下逆向难点:
- 运算过程中存在数据丢失(模运算和位运算)
- 无法直接逆向计算,因为一个输出可能对应多个输入
- 模9运算导致每个位置可能有多个解
4. 爆破解法实现
4.1 DFS深度优先搜索
由于无法直接逆向计算,我们采用DFS(深度优先搜索)来爆破所有可能的解。基本思路:
- 从最后一个字节开始(已知v9[15] = 0xC4)
- 对每个位置尝试所有可能的字节值(0-255)
- 验证是否满足加密等式
- 递归处理前一个字节
实现代码:
python复制def dfs(list, index):
if index < 0:
lists.append(list.copy())
return
newlist = list
for j in range(255):
if ((2 *(j ^ 0x13) + 7) ^ (j % 9 + newlist[index + 1] + 2)) & 0xFF == byte_604100[index]:
newlist[index] = j
dfs(newlist, index - 1)
byte_604100 = [
0xBD, 0xAD, 0xB4, 0x84, 0x10, 0x63, 0xB3, 0xE1,
0xC6, 0x84, 0x2D, 0x6F, 0xBA, 0x88, 0x74, 0xC4,
0x90, 0x32, 0xEA, 0x2E, 0xC6, 0x28, 0x65, 0x70,
0xC9, 0x75, 0x78, 0xA0, 0x0B, 0x9F, 0xA6, 0x00
]
lists = []
test = [0] * 32
test[31] = 0xC4 # 已知最后一个字节
dfs(test, 30) # 从倒数第二个字节开始爆破
4.2 后续解密处理
得到所有可能的解后,还需要进行两步解密:
- 异或处理:每个字节与其前面每4个字节进行异或
- AES解密:使用之前得到的"th1s1sth3n1c3k3y"作为密钥
实现代码:
python复制from Crypto.Cipher import AES
key = b'th1s1sth3n1c3k3y'
for s in lists:
# 第一步:异或处理
for i in range(31, -1, -1):
for j in range(i // 4):
s[i] ^= s[j]
# 第二步:AES解密
cipher = AES.new(key, AES.MODE_ECB)
try:
print(cipher.decrypt(bytes(s)).decode())
except:
continue # 过滤非可打印字符
5. 结果分析与总结
5.1 最终flag
经过上述步骤,我们最终得到flag:
GWHT{th1s_gam3_1s_s0_c00l_and_d}
5.2 技术要点总结
-
加密算法识别:题目使用了DES和AES两种对称加密算法
- DES:CBC模式,8字节密钥
- AES:ECB模式,16字节密钥
-
SMC处理技巧:
- 动态调试识别自修改代码
- 解密后重新分析代码区域
- 使用IDAPython脚本可以自动化这一过程
-
复杂表达式处理:
- 当遇到不可逆的加密表达式时,考虑爆破解法
- DFS适合处理这种多解情况
- 合理利用已知条件减少搜索空间(如已知最后一个字节)
5.3 实战经验分享
-
动态调试技巧:
- 在gdb中使用
catch syscall ptrace可以捕获SMC行为 - IDA的调试器可以设置内存断点监控代码修改
- 在gdb中使用
-
性能优化:
- 爆破时可以使用多线程加速
- 先过滤明显无效的解(如非ASCII字符)
-
错误处理:
- 解密结果可能包含非可打印字符,使用try-except避免程序中断
- 对中间结果进行校验,避免无效解影响后续步骤
这道题目综合考察了逆向工程中的多种技能,包括加密算法分析、动态调试、SMC处理和爆破技术。在实际CTF比赛中,这类题目通常出现在中等难度区间,掌握这些技巧对提升逆向能力很有帮助。