前几天发了一篇关于一个缓冲区溢出问题的讨论。
原文地址当然是饱受非意。有人说这是撞大运,有人说这是无聊。但是呢,从讨论中,我们发现了更多的问题。学到了更多的知识。 其实许多时候我们有必要“撞大运”,但是在撞大运出问题之后,一定要弄清楚事情的原因。 博友的回复已经充分说明了当时的问题。 但是提出了一个新问题:就是临时变量分配时的空间问题。
比如说有分连续分配了3个临时变量,却发现这3个临时变量的址址不是按变量大小连续。(如两个INT变量间相差是12,而非预期的4) 又或者后声明的变量地址却跑在了前头)。
这也形成了许多我提出的讨论问题是撞大运的说法。 其实这个问题许多人都试过,能不能运行成功输出success也要看编译器版本和编译器环境。
关于变量空间的问题,我想在
这篇文章 中你们能得到满意的答案。
并且,同样关于本文讨论的问题,我朋友的一个
博文中也已经给出了分析,并且给出了返回地址被覆盖时,平衡堆栈的措施。
我的目的在于让大家一起讨论,不管这算不算是无聊,我们总会有些收获。
下面是一些博友的回复,也可以跳转到 原文地址 查看更多
# re: 讨论会:高手们都来看看,这个如何解释。 2010-05-06 13:11 skykrnl
其实原理很简单,系统调用 main 函数的时候先压入了 返回地址,
现在 p 恰好位于栈中返回地址处,然后你修改成了test函数,main函数退出后发现将返回地址是test函数,于是跳过去执行啦。
程序崩溃时必然的,你没有ExitProcess.
# re: 讨论会:高手们都来看看,这个如何解释。 2010-05-06 13:25 打酱油的
这个问题以前试验过了,但是gcc没有生成对main的函数调用,所以这个效果没有出来。改一下就可以了:
#include <iostream>
using namespace std;
void test( void )
{
cout << "Success!" << endl;
}
void test2(void)
{
int a[ 1 ];
int* p = (int*)&a[0]+2;
*p = ( int )test;
}
int main( )
{
test2();
return 0;
}
# re: 讨论会:高手们都来看看,这个如何解释。 2010-05-06 13:58 Kevin Lynx
这个可以从call和ret指令所做的事情来看,更涉及到函数调用在编译器以及目标机器指令问题。不过因为这里不存在虚拟机问题,都是x86,也就只针对call和ret而言:
不难想象在main之前的地方有如下代码:
; 压参数
push xxx
push xxx
push xxx
call main
;main
xxx
xxx
ret
首先call的动作主要包括:先压入返回地址到堆栈上(ebp指向),而c函数中,函数负责堆栈平衡,那么main中清除局部变量,改变ebp后,可以肯定ebp指向的当前堆栈中的值就是返回地址。ret指令则是从栈顶取出该地址并执行PC寄存器的跳转。
另一方面,函数调用时的运行时堆栈问题:首先栈是向下增长的,函数A调用函数B,那么首先压入参数到栈中,在函数B中因为局部变量的增长栈继续向下增长,也就是说,最终可以通过ebp的偏移取得函数A中局部变量的信息。他们贡献同一个栈:
--stack--
A:local_var1
A:local_var2
A:ret_addr
B:arg_var1
B:arg_var2
B:local_var1
....
基于以上两个条件,指针a[0]+3,则向高地址偏移了12字节的地址(3*sizeof(int)),看下main函数的参数,实际上是3个:argc, argv, env。这样偏移后,恰好就是调用main那个函数在使用call时,压入的返回地址。
因此,在main返回时,ret弹出的地址已经被改变。
ps:
在错误地跳转到test后,test执行完去ret时,堆栈上提供的返回地址是不定的,崩溃也很正常了。
# re: 讨论会:高手们都来看看,这个如何解释。 2010-05-06 14:03 小时候可靓了
@Kevin Lynx
嗯,分析得很好哦。。但是,我觉得这和main的参数没关系。。偏移到ret_addr就已经停下了。还没经过B:arg_var1 B:arg_var2 B:local_var1
# re: 讨论会:高手们都来看看,这个如何解释。 2010-05-06 15:11 饭中淹
1- CALL会把下一个指令的地址放进堆栈。
2- RET就让这个地址出栈,并跳转至这个地址。
3- 局部变量也是在栈上的。
代码中,你用局部变量的地址定位到栈内的ret返回地址,然后将其修改为TEST的函数地址。RET后,就跳转到TEST函数了。因为没有CALL,所以栈内不会压入返回地址,然后栈就乱掉了,后面依赖栈的指令,就可能会导致出错。
在一些软件保护里面,经常会用到这种手段,PUSH FUNCPTR, RET。这样可以用CALL来调用函数。从而迷惑分析者。通过ESP寄存器直接操作,更让分析者头大。再用一些无效指令插在其中,做成花指令,就更高端了。特别是花连跳,分析者就很难一眼分辨出走向了。
# re: 讨论会:高手们都来看看,这个如何解释。 2010-05-06 15:19 Kevin Lynx
@小时候可靓了
我说的是有点问题。跟参数没关系。参数先于返回地址压栈。- - 昏头了。
话说回来,仔细分析的话,我突然发觉:
int* p = (int*)&a[0]+3;
这里为什么会是3呢?跟了下汇编,发觉直接被翻译为ebp+4了:
push ebp
mov ebp, esp
...
mov eax, [ebp+4]
不是很明白这个地方。
饭老大说得和我一样。
# re: 讨论会:高手们都来看看,这个如何解释。 2010-05-06 16:42 Kevin Lynx
@小时候可靓了
饭给的解释是我在群里跟他谈过的。
这个解释是我在看汇编的时候看到的:
00401750 push ebp
00401751 mov ebp,esp
00401753 sub esp,0Ch
00401756 lea eax,[ebp+4]
00401759 mov dword ptr [p],eax
恰好a莫名其妙地出现在栈顶,而不是p,(而在我举的包含i的例子中,作为出现在最后定义的i却莫名其妙地出现在栈顶),加上这个push ebp,就出现了3。
谁能给个解释:为什么a、p、i三者的相对地址和其定义顺序存在差别?