x86对抗栈回溯检测

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 01027000
    ExceptionList:        012ffdc8
    StackBase:            01300000
    StackLimit:           012fe000
    SubSystemTib:         00000000
    FiberData:            00001e00
    ArbitraryUserPointer: 00000000
    Self:                 01027000
    EnvironmentPointer:   00000000
    ClientId:             00005644 . 00003e20
    RpcHandle:            00000000
    Tls Storage:          0102702c
    PEB Address:          01024000
    LastErrorValue:       0
    LastStatusValue:      c0000139
    Count Owned Locks:    0
    HardErrorMode:        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 被内联汇编代码修改。直接无视即可。

 

 

 

 

 

<think>好的,我现在需要回答用户关于Linux系统中回溯的实现细节和调试技巧的问题。用户提到了引用中的内容,特别是引用4中的内联汇编获取ebp,以及引用2中的/proc/pid/maps信息。首先,我需要理解回溯的基本原理。 回溯(Stack Backtrace)通常用于调试和性能分析,帮助开发者了解函数调用链。在Linux环境下,这通常涉及到处理器的帧结构和调试信息。例如,x86架构使用ebp寄存器指向当前帧的基址,每个帧保存了返回地址和上一个帧的ebp值,这样可以通过遍历ebp链来获取调用链。引用4中提到的内联汇编指令movl %%ebp, %0,正是获取当前ebp的值,这是回溯的关键一步。 接下来,需要了解具体的实现方法。手动实现回溯可能需要编写代码来遍历帧,例如通过循环获取ebp并解析返回地址。此外,调试信息如DWARF格式可以帮助将地址映射到函数名和行号。用户提到的/proc/pid/maps(引用2)可以用来查看进程的内存映射,这可能与解析地址有关,比如确定某个返回地址属于哪个可执行段或共享库。 然后,调试技巧方面,用户可能想知道如何利用工具如GDB的backtrace命令,或者使用libunwind这样的库来简化回溯。另外,动态插桩工具如Valgrind(引用1提到的内存分配跟踪)也可能在调试内存问题时结合回溯使用。 还需要注意不同架构的差异,比如x86和ARM的帧结构可能不同,影响实现方式。此外,优化编译可能会改变帧布局,导致回溯困难,这时需要调试符号的支持。 最后,结合用户提供的引用,特别是引用3提到的本机内存和调试方法,可能需要将回溯与内存分析结合,比如检测溢出或内存泄漏时的调用路径。 总结来说,回答需要涵盖回溯的原理、手动实现方法、相关工具和库的使用、调试技巧以及注意事项,同时引用相关的资料来支持说明。</think>### Linux系统中回溯的实现细节与调试技巧 #### 1. **回溯的基本原理** 回溯(Stack Backtrace)通过解析程序运行时的帧结构,还原函数调用链。在x86架构中,每个帧包含以下信息: - **返回地址**:调用函数后下一条指令的地址。 - **上一个帧的基地址**(通过`ebp`寄存器保存)。 通过遍历`ebp`寄存器形成的链表,可以逐级回溯调用。例如,引用[^4]中的内联汇编`asm("movl %%ebp, %0" : "=r" (bp))`直接获取当前`ebp`值,用于手动实现遍历。 #### 2. **实现回溯的步骤** **手动实现示例(x86架构):** ```c void backtrace() { void **bp; get_bp(bp); // 获取当前ebp值,如引用 while (bp) { void *ret_addr = *(bp + 1); // 返回地址位于ebp+4的位置 printf("Return address: %p\n", ret_addr); bp = (void**)*bp; // 跳转到上一帧的ebp } } ``` **依赖调试信息解析函数名**: - 返回地址需要通过符号表(如ELF文件的`.symtab`或`.dynsym`)或DWARF调试信息映射到函数名。工具如`addr2line`或`dladdr()`函数可实现此功能。 #### 3. **系统工具与库支持** - **GDB的`backtrace`命令**: 直接调用GDB内置的回溯功能,依赖调试符号(需编译时添加`-g`选项)。 - **libunwind库**: 提供跨平台的回溯接口,支持解析局部变量和寄存器状态。 - **/proc文件系统**: 如引用[^2]中`cat /proc/[pid]/maps`可查看进程的内存映射,辅助判断地址所属的代码段或库。 #### 4. **调试技巧与注意事项** - **优化编译的影响**: 编译器优化(如`-O2`)可能省略帧指针(`-fomit-frame-pointer`),此时需依赖异常处理信息(如`.eh_frame`)或特定展开策略。 - **信号处理中的回溯**: 在信号处理函数中使用`backtrace_symbols()`(需`libexecinfo`)可捕获崩溃时的调用。 - **动态插桩工具**: Valgrind的Memcheck(如引用[^1])结合回溯,可定位内存错误(如越界访问)的触发路径。 #### 5. **示例:结合/proc文件系统分析** ```c // 获取当前进程的内存映射(引用[^2]) sprintf(szCommand, "cat /proc/%d/maps", getpid()); system(szCommand); ``` 输出结果包含各内存区域的起始-结束地址、权限和映射文件,可用于验证返回地址是否落在有效代码段内。 #### 6. **高级应用:性能分析与内存调试** - **性能分析**: 回溯可用于生成火焰图(FlameGraph),可视化函数调用热点。 - **内存泄漏调试**: 结合引用[^1]的内存追踪库,记录分配调用,辅助定位未释放的内存块。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值