4.3 修改函数返回地址
4.3.1 返回地址与程序流程
上节实验介绍的改写邻接变量的方法是很有用的,但这种漏洞利用对代码环境的要求相对比较苛刻。更通用、更强大的攻击通过缓冲区溢出改写的目标往往不是某一个变量,而是瞄准栈帧最下方的EBP和函数返回地址等栈帧状态值。
回顾上节实验中输入7个‘q’程序正常运行时的栈状态,如表4-3-1所示。
表4-3-1 栈帧数据
|
局部变量名 |
内 存 地 址 |
偏移3处的值 |
偏移2处的值 |
偏移1处的值 |
偏移0处的值 |
|
buffer |
0x0012FB18 |
0x71 (‘q’) |
0x71 (‘q’) |
0x71 (‘q’) |
0x71 (‘q’) |
|
|
0x0012FB1C |
NULL |
0x71 (‘q’) |
0x71 (‘q’) |
0x71 (‘q’) |
|
authenticated |
0x0012FB20 |
0x00 |
0x00 |
0x00 |
0x01 |
|
前栈帧EBP |
0x0012FB24 |
0x00 |
0x12 |
0xFF |
0x80 |
|
返回地址 |
0x0012FB28 |
0x00 |
0x40 |
0x10 |
0xEB |
如果继续增加输入的字符,那么超出buffer[8]边界的字符将依次淹没authenticated、前栈帧EBP、返回地址。也就是说,控制好字符串的长度就可以让字符串中相应位置字符的ASCII码覆盖掉这些栈帧状态值。
按照上面对栈帧的分析,不难得出下面的结论。
(1)输入11个‘q’,第9~11个字符连同NULL结束符将authenticated冲刷为0x00717171。
(2)输入15个‘q’,第9~12个字符将authenticated冲刷为0x71717171;第13~15个字符连同NULL结束符将前栈帧EBP冲刷为0x00717171。
(3)输入19个‘q’,第9~12个字符将authenticated冲刷为0x71717171;第13~16个字将前栈帧EBP冲刷为0x71717171;第17~19个字符连同NULL结束符将返回地址冲刷为0x00717171。
这里用19个字符作为输入,看看淹没返回地址会对程序产生什么影响。出于双字对齐的目的,我们输入的字符串按照“4321”为一个单元进行组织,最后输入的字符串为“4321432143214321432”,运行情况如图4.3.1所示。
|
| 图4.3.1 栈溢出导致程序崩溃 |
用OllyDbg加载程序,在字符串拷贝函数调用结束后观察栈状态,如图4.3.2所示。
|
| 图4.3.2 溢出前栈中的布局 |
实际的内存状况和我们分析的结论一致,此时的栈状态如表4-3-2所示。
表4-3-2 栈帧数据
|
局部变量名 |
内 存 地 址 |
偏移3处的值 |
偏移2字节 |
偏移1字节 |
偏移0字节 |
|
buffer[0~3] |
0x0012FB18 |
0x31 (‘1’) |
0x32 (‘2’) |
0x33 (‘3’) |
0x34 (‘4’) |
|
buffer[4~7] |
0x0012FB18 |
0x31 (‘1’) |
0x32 (‘2’) |
0x33 (‘3’) |
0x34 (‘4’) |
|
authenticated(被覆盖前) |
0x0012FB20 |
0x00 |
0x00 |
0x00 |
0x01 |
|
authenticated(被覆盖后) |
0x0012FB20 |
0x31 (‘1’) |
0x32 (‘2’) |
0x33 (‘3’) |
0x34 (‘4’) |
|
前栈帧EBP(被覆盖前) |
0x0012FB24 |
0x00 |
0x12 |
0xFF |
0x80 |
|
前栈帧EBP(被覆盖后) |
0x0012FB24 |
0x31 (‘1’) |
0x32 (‘2’) |
0x33 (‘3’) |
0x34 (‘4’) |
|
返回地址(被覆盖前) |
0x0012FB28 |
0x00 |
0x40 |
0x10 |
0xEB |
|
返回地址(被覆盖后) |
0x0012FB28 |
0x00(NULL) |
0x32 (‘2’) |
0x33 (‘3’) |
0x34 (‘4’) |
前面已经说过,返回地址用于在当前函数返回时重定向程序的代码。在函数返回的 “retn” 指令执行时,栈顶元素恰好是这个返回地址。“retn”指令会把这个返回地址弹入EIP寄存器,之后跳转到这个地址去执行。
在这个例子中,返回地址本来是0x004010EB,对应的是main函数代码区的指令,如图4.3.3所示。
|
| 图4.3.3 正常情况下函数返回后的指令 |
现在我们已经把这个地址用字符的ASCII码覆盖成了0x00333231,函数返回时的状态如图4.3.4所示。
|
| 图4.3.4 溢出后程序返回到无效地址0x00323334 |
我们可以从调试器中的显示看出计算机中发生的事件。
(1)函数返回时将返回地址装入EIP寄存器。
(2)处理器按照EIP寄存器的地址0x00323334取指。
(3)内存0x00323334处并没有合法的指令,处理器不知道该如何处理,报错。
由于0x00323334是一个无效的指令地址,所以处理器在取指的时候发生了错误使程序崩溃。但如果这里我们给出一个有效的指令地址,就可以让处理器跳转到任意指令区去执行(比如直接跳转到程序验证通过的部分),也就是说,我们可以通过淹没返回地址而控制程序的执行流程。以上就是通过淹没栈帧状态值控制程序流程的原理,也是本节实验要做的事。