# 3.间接跳转与间接调用的处理

上一章“递归下降的反汇编”还留了另一个问题，即“间接跳转与间接调用”应该如何跟踪控制流。这一章节将针对这个话题进行扩展，尽可能的将反汇编过程常见的中跳转与调用的特殊情况都覆盖了。

## 问题来了，应该继续吗？

如下图，如果IDA无法正常的处理间接跳转，它可能会直接内联一个jmp \<register> 进来。

<figure><img src="https://2050902342-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FFIjush9EwySt5d8KqOoI%2Fuploads%2FaudlQWWpPPIlvJhkoa1i%2Fimage.png?alt=media&#x26;token=7bd07210-9cef-4ce0-ae18-10418895897f" alt=""><figcaption></figcaption></figure>

典型场景举例1-间接调用：

```jsx
//省略之前的反汇编结果
call eax // <-- 出现了一个间接调用，他会跳转去哪里呢？
;; < -- 后面的“机器码”应该继续翻译下去吗？
;; < -- 会不会call了之后不再回来了？
;; < -- 会不会，后面的内容根本不是合法的指令？
```

典型场景举例2-间接跳转

```c
//省略之前的反汇编结果
jmp eax // <-- 出现了一个间接跳转，他会跳转去哪里呢？
;; < -- 这种情况，后面的“机器码”应该继续翻译下去吗？
```

那么什么情况下，编译器会生成间接跳转或者间接调用？

函数调用一定使用Call指令吗，还可能存在什么特殊的情况？

反编译器又是如何判断这些情况的呢？

在什么情况下，编译器会生成这几类特殊的代码？

那么反编译器又怎么处理与识别？

## 核心问题：反汇编是如何处理间接跳转？

间接跳转的处理主要包含在二进制翻译以及数据流分析的步骤中，可以说是一个混合功能。当然，前面的章节先不讨论数据流分析的复杂场景；这里我尽量列举所有的可能场景，并尝试说明解决方案。

下图为学术界中对于相对跳转的一种处理方法，我称为”不完全流跟踪技术“，Ghidra就使用了这种技术。将在后面的章节单独描述

<figure><img src="https://2050902342-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FFIjush9EwySt5d8KqOoI%2Fuploads%2F1rCoMHgKmHhAj3xENpLW%2Fimage.png?alt=media&#x26;token=ac592cfa-aae5-4876-996c-759fe96f1193" alt=""><figcaption></figcaption></figure>

#### 0、默认场景

如果反编译器无法推断出间接跳转、间接调用的类型或者目标。一般情况下，反编译器使用默认的规则，即：

* 对于条件跳转，直接顺着控制流依次翻译两个分支。
* 对于间接跳转 jmp eax 这种，如果不知道目标位置，停止翻译，进行标记。
* 对于间接调用 call eax 这种，如果不知道目标位置，则翻译成函数指针，并继续顺着流程翻译。

#### 1、tail-call场景

示例。考虑下面这个代码

```c
#include <stdio.h>
void test(int a, int b, int c)
{
    int ADD = a + b + c + 100;
    printf("%d", ADD);
    int SUB = a - b - c -100;
    printf("%d", SUB);
    printf("%d", ADD * SUB * 100); //<-- 这里最后一句是printf
}
```

如果使用GCC-X86\_64-O3编译，会将最后一个printf优化为jmp，像这样

```c
...省略前面的汇编
lea     rsi, aDDD+8     ; "%d"
pop     r12
xor     eax, eax
pop     r13
pop     r14
jmp     ___printf_chk  ; 直接变成一个jmp-tail-call. 函数结束了
```

这个场景，我们想要讨论的是**控制流依赖场景中**如何处理jmp类型的语句。这种情况被称为“tail-call”

根据经验，tail-call可能会有一下的特征，反编译器也是基于这些特征识别的

