前几天发了一篇关于一个缓冲区溢出问题的讨论。
原文地址当然是饱受非意。有人说这是撞大运,有人说这是无聊。但是呢,从讨论中,我们发现了更多的问题。学到了更多的知识。 其实许多时候我们有必要“撞大运”,但是在撞大运出问题之后,一定要弄清楚事情的原因。 博友的回复已经充分说明了当时的问题。 但是提出了一个新问题:就是临时变量分配时的空间问题。
比如说有分连续分配了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三者的相对地址和其定义顺序存在差别?