1.原理
函数调用 CALL 指令可拆分为两步操作:
1)、将调用者的下一条指令(EIP)的地址压栈
2)、跳转至将要调用的函数地址中(相对偏移或绝对地址)

那么在执行到子函数首地址位置时,返回地址(即调用函数中调用位置下一条指令的地址)就已经存在于堆栈中了,并且是 ESP 指向地址的值。下面通过栈帧的概念,了解编译器在接下来对堆栈进行的操作。
简言之,栈帧就是利用 EBP(栈帧指针,请注意不是 ESP)寄存器访问栈内部局部变量、参数、函数返回地址等的手段。程序运行中,ESP 寄存器的值随时变化,访问栈中函数的局部变量、参数时,若以 ESP 值为基准编写程序会十分困难,并且也很难使 CPU 引用到正确的地址。
所以,调用某函数时,先要把用作基准点(函数起始地址)的 ESP 值保存到 EBP,并维持在函数内部。这样,无论 ESP 的值如何变化,以 EBP 的值为基准能够安全访问到相关函数的局部变量、参数、返回地址,这就是 EBP 寄存器作为栈帧指针的作用。

在函数体代码的任何位置,EBP 寄存器指向的地址始终存储属于它的调用函数的 EBP 的值,根据这个原理可逐级向调用函数、调用函数的调用函数进行遍历,向上回溯。

这样有什么用呢?在将属于调用函数的 EBP 的值压栈之前,ESP 指向的地址存储的是由 CALL 指令压栈的调用函数中调用位置的下一条指令的地址(原 EIP)。那么根据这个逻辑,可以通过上面回溯的各级 EBP 的值,并根据 EBP+sizeof(ULONG_PTR) 获取到函数调用者函数体中的地址(当前函数的返回地址)。有了每级调用的函数体中的地址,那么获取该函数的详细信息及函数符号就变得容易了。

2.对抗思路
分配内存地址作为基地址的内存空间,并将以当前 ESP 为基地址的一段栈内存片段的数据拷贝到了新分配的内存空间的高内存区域中,修改 ESP 和 EBP 寄存器的值为新缓冲区中对应的两个寄存器指针应该指向的位置,相当于对堆栈片段进行了平移。

平移时首先根据 ESP 和 EBP 寄存器指向的内存地址定位需要拷贝的数据范围。在这里可能会向 EBP 指向的地址上面多拷贝一部分数据,以将参数和返回地址等数据一并拷贝到新分配的缓冲区中。拷贝完成之后,将 ESP 和 EBP 寄存器指向新缓冲区中对应的位置。

这时开始程序对堆栈的操作将会在新分配的内存缓冲区中进行。在 ShellCode 代码执行即将完成时,应会再将 ESP 和 EBP 的值还原回原来真正栈里的地址,避免弹栈时进入上面未知的内存区域导致程序异常。

3.验证

为了验证这个判断是否有效和真实,接下来需要实现上面猜想中描述的操作,看看调试器或检测系统是否能够成功地进行栈回溯。

下面的代码片段实现了分配新的缓冲区,并拷贝从 ESP 指针指向位置到 调用函数的 EBP 在栈中存储位置加上调用函数的返回地址的存储位置这个范围的栈片段,到新分配的缓冲区中最高位置区域,为低内存预留了 0x100000 字节的空间。

void simplesubfunc() {printf("a simple sub function!\n");
}void buildmystack() {ULONG_PTR stackbase, stacklimit;ULONG_PTR p_ebp, pp_ebp = 0, p_esp, delta;ULONG_PTR p_new_esp = 0, pp_delta;PVOID p_new_stack = NULL;__asm pushad;__asm pushfd;__asm push 0;__asm push 0;__asm push 0;__asm push 0;// 获取栈的基本信息__asm mov   eax,        fs:[0x04] ; 取 StackBase 域的值__asm mov   stackbase,  eax       ;__asm mov   ebx,        fs:[0x08] ; 取 StackLimit 域的值__asm mov   stacklimit, ebx       ;__asm mov   p_ebp,      ebp       ;__asm mov   p_esp,      esp       ;stackbase -= 2 * sizeof(ULONG_PTR);delta = p_ebp - p_esp;// 获取调用者的 EBP 在栈中的位置if (p_esp > stacklimit &&p_esp < stackbase  &&p_ebp > stacklimit &&p_ebp < stackbase) {pp_ebp = *(ULONG_PTR *)p_ebp;}// 搭建新的栈空间并移动栈指针if (pp_ebp > stacklimit &&pp_ebp < stackbase) {pp_delta = pp_ebp - p_esp;p_new_stack = malloc(pp_delta + 0x100000 + 2 * sizeof(ULONG_PTR));p_new_esp = (ULONG_PTR)p_new_stack + 0x100000;memcpy((PVOID)p_new_esp, (PVOID)p_esp, pp_delta + 2 * sizeof(ULONG_PTR));__asm mov   eax,   p_new_esp  ;__asm mov   esp,   eax        ;__asm mov   ebx,   eax        ;__asm add   eax,   delta      ; 计算当前 ebp 应指向的位置__asm mov   ebp,   eax        ;__asm add   ebx,   pp_delta   ;__asm mov   [eax], ebx        ; 修正调用者 ebp 在栈中位置}// 执行正式函数体代码simplesubfunc();// 恢复栈指针到原栈中的位置并释放内存if (p_new_stack) {__asm mov   esp,   p_esp      ;__asm mov   ebp,   p_ebp      ;__asm mov   eax,   ebp        ;__asm mov   ebx,   pp_ebp     ;__asm mov   [eax], ebx        ;free(p_new_stack);}__asm pop  eax;__asm pop  eax;__asm pop  eax;__asm pop  eax;__asm popfd;__asm popad;
}void helloworld() {buildmystack();printf("hello world!\n");
}int main(int argc, char* argv[]) {helloworld();return 0;
}