* 跳转的位置是一个已经被识别到的”函数起始地址”
* 跳转的距离很长，而且绝不会跳转到正在翻译的函数体内。
* 跳转的距离很长，越过了好几个已经被识别到的函数，即跨函数跳转
* tail-call的跳转不会是条件跳转
* tail-call之前的汇编，为了平衡栈，往往会执行出栈操作
* tail-call跳转的目标（target），不会是其它条件跳转的目标

反编译器会智能的识别tail-call场景，并停止进行翻译。

#### 1-1、更为极端的tail-call场景

还存在一种更为极端的tail-call场景，即函数指针参数与tail-call混合的情况，看下面这个代码示例

```c
int som_func( int(*myFuncVar)(int a, int b) )
{
    return myFuncVar(1, 2);
}
```

这种情况下，生成的函数将会出现一个相对跳转的情况（O3优化）

```c
myMianOp(int (*)(int, int)):
        mov     rax, rdi
        mov     esi, 2
        mov     edi, 1
        jmp     rax
```

虽然真实世界中存在的可能性不多，但是这个例子很好的展示出了“编译器多种优化混合”导致的复杂场景，也是反编译器的主要麻烦来源。

#### 2、switch语句中的间接跳转

switch语句主要会被翻译成两种形态的汇编

ref: <https://github.com/snowcra5h/branch-tables-and-jump-tables>

* 最直接就是将switch翻译成多个同语义的if-else-if语句。
* **这里讨论的典型场景，基于跳转表的实现场景。**

注意，在通用的逆向软件中，switch跳转的实现与恢复都是依赖于目标编译器的模型的。

虽然学术界后续也提出了一些不需要依赖编译器模型的算法，但是这不是这里的重点（少一点算法，多一点示例）。

代码示例：

```jsx
int test_switch(){
    int i ;
    int a = std::rand();
    switch(a){
        case 0: i = 0;break;
        case 1: i = 1;break;
        case 2: i = 2;break;
        case 3: i = 3;break;
        case 4: i = 4;break;
        case 5: i = 5;break;
        case 6: i = 6;break;
        case 7: i = 7;break;
        case 8: i = 8;break;
        case 9: i = 9;break;
        default: i = 10;break;
    }
    return i;
}
```

这种每个case的条件值很近的情况，有可能生成跳转表形式的汇编指令。

例如上面这个例子，i的取值是1\~10，可以用10个元素的静态数组存储switch的跳转地址，这样跳转的目标地址可以直接通过i的值索引到。

最终生成类似与下图的使用间接跳转实现的跳转表

<figure><img src="https://2050902342-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FFIjush9EwySt5d8KqOoI%2Fuploads%2Fzf40yyjYnPDTYD48P12l%2Fimage.png?alt=media&#x26;token=3eff82fe-09cb-4b51-b47f-2cbc05b11716" alt=""><figcaption></figcaption></figure>

<figure><img src="https://2050902342-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FFIjush9EwySt5d8KqOoI%2Fuploads%2FrGZxSejxst1FexCWtqSu%2Fimage.png?alt=media&#x26;token=16c39545-2918-49d5-934c-95c7efe17e9c" alt=""><figcaption></figcaption></figure>

反编译器将在中间语言翻译时，还原上面这种情况的switch语句。

更为准确的说法应该叫做 ”jump-table还原“，因为在这里反编译器的目标是找到并格式化跳转表，并将跳转表关联到中断的控制流上，而不是还原switch-case-break-default这一套语句。

基于跳转表生成的switch语句，一般来说有下面几种形态（限定gcc与clang编译器）

其中jump address是最终的跳转地址，table\_X为跳转表，expression是计算跳转的表达式，padding为用于增加的偏移常量，%PC为当前的指令地址。

