# 3.汇总-反编译器中的反汇编

这是第三章的最后一个小节；这一节将会把之前的内容进行整理，并分析这些之前的组件在IDA和Ghidra中的具体实现方法。

并且，这一章节与下一章节是从反编译器前端到中端的过渡章节，会涉及到一部分中端优化的情况。

当然，先假定一个场景：**“有一套新的指令集或者字节码，需要编写反编译器插件，让反编译器支持新指令集架构的反汇编功能”。**

那么如何在在IDA和Ghidra中实现这个场景，就是这一章节的主要内容。

## IDA与反汇编

ida的中间语言使用的是micorocode，这个中间语言在30年前就已经设计出来了，IDA从7.0版本开始提供中间语言的相关API。具体的中间语言的相关内容知识，将在下一章细说。

IDA的指令集支持插件，称为“IDA processor module”，这个在栈平衡中章节已经介绍过了。如果需要支持一套新的指令集，需要在这个模块中完成以下的组件。

* 指令的二进制数据与中间语言的转换（体力活，要定义所有的指令、寄存器等等）
* 注册所有的跳转指令，声明跳转指令的类型，是绝对跳转、条件跳转、间接跳转、函数调用、函数返回等等
* 处理所有可能的数据段引用场景
* 处理所有可能的指令段引用场景
* （可选）定义栈指针，并定义所有的对栈的操作，IDA将基于这个定义计算栈平衡
* （可选）处理并分析间接跳转，将switch所产生的间接跳转还原
* （可选）分析并还原栈帧。

这么一看，会发现，如果要完整的支持一套新的指令集，那要做的事情也太多了吧！而且好难。

毕竟很少有人从事专业的反编译开发工作；尤其是还原switch语句以及还原栈帧等复杂场景，从无到有要做很多功课也不一定写的好。

### 函数识别

IDA的函数识别使用了通用的方法，即通过call指令以及符号和文件入口识别函数。

这也是为什么我们经常能在IDA中看到红色的代码的原因，因为它虽然能够正常的进行反汇编，但是却无法

### 栈指针与栈平衡

参考上一章的内容

## Ghidra与反汇编

ghidra的中间语言来自于QUBT，称为pcode，可以说是学术界教科书式的中间语言设计，这一部分将在下一章进一步说明。

Ghidra工程本身的设计存在一个严重的缺陷，它的函数级的反编译器和上层的是割裂的，中间通过符号流进行交互。可以说是解耦结果头了，反编译器的调试的确方便了，很多功能却不好实现了。

主要问题在于，底层的反编译是不支持文件结构的解析与函数识别的功能；需要在上层java中实现，而反编译器本体只支持单个函数的分析，跨函数的全局程序分析的反编译变得更为复杂。

### 流敏感的反编译

Ghidra反编译器内部实现了流敏感的反编译能力；因此只需要翻译好指令到中间语言的部分，其它的事情内部引擎会处理。

注意，由于流敏感的识别的所有工作，都是在Ghidra内部引擎完成的，因此从机器码到中间语言的翻译必须足够准确。尤其是函数的返回本身依赖于中间语言的翻译。

### switch间接跳转

Ghidra内置了后向切片与部分控制流分析的能力，以此来实现将间接跳转中还原出jump-table的技术。

也就是说Ghidra内置了switch语句的还原能力，这个工作反编译器帮你完成了，你看这不比IDA舒服的多。

Ghidra将switch间接跳转特征分为三个部分：Guard, jump-table, jup-register

