• 【旧文章搬运】简单栈回溯及应用


    原文发表于百度空间,2008-12-18
    ==========================================================================

    标准栈回溯要求回溯中的每个函数都以如下指令作为开头(当然不是说不这样开头就不能回溯,那样就得特殊处理了):
    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!
    ");
    return 0;
    }
    
    ULONG FunA(ULONG para1,ULONG para2)
    {
    printf("FunA Called!
    ");
    return FunB(0xB0000001,0xB0000002,0xB0000003);
    }
    
    ULONG FunB(ULONG para1,ULONG para2,ULONG para3)
    {
    printf("FunB Called!
    ");
    ULONG tmp=FunC(0xC0000001,0xC0000002);
    printf("After FunB Called!
    ");
    return tmp;
    }
    
    ULONG FunC(ULONG para1,ULONG para2)
    {
    ULONG *pEBP;
    char **mainargv;
    ULONG *addr;
    printf("FunC Called!
    ");
    _asm 
    {
       mov pEBP,ebp
    }
    //调用来自FunB,改变返回地址
    addr=pEBP+1;//此时addr指针里面放的就是返回地址,保存一下,等下拿到父函数的返回地址就放这里了
    printf("Call returned to 0x%08X ,From 0x%08X
    ",pEBP[1],pEBP[1]-5);//ebp+4
    printf("Argv1=0x%08X	Argv2=0x%08X
    ",pEBP[2],pEBP[3]);
    printf("=======================================
    ");
    //向上回溯一层
    pEBP=(ULONG*)(*pEBP);//FunB
    *addr=pEBP[1];//pEBP[1]是父函数FunB的返回地址,以它替换FunC的返回地址
    printf("Call returned to 0x%08X ,From 0x%08X
    ",pEBP[1],pEBP[1]-5);//ebp+4
    printf("Argv1=0x%08X	Argv2=0x%08X	Argv3=0x%08X
    ",pEBP[2],pEBP[3],pEBP[4]);
    printf("=======================================
    ");
    pEBP=(ULONG*)(*pEBP);//FunA
    printf("Call returned to 0x%08X ,From 0x%08X
    ",pEBP[1],pEBP[1]-5);//ebp+4
    printf("Argv1=0x%08X	Argv2=0x%08X
    ",pEBP[2],pEBP[3]);
    printf("=======================================
    ");
    pEBP=(ULONG*)(*pEBP);//main
    printf("Call returned to 0x%08X ,From 0x%08X
    ",pEBP[1],pEBP[1]-5);//ebp+4
    printf("Argv1=0x%08X	Argv2=0x%08X
    ",pEBP[2],pEBP[3]);
    mainargv=(char**)pEBP[3];
    for (ULONG i=0;i<pEBP[2];i++)
    {
       printf("%s
    ",mainargv[i]);
    }
    printf("=======================================
    ");
    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.
    这在老V的blog里“技巧”分类中有一篇关于这个的文章,有兴趣的自己看看~~
    说的差不多了,感觉我的想法还是很局限哦~~

  • 相关阅读:
    SCOI2014 方伯伯的OJ onlinejudge
    【JZOJ4854】【NOIP2016提高A组集训第6场11.3】小澳的坐标系
    【JZOJ4847】【NOIP2016提高A组集训第5场11.2】夕阳
    【JZOJ4846】【NOIP2016提高A组集训第5场11.2】行走
    【JZOJ4845】【NOIP2016提高A组集训第5场11.2】寻找
    【JZOJ4841】【NOIP2016提高A组集训第4场11.1】平衡的子集
    【JZOJ4840】【NOIP2016提高A组集训第4场11.1】小W砍大树
    【JZOJ4835】【GDOI2017模拟10.31】量化交易
    【JZOJ4831】【NOIP2016提高A组集训第3场10.31】方程式
    【JZOJ4832】【NOIP2016提高A组集训第3场10.31】高维宇宙
  • 原文地址:https://www.cnblogs.com/achillis/p/10180885.html
Copyright © 2020-2023  润新知