在 IDA Pro 中恢复 switch 语句

在对程序进行逆向分析时,可能会遇到 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>

int main() {
int input;
while (1) {
scanf("%d", &input);
switch(input) {
case 1:
printf("your input is 1");
break;
case 2:
printf("your input is 2");
break;
case 3:
printf("your input is 3");
break;
case 4:
printf("your input is 4");
break;
case 5:
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 对应目标代码与跳转表之间的相对偏移。

接着我们来阅读一下 main 函数中的关键代码,在 main 函数中,调用完 scanf 函数之后,就开始了 switch 语句的部分,首先从栈中取出 input 变量放到 eax 寄存器中,对输入进行判断,如果大于 5 则跳出到 L2 标签(default case),否则继续向下执行。这时候可能有人就问了:我 input 变量定义了一个 int 类型,那它要是个负值咋整?是不是还得加一个负值的判断?这里很巧妙地用了ja(Jump Above)这个指令,它把操作数都当做无符号数进行比较,也就是说,如果取到 eax 寄存器里的是一个负值,比如 -1,即 0xffffffff,作为无符号数它的值是 4294967295,是远大于 5 的,所以同样会跳出到 L2 标签。

然后程序把数据段 rax*4 得到的地址放到 rdx 寄存器中,这实际上就是跳转表的索引;接着将跳转表地址取到 rdx 寄存器中,取出跳转表中元素的值放到 eax 寄存器并带符号扩展为 8 个字节。之后取跳转表基址到 rdx 寄存器,两者相加得到跳转目标地址。

switch.s
...
; --------------------
; 字符串常量定义
; --------------------
.LC1:
.string "your input is 1"
.LC2:
.string "your input is 2"
.LC3:
.string "your input is 3"
.LC4:
.string "your input is 4"
.LC5:
.string "your input is 5"
.LC6:
.string "what's your input?"
...
; --------------------
; main 函数代码
; --------------------
main:
...
.L10: ; 由于 while(1)循环,程序的结尾会跳回 L10 标签
...
call __isoc99_scanf@PLT
movl -12(%rbp), %eax ; switch 语句的第一条指令
cmpl $5, %eax
ja .L2 ; 如果输入值大于 5 则跳转到 L2,即 default case
movl %eax, %eax
leaq 0(,%rax,4), %rdx ; rdx <- &(ds:[rax*4])
leaq .L4(%rip), %rax ; rax <- &(rip + .L4)
movl (%rdx,%rax), %eax ; eax <- [rax + rdx] 取出跳转表中元素
cltq ; 将 4 字节带符号扩展为 8 字节(Convert Long To Quad)
; rax <- sign-extend of eax
leaq .L4(%rip), %rdx
addq %rdx, %rax ; rax <- rax + rdx 计算得到跳转目标地址
notrack jmp *%rax ; 跳转到目标地址
; -------------
; 跳转表
; -------------
.section .rodata
.align 4
.align 4
.L4: ; 跳转表,每一个元素字长是 4 字节
.long .L2-.L4
.long .L8-.L4
.long .L7-.L4
.long .L6-.L4
.long .L5-.L4
.long .L3-.L4
; -------------------
; 跳转目标代码
; -------------------
.text
.L8:
leaq .LC1(%rip), %rdi ; case 1 入口
movl $0, %eax
call printf@PLT
jmp .L9
.L7: ; case 2 入口
leaq .LC2(%rip), %rdi
movl $0, %eax
call printf@PLT
jmp .L9
.L6: ; case 3 入口
leaq .LC3(%rip), %rdi
movl $0, %eax
call printf@PLT
jmp .L9
.L5: ; case 4 入口
leaq .LC4(%rip), %rdi
movl $0, %eax
call printf@PLT
jmp .L9
.L3: ; case 5 入口
leaq .LC5(%rip), %rdi
movl $0, %eax
call printf@PLT
jmp .L9
.L2: ; default 入口(case 0 入口)
leaq .LC6(%rip), %rdi
movl $0, %eax
call printf@PLT
nop
.L9:
jmp .L10 ; while(1)
...

在 IDA Pro 中恢复 switch 语句

现在我们来看一下 IDA Pro 对这个 switch 语句的识别情况。使用 gcc -o switch switch.c 命令进行编译和汇编、链接,得到二进制程序 switch。使用 IDA Pro 7.5 加载程序,反汇编得到的代码如下,和上面直接编译得到的汇编代码逐行进行对比,除了指令的表示形式不一样之外,内容基本上是一样的。可以注意到 0x11c8 地址处有一个奇怪的 db 3Eh 没有被解析,这实际上就是 notrack 指令,由于比较新所以 IDA Pro 7.5 并不能识别出这个指令,但它对我们分析和恢复 switch 语句没有任何影响,不必管它。

.text:0000000000001197                 call    ___isoc99_scanf
.text:000000000000119C mov eax, [rbp+var_C]
.text:000000000000119F cmp eax, 5
.text:00000000000011A2 ja loc_122A
.text:00000000000011A8 mov eax, eax
.text:00000000000011AA lea rdx, ds:0[rax*4]
.text:00000000000011B2 lea rax, unk_206C
.text:00000000000011B9 mov eax, [rdx+rax]
.text:00000000000011BC cdqe
.text:00000000000011BE lea rdx, unk_206C
.text:00000000000011C5 add rax, rdx
.text:00000000000011C8 db 3Eh
.text:00000000000011C8 jmp rax
.text:00000000000011CB ; ---------------------------------------------------------------------------
.text:00000000000011CB lea rdi, aYourInputIs1 ; "your input is 1"
.text:00000000000011D2 mov eax, 0
.text:00000000000011D7 call _printf
.text:00000000000011DC jmp short loc_123C
.text:00000000000011DE ; ---------------------------------------------------------------------------
.text:00000000000011DE lea rdi, aYourInputIs2 ; "your input is 2"
.text:00000000000011E5 mov eax, 0
.text:00000000000011EA call _printf
.text:00000000000011EF jmp short loc_123C
.text:00000000000011F1 ; ---------------------------------------------------------------------------
.text:00000000000011F1 lea rdi, aYourInputIs3 ; "your input is 3"
.text:00000000000011F8 mov eax, 0
.text:00000000000011FD call _printf
.text:0000000000001202 jmp short loc_123C
.text:0000000000001204 ; ---------------------------------------------------------------------------
.text:0000000000001204 lea rdi, aYourInputIs4 ; "your input is 4"
.text:000000000000120B mov eax, 0
.text:0000000000001210 call _printf
.text:0000000000001215 jmp short loc_123C
.text:0000000000001217 ; ---------------------------------------------------------------------------
.text:0000000000001217 lea rdi, aYourInputIs5 ; "your input is 5"
.text:000000000000121E mov eax, 0
.text:0000000000001223 call _printf
.text:0000000000001228 jmp short loc_123C
.text:000000000000122A ; ---------------------------------------------------------------------------
.text:000000000000122A
.text:000000000000122A loc_122A: ; CODE XREF: main+39↑j
.text:000000000000122A lea rdi, format ; "what's your input?"
.text:0000000000001231 mov eax, 0
.text:0000000000001236 call _printf
.text:000000000000123B nop
.text:000000000000123C
.text:000000000000123C loc_123C: ; CODE XREF: main+73↑j
.text:000000000000123C ; main+86↑j ...
.text:000000000000123C jmp loc_1184
.text:000000000000123C ; } // starts at 1169
.text:000000000000123C main endp

从上面反汇编得到的代码不难看出 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

点击鼠标右键,逐个将数据类型修改为“Double Word”,然后点击 “Edit” -> “Operand type” -> “Offset” -> “Offset (user-defined)”,“Base address” 填入跳转表自身的地址 0x206c,其他保持默认,这样可以把这些值恢复成偏移的表示形式,现在看起来是不是和上面编译直接得到的汇编代码很像了?这个步骤对于恢复 switch 语句来说不是必须的,只是为了讲解起来更清楚一些。

.rodata:000000000000206C jumptable_206C  dd offset loc_122A - $  ; DATA XREF: main+49↑o
.rodata:000000000000206C ; main+55↑o ...
.rodata:0000000000002070 dd offset loc_11CB - offset jumptable_206C
.rodata:0000000000002074 dd offset loc_11DE - offset jumptable_206C
.rodata:0000000000002078 dd offset loc_11F1 - offset jumptable_206C
.rodata:000000000000207C dd offset loc_1204 - offset jumptable_206C
.rodata:0000000000002080 dd offset loc_1217 - offset jumptable_206C

如果我们尝试直接按 F5 对 main 函数进行反编译,会得到什么样的结果呢?可以看到,while 语句识别出来了,但 switch 语句并没有被识别出来,而且,很明显少了许多代码,和源代码相比不能说一模一样吧,可以说是毫不相干。

int __cdecl main(int argc, const char **argv, const char **envp)
{
int result; // eax
unsigned int input; // [rsp+4h] [rbp-Ch] BYREF
unsigned __int64 v5; // [rsp+8h] [rbp-8h]

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”,可以看到这样一个窗口。

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:

&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;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 反编译得到的汇编代码中识别并填写这些值。我把前面的一部分代码粘贴到这里,便于对照查看。

.text:0000000000001197                 call    ___isoc99_scanf
.text:000000000000119C mov eax, [rbp+var_C]
.text:000000000000119F cmp eax, 5
.text:00000000000011A2 ja loc_122A
.text:00000000000011A8 mov eax, eax
.text:00000000000011AA lea rdx, ds:0[rax*4]
.text:00000000000011B2 lea rax, unk_206C
.text:00000000000011B9 mov eax, [rdx+rax]
.text:00000000000011BC cdqe
.text:00000000000011BE lea rdx, unk_206C
.text:00000000000011C5 add rax, rdx
.text:00000000000011C8 db 3Eh
.text:00000000000011C8 jmp rax

首先是 Address of jump table 和 Element base value,这两个都填写跳转表的地址,可以看到 0x11b2 地址处和 0x11be 地址处分别把 unk_206c 的地址取到 rax 和 rdx 寄存器,然后分别进行了从跳转表中取元素和将取出的元素与跳转表基址相加的操作,那显然 0x206c 就是跳转表的地址。

接着是跳转表中元素字长(Size of table element)的识别,如何看出跳转表中每个元素的字长呢?去看跳转表吗?不,我们看程序是如何从跳转表中取值的。0x11aa 处的代码 lea rdx, ds:0[rax*4] 已经告诉我们,跳转表中的每个元素是 4 个字节。有了元素字长,那么跳转表中元素个数(Number of elements)自然也有了,(跳转表结束地址 - 跳转表起始地址 + 1) / 字长 就可以得到元素个数为 6,也可以将跳转表的元素恢复成 Double Word(4 字节)之后直接数一下有几个。

switch 语句的起始地址(Start of the switch idiom)已经自动填写好了,就是我们打开“Specify switch idiom”窗口时光标所在的位置。那么这个位置是怎么来的呢?目测一下,scanf 函数 把输入值存到了 input 变量,switch 语句把 input 变量的值取到 eax 寄存器,然后与 case 总数进行比较,那么 mov eax, [rbp+var_C] 就是 switch 语句编译出来的第一条指令。通过这个分析,switch 语句存储输入的寄存器(Input register of switch)也有了,就是这里的 eax。

Default case 的跳转目标地址(Default jump address),可以拉到下面去看每一个目标地址处的指令来进行判断,更简单的方法是看 0x11a2 地址处的ja loc_122A,如果输入值大于 5,则直接跳转到 0x122a 处,那么这个 0x122a 显然就是 default case 的跳转目标地址。

最后,Signed jump table elements 是否需要勾选?跳转表的基址是 0x206c,跳转目标地址的范围在 0x11cb 到 0x122a,在编译时,跳转表中存储的元素是跳转目标地址减去跳转表的基址,那显然这些值都是负值,所以需要勾选。

.rodata:000000000000206C jumptable_206C  dd offset loc_122A - $  ; DATA XREF: main+49↑o
.rodata:000000000000206C ; main+55↑o ...
.rodata:0000000000002070 dd offset loc_11CB - offset jumptable_206C
.rodata:0000000000002074 dd offset loc_11DE - offset jumptable_206C
.rodata:0000000000002078 dd offset loc_11F1 - offset jumptable_206C
.rodata:000000000000207C dd offset loc_1204 - offset jumptable_206C
.rodata:0000000000002080 dd offset loc_1217 - offset jumptable_206C

其他没有提到的参数保持默认即可,为了便于查阅,我也画了一张填写图示。

填写 Manual switch declaration 窗口

完成之后,再次按 F5 进行反编译,可以看到,IDA Pro 很完美地还原了 switch 语句的结构。

int __cdecl main(int argc, const char **argv, const char **envp)
{
int input; // [rsp+4h] [rbp-Ch] BYREF
unsigned __int64 v5; // [rsp+8h] [rbp-8h]

v5 = __readfsqword(0x28u);
while (1 )
{
switch ((unsigned int)__isoc99_scanf("%d", &input) )
{
case 1u:
printf("your input is 1");
break;
case 2u:
printf("your input is 2");
break;
case 3u:
printf("your input is 3");
break;
case 4u:
printf("your input is 4");
break;
case 5u:
printf("your input is 5");
break;
default:
printf("what's your input?");
}
}
}

那如果说你恢复完得到的 switch 语句不这么完美而是有点奇奇怪怪的,识别出了 switch 语句但又没有完全识别,请回去检查一下你的“Start of the switch idiom”参数是不是给对了,比如说我如果给的是那句 lea rdx, ds:0[rax*4] 的地址 0x11aa,那么我反编译得到的结果是这个样子:

int __cdecl main(int argc, const char **argv, const char **envp)
{
unsigned int input; // [rsp+4h] [rbp-Ch] BYREF
unsigned __int64 v5; // [rsp+8h] [rbp-8h]

v5 = __readfsqword(0x28u);
while (2 )
{
while (1 )
{
__isoc99_scanf("%d", &input);
if (input <= 5 )
break;
LABEL_9:
printf("what's your input?");
}
switch (input)
{
case 1u:
printf("your input is 1");
continue;
case 2u:
printf("your input is 2");
continue;
case 3u:
printf("your input is 3");
continue;
case 4u:
printf("your input is 4");
continue;
case 5u:
printf("your input is 5");
continue;
default:
goto LABEL_9;
}
}
}
作者

ChinaNuke

发布于

2021-08-06

更新于

2021-08-11

许可协议

评论