* jump address = table\_1\[expression \* 4]；最经典的场景，直接使用一个静态数组存储跳转表。
* jump address = table\_2\[table\_1\[expression \* 4]]；不太常见的场景，使用了两级跳转表一般出现在跳转情况较为复杂，case中的代码特别长的场景
* jump address = table\_1\[%PC + expression \* 4] + %PC；跳转表存储在代码段的情况，表中存储的将会是一个对于当前地址的相对偏移
* jump address = table\_1\[%PC + expression \* 4 - padding] + %PC - padding； 更为复杂的情况，跳转不仅查表，还需要进行进一步的计算，甚至使用减法替代加法。
* 其它场景更为罕见的情况，这取决于编译器的优化，例如跳转表中不存储地址而是存指令等等。

还原switch的间接跳转的算法有很多，主要的手段都是通过控制流跟踪+规则匹配的方法的方法还原跳转表。具体方法将在后续章节中针对不同的反编译器专门介绍。

#### 2-1、手动定位switch跳转表（以IDA作为示例）

有时候IDA不太聪明，无法正确的还原switch的跳转表，就需要我们手动 帮帮忙

指针选中应该是switch的jump \<register> 语句上，点击edit->other->specify switch idiom

<figure><img src="https://2050902342-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FFIjush9EwySt5d8KqOoI%2Fuploads%2F9xac7CCdMkIswX7C89mM%2Fimage.png?alt=media&#x26;token=eb5a39fa-a38b-4d1a-a233-f5dc05957c15" alt=""><figcaption></figcaption></figure>

<figure><img src="https://2050902342-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FFIjush9EwySt5d8KqOoI%2Fuploads%2FDsMSS4HLtlIRd6JBq7Oe%2Fimage.png?alt=media&#x26;token=984e9b6e-594f-4cfe-9220-c3cb5ec90fb4" alt=""><figcaption></figcaption></figure>

这个表很复杂，但是利用上面的知识，我们可以知道它的意思

**跳转表信息**

1、Address of jump table：跳转表的地址。

1、Address of jump table：跳转表的地址。

2、Number of elements：跳转表中元素的个数。

3、Size of table element：跳转表中每个元素的字节数（1/2/4/8）。

4、Element shift amount：**一般情况下保持默认的 0** 即可。除非跳转表中存储的元素并不是跳转的目标地址，而是需要通过 target = base +/- (table\_element << shift) 这个公式计算得出，这种情况需要作为 shift 的值提供。

5、Element base value：参考计算方法中的说明，如果是绝对地址填0，如果是相对偏移填写相对偏移的基址（一般来说是和Address of jump table 相同）。

**跳转信息**

6、Start of the switch idiom：switch 语句的首个指令的地址，在打开“Specify switch idiom”窗口时，光标处的地址会被自动填写到这里，就是jump \<register>的指令地址

7、Input register of switch：存储 switch 语句输入的寄存器，即存储 switch(input) {...} 中input变量的寄存器，也是jump \<register>中的寄存器。

8、First(lowest) input value：最小的 case 值，如果有default情况，default会占用0。

9、Default jump address：default case 的跳转目标地址，可以不指定，不指定时对于 default case 以 case 0 的形式显示。

**特殊信息**

10、Separate value table is present：当switch是以二级跳转表形态表示时，启用这个选项；二级跳转对应这上述的jump address = table\_2\[table\_1\[expression \* 4]] 这种形态。&#x20;

11、Signed jump table elements：跳转表中的元素是以有符号值时需要勾选。&#x20;

12、Subtract table elements：计算跳转表元素时用减法而不是用加法，参考计算方法中的说明。&#x20;

13、Table element is insn：跳转表中存储的不是目标地址而直接是指令时需要勾选。这种情况比较罕见，在ref中提供了ARM的直接使用跳转表存储jump指令的示例情况。

ref:<https://hex-rays.com/blog/igors-tip-of-the-week-53-manual-switch-idioms/>

#### 3、no-return 场景

本来这个情况不应该放在这里的，但是别的地方更加不合适，所以放在这里了。

看下面这个代码示例

