本文的起源,来自于在学校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]=' '; printf("n%sn",str1); return 0;--※ 来源:·飘渺水云间 zju88.org·[FROM: lisanbai]
上面的代码看起来,是把 str2 的内容附加到 str1 的尾部(完成 strcat 的功能),很显然,他的错误是 str1 的空间不足够容纳 str2,作者之所以犯了这个错误,可能是因为他对内存管理不够熟悉导致的。下面是我对该贴的回复:
(1)你的str1恐怕不够容纳str2的内容,换句话说,你写内存的时候越界了。
(2)我用IDA看了下在函数的栈上的分布,大致情况如下:
低地址:(栈顶部方向)
-14hj: ....
-10hi: [0][0][0][0]
-0Chstr2: [b][a][c][k][.][0][0][0]
-04hstr1: [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 exitn"); 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 exitn"); return 0;}
--hoodlum1980
--2010-9-20
原文链接: https://www.cnblogs.com/hoodlum1980/archive/2010/09/20/1832048.html
欢迎关注
微信关注下方公众号,第一时间获取干货硬货;公众号内回复【pdf】免费获取数百本计算机经典书籍
原创文章受到原创版权保护。转载请注明出处:https://www.ccppcoding.com/archives/15235
非原创文章文中已经注明原地址,如有侵权,联系删除
关注公众号【高性能架构探索】,第一时间获取最新文章
转载文章受原作者版权保护。转载请注明原作者出处!