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

## 典型问题示例

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

<figure><img src="https://2050902342-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FFIjush9EwySt5d8KqOoI%2Fuploads%2FLcDNV2HwITiECRVq5gy8%2Fimage.png?alt=media&#x26;token=f04bc0fe-52c2-4c3d-85ff-1aebd1c8d00d" alt=""><figcaption></figcaption></figure>

<figure><img src="https://2050902342-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FFIjush9EwySt5d8KqOoI%2Fuploads%2FtWUdSc5qEKAS8KziMAcl%2Fimage.png?alt=media&#x26;token=efe7c651-1970-44d2-af59-43755199b4f1" alt=""><figcaption></figcaption></figure>

* **“sp-analysis failed”**
* **“positive sp value has been detected”**

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

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

```c
//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/](https://ctf-wiki.org/pwn/linux/user-mode/stackoverflow/x86/stack-intro/)

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

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

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

但是总有些特殊场景：

* 在C语言可以使用alloc动态分配栈长度，函数中的栈顶（sp）可能是会动态变化的。
*   C语言编译器支持可变的局部数组，可能会动态分配栈空间，例如下面这个代码

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

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

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

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

### 通用栈平衡推导过程

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

* 反编译器前端转换过程中，中间语言会处理记录所有对于栈指针的操作。
* 数据流分析，栈操作是识别函数调用参数的重要信息。
* 数据流分析，通过栈指针的操作识别局部变量。
* 数据流分析，将对于栈指针+偏移的”读取”与”写入“内存操作转换为对函数局部变量的修改。
* 代码生成，会标记局部变量在栈上的偏移。

<figure><img src="https://2050902342-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FFIjush9EwySt5d8KqOoI%2Fuploads%2FCGy935NodwV2QKEs3MLJ%2Fimage.png?alt=media&#x26;token=e5e91565-4c4c-467e-af34-6dcc04c7980d" alt=""><figcaption></figcaption></figure>

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

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

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

<figure><img src="https://2050902342-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FFIjush9EwySt5d8KqOoI%2Fuploads%2FUpEg7QwzALChaCMe6XoC%2Fimage.png?alt=media&#x26;token=71fa2780-24bb-4fdd-b624-41ee2ba3ff03" alt=""><figcaption></figcaption></figure>

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

栈局部变量var\_D8 = qword ptr -0D8h

栈局部变量var\_100 = qword ptr -100h

<figure><img src="https://2050902342-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FFIjush9EwySt5d8KqOoI%2Fuploads%2FDsHyuhKa3vi06pr97h8l%2Fimage.png?alt=media&#x26;token=d538f286-86b9-4efb-8ee7-fc5b61744b96" alt=""><figcaption></figcaption></figure>

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

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

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

<figure><img src="https://2050902342-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FFIjush9EwySt5d8KqOoI%2Fuploads%2FaNwPtW74zDrS1SZKIXPR%2Fimage.png?alt=media&#x26;token=6a9ec10e-8f9e-4536-934e-f8b82f2c9e32" alt=""><figcaption></figcaption></figure>

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

<figure><img src="https://2050902342-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FFIjush9EwySt5d8KqOoI%2Fuploads%2FtBwi353oMvYKI6KdV16L%2Fimage.png?alt=media&#x26;token=8ebe8139-0454-473b-a1b4-315bd769a8c9" alt=""><figcaption></figcaption></figure>

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

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

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

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

### IDA控制流的栈偏移的不足

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

1、无法处理的分支情况

再次看一下这个代码

```c
//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就直接“先到先得”哪个控制流先处理，就用哪个控制流的栈偏移。

<figure><img src="https://2050902342-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FFIjush9EwySt5d8KqOoI%2Fuploads%2F0JWCdKr5YivU13UcO0zU%2Fimage.png?alt=media&#x26;token=b27538ac-2b16-4288-9b02-7d343ae3439f" alt=""><figcaption></figcaption></figure>

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

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

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

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

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

<figure><img src="https://2050902342-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FFIjush9EwySt5d8KqOoI%2Fuploads%2FmaZtx6sGIQEWji7e5LMb%2Fimage.png?alt=media&#x26;token=2249ccb1-c8c7-40b1-86c6-dac384c30a0c" alt=""><figcaption></figcaption></figure>

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

ref:[https://hex-rays.com/blog/igors-tip-of-the-week-51-custom-calling-conventions/](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。

<figure><img src="https://2050902342-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FFIjush9EwySt5d8KqOoI%2Fuploads%2FgHI5O6aztbxY1PxqHW9p%2Fimage.png?alt=media&#x26;token=e46a0e31-9bb4-4849-9a79-72cafb0954fd" alt=""><figcaption></figcaption></figure>

ref:[https://blog.quarkslab.com/ida-processor-module.html](https://blog.quarkslab.com/ida-processor-module.html)