在函数 simplesubfunc() 处下断点,用 windbg 启动执行,命中断点后通过 kv 指令观察调用栈,发现调用序列中已经不能回溯到上级各层的调用了。

(5644.3e20): Break instruction exception - code 80000003 (first chance)
eax=016e40d0 ebx=012fe000 ecx=00000000 edx=000000e4 esi=013b1d40 edi=013b1d40
eip=013b1129 esp=016e3fec ebp=016e4038 iopl=0         nv up ei pl nz na po nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000202
*** WARNING: Unable to verify checksum for HookDemo.exe
HookDemo!simplesubfunc+0x9:
013b1129 cc              int     3
0:000> kv
ChildEBP RetAddr  Args to Child
016e4038 00000000 00000206 013b1d40 013b1d40 HookDemo!simplesubfunc+0x9 (FPO: [Non-Fpo])
0:000> !teb
TEB at 01027000ExceptionList:        012ffdc8StackBase:            01300000StackLimit:           012fe000SubSystemTib:         00000000FiberData:            00001e00ArbitraryUserPointer: 00000000Self:                 01027000EnvironmentPointer:   00000000ClientId:             00005644 . 00003e20RpcHandle:            00000000Tls Storage:          0102702cPEB Address:          01024000LastErrorValue:       0LastStatusValue:      c0000139Count Owned Locks:    0HardErrorMode:        0

对比 TEB 中 StackBase 和 StackLimit 域的值和命中断点时 CPU 寄存器状态中 ESP 和 EBP 指向的值,发现 ESP 和 EBP 已经不在线程栈的范围中了。但是程序的向下执行并没有受到任何影响:

a simple sub function!
hello world!
请按任意键继续. . .

这就说明,这个判断至少到目前为止是正确的。

4.应对

栈回溯时以 TEB 的成员 StackBase 和 StackLimit 的值作为限制范围,而栈顶和栈底指针一开始就不在范围之中,那么栈回溯循环过程会在遍历第一个栈帧时就跳出遍历。

那么可不可以在栈回溯的时候,去掉通过这两个成员的值进行的限制呢?

这样考虑和推测,当然要想到任何一种可能出现的不正常的情况。ShellCode 中构造的新的栈片段中,最上级调用的栈区域可能并未赋给正确的值,包括原 EBP 或原 EIP 的值,比如这两个域在 ShellCode 代码中被临时地给简单地置为 0x00000000 了。那么放开 StackBase 和 StackLimit 的限制而直接地通过调用序列向上回溯,如果未处理好的话,很可能会在检测模块中发生非法访问等异常情况。

那么如果对原 EBP 或原 EIP 判断得好的话,比如对内存地址的有效性进行谨慎的判断,那么放开限制是否就可以了?

根据前面表达过的意思,你不能清楚地知道在 ShellCode 中对原 EBP 或原 EIP 的值改成什么样了,如果是非法的地址还算是比较好判断的。但是如果是正常的属于堆栈地址呢?这里的“正常”的意思是,原 EBP 或原 EIP 的值确实是“原 EBP 或原 EIP 的值”,但不是应该出现在这里的,而是诸如应该出现在下级调用中的“原 EBP 或原 EIP 的值”这样的。如此一来,将会导致无限循环遍历等问题。

要是样本的 ShellCode 更进一步,窃取其他线程的堆栈部分数据覆盖到自己构造的堆栈的高内存部分,那么在调试器或检测系统在栈回溯时,遍历到上层的调用项,被诱导进入另一个线程的调用栈序列中,那么获取到的数据就可能已经不是当前线程的数据了。

5.说明
本文中的代码片段在任意版本的 Visual Studio 或 Visual C++ 中均可编译通过,感兴趣的可自行测试。未贴出完整代码内容,需自行补充头文件包含等。另外上面部分代码在编译的时候会报出 warning C4731 的警告,提示栈帧指针寄存器 ebp 被内联汇编代码修改。直接无视即可。