并常识通过后向切片与copy-propagation (拷贝传播），在ghidra中也被称为还原高层变量varnode。依次识别这三个元素

参考论文：<http://www.sciencedirect.com/science/article/pii/S0167642301000144/pdf?md5=ed116622d77cdb914fb644cad3604bb3&pid=1-s2.0-S0167642301000144-main.pdf>

快速解读原理：

这种恢复技术的前提是，switch的实现符合论文中的三类模型。

<figure><img src="https://2050902342-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FFIjush9EwySt5d8KqOoI%2Fuploads%2FUA58dI6j605TloMdvvEv%2Fimage.png?alt=media&#x26;token=69ee88cb-577b-42f5-8c36-e20a2dbbf176" alt=""><figcaption></figcaption></figure>

这三类模型实际上都有这么些特征，伪代码举例

```jsx
int a = input()
switch(a) {
	case 1:
		// something
	case 2:
		// something
......
	default:
		// something
}
```

* 第一步，最开始，一定有个判断jcond，判断switch的对象是否太大，而走到default或者一个都匹配不上
* 第二步，通过三类之一的跳转表，计算目标地址
* 第三步，完成跳转

跳转表的识别方法，在跳转点生成后向切片，获得程序的状态。

通过上述的切片，使用程序分析技术（主要是copy传播），找到第一步的判断逻辑（称为guard）

最后，使用论文中的模型，找到switch中跳转的计算方法，还原跳转表的形态以及大小。

### 函数识别

使用了两套函数识别机制，都在上层中定义

Ghidra支持配置指令特征进行函数起始位置识别，例如在x86\_64指令集中，几种常见的函数起始汇编格式为：

```c
push    rbp
mov     rbp, rsp
==========================
push    r15
push    r14
push    r13
push    r12
========================
push    rbp
push    rbx
mov     rxx, rxx
```

Ghidra有自定义的一套二进制匹配的函数头识别功能，例如x86\_64体系下是的就在

\Ghidra\Processors\x86\data\patterns\x86-64gcc\_patterns.xml

文件中

<figure><img src="https://2050902342-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FFIjush9EwySt5d8KqOoI%2Fuploads%2F0hrAi2tnCAOLqDLE54O4%2Fimage.png?alt=media&#x26;token=0af414a8-004b-4f5f-9496-67dce0324e2d" alt=""><figcaption></figcaption></figure>

较为常见指令集的都已经支持了这种匹配方法。具体可以去每个指令集下的patterns目录里找。

剩下的函数入口识别组件就是通用的做法了，可以在主页的analysis功能中查看，也可以自己勾选上跑一下试试。相关信息在“递归下降的反汇编”章节已经有说明。

<figure><img src="https://2050902342-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FFIjush9EwySt5d8KqOoI%2Fuploads%2FK3KBH7FvlQO0nPK9FAPp%2Fimage.png?alt=media&#x26;token=40159d98-2688-4ff0-a5e8-afa95b36714e" alt=""><figcaption></figcaption></figure>

### 栈指针与栈偏移计算

与IDA不同，Ghidra的栈局部变量展示是基于栈顶(rsp)的。

这种做法的好处是反编译器全程只需要跟踪栈顶以及栈空间的大小，这只需要一个寄存器，毕竟在绝大多数场景，栈帧大小在函数头开辟后就固定了。

缺点是，有时候，有的人看栈上的局部变量偏移会觉得有些困惑。可以参考ref2中的配置进行修改。

ref1:<https://github.com/NationalSecurityAgency/ghidra/issues/223>

ref2:<https://liwugang.github.io/2020/08/01/ghidra.html>

并且，Ghidra的内部反编译器在反编译过程中不动态计算栈平衡，且不处理动态开辟栈指针的情况。在某些alloc的情况下会导致生成奇怪的反编译代码。它默认认为，栈顶除了函数调用情况，在一个函数内部作不会再有变化。

## 不转换中间语言的反编译器

在通用反编译器中，中间语言的主要功能之一，是为了兼容多种指令集架构；

与编译器类似，反编译器前端将各种指令集架构翻译为中间语言，后续的中端在中间语言上直接执行各种优化。那么支持一个新指令集的工作就会简单很多，因为很多的事情框架帮你实现了。

但是，如果反编译器的目标明确且固定，那么它也可以不需要执行“中间语言转换”。

最典型的就是java的JVM字节码、android的Dex字节码等，这一类完全特化的反编译器，它的目标十分明确就是输出尽可能精确的源码。

注意，**不转换中间语言，不意味着不使用中间语言；**

或者说，学术界认为**jvm的字节码本身就是一种中间语言**，因此这一类的反编译器“直接拿来用就行”，不再需要额外的转换，因此后续的中端优化还是通用的。

不做额外的转换，也是这一类完全特化的反编译器能输出和源码十分相近的主要因素。例如jadx工作已经支持直接把apk导出成gradle工程了。
