1. 挑战概述与目标拆解
Pwnable-collision是来自pwnable.kr平台的一道经典二进制题目,考察选手对内存布局、整数溢出和命令行参数传递的理解。这道题的核心在于构造特定的20字节输入,使得程序将其解释为5个4字节整数后,求和结果等于目标哈希值0x21DD09EC(十进制568134124)。
登录目标服务器后,我们能看到三个关键文件:
- col:待破解的可执行程序
- col.c:程序源代码(解题关键)
- flag:需要读取的目标文件
程序的核心验证逻辑非常直接:要求用户输入一个20字节的字符串,程序将其强制转换为int型数组(5个元素),求和后与硬编码的哈希值比较。验证通过则输出flag,否则报错退出。
2. 源码深度解析
2.1 关键数据结构分析
程序定义了一个固定哈希值:
c复制unsigned long hashcode = 0x21DD09EC; // 568134124 in decimal
验证函数将输入字符串强制转换为int指针:
c复制int* ip = (int*)p; // 关键类型转换
这种强制类型转换意味着程序不再将输入视为字符序列,而是直接按照内存中的二进制数据解释为整数。这种操作在安全领域非常危险,也是本漏洞的根源。
2.2 验证流程分解
- 长度检查:首先确保输入长度严格等于20字节(strlen(argv[1]) == 20)
- 类型转换:将char指针强制转换为int指针(假设系统int为4字节)
- 求和计算:将转换后的5个整数相加(res += ip[i])
- 结果比对:检查求和结果是否等于0x21DD09EC
2.3 内存布局关键点
在小端序系统(x86/x64架构)中,多字节数据的低位字节存储在低地址。例如:
- 整数0x12345678在内存中的存储顺序是:0x78 0x56 0x34 0x12
- 输入字符串"\x78\x56\x34\x12"被解释为整数时就是0x12345678
理解这一点对构造有效载荷至关重要。
3. 攻击向量构造方法论
3.1 数学构造思路
我们需要找到5个整数(x1,x2,x3,x4,x5)满足:
- x1 + x2 + x3 + x4 + x5 = 568134124
- 每个xi对应4字节的二进制数据
最简单的构造方法是:
- 令前4个整数相同(x)
- 第5个整数为y = 568134124 - 4x
- 确保y也是有效整数(不考虑符号位)
经过计算,可以取:
- x = 0x01010101(十进制16843009)
- y = 568134124 - 4*16843009 = 568134124 - 67372036 = 500762088(0x1DD905E8)
但需要注意内存布局,实际构造时需要反转字节序。
3.2 有效载荷构造示例
方案一:均匀分布法
bash复制./col "$(python3 -c 'import sys; sys.stdout.buffer.write(b"\x01\x01\x01\x01"*4 + b"\xe8\x05\xd9\x1d")')"
解释:
- 前16字节:4个0x01010101(小端表示为"\x01\x01\x01\x01")
- 后4字节:0x1DD905E8(注意小端序反转)
方案二:非均匀分布
bash复制./col "$(python -c "print('\xe8\x05\xd9\x1d' + '\x01\x01\x01\x01'*4)")"
这种构造将大数放在前面,原理相同但布局不同。
3.3 命令行参数传递的坑
在实际操作中,最大的挑战是如何正确传递包含特殊字符的20字节参数。不同shell对引号的处理方式不同:
-
单引号与双引号的区别:
- 单引号:所有字符保持字面值,不进行任何替换
bash复制echo '$PATH' # 输出$PATH- 双引号:允许变量替换和命令替换
bash复制echo "$PATH" # 输出PATH变量的值 -
Python命令的引号嵌套:
- 外层使用双引号,内层python字符串使用单引号
bash复制"$(python -c 'print("\\x01"*4)')"- 需要特别注意反斜杠的转义
-
字节与字符串的转换:
- Python3严格要求bytes和str区分,建议使用sys.stdout.buffer.write处理二进制数据
- Python2中str就是字节序列,可以直接print
4. 实战操作与排错指南
4.1 完整攻击流程
- 登录目标服务器:
bash复制ssh col@pwnable.kr -p2222 # 密码guest
- 验证文件权限:
bash复制ls -l
# 确认col有执行权限,flag可读但需要破解
- 构造payload并执行:
bash复制./col "$(python -c 'print("\xe8\x05\xd9\x1d" + "\x01\x01\x01\x01"*4)')"
4.2 常见错误与解决方案
问题1:报错"passcode length should be 20 bytes"
- 原因:实际输入长度不等于20字节
- 解决:检查python命令是否正确生成20字节,可用wc验证:
bash复制python -c 'print("\xe8\x05\xd9\x1d" + "\x01\x01\x01\x01"*4)' | wc -c
问题2:输出乱码或错误结果
- 原因:字节序错误或求和计算不对
- 解决:用gdb调试查看内存中的实际整数值:
bash复制gdb ./col
break *check_password+XX # 在求和循环处设断点
x/5xw $esp # 查看栈上的5个整数
问题3:特殊字符被shell解释
- 原因:引号嵌套不当
- 解决:尝试交换单双引号,或使用转义字符:
bash复制./col "$(python -c 'print("\x01\x01\x01\x01"*4 + "\xe8\x05\xd9\x1d")')"
4.3 高级调试技巧
- 使用strace追踪系统调用:
bash复制strace ./col "payload"
观察程序如何处理输入参数
- 检查内存布局:
bash复制gdb ./col
x/20xb argv[1] # 查看输入参数的原始字节
- 验证整数求和:
在gdb中打印求和结果:
bash复制print/x ip[0] + ip[1] + ip[2] + ip[3] + ip[4]
5. 安全启示与防御方案
5.1 漏洞根源分析
这个挑战暴露了几个关键安全问题:
-
不安全的类型转换:
直接将用户输入强制转换为int指针,完全信任输入数据的格式 -
缺乏输入验证:
只检查长度,不验证内容是否合法 -
整数溢出风险:
求和操作可能溢出,但题目中由于目标值较小没有利用
5.2 安全编程建议
-
避免危险的类型转换:
c复制// 不安全 int* ip = (int*)p; // 相对安全的方式 int val; memcpy(&val, p, sizeof(int)); // 明确拷贝指定字节数 -
增加输入验证:
c复制// 检查是否可打印字符 int is_printable(const char* p, size_t len) { for(size_t i=0; i<len; i++) { if(!isprint(p[i])) return 0; } return 1; } -
使用安全的哈希比较:
c复制// 使用恒定时间比较防止时序攻击 int safe_compare(unsigned long a, unsigned long b) { unsigned long diff = a ^ b; return (1 & ((diff - 1) >> 31)) - 1; }
5.3 扩展思考
这个简单的挑战实际上演示了现实世界中缓冲区溢出攻击的基本原理。现代系统虽然有多重防护(ASLR、NX、Stack Canaries等),但不安全的类型转换和缺乏输入验证仍然是许多安全漏洞的根源。
在实际开发中,应当:
- 使用类型安全的语言特性
- 对用户输入保持零信任原则
- 进行严格的边界检查
- 使用安全的库函数替代危险操作
6. 变种挑战与进阶学习
6.1 题目变种思路
-
改变哈希值:
修改目标哈希值,要求构造不同的payload -
增加约束条件:
- 要求某些字节必须可打印
- 限制某些字节的取值范围
-
改变求和方式:
- 使用乘法而非加法
- 引入异或等位运算
6.2 推荐学习资源
-
二进制安全入门:
- 《Hacking: The Art of Exploitation》
- LiveOverflow YouTube频道
-
CTF平台:
- pwnable.kr(更多类似挑战)
- CTFtime.org(比赛汇总)
-
调试工具:
- GDB with pwndbg插件
- radare2逆向框架
6.3 个人实战心得
在解决这类题目时,我总结出几个关键点:
-
先静态分析再动态调试:
先彻底理解源代码,再通过调试验证假设 -
注意字节序和内存对齐:
不同架构的处理方式可能不同 -
构造payload要系统化:
从数学原理出发,而不是盲目尝试 -
记录所有尝试:
保留每次测试的payload和结果,方便回溯
最后需要强调的是,这类技术只应用于合法授权的安全测试和CTF比赛。未经授权的系统攻击是违法行为,与安全研究的初衷背道而驰。