1. 逆向新手实战:攻防世界re-for-50-plz-50解题全解析
第一次看到这个题目时,我以为是道送分题——毕竟分值只有50分。但真正开始分析后才发现,这道题完美诠释了"逆向工程中每个字节都值得怀疑"的真理。作为攻防世界Reverse方向的经典入门题,它考察了逆向分析中最基础的技能:阅读汇编代码、理解程序逻辑、识别简单加密算法。下面我就带大家完整走一遍解题流程,顺便分享几个新手容易踩的坑。
1.1 题目初探与环境准备
首先下载题目附件(一个名为re-for-50-plz-50的ELF可执行文件),用file命令查看基本信息:
bash复制$ file re-for-50-plz-50
re-for-50-plz-50: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 2.6.24, BuildID[sha1]=3c1e8b5897d4a0a15760aa2b9ef5e0ff2592c8e3, stripped
关键信息:
- 32位x86架构
- 动态链接
- 去除了符号表(stripped)
建议使用以下工具链:
- 反汇编:IDA Pro Freeware/Ghidra
- 动态调试:GDB配合GEF/PEDA插件
- 辅助工具:radare2、objdump
注意:新手常犯的错误是直接扔进strings找flag。这道题确实能用strings看到部分线索,但正规解法需要分析程序逻辑。
1.2 静态分析:从main函数入手
用IDA加载文件,查看函数列表(由于stripped,需要手动定位main函数)。通过识别libc_start_main的调用约定,可以找到main函数地址0x08048A20。
反编译后的关键代码段:
c复制int __cdecl main(int argc, const char **argv, const char **envp)
{
char v4[32]; // [esp+10h] [ebp-28h]
strcpy(v4, "c61b68366edeb7bdce3c6820314b7498");
puts("Enter the password:");
__isoc99_scanf("%s", v4);
if ( check_password(v4) )
puts("Congratulations!");
else
puts("Wrong password");
return 0;
}
程序逻辑很清晰:
- 初始化一个缓冲区v4(奇怪的是先strcpy再scanf覆盖)
- 提示输入密码
- 调用check_password验证
- 输出结果
1.3 深入check_password函数
跟进到check_password函数(0x080486FD),IDA反编译如下:
c复制_BOOL4 __cdecl check_password(const char *a1)
{
int i; // [esp+10h] [ebp-8h]
for ( i = 0; i < 32; ++i )
{
if ( (a1[i] ^ 0x55) != byte_8048B90[i] )
return 0;
}
return 1;
}
这就是核心验证逻辑:
- 对输入的每个字符与0x55进行异或
- 结果与预设数组byte_8048B90比较
- 全部匹配则返回成功
1.4 关键数据提取
查看byte_8048B90所在的内存区域:
python复制.rodata:08048B90 byte_8048B90 db 24h, 27h, 13h, 0C6h, 0C6h, 13h, 16h, 0E6h, 47h, 0F5h
.rodata:08048B90 db 26h, 96h, 47h, 0F5h, 46h, 27h, 13h, 26h, 26h, 0C6h
.rodata:08048B90 db 56h, 0F5h, 0C3h, 0C3h, 0F5h, 0E3h, 0E3h
这就是我们要比较的目标数组,共32字节。根据算法逻辑,flag就是这些字节分别与0x55异或的结果。
1.5 编写解密脚本
用Python实现解密算法:
python复制encrypted = [
0x24, 0x27, 0x13, 0xC6, 0xC6, 0x13, 0x16, 0xE6,
0x47, 0xF5, 0x26, 0x96, 0x47, 0xF5, 0x46, 0x27,
0x13, 0x26, 0x26, 0xC6, 0x56, 0xF5, 0xC3, 0xC3,
0xF5, 0xE3, 0xE3
]
flag = ''.join([chr(c ^ 0x55) for c in encrypted])
print(flag)
运行结果:
code复制TUCTF{but_really_whoisjohngalt}
1.6 验证flag
将得到的字符串作为密码输入程序:
bash复制$ ./re-for-50-plz-50
Enter the password:
TUCTF{but_really_whoisjohngalt}
Congratulations!
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 技术细节深度解析
2.1 异或加密原理
这道题使用了最简单的单字节异或加密:
- 加密:flag[i] ^ 0x55 → cipher[i]
- 解密:cipher[i] ^ 0x55 → flag[i]
异或的特性:
- 可逆性:A ^ B ^ B = A
- 单字节密钥容易被暴力破解
- 相同密钥加密长文本会保留频率特征
实际CTF中,异或加密常与其他算法组合出现。单独使用时,strings命令可能直接暴露部分原文。
2.2 函数调用约定分析
在32位x86 Linux中,函数调用使用cdecl约定:
- 参数通过栈传递(从右向左压栈)
- 调用者负责清理栈
- 返回值存放在EAX寄存器
观察check_password的汇编:
assembly复制push [ebp+arg_0] ; 压入参数a1
call check_password
add esp, 4 ; 调用者清理栈
test eax, eax ; 检查返回值
2.3 调试技巧:动态验证
使用GDB动态调试验证我们的分析:
bash复制gdb ./re-for-50-plz-50 -q
gef➤ b *0x08048734 # 在check_password返回前断点
gef➤ r
Enter the password: TUCTF{but_really_whoisjohngalt}
gef➤ p $eax
$1 = 0x1 # 返回值为1表示验证通过
3. 常见问题与解决
3.1 字符串提取问题
新手常见错误:
- 直接复制IDA显示的十六进制数组,可能漏掉部分字节
- 错误计算数组长度
正确做法:
- 在IDA中选中整个数组区域
- 使用Edit → Export Data功能
- 选择C语言数组格式
3.2 字节序问题
如果遇到:
- 解密结果部分正确部分乱码
- 可能混淆了大端序/小端序
x86架构是小端序,但某些加密算法会显式处理字节序。本题不涉及此问题。
3.3 算法实现错误
Python脚本常见错误:
- 忘记将十六进制数转换为整数
- 异或运算优先级问题
错误示例:
python复制flag = chr(0x24 ^ '0x55') # 类型不匹配
正确写法:
python复制flag = chr(0x24 ^ 0x55) # 两个整数运算
4. 扩展思考与技巧
4.1 自动化分析技巧
对于简单异或加密,可以使用以下方法快速识别:
- 查找特征指令:xor [reg], imm
- 观察循环长度:单字节加密通常是0x20/0x10等规整长度
- 搜索常见魔数:0x55、0xAA、0xFF等
4.2 防护措施
作为开发者,如何防止这种简单加密被逆向:
- 使用标准加密算法(AES等)
- 组合多种加密方式
- 加入反调试检测
- 关键代码动态生成
4.3 题目变种思路
这道题的几个变种方向:
- 多字节异或(需要找出密钥长度)
- 异或与其他运算结合(如加减、移位)
- 动态密钥(密钥来自某个计算过程)
- 分段不同加密方式
5. 逆向工程学习路径建议
从这道题出发,建议新手的学习路线:
- 掌握基础汇编(x86/ARM架构)
- 熟悉常见加密算法特征
- 练习Linux/Win平台调试技巧
- 学习反编译工具的高级用法
- 参与CTF比赛积累实战经验
推荐工具进阶:
- 高级反编译:Binary Ninja
- 脚本化分析:angr框架
- 动态插桩:Intel Pin/DynamoRIO
- 漏洞分析:GDB配合pwntools
这道re-for-50-plz-50虽然简单,但涵盖了逆向工程的基础要素:静态分析、动态调试、算法识别和脚本编写。建议新手在解题后,尝试修改二进制文件绕过密码验证,这能加深对程序运行机制的理解。
