3.栈偏移平衡与计算(使用IDA举例)

典型问题示例

相信大家在按F5时,一定遇到过类似的情况

  • “sp-analysis failed”

  • “positive sp value has been detected”

https://hex-rays.com/blog/igors-tip-of-the-week-27-fixing-the-stack-pointer/

如果没有遇到过,那这里我造一个类似的情况(注意编译时添加-masm=intel参数):

//https://blog.ret2.io/2017/11/16/dangers-of-the-decompiler/
// gcc ./test.c -masm=intel
#include <stdio.h>

#define positive_sp_predicate \\
    __asm__ ("  push     rax      \\n"\\
             "  xor      eax, eax \\n"\\
             "  jz       opaque   \\n"\\
             "  add      rsp, 4   \\n"\\
             "  nop               \\n"\\
             "opaque:             \\n"\\
             "  pop      rax      \\n");
void protected()
{
    positive_sp_predicate;
    puts("Can't decompile this function");
}

void main()
{
    protected();
}

这一章的主题,说明“为什么反编译器要处理栈平衡”,”反编译器如何处理栈平衡“、“为什么IDA会出现上面这些问题”、“遇到上面的问题应该怎么办”

反编译器与栈偏移

不同的反编译器对于栈平衡与栈上局部变量的处理方法不同,这里我以最常用的IDA作为举例。

什么是栈平衡(栈帧)?

这个东西应该属于基础知识,这里不做介绍。

有需要可以看下面的材料,以及自己搜索一下:

https://ctf-wiki.org/pwn/linux/user-mode/stackoverflow/x86/stack-intro/

为什么反编译器需要推导栈偏移

最重要的两个目标,一个是识别栈上的局部变量,一个是用于判断函数调用约定。

在一般情况下,栈的开辟任务在函数入口完成,并在函数退出时进行回收(依照具体的调用约定),在大部分场景下,栈空间(栈顶)的大小不会变化,调用约定往往也会处理好栈的平衡,保证出入一致;

但是总有些特殊场景:

  • 在C语言可以使用alloc动态分配栈长度,函数中的栈顶(sp)可能是会动态变化的。

  • C语言编译器支持可变的局部数组,可能会动态分配栈空间,例如下面这个代码

    void function_test(int n) {
    		int array[n] = {0};
    // 省略
    }

并且,无论是使用栈底(bp)还是使用栈顶(sp)来引用栈上的局部变量,反编译器都需要归一化的处理方法,归一化的方案就是栈平衡的计算。

在IDA中栈平衡就是计算SP的偏移,局部变量的识别也是基于栈底计算的,后面作为主要的举例。

如果栈平衡推导出现错误,那么对于栈上局部变量的还原就可能出错,因为对应到了错误的偏移。

通用栈平衡推导过程

栈偏移的处理存在与反编译器的多个阶段,包括

  • 反编译器前端转换过程中,中间语言会处理记录所有对于栈指针的操作。

  • 数据流分析,栈操作是识别函数调用参数的重要信息。

  • 数据流分析,通过栈指针的操作识别局部变量。

  • 数据流分析,将对于栈指针+偏移的”读取”与”写入“内存操作转换为对函数局部变量的修改。

  • 代码生成,会标记局部变量在栈上的偏移。

当然,在这一章节,我们只讨论最上面的**“反编译器前端转换过程中,中间语言会处理记录所有对于栈指针的操作”**这一步骤

一旦栈偏移的推导完成,对于所有局部变量的引用,反编译器就会归一化,在IDA中就是变为局部变量偏移的引用。

如下图,原先的汇编是[rsp+0x30]和[rsp+0x08]

IDA判断出当前栈偏移是108,就会将[rsp+0x30] 和 [rsp+0x08]转换为相对于栈底位置的局部变量的操作,当然最后算出来的实际值是一样的。

栈局部变量var_D8 = qword ptr -0D8h

栈局部变量var_100 = qword ptr -100h

IDA中控制流的栈偏移计算方法

在IDA中,栈偏移的计算是在反汇编步骤就已经完成了。栈偏移作为基础信息参与后面的工作。

IDA中的Options → General 中可以开启栈指针的显示

开启后在汇编代码的这部分,就会显示栈的偏移了,简单的说前面的数字记录了这行汇编执行前,栈顶相对于栈底的偏移。

这个数字的计算方法也很简单,在中间语言生成时,计算所有栈操作的指令对栈顶的影响(比如push eax 就+4, push rbp 就+8)

在遇到函数调用时,处理调用约定对于栈的影响。

函数开始时这个值为0,如果一切顺利,函数结束时这个值也为零(栈平衡了)。

当遇到通过栈指针+偏移对于栈的内存读取与写入操作,反编译器依赖于这个值,就能知道任意指令情况下的栈顶与栈底的偏移,很容易的就能转换为对实际栈变量的操作。进而识别局部变量在栈上的位置。

IDA控制流的栈偏移的不足

看到了上面的例子,聪明的你一定想到了,IDA这种方法虽然适用与大部分情况,但是存在一定的不足。这也是导致出现**“sp-analysis failed”“positive sp value has been detected”**的主要原因。

1、无法处理的分支情况

再次看一下这个代码

//https://blog.ret2.io/2017/11/16/dangers-of-the-decompiler/
// gcc ./test.c -masm=intel
#include <stdio.h>

#define positive_sp_predicate \\
    __asm__ ("  push     rax      \\n"\\
             "  xor      eax, eax \\n"\\
             "  jz       opaque   \\n"\\
             "  add      rsp, 4   \\n"\\
             "  nop               \\n"\\
             "opaque:             \\n"\\
             "  pop      rax      \\n");
void protected()
{
    positive_sp_predicate;
    puts("Can't decompile this function");
}

void main()
{
    protected();
}

在IDA他会出现这样的问题,当两个控制流合并时,一边栈偏移是0x10,一边栈偏移是0x0C。IDA不知道应该使用哪个,就会出现栈偏移错误的问题。

遇到这种问题,IDA就直接“先到先得”哪个控制流先处理,就用哪个控制流的栈偏移。

2、依赖于调用约定的识别

一些函数调用约定会影响栈的平衡,如果函数调用约定以及调用参数没有正确的识别,就会导致栈平衡的问题。

栈平衡错误的常见场景与应对方法

在IDA中最常用的就是强行调整栈平衡以及配置调用约定.

把光标放到需要调整的汇编语句,使用快捷键alr+k,强制修改栈顶的相对偏移。注意,哪怕修改后IDA的**“sp-analysis failed”**等告警仍可能会存在,只要保证反编译F5能够正常使用就行了。

至于调用约定的识别错误导致的反编译问题,可以通过修改调用约定或者自定义调用约定实现

ref:https://hex-rays.com/blog/igors-tip-of-the-week-51-custom-calling-conventions/

IDA中栈计算平衡的内部实现

在IDA中的架构中,栈平衡的计算,在机器码转汇编语言时顺带完成了。

这一组件称为“IDA processor module”,它在ida-sdk的idasdkXX\idasdkXX\module文件夹内。

参考下图,指令集中会定义trace_sp功能,专门对可能影响栈的指令进行处理(先判断操作的寄存器是不是栈寄存器,在判断操作数)

例如,下图中的add_stkpnt,就是对栈操作的IDA-API。

ref:https://blog.quarkslab.com/ida-processor-module.html

Last updated