越过 __chkesp 检测的缓冲区溢出
本文的起源,來自于在學校BBS上的C++版上,有一個人問了一個問題,然后我給他已解答。這個帖子的原文是這樣的:
?
代碼 發信人: lisanbai (李三白), 板面: C++標 題: 這個怎么一直不停輸出啊,菜鳥求教
發信站: 飄渺水云間 (Mon Sep 20 16:52:30 2010), 轉信
看了半天找不出毛病
char str1[]="go?";
char str2[]="back.";
int i=0;
int j=0;
int count=0;
while(str1[i])
i++;
while(str2[j])
j++;
for(;count<j;count++)
str1[i++]=str2[count];
str1[i]='\0';
printf("\n%s\n",str1);
return 0;
--
※ 來源:·飄渺水云間 zju88.org·[FROM: lisanbai]
?
上面的代碼看起來,是把 str2 的內容附加到 str1 的尾部(完成 strcat 的功能),很顯然,他的錯誤是 str1 的空間不足夠容納 str2,作者之所以犯了這個錯誤,可能是因為他對內存管理不夠熟悉導致的。下面是我對該貼的回復:
?
(1)你的str1恐怕不夠容納str2的內容,換句話說,你寫內存的時候越界了。
(2)我用IDA看了下在函數的棧上的分布,大致情況如下:
低地址:(棧頂部方向)
? -14h j:???????? ....
-10h i:?????? [0][0][0][0]
??? -0Ch str2:? [b][a][c][k][.][0][0][0]
??? -04h str1:? [g][o][?][0]
????? 00h????? :??被保護的寄存器值(如果有的話)
???????????? ebp
?????????????????? ???????????? 返回跳轉地址
高地址:
你附加字符的時候,把函數返回地址那里給覆蓋了。換句話說,這個函數的Stack Frame被你給破壞掉了。
?
(3)假設這些代碼放在 main 函數里,用 IDA 運行這個程序,在函數返回前,可以手工把棧里的被破壞掉的ebp復原,但是返回地址好像沒法復原(沒法直接改棧上的數據),因為返回地址不對(返回地址最低位的字節被改成0)又跳回到 mainCRTStartup 里面的比較靠前的地方去了,正好跳到調用GetVersion的地方。然后過會又執行到調用 main 函數,然后又進入我在 main 函數里設的斷點位置(如果反復手工修改被破壞的ebp,就形成了一個死循環狀態)。
如果不手工復原 ebp ,回到 mainCRTStartup 里面的時候,在會報一個內存不能寫的異常。。。(還好是在自己的進程空間里)因為ebp的原來的值是 mainCRTStartup 里的可能也是用于訪問棧的一個指針,總之在 mainCRTStartup 的開頭的地方有mov ebp, esp。
這里需強調的是,這個代碼是可以通過 __chkesp 的檢測。因為 __chkesp 只檢測ebp的當前值(函數入口點的棧頂地址)和函數釋放棧上空間以后的esp是否一致。這個代碼不會影響到 ebp 當前值(函數入口點的棧頂),也不會沒有破壞 esp 當前值,而是破壞了 ebp 的原值(在上一個函數中的值)和返回地址。因此這個代碼屬于緩沖區溢出,__chkesp 對此情況無法檢測。
?
(4)注:改正方法很簡單,第一行代碼改成例如 char str[32]="go?" 即可。該數字保證大于 str1+str2 的長度即可。
?
=================================================================
盡管這個問題應該說很容易解答,到這里也基本算可以了。不過我在 IDA 調試的時候發現編譯器附加的 __chkesp 函數對這個問題里的代碼是不起作用的,這引起了我的注意。通常人們不可能故意的讓自己的函數產生緩沖區溢出這樣的錯誤(本文的提問者是無意的),常見的比較底層的方法例如使用 FlushInstructionCache 修改函數入口地址來完成一些 hook。但是如果我們自己故意讓我們的代碼產生緩沖區溢出則另當別論了,所以我按照這個代碼的思路,可修改函數返回時跳轉的地址,讓函數返回時進入另一個函數,這也是比較有趣的一個事情。為了不能讓系統覺察到異常,必須再無痕跡跳轉回正確的位置,相當于我們自己hack自己了。
?
下面我提供一個演示的代碼,首先簡單介紹以下原理,這里存在一些沒有保障的假設,例如在進入函數的時候,我們認為函數的棧是這樣的分布:
?
ebp的原值(通常是上一個函數中的棧指針)
函數返回跳轉地址(調用者中的某個地址)
然后我修改函數(NormalFunc)的返回地址,讓他跳轉到另一個函數(Test2),注意這和常規的函數調用不同!如果這個函數有編譯器產生的開場白(prolog),必須手動先添加一個收場白(epilog)去抵消掉開場白的影響(稍后我再介紹這一點)。為了簡單起見,我使用 naked 關鍵字,要求編譯器不要添加開場白和收場白,這樣進入這個函數的時候可以直接去執行我們自己的代碼,執行完用戶代碼以后再跳回正確的地址(調用者 main 的內部)。這樣在系統不知道的情況下,我們就用“神不知鬼不覺的方式”“調用”了另一個函數(Test2)!
?
下面是范例的代碼,使用VC6.0,Console Application:
?
?
code_buf_overflow // BufferOverflow.cpp : Defines the entry point for the console application.//
#include "stdafx.h"
//保存函數返回地址(跳回到main)
unsigned int retAddress;
void Test2();
void NormalFunc()
{
//data[1]: ebp的值;data[2]:函數返回地址
unsigned int data[1] = { 0x0 };
//保存返回地址
retAddress = data[2];
data[2] = (unsigned int)Test2;
return;
}
//naked函數(手工指定prolog 和 epilog)
__declspec(naked) void Test2()
{
printf("Naked: hello world!\n");
//跳回到main函數體中!
__asm
{
jmp [retAddress]
}
}
int main(int argc, char* argv[])
{
NormalFunc();
printf("before exit\n");
return 0;
}
?
這個函數產生下面的輸出,看起來就和調用了 Test2 一樣:
?
Naked: hello world!
before exit
?
在 main 函數里本質上調用的是 NormalFunc 函數, 在這個函數里我修改了它返回時的跳轉地址,同時也把正確的返回時跳轉地址保存到了一個全局變量(retAddress)中。然后這個函數返回時進入了 Test2, 在 Test2 里執行了一些代碼以后,再通過全局變量跳回到 main 中的正確位置,這個過程對編譯器和系統來說是透明的。
?
在 Test2 里我們使用 naked 關鍵字防止編譯器自動產生那些開場白和收場白。如果沒有加這個關鍵字,函數的開頭和結尾會有系統產生的那些開場白和收場白,因為我們并非常規的函數調用,可以理解為我們還位于原來的函數體中,所以在執行我們自己的代碼前,需要手工抵消掉函數的開場白(只需要把編譯器產生的收場白嵌入到函數的用戶代碼前面即可,為此,首先觀察編譯器產生的開場白和收場白(函數主體部分省略):
?
asm_test ;函數的開頭部分.text:00401070 push ebp
.text:00401071 mov ebp, esp
.text:00401073 sub esp, 40h
.text:00401076 push ebx
.text:00401077 push esi
.text:00401078 push edi
.text:00401079 lea edi, [ebp+var_40]
.text:0040107C mov ecx, 10h
.text:00401081 mov eax, 0CCCCCCCCh
.text:00401086 rep stosd??
;函數的結尾部分
.text:0040109B pop edi
.text:0040109C pop esi
.text:0040109D pop ebx
.text:0040109E add esp, 40h
.text:004010A1 cmp ebp, esp
.text:004010A3 call __chkesp
.text:004010A8 mov esp, ebp
.text:004010AA pop ebp
.text:004010AB retn
?
現在我們寫一個普通的函數(不加 naked 關鍵字),我們在函數前面嵌入“收場白”的等效匯編代碼,則非 naked 的 Test2 函數的代碼如下,具體的開場白有可能會依賴編譯器,嵌入的收場白代碼怎樣寫,最好還是用反匯編查看一下再確定(本例使用的是VC6.0):
?
code_test2_normal void Test2(){
//普通函數,我們必須手工抵消函數的開頭
__asm
{
pop edi
pop esi
pop ebx
mov esp, ebp
pop ebp
}
//現在做一些事情
printf("hello world!\n");
//跳回到main函數體中!
__asm
{
jmp [retAddress]
}
}
?
范例中的 Test2 函數很顯然是不能直接調用的,因為全局變量 retAddress 的初值是 0,直接調用會導致進程異常終止。但如果我們先調用 NormalFunc 是全局變量(retAddress)被賦正確的值?,Test2 就可以正常調用了,但是 Test2 返回時是從 NormalFunc 函數調用后面的語句繼續執行的,所以這樣會產生死循環。所以我們可以少許改造下 Test2,讓它最多被調用 5 次以后進程退出(否則因為死循環屏幕將一直輸出字符串)。改造后的代碼可以在屏幕上打印五行字符串內容:
?
code_buf_overflow_2 #include "stdafx.h"#include <stdlib.h>
//保存函數返回地址(跳回到main)
unsigned int retAddress;
void Test2();
void NormalFunc()
{
//data[1]: 可能是
unsigned int data[1] = { 0x0 };
//保存返回地址
retAddress = data[2];
data[2] = (unsigned int)Test2;
return;
}
//naked函數(手工指定prolog 和 epilog)
__declspec(naked) void Test2()
{
static int i;
printf("Naked: hello world!\n");
i++;
if(i == 5)
exit(0);
//跳回到main函數體中!
__asm
{
jmp [retAddress]
}
}
int main(int argc, char* argv[])
{
NormalFunc();
Test2();
printf("before exit\n");
return 0;
}
?
?
--hoodlum1980
--2010-9-20
轉載于:https://www.cnblogs.com/hoodlum1980/archive/2010/09/20/1832048.html
總結
以上是生活随笔為你收集整理的越过 __chkesp 检测的缓冲区溢出的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Java正则表达式匹配、替换HTML内容
- 下一篇: Active Directory相关博客