在对程序进行逆向分析时,可能会遇到 IDA Pro 工具无法正确识别出编译后的 switch 语句的情况,增加了我们理解代码的难度。本文从编译器对 switch 语句的优化入手,先正向分析编译器会把 switch 语句编译成什么样的汇编代码,这些代码由哪些成分组成,之后介绍如何在 IDA Pro 工具中恢复出 switch 语句。
本文中所使用的环境为:
OS:Ubuntu 20.04 x64
GCC:9.3.0
IDA Pro: 7.5 for Windows
理解编译器对 switch 语句的优化
出于对代码执行效率的考虑,编译器会对源程序中的 switch 语句进行优化,而这种优化可能导致 IDA Pro 工具无法正确理解程序中的 switch 语句,从而影响我们进行逆向分析。编译器对 switch 语句的优化按照 case labels 是否紧凑和连续分为多种情况,本文只讨论 case labels 紧凑连续时被优化成跳转表的情况。
我们先从一个例子入手,观察我们在源代码中编写的 switch 语句是如何被编译器处理的。比如说,我们写这样一段简单的代码,获取一个输入值 input,然后将这个输入作为 switch 语句的判断条件,根据 input 值的不同来输出不同的结果,这个 switch 语句包含 6 个 case(default case 算作 case 0)。
switch.c
#include<stdio.h>
intmain() { int input; while (1) { scanf("%d", &input); switch(input) { case1: printf("your input is 1"); break; case2: printf("your input is 2"); break; case3: printf("your input is 3"); break; case4: printf("your input is 4"); break; case5: printf("your input is 5"); break; default: printf("what's your input?"); } } }
使用 gcc -S switch.c 命令进行编译,不进行汇编和链接,获得汇编代码 switch.s。我的 gcc 编译出来自己带了这些注释,你们的应该也一样吧。代码首先在开头定义了 LC1~LC6 这六个字符串常量,后面我们可以借助这些常量的标签去识别跳转的目标地址对应哪一个 case。接着是 main 函数的指令部分,再后面是一个跳转表,最后是我们跳转的目标代码部分。可以看到,标签 L8、L7、L6、L5、L3、L2 处分别对应 6 个 case 的目标代码,其中 L2 标签是 default case 的情况。跳转表中定义了 6 个 .long 类型(4 字节)的数据,每一个数据的值都是 Ln-L4 这样的形式,不难推测出跳转表中存储的是 6 个 case 对应目标代码与跳转表之间的相对偏移。
现在我们来看一下 IDA Pro 对这个 switch 语句的识别情况。使用 gcc -o switch switch.c 命令进行编译和汇编、链接,得到二进制程序 switch。使用 IDA Pro 7.5 加载程序,反汇编得到的代码如下,和上面直接编译得到的汇编代码逐行进行对比,除了指令的表示形式不一样之外,内容基本上是一样的。可以注意到 0x11c8 地址处有一个奇怪的 db 3Eh 没有被解析,这实际上就是 notrack 指令,由于比较新所以 IDA Pro 7.5 并不能识别出这个指令,但它对我们分析和恢复 switch 语句没有任何影响,不必管它。
从上面反汇编得到的代码不难看出 unk_206c 就是 switch 语句的跳转表,双击查看这个跳转表,可以看到 IDA 只是将这部分内容识别成一连串的字节数据,并不能理解数据的含义。
.rodata:000000000000206C unk_206C db 0BEh ; DATA XREF: main+49↑o .rodata:000000000000206C ; main+55↑o .rodata:000000000000206D db 0F1h .rodata:000000000000206E db 0FFh .rodata:000000000000206F db 0FFh .rodata:0000000000002070 db 5Fh ; _ .rodata:0000000000002071 db 0F1h .rodata:0000000000002072 db 0FFh .rodata:0000000000002073 db 0FFh .rodata:0000000000002074 db 72h ; r .rodata:0000000000002075 db 0F1h .rodata:0000000000002076 db 0FFh .rodata:0000000000002077 db 0FFh .rodata:0000000000002078 db 85h .rodata:0000000000002079 db 0F1h .rodata:000000000000207A db 0FFh .rodata:000000000000207B db 0FFh .rodata:000000000000207C db 98h .rodata:000000000000207D db 0F1h .rodata:000000000000207E db 0FFh .rodata:000000000000207F db 0FFh .rodata:0000000000002080 db 0ABh .rodata:0000000000002081 db 0F1h .rodata:0000000000002082 db 0FFh .rodata:0000000000002083 db 0FFh
v5 = __readfsqword(0x28u); while (1 ) { __isoc99_scanf("%d", &input); if (input <= 5 ) break; printf("what's your input?"); } __asm { jmp rax } return result; }
接下来进入正文,使用 IDA Pro 提供的“Specify switch idiom”功能恢复出 switch 语句。将光标点在 switch 语句的起始地址 0x199c 处,点击 “Edit” -> “Other” -> “Specify switch idiom”,可以看到这样一个窗口。
对于这些值的含义,IDA Pro 自身其实做了一些简要的说明,点击窗口下方的“Help”按钮,可以看到这样的内容:
Please specify the jump table address, the number of its elements and their widths(1,2,4,8). The element shift amount and base value should be specified only if the table elements are not plain target addresses but must be converted using the following formula:
target = base +/- (table_element << shift)
(only this formula is supported by the kernel; other cases must be handled by plugins and ‘custom’ switch idioms).
If you specify BADADDR as the element base then the base of the switch segment will be used
The start of the switch idiom is the address of the first instruction in the switch idiom.
Subtraction is used instead of addition if “Subtract table elements“ is selected.
When table element is an instruction then you should select “Table element is insn“.
If you specify that a separate value table is present, an additional dialog box with its attributes will be displayed.
但是,IDA Pro 提供的这个说明只解释了一部分的参数,而且有些参数的解释也不很明确,经过一番尝试和理解,我得出了如下解释,其中加粗的参数是我们需要关注的参数。
Address of jump table:跳转表的地址。
Number of elements:跳转表中元素的个数。
Size of table element:跳转表中每个元素的字长(1/2/4/8)。
Element shift amount:一般情况下保持默认的 0 即可。除非跳转表中存储的元素并不是跳转的目标地址,而是需要通过 target = base +/- (table_element << shift) 这个公式计算得出,这种情况需要作为 shift 的值提供。
Element base value:与 Address of jump table 保持相同的值,对应上述公式中的 base。
Start of the switch idiom:switch 语句的首个指令的地址(比如上面例子中的 0x199c),在打开“Specify switch idiom”窗口时,光标处的地址会被自动填写到这里,这就是前面把光标点在地址 0x199c 处的原因。
Input register of switch:存储 switch 语句输入的寄存器,即存储 switch(input) {...} 中input变量的寄存器。
First(lowest) input value:最小的 case 值,比如 case 有 1、2、3、4、5,则填写 0,因为 default 占用了 case 0。
Default jump address:default case 的跳转目标地址,可以不指定,不指定时对于 default case 以 case 0 的形式显示。
Separate value table is present:暂时没搞清,用不到。
Signed jump table elements:跳转表中的元素是有符号值时需要勾选。
Subtract table elements:计算跳转表元素时用减法而不是用加法。
Table element is insn:跳转表中存储的不是目标地址而直接是指令时需要勾选。
还是以上面的程序为例,我来逐个解释如何从 IDA Pro 反编译得到的汇编代码中识别并填写这些值。我把前面的一部分代码粘贴到这里,便于对照查看。