```jsx
#include <stdio.h>
#include <stdlib.h>

void some_func() {
    //跳过一大堆代码
	printf("exit");
	exit(-2); // 这里直接退出了, 函数永远不会返回
}
int main() {
  some_func();
	printf("never here");
	return 0;
}
```

在优化后的代码中，main函数中的some\_func后的内容printf("never here");不会被生成，即被优化掉。因为main函数不再返回，后面的代码也就是死代码了。

这种情况在真实代码中很常见，比如各种assert 或者 error 或者 exception函数的使用。

但是反编译器未必知道some\_func函数不会再返回，可能还会继续翻译main函数下去，获得错误的内容。

#### 3-1、IDA中的no\_return函数判断规则

对于no-return场景当前主要的判断还是基于规则。可以参考IDA-TIPS中对于no\_return\_function 特征识别的描述：”IDA使用函数名来识别non-returning 函数“规则

大家也可以看一下自己的IDA中“IDA Pro\cfg\noret.cfg”文件的配置，一般来说配置了100多个函数名。

<figure><img src="https://2050902342-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FFIjush9EwySt5d8KqOoI%2Fuploads%2FjgSdvYgg5EBuKDUuxX8s%2Fimage.png?alt=media&#x26;token=9eec8202-f885-4e80-9920-561cad71d3db" alt=""><figcaption></figcaption></figure>

ref:<https://hex-rays.com/blog/igors-tip-of-the-week-126-non-returning-functions/>

#### 3-2、Ghidra中的no\_return函数判断规则

同理，在Ghidra中也有类似的配置文件，不过Ghidra的配置更为细致；对于不同的二进制格式（ELF、PE、Mach-O等）都有独立的判断规则。

规则文件在 “Ghidra\Features\Base\data\noReturnFunctionConstraints.xml”

```c
<noReturnFunctionConstraints>
  <executable_format name="Executable and Linking Format (ELF)">
  	<compiler id="golang">
      <functionNamesFile>GolangFunctionsThatDoNotReturn</functionNamesFile>
  	</compiler>
    <functionNamesFile>ElfFunctionsThatDoNotReturn</functionNamesFile>
  </executable_format>
  <executable_format name="Mac OS X Mach-O">
    <functionNamesFile>MachOFunctionsThatDoNotReturn</functionNamesFile>
  </executable_format>
  <executable_format name="DYLD Cache">
    <functionNamesFile>MachOFunctionsThatDoNotReturn</functionNamesFile>
  </executable_format>
  <executable_format name="Portable Executable (PE)">
  	<compiler id="golang">
      <functionNamesFile>GolangFunctionsThatDoNotReturn</functionNamesFile>
  	</compiler>
    <functionNamesFile>PEFunctionsThatDoNotReturn</functionNamesFile>
  </executable_format>
</noReturnFunctionConstraints>
```

#### 4、函数指针

最后一种场景，也是比较常见的场景，即函数指针。

函数指针的使用在多态中十分常见，比如C++的虚表指针，或者C语言中各种回调等等。

反编译使用预置的调用约定，很容易识别函数指针。但是在逆向工作中真正的困难是没有高层次的类型结构。

然而很不幸，当前常用的反编译器的反编译维度都只有函数级（我不确定binary-ninja对于C++语言的类型结构恢复能力，我看它的产品宣传有这部分的能力）。即，不支持对于类层次结构的恢复。关于高级语义恢复的话题，只能留到后面讨论了。

### 3、其它情况：隐式控制流

这一章节虽然讨论了一些主流的场景，但是这些场景的前提都是“显式控制流”，即，有专门的跳转指令（虽然反编译器未必可以正确的推导出来对应的地址）

还有一大部分没有考虑到的隐式控制流场景，他们往往和语言特性以及操作系统特性有关。例如：系统中断、try-catch异常、系统回调的注册、信号处理函数。

当前的常用反编译器往往也不能正确的处理这些场景，我目前没有对这部分有较为深入的研究。如果有时间，以后再把这部分补上。
