[网安实践III] 实验5.补丁
冷补丁
该作业主要考察同学们对二进制文件补丁修补方法及栈溢出漏洞的理解与掌握. 所用的实验环境和工具为:
ubuntu 18.04 LTS
gcc 7.5.0
IDA Pro 7.0
Keypatch v2.2
程序的功能是实现一个非常简单的用户交互: 要求同学们输入自己的学号, 若输入的学号为10个字符, 则在屏幕上打印一段感谢和表扬的话. 源代码中共设计了一个逻辑缺陷和一个栈溢出漏洞, 要求同学们在没有程序源代码的情况下对二进制文件进行修补.
程序的逻辑缺陷是当输入的学号为10个字符时, 重复打印了对男孩和女孩进行感谢和表扬的话, 正常情况下只需打印其中一句即可. 修补的方法比较简单, 直接用IDA中的Keypatch插件将其中一条打印语句改为空指令即可.
1 修改逻辑缺陷
程序的功能是实现一个非常简单的用户交互: 要求同学们输入自己的学号, 若输入的学号为 10 个字符, 则在屏幕上打印一段感谢和表扬的话. 源代码中共设计了一个逻辑缺陷和一个栈溢出漏洞, 要求同学们在没有程序源代码的情况下对二进制文件进行修补.
程序的逻辑缺陷是当输入的学号为10个字符时, 重复打印了对男孩和女孩进行感谢和表扬的话, 正常情况下只需打印其中一句即可. 修补的方法比较简单, 直接用 IDA 中的 Keypatch 插件将其中一条打印语句改为空指令即可.
- 修改前运行截图, 可以看到会同时输出两句话
- 修改过程截图
修改前程序汇编代码:
- 将其中一个
call _puts
指令替换为空指令. 修改后程序汇编代码:
- 最后使用 Edit->Patch program->Apply patches to input file… 将修改保存到原二进制文件中. 运行结果如图, 仅显示其中一条语句:
2 修改栈溢出函数
gets()
函数是一个非常典型的栈溢出函数, 常用的修补方法是改用 read()
函数来实现 gets()
函数的功能. 由于原始的程序中没有调用 read()
函数, 因此只能利用 .eh_frame
段, 通过 3 号 syscall 调用 read()
函数来实现补丁修补.
注: .eh_frame
段是 gcc 生成的用于异常处理的代码, 它生成一个用于描述如何展开函数调用栈的表. (Ref: .eh_frame的一些资料 - CSDN)
- 修改前运行截图, 可以看到当输入字符超过 10 个时会使程序崩溃
- 修改前程序汇编代码:
- 在
.eh_frame
段添加调用read()
的代码. 使用 IDA Pro 通过 Edit->Patch Program->Change byte… 修改.eh_frame
中的机器码, 机器码如下:
BA 0A 00 00 00 48 8D 75 F5 48 C7 C7 00 00 00 00 0F 05 E9 6A FE FF FF
修改后汇编代码如图:
注: 按 C
键可以进行数据段到代码段的表示形式的切换.
4. 在 main()
函数中将 gets()
函数替换为调用 read()
函数的代码. 方法与上述相同, 修改 main
中的 call _gets
指令为 jmp loc_880
, 机器码如下:
E9 7F 01 00 00
修改后汇编代码如图:
5. 最后使用 Edit->Patch program->Apply patches to input file… 将修改保存到原二进制文件中. 最后运行程序, 如图, 同样输入超过 10 个字符, 但程序并不会崩溃, 不会引发栈溢出漏洞.
热补丁
- 原始程序的功能包括一个简单的用户交互: 要求同学们输入自己的学号用于进行一些运算, 运算完成之后, 程序sleep若干秒, 之后退出. 原始程序的执行效果如下图所示:
- 查看程序的源代码, 如图. 由于使用了不安全的
gets()
函数处理输入数据, 且缓冲区大小仅 11 个字节, 因此存在栈溢出可能性. 需要进行修补.
- 为修补
gets()
函数栈溢出漏洞, 可以使用安全的fgets()
函数替换gets()
函数, 编写的补丁代码如下(假设学号为 10 位, 另含一个结束符\0
)
- 为了修改程序的功能, 定义了新的
sleep()
函数, 代码如下:
- 对修改后的代码进行编译, 生成
.so
链接库文件. 代码如下:
$ gcc -fPIC --shared patch.c -o patch.so
- 利用 PRELOAD HOOK 进行补丁修补, 使用命令:
$ LD_PRELOAD=./patch.so ./original
新的程序执行效果如图: