栈溢出
目标
学会使用淹没相邻变量或返回地址的方法利用缓冲区溢出漏洞。
测试步骤与结果
本次实验的源码如下所示
1 |
|
验证缓冲区溢出的发生
- 正常情况下,输入正确密码,程序提示 “输入正确”;输入错误密码(少于 8 位),程序提示 “输入错误”。
- 进入 ollydbg,在函数调用处打断点。开始运行程序,输入一个较短的密码,就以
"444"
为例。 strcpy
的调用点在0x401055
处。- 输入的密码存储在
0x12fb7c
处,要拷贝到0x12fb18
处。 0x12fb18
处的数据如下,显示为00 34 34 34
,恰好是"4"
的 ascii 码十六进制表示
淹没相邻变量改变程序流程
淹没返回地址改变程序流程
为了方便调试,我们使用文件来输入 “密码”。
1 | if(!fp=fopen("password.txt","rw+")) |
- 找到输入点。这个地址跳转到 “验证成功” 的输出上。
- 先填满 8 字节的
buffer
数组,4 字节的authenticated
变量,4 字节的 ebp,接下来的 4 个字节我们就可以放上我们的返回地址。 - 运行程序,显示 “验证成功”,但由于直接跳转地址破坏了栈平衡,程序崩溃。 此时的栈结构如此图所示
测试结论
覆盖的过程中发生了什么?我认为可以用以下几张图来表示
思考题
程序的源码如下
1 | /* |
本次的目标是通过栈溢出,想办法使 bar
函数得到执行。
-
程序通过命令行参数执行了
foo
函数,在foo
函数中,有一个 10 个字节长的buf
字符数组,在第 14 行中发生了未经检查的无界向有界拷贝的行为,很容易引发溢出。分析到这里,思路就很简单了:参数中先用任意字符填满buf
,然后再填入 4 字节的bar
函数的地址。 -
先调试,填上一个较短的参数看看发生了什么
-
发现我们给的这个参数在 DS 段中存储着,而且运行时程序会打印出
foo
和bar
函数的地址值。所以bar
函数地址我们已经有了。 -
我们的命令行参数从 DS 段拷贝到了
0x12ff60
的位置。 -
构造 payload 时要注意考虑到内存的对齐因素,缓冲区是 10 个字节长没错,但是填充时要淹没掉
0x40109b
的返回地址值,又要使最后 4 个字节为bar
函数的地址值,所以要填上 12 个任意字符。综合考虑,构造 payload 如下。
-
把这个值复制到调试命令行中,重启程序
-
此时已经可以看到
0x12ff6c
中存储的返回地址值变成了0x401060
。前面的0x12ff60~0x12ff68
也已悉数占满。
-
按 F8 单步跟下来,可以发现程序紧接着跳到了
0x401060
,bar
函数的地址,目标达到了。紧接着程序就因为栈不平衡而崩溃了。 -
覆盖返回地址法利用全过程图示
接下来,我们使用 jmp esp
的方法来完成程序的破解。
-
使用 od 自带的插件 overflow return address 寻找可用的
jmp esp
地址,查找结果如下 -
本次实验选用了一个位于
ntdll.text
段的jmp esp
,地址为77f8948bh
。根据
jmp esp
构造相关知识,构造 payload 为 12 个填充字符 "a"+jmp esp
地址0x77f8948b
(大端书写)+shellcode(call bar()
的机器码)。下面简单看一下运行该参数之后,栈的情况。
-
foo
和bar
的地址输出等同于方法一 -
将命令行的内容复制到缓冲区时,观察输出,可以发现
0x12ff6c
被覆盖成了jmp esp
的地址 -
在函数
return
之前,注意到它即将返回到一个内核地址77f8948b
-
陷入内核的一瞬间,可以看到
eip
指针所指向的位置上,命令为jmp esp
。观察右侧知esp
现在的值为12ff70h
,接下来,程序到12ff70h
执行内容。 -
跳转到
12ff70
地址后,其中所写入的数据都被当作指令来执行,我们的 shellcode 被成功解析为了call
指令。 -
果然它紧接着调用了
00401060
处的bar
函数。 -
调用完成后,它返回了
12ff70
处,然后继续向下执行指令,直到崩溃。如果没有任何中断的话,接下来地址的所有的 hex 数据都会按照命令来执行,相当危险……
-
刚才执行的过程中成功产生预期输出。
-
刚才的 payload 可以正常在命令行中传参执行,成功触发
bar
函数。 -
jmp esp
法利用全过程图示