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