> For the complete documentation index, see [llms.txt](https://tommy-3.gitbook.io/my_reverse_book/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://tommy-3.gitbook.io/my_reverse_book/my_reverse_notes/3.-zhan-pian-yi-ping-heng-yu-ji-suan-shi-yong-ida-ju-li.md).

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

## 典型问题示例

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

<figure><img src="/files/wClksFBEpuB77TUMtOrT" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/9OFoV1t0ATSwttiWiNa9" 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/>

如果没有遇到过，那这里我造一个类似的情况（注意编译时添加-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/>

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

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

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

但是总有些特殊场景：

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

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

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

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

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

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

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

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

<figure><img src="/files/la6jrJdX7ZUY1Ba9iscc" alt=""><figcaption></figcaption></figure>

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

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

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

<figure><img src="/files/PoPBvbdrFPhv9G3NYIWE" alt=""><figcaption></figcaption></figure>

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

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

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

<figure><img src="/files/E7nyWHnmWoSfg3j7Yj3Z" alt=""><figcaption></figcaption></figure>

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

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

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

<figure><img src="/files/XhjaAQcBvn5uOXPlnm4n" alt=""><figcaption></figcaption></figure>

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

<figure><img src="/files/71lwH6ImvdzuDB4J5nOA" 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="/files/WfvnSZU132kOUIFMqu5a" alt=""><figcaption></figcaption></figure>

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

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

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

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

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

<figure><img src="/files/7aPBdghRYtl04lHOyisr" alt=""><figcaption></figcaption></figure>

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

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。

<figure><img src="/files/plJbt45ADyxOGSZnCtzI" alt=""><figcaption></figcaption></figure>

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