前幾天發(fā)了一篇關(guān)于一個緩沖區(qū)溢出問題的討論。
原文地址當(dāng)然是飽受非意。有人說這是撞大運,有人說這是無聊。但是呢,從討論中,我們發(fā)現(xiàn)了更多的問題。學(xué)到了更多的知識。 其實許多時候我們有必要“撞大運”,但是在撞大運出問題之后,一定要弄清楚事情的原因。 博友的回復(fù)已經(jīng)充分說明了當(dāng)時的問題。 但是提出了一個新問題:就是臨時變量分配時的空間問題。
比如說有分連續(xù)分配了3個臨時變量,卻發(fā)現(xiàn)這3個臨時變量的址址不是按變量大小連續(xù)。(如兩個INT變量間相差是12,而非預(yù)期的4) 又或者后聲明的變量地址卻跑在了前頭)。
這也形成了許多我提出的討論問題是撞大運的說法。 其實這個問題許多人都試過,能不能運行成功輸出success也要看編譯器版本和編譯器環(huán)境。
關(guān)于變量空間的問題,我想在
這篇文章 中你們能得到滿意的答案。
并且,同樣關(guān)于本文討論的問題,我朋友的一個
博文中也已經(jīng)給出了分析,并且給出了返回地址被覆蓋時,平衡堆棧的措施。
我的目的在于讓大家一起討論,不管這算不算是無聊,我們總會有些收獲。
下面是一些博友的回復(fù),也可以跳轉(zhuǎn)到 原文地址 查看更多
# re: 討論會:高手們都來看看,這個如何解釋。 2010-05-06 13:11 skykrnl
其實原理很簡單,系統(tǒng)調(diào)用 main 函數(shù)的時候先壓入了 返回地址,
現(xiàn)在 p 恰好位于棧中返回地址處,然后你修改成了test函數(shù),main函數(shù)退出后發(fā)現(xiàn)將返回地址是test函數(shù),于是跳過去執(zhí)行啦。
程序崩潰時必然的,你沒有ExitProcess.
# re: 討論會:高手們都來看看,這個如何解釋。 2010-05-06 13:25 打醬油的
這個問題以前試驗過了,但是gcc沒有生成對main的函數(shù)調(diào)用,所以這個效果沒有出來。改一下就可以了:
#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指令所做的事情來看,更涉及到函數(shù)調(diào)用在編譯器以及目標機器指令問題。不過因為這里不存在虛擬機問題,都是x86,也就只針對call和ret而言:
不難想象在main之前的地方有如下代碼:
; 壓參數(shù)
push xxx
push xxx
push xxx
call main
;main
xxx
xxx
ret
首先call的動作主要包括:先壓入返回地址到堆棧上(ebp指向),而c函數(shù)中,函數(shù)負責(zé)堆棧平衡,那么main中清除局部變量,改變ebp后,可以肯定ebp指向的當(dāng)前堆棧中的值就是返回地址。ret指令則是從棧頂取出該地址并執(zhí)行PC寄存器的跳轉(zhuǎn)。
另一方面,函數(shù)調(diào)用時的運行時堆棧問題:首先棧是向下增長的,函數(shù)A調(diào)用函數(shù)B,那么首先壓入?yún)?shù)到棧中,在函數(shù)B中因為局部變量的增長棧繼續(xù)向下增長,也就是說,最終可以通過ebp的偏移取得函數(shù)A中局部變量的信息。他們貢獻同一個棧:
--stack--
A:local_var1
A:local_var2
A:ret_addr
B:arg_var1
B:arg_var2
B:local_var1
....
基于以上兩個條件,指針a[0]+3,則向高地址偏移了12字節(jié)的地址(3*sizeof(int)),看下main函數(shù)的參數(shù),實際上是3個:argc, argv, env。這樣偏移后,恰好就是調(diào)用main那個函數(shù)在使用call時,壓入的返回地址。
因此,在main返回時,ret彈出的地址已經(jīng)被改變。
ps:
在錯誤地跳轉(zhuǎn)到test后,test執(zhí)行完去ret時,堆棧上提供的返回地址是不定的,崩潰也很正常了。
# re: 討論會:高手們都來看看,這個如何解釋。 2010-05-06 14:03 小時候可靚了
@Kevin Lynx
嗯,分析得很好哦。。但是,我覺得這和main的參數(shù)沒關(guān)系。。偏移到ret_addr就已經(jīng)停下了。還沒經(jīng)過B:arg_var1 B:arg_var2 B:local_var1
# re: 討論會:高手們都來看看,這個如何解釋。 2010-05-06 15:11 飯中淹
1- CALL會把下一個指令的地址放進堆棧。
2- RET就讓這個地址出棧,并跳轉(zhuǎn)至這個地址。
3- 局部變量也是在棧上的。
代碼中,你用局部變量的地址定位到棧內(nèi)的ret返回地址,然后將其修改為TEST的函數(shù)地址。RET后,就跳轉(zhuǎn)到TEST函數(shù)了。因為沒有CALL,所以棧內(nèi)不會壓入返回地址,然后棧就亂掉了,后面依賴棧的指令,就可能會導(dǎo)致出錯。
在一些軟件保護里面,經(jīng)常會用到這種手段,PUSH FUNCPTR, RET。這樣可以用CALL來調(diào)用函數(shù)。從而迷惑分析者。通過ESP寄存器直接操作,更讓分析者頭大。再用一些無效指令插在其中,做成花指令,就更高端了。特別是花連跳,分析者就很難一眼分辨出走向了。
# re: 討論會:高手們都來看看,這個如何解釋。 2010-05-06 15:19 Kevin Lynx
@小時候可靚了
我說的是有點問題。跟參數(shù)沒關(guān)系。參數(shù)先于返回地址壓棧。- - 昏頭了。
話說回來,仔細分析的話,我突然發(fā)覺:
int* p = (int*)&a[0]+3;
這里為什么會是3呢?跟了下匯編,發(fā)覺直接被翻譯為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莫名其妙地出現(xiàn)在棧頂,而不是p,(而在我舉的包含i的例子中,作為出現(xiàn)在最后定義的i卻莫名其妙地出現(xiàn)在棧頂),加上這個push ebp,就出現(xiàn)了3。
誰能給個解釋:為什么a、p、i三者的相對地址和其定義順序存在差別?