# 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>


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET 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?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