x86对抗栈回溯检测相关推荐

  1. 以SIGSEGV为例详解信号处理(与栈回溯)

    以SIGSEGV为例详解信号处理(与栈回溯) 信号是内核提供的向用户态进程发送信息的机制, 常见的有使用SIGUSR1唤醒用户进程执行子程序或发生段错误时使用SIGSEGV保存用户错误现场. 本文以S ...

  2. Unwind 栈回溯详解:libunwind

    目录 1. 历史背景 1.1 frame pointers 1.2 .debug_frame (DWARF) 1.3 .eh_frame (LSB) 1.4 CFI directives 2. .de ...

  3. 项目经验之谈--驱动崩溃分析之栈回溯技术与反汇编

    1.序言 驱动往往是芯片厂商提供的,而且是不开源的. 一旦崩溃很难查找原因,当然办法是有的,比如内核为此也提供栈回溯技术(低版本的好像没有实现)来定位分析驱动问题.再不济也可以反汇编ko文件. 2.栈 ...

  4. Unwind 栈回溯详解

    文章目录 1. 历史背景 1.1 frame pointers 1.2 .debug_frame (DWARF) 1.3 .eh_frame (LSB) 1.4 CFI directives 2. . ...

  5. 武汉大学提出ARGAN:注意力循环生成对抗模型用于检测、去除图像阴影 | ICCV 2019...

    作者 | 王红成 出品|AI科技大本营(ID:rgznai100) [导读]如何去除一张图像中的阴影部分?在ICCV 2019会上,武汉大学的一篇论文针对这一问题提出了一种用于阴影检测和去除的注意循环 ...

  6. 武汉大学提出ARGAN:注意力循环生成对抗模型用于检测、去除图像阴影 | ICCV 2019

    [导读]如何去除一张图像中的阴影部分?在ICCV 2019会上,武汉大学的一篇论文针对这一问题提出了一种用于阴影检测和去除的注意循环生成对抗网络--ARGAN.论文中通过生成一张更加准确的注意力图,用 ...

  7. Linux内核出错的栈打印详解,linux内核中打印栈回溯信息 - dump_stack()函数分析

    简介 当内核出现比较严重的错误时,例如发生Oops错误或者内核认为系统运行状态异常,内核就会打印出当前进程的栈回溯信息,其中包含当前执行代码的位置以及相邻的指令.产生错误的原因.关键寄存器的值以及函数 ...

  8. 深入浅出根据函数调用过程谈栈回溯原理

                通过分析函数调用过程的堆栈变化,可以看出在被调函数的EBP寄存器地址存放的是调用函数的EBP寄存器地址,EBP地址+4存放的是函数调用完成后的下一条指令存放地址,该指令的前一条 ...

  9. 栈回溯技术arm_v5t_le版

    栈回溯技术arm_v5t_le版 From:韦东山 2007.04.03栈回溯技术及uClibc的堆实现原理.doc 1.    前言 段错误.非法地址访问等问题导致程序崩溃的现象屡屡发生,如果能找到 ...

最新文章

  1. SAP ABAP 因系统维护使ABAP语法不再被支持导致使用很久的程序报错问题之分析
  2. IE6/7/8/9中Table/Select的innerHTML不能赋值
  3. Holt-Winters模型原理分析
  4. python爬虫入门代码-Python爬虫入门(一) 网络爬虫之规则
  5. Python中的sorted函数以及operator.itemgetter函数
  6. 订单不断,我是这样做的
  7. er图用什么软件_工艺流程图用什么软件做?规范实用的流程图工具
  8. 装饰器Decorator(函数的装饰)
  9. matlab实心黑点怎么活,matlab画实心圆点
  10. matlab二阶系统曲线,基于matlab的二阶系统的阶跃响应曲线分析
  11. windows系统上PrtSc,ScroLk,Pause等三个功能按钮的作用
  12. 如何将CAD格式转成可以编辑的矢量图
  13. FPGA工程师面试试题集锦41~50
  14. 6种继承的优点和缺点
  15. take off用法
  16. 基于Java的微小企业人事管理系统的设计与实现 毕业设计-附源码231012
  17. 攻防世界逆向高手题之reverse-for-the-holy-grail-350
  18. 从苏宁易购2020双十一战报窥探各品牌座次排行
  19. INFO zkclient.ZkEventThread - Starting ZkClient
  20. 如何安装第三方站点下载的Matlab工具箱或硬件支持包,亲测有效。

热门文章

  1. Word中页眉横线死活删除不掉?按下这个键,立马帮你去掉
  2. selenium3测试126邮箱登录日志
  3. 还有人不知道,加盟连锁店该怎么做?酒店加盟连锁店如何招商?
  4. 按键精灵定义数组和遍历数组的方法
  5. php利用ob_start()清除输出和选择性输出
  6. 【优化算法】蚁狮优化算法(ALO)【含Matlab源码 1307期】
  7. 融合莱维飞行与黄金正弦的蚁狮优化算法-附代码
  8. LoLSkip2.0使用说明(英雄联盟换肤全皮肤版本)
  9. ArcMap在地图上绘画点要素以及画线要素详细步骤
  10. 用android studio写一个简单的记单词?????App