| 
注册时间2010-4-1
阅读权限30
最后登录1970-1-1UID66114 龙战于野 
 
 TA的每日心情|  | 慵懒 2019-3-12 17:25
 | 
|---|
 签到天数: 3 天 [LV.2]偶尔看看I | 
 
| 本帖最后由 whypro 于 2010-6-3 07:21 编辑 
 标准栈回溯要求回溯中的每个函数都以如下指令作为开头(当然不是说不这样开头就不能回溯,那样就得特殊处理了):
 push ebp
 mov ebp,esp
 接下来的工作通常是为临时变量开辟空间
 sub esp,0x40
 ...
 
 在函数结束时,会还原ebp和esp寄存器的值,即
 mov esp,ebp
 pop ebp
 retn 0xC
 不过有时候你看不到这两条指令,取而代之的是leave指令,两者是等效的
 
 下面是实验用的代码:
 // ShowCallStack.cpp : Defines the entry point for the console application.
 //
 
 #include "stdafx.h"
 #include <windows.h>
 
 ULONG FunA(ULONG para1,ULONG para2);
 ULONG FunB(ULONG para1,ULONG para2,ULONG para3);
 ULONG FunC(ULONG para1,ULONG para2);
 
 int main(int argc, char* argv[])
 {
 FunA(0xA0000001,0xA0000002);
 printf("Finish!\n");
 return 0;
 }
 
 ULONG FunA(ULONG para1,ULONG para2)
 {
 printf("FunA Called!\n");
 return FunB(0xB0000001,0xB0000002,0xB0000003);
 }
 
 ULONG FunB(ULONG para1,ULONG para2,ULONG para3)
 {
 printf("FunB Called!\n");
 ULONG tmp=FunC(0xC0000001,0xC0000002);
 printf("After FunB Called!\n");
 return tmp;
 }
 
 ULONG FunC(ULONG para1,ULONG para2)
 {
 ULONG *pEBP;
 char **mainargv;
 ULONG *addr;
 printf("FunC Called!\n");
 _asm
 {
 mov pEBP,ebp
 }
 //调用来自FunB,改变返回地址
 addr=pEBP+1;//此时addr指针里面放的就是返回地址,保存一下,等下拿到父函数的返回地址就放这里了
 printf("Call returned to 0x%08X ,From 0x%08X\n",pEBP[1],pEBP[1]-5);//ebp+4
 printf("Argv1=0x%08X\tArgv2=0x%08X\n",pEBP[2],pEBP[3]);
 printf("=======================================\n");
 //向上回溯一层
 pEBP=(ULONG*)(*pEBP);//FunB
 *addr=pEBP[1];//pEBP[1]是父函数FunB的返回地址,以它替换FunC的返回地址
 printf("Call returned to 0x%08X ,From 0x%08X\n",pEBP[1],pEBP[1]-5);//ebp+4
 printf("Argv1=0x%08X\tArgv2=0x%08X\tArgv3=0x%08X\n",pEBP[2],pEBP[3],pEBP[4]);
 printf("=======================================\n");
 pEBP=(ULONG*)(*pEBP);//FunA
 printf("Call returned to 0x%08X ,From 0x%08X\n",pEBP[1],pEBP[1]-5);//ebp+4
 printf("Argv1=0x%08X\tArgv2=0x%08X\n",pEBP[2],pEBP[3]);
 printf("=======================================\n");
 pEBP=(ULONG*)(*pEBP);//main
 printf("Call returned to 0x%08X ,From 0x%08X\n",pEBP[1],pEBP[1]-5);//ebp+4
 printf("Argv1=0x%08X\tArgv2=0x%08X\n",pEBP[2],pEBP[3]);
 mainargv=(char**)pEBP[3];
 for (ULONG i=0;i<pEBP[2];i++)
 {
 printf("%s\n",mainargv);
 }
 printf("=======================================\n");
 return 0xCCCCCCCC;
 }
 
 我们看一下函数调用的过程,以代码中的FunB调用FunC为例,调用时代码如下:
 
 push C0000002
 push C0000001
 call FunC
 
 在FunC内部,
 push ebp
 mov ebp,esp
 sub esp,50
 
 把这个过程稍稍变变形,上面指令告诉我们:
 push C0000002
 push C0000001
 push eip+5 //返回地址
 jmp FunC
 push ebp 此时[esp]=ebp
 mov ebp, esp //把此时的栈顶指针赋值给了ebp,此时ebp=esp,因此[ebp]=ebp,当然后一个ebp是父函数的ebp了
 
 此时栈的布局是这样的:
 
   
 
 栈最上面的内容就是ebp,这个ebp与FunB中的ebp是相等的,因为控制从FunB转到FunC的中间并没有改变它
 
 那我们可以很清楚的知道:
 [ebp+0x0]是父函数的ebp
 [ebp+0x4]就是返回地址
 [ebp+0x8]是第一个参数
 [ebp+0xC]是第二个参数
 [ebp+0x10]是第三个参数(如果有的话)
 
 当前ebp的内容,是上一个ebp的位置(这是由push ebp;mov ebp,esp两句所决定的了),因为调用FunC时,ebp是不动的,一直到FunC内部mov ebp,esp时才被改变
 
 当函数嵌套调用时,ebp就成了一条链。如下;
 
 FunA
 {
 save ebp in main
 
 FunB()
 {
 save ebp in FunA
 
 FunC()
 {
 save ebp in FunB
 }
 }
 
 }
 
 所以,能过当前函数的ebp处,存放的是其父函数的ebp,父函数的ebp处,那就是爷爷辈的了~~
 我来串联张图:
 
   
 从当前函数的ebp出发,可以得到当前函数的返回地址(即调用者地址+5),参数等信息
 进一步回溯,可以从得到的父函数的ebp得到父函数的返回地址和参数;
 我们感兴趣的也就是返回地址和参数,如果你仅对返回地址感兴趣的话,推荐一个函数:
 ULONG //有效的返回地址数
 RtlWalkFrameChain (
 OUT PVOID *Callers, //一个数组,存放回溯到的返回地址
 IN ULONG Count,     //最多回溯几层
 IN ULONG Flags //回溯标志,用户态下应置0
 )
 很容易就到返回地址,其内部原理是一样,只是包装了一下而已
 ntdll.dll和ntoskrnl.exe都导出了此函数,只是内核中Flags若为1,表示在内核状态下回溯用户态栈。如果你有兴趣,当然还可以继续回溯,直至ebp中的内容为0,因为一个线程的CONTEXT最初被初始化时,其ebp的值被置0.
 这样程序中的参数和调用地址就一层层的被回溯出来。
 如果你用调试器进入FunC内部,然后查看此时的调用栈,会发现跟这个输出结果是一样的~~
 栈回溯至此基本清楚了!
 这时再看MJ0011的《基于CallStack的Anti-Rootkit HOOK检测思路》和gzzy的《基于栈指纹检测缓冲区溢出的一点思路》应该就比较轻松了。
 
 关于栈回溯的应用,我举一个小小的例子:
 用回溯做一点点“非法”的事情:篡改返回地址!
 FunC本应该返回到FunB,现在我们让它直接返回到FunA!
 首先,得到本函数的ebp,ebp+4是当前函数返回地址存放的位置,也就是要修改的位置,保存此指针
 向上回溯一层,得到FunB的返回地址,这个地址是在FunA中,将它赋值给刚才保存的指针,以它来替代当前FunC的返回地址。
 重新编译修改后的程序,运行一下
 现在你会发现FunB中那句"After FunB Called!"打印不出来了,因为我们从FunC直接返回到了FunA~~~
 
 只是一个小小例子,具体应用嘛,就随便发挥想像了,我举几个例子:
 比如我的一个程序中,hook了某函数XXX中的第一个call,然后通过栈回溯获取相关参数进行判断来决定是否修改返回值,而返回时直接返回到了XXX的调用者。
 记得以前卡巴检测缓冲区溢出时,是Hook了LoadLibraryExA(W)和GetProcAddress以检测返回地址是否在栈中(TEB的NT_TIB结构中的当前线程栈的基址和大小),那么retn to lib就可以简单绕过了。
 怎么return to lib 呢?
 具体作法如下:
 在系统dll中找一处retn,机器码为0xC3,记下它的位置,这将作为返回地址
 以模拟call的方式调用LoadLibraryExA
 push retaddr //这里是真实的返回地址
 push 0
 push 0
 push 00403000 "kernel32.dll"
 push 7C81756A //这是前面找到的retn的位置,在kernel32.dll中,即(char*)7C81756A的值为0xC3,作为伪造的返回地址
 jmp 7C801D4F //这是LoadLibraryExA的地址
 这样,返回时将返回到7C81756A,而栈顶是retaddr,7C81756A处又是我们找好的retn指令
 再retn一次,我们就成功回到了retaddr处~~~
 当然对付这种方式的检测方式已经有了,但是效果不怎么好~retn可以找,也可以自个儿找个空地儿写点东西进去来实现~
 但是这样卡巴就检测不出来了,因为7C81756A这个地址确实不在栈中~~~
 其它的,RKU貌似Hook了ExAllocPool及其它部分函数并记录返回地址,以检测那些把自己代码扔在NopPagedPool中跑起来就退出的RK.
 
 
  ShowCallStack.rar
(783 Bytes, 下载次数: 0) | 
 |