# 3. 递归下降反汇编-代码与数据混合场景的处理

这一章将介绍如何将机器码“反汇编”；常用语句上的“反汇编”指的是将机器码转义为汇编；不过在反编译的语境中，“反汇编”往往是指将机器码转换为中间语言。

由于冯·诺依曼体系下的计算机结构在主存中是不区分指令和数据的（对应，哈弗结构将指令和数据存储分离）。反汇编的第一个难题就是从存储中区分数据与指令。

精确的反汇编方案不能无条件的相信.text段里都全是指令；尤其实在CSIC复杂指令集的场景中，不定长的指令使用“线性扫描”功能很容易出现问题。

## 介绍：控制流依赖的中间语言翻译（反汇编）技术

控制流依赖的中间语言翻译，或者控制流依赖的反汇编技术目的是为了处理可能存在的数据与代码的混合情况。

### **问题举例**

为了避免将数据翻译成代码，现在的反编译工具使用基于控制流的递归下降算法进行反编译，来看下面这个示例。

在较为复杂的switch代码中，往往使用跳转表甚至多级跳转表进行判断，而有的编译器会将跳转表记录在函数体内部。例如glibc的pathconf函数，它本体是个巨大的switch语句

<https://codebrowser.dev/glibc/glibc/sysdeps/posix/pathconf.c.html>

在IDA中反编译这个函数，可以看到0x00B9D11这部分是数据，但是被保存在text段中。

<figure><img src="https://2050902342-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FFIjush9EwySt5d8KqOoI%2Fuploads%2Fjnuv5luk5VMHSwMrcX2e%2Fimage.png?alt=media&#x26;token=85c77310-18d6-4450-83ab-c7d81002630d" alt=""><figcaption></figcaption></figure>

如上图，跳转表在.text段并且IDA正确的识别出来。

但是如果我们使用objdump这一类的软件，它就啥都不管了“把text全反会白嫩了”；最终，就会变成下图这样，错误的将数据识别成汇编代码。

<figure><img src="https://2050902342-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FFIjush9EwySt5d8KqOoI%2Fuploads%2F2d2tww0BUbp5cce9NNcm%2Fimage.png?alt=media&#x26;token=899f640d-3955-467d-9bcf-9e93744cbf73" alt=""><figcaption></figcaption></figure>

### 实现简述：

当前主流的反汇编算法分为两大类：线性扫描和递归下降。

现在的商用反编译器会同时执行线性扫描和递归下降两种策略，其中线性扫描方案执行起来更为谨慎，限制更多。

何为递归下降反汇编，简单说就是程序运行他得有个入口点是吧，经过这个入口点往下一直去找他调用的函数地址，把那个地址当作函数反汇编，层层递归去反汇编。

而线性扫描就是通过匹配等方法寻找函数入口进行反汇编，然后尝试去寻找函数的结尾完成反汇编。

ref: <https://bbs.kanxue.com/thread-269617.htm>

递归下降的实现基于控制流的反汇编能力。(注，现在的反编译器往往直接将机器码翻译成中间语言，汇编本身和中间语言在一个层次上）

并且在翻译中间语言过程中会注意到跳转与函数调用语义，根据内部的规则进一步判断如何更具控制流进行翻译。换句话说，在反汇编的时候，翻译器知道程序会如何运行。如下图

| 普通指令  | 不改变流程        |
| ----- | ------------ |
| 条件跳转  | 将两个分支都加入处理队列 |
| 无条件跳转 | 改变流程，进行跳转    |
| 函数调用  | 目标加入子函数流程    |
| 返回    | 结束流程         |

但是还有两个问题不好解决：

* 我该从哪里开始翻译机器码，换句话说，需要找到所有函数的入口
* 间接跳转，间接调用，比如 call eax、 jmp eax； 以及no-return-function 和 尾递归、尾调用应该怎么处理，反汇编的时候我怎么知道它会跳转到哪里去？函数会不会一去就不回来了。

## 难题：从哪里开始反汇编

那么剩下的第一个问题，找到函数的入口，确认反汇编起始点。

当前逆向工具的识别技术基本上有这么几类：

* 通过符号识别函数起始位置，比如main等等
* 基于规则匹配函数起始位置
* 基于Xref寻找函数起始位置

对于main函数识别通用方法：

* 基于\_libc\_start\_main 符号寻找
* 去\_start/\_scrt\_common\_main\_seh里找

对于常用函数识别的通用方法：

* 去二进制符号里找
* 在异常处理结构里找
* 识别并标记直接的函数调用，并尝试处理间接跳转
* 基于特殊的决策树/规则直接匹配二进制寻找（较为少见）
* 基于通用的二进制入口结构，比如\_init\_array和\_fini\_array

## 人工指定反编译起始位置

当然，如果上面的方法，都用不了，还可以在我们逆向的时候对函数人工进行标记。

对于控制流无法正常获取，或者奇奇怪怪的回调函数经常能用到这种方法。

在IDA中就能使用create-function功能强制标记，甚至可以指定方法的起始与结束位置

<figure><img src="https://2050902342-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FFIjush9EwySt5d8KqOoI%2Fuploads%2FYXWu7FVN1GDgHmzf3a9b%2Fimage.png?alt=media&#x26;token=66838b84-fb7c-4630-98de-aa41bab5ace8" alt=""><figcaption></figcaption></figure>

ref:<https://hex-rays.com/blog/igors-tip-of-the-week-87-function-chunks-and-the-decompiler/>

## 衍生：花指令对抗技术

看看下面这个代码示例：

```jsx
// 正常的函数代码
int add(int a, int b){
  int c = 0;
  c = a + b;
  return c;
}
// 添加花指令的函数代码
int add_with_junk(int a, int b){
    int c = 0;
    __asm{
        jz label;
        jnz label;
        _emit 0xe8;    //0xe8是call 指令，后面加4bytes的地址偏移，因此导致反汇编器错误识别
label:
    }
    c = a + b;
    return c;
}
```

十分有趣的花指令小玩意，其本身的防护强度不高。处理办法有很多，现场写pattern+patch都可以。大家在一些简单的ctf题目中一定经常遇到

\--—————————————————————-

**为什么花指令能够对抗基于控制流的反编译？**

因为，若不添加额外的转换规则，反编译器并不知道 jz+jnz ==> jmp;本质上就是一个跳转。

基于控制流的反编译处理就会把跳转的目标加入处理队列，使用默认方法继续顺着控制流往下翻译（将0xe8当成call进行翻译），最后产生反编译的冲突。
