前幾天發了一篇關于一個緩沖區溢出問題的討論。
原文地址當然是飽受非意。有人說這是撞大運,有人說這是無聊。但是呢,從討論中,我們發現了更多的問題。學到了更多的知識。 其實許多時候我們有必要“撞大運”,但是在撞大運出問題之后,一定要弄清楚事情的原因。 博友的回復已經充分說明了當時的問題。 但是提出了一個新問題:就是臨時變量分配時的空間問題。
比如說有分連續分配了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三者的相對地址和其定義順序存在差別?