Hex-Rays 官方文档:Failures and troubleshooting
函数大小限制
IDA 反编译函数有一个最大大小限制(默认为 64 K),超过这个大小就会反编译失败。
在 IDA 安装目录下的 cfg > hexrays.cfg 中可以修改 MAX_FUNCSIZE
的值解决
在 IDA Pro 9.1 上,MAX_FUNCSIZE
最大为 256。
栈不平衡 & 栈过大
花指令那篇文章已经有提到过栈平衡,即 IDA 报错 positive sp value
。
解决思路就是:检查函数中对栈进行操作的指令,并判断是否平衡。如果某些语句导致静态分析出来栈不平衡,就直接 nop
掉。
“指定地址处的栈指针高于初始栈指针。函数表现得如此奇怪导致无法反编译。如果您发现栈指针值不正确,请使用 IDA 中的 Alt-K(Edit > Functions > Change stack pointer)命令进行修改。”
这是 Hex-Rays 官方的说法,因此有的教程会教看栈指针,然后用 Ctrl + K 来平衡栈。但没有触及本质。看上去好像栈平衡了,IDA 还是报错。
栈指针主要是用来判断栈的偏移处于哪种状态下是平衡的。占比多的偏移值一般就是平衡状态下的偏移值。
步骤:
- 看函数头尾,判断栈平衡
头部push
、call
、sub
/add
了多少寄存器,尾部就要pop
、ret
、add
/sub
多少寄存器。 - 看函数中间,找破坏栈者
一种是使用了栈的指令,另一个是函数调用约定。
调用约定 (Calling Conventions) 描述了在特定架构和操作系统上函数传递参数以及返回值的方式。它还指定了函数名称的修饰方式。
完整的调用约定发展表可见于 x86 calling conventions
栈过大就是 IDA 报错 too big stack frame
,本质上和栈不平衡一样,就是 add esp, <value>
时将栈改得非常大。
1 | add rsp, 4489FFDEh ; stack point: 1C8 |
switch
无法识别
switch
是一个多路分支,对于分支数较少的情况,编译器会直接编译成一堆 if
-else
语句,于是反编译器也会反编译为一堆 if
-else
语句。
但如果 case
数量比较多,if
-else
就显得低效起来。此时开启优化的编译器会为 switch
编制一个跳转表 (jump table),例如:
1 | .L4: |
跳转表中,每行放一个 case
代码段的首地址。
根据我的实验,在 GCC 15.1,开启优化的情况下,5 个 case
(不计 default
)就会触发多路分支优化,生成跳转表。
然后程序先判断 switch
的控制表达式是否大于 case
范围,如果大于就跳到 .L2
(对应 default
或者结束 switch
);如果在 case
范围内,就按照跳转表跳转到对应分区(64 位程序对应地址大小是 8 字节,还记得吗):
1 | cmp DWORD PTR [rsp+12], 5 |
此时反编译器才会反编译为我们熟悉的 switch
语句。
总结起来,对于反编译器,这个跳转表就是识别 switch
语句的关键。跳转表分析不出来,我们就只有一条死路可走。
IDA 提供了修复跳转表的工具,即 Edit > Other > Specify switch idiom:
需要填写的字段有:
- Address of jump table:跳转表的首地址,例如上面
.L4
的地址值 - Number of elements:跳转表元素个数,即
case
数(包括default
) - Size of table element:跳转表元素大小。一般从
rax*<number>
处或者直接看跳转表元素得到,也可以根据程序架构判断 - Element base value:跳转表偏移的基地址。可以是跳转表首地址,也可以是其他固定值
- Start of the switch idiom:
switch
语句开始匹配case
的起始地址,例如上面mov eax, DWORD PTR [rsp+12]
- Input register of switch:
switch(var)
中var
对应的寄存器
另外需要勾选 Signed jump table elements
Decompile as call
一种是非标准的汇编指令(过于有能的手搓大佬……)导致 IDA 无法生成等效的 C 伪代码,并回退到 _asm
语句。
“手工编写的汇编代码” 是准确反汇编的主要障碍之一。
——Disassembly Challenges
例如,下面的汇编来自 PowerPC 固件中的某个函数:
1 | 00000CB4 mtsprg0 r29 |
sprg0
是一个特殊寄存器 (Special Purpose Register),但这里当成通用寄存器使用。由于一般用户不会调用系统指令,IDA 不会擅作处理,而是反汇编为:
1 | __asm { mtsprg0 r29 } |
但我们可以告诉反汇编器将特定的指令视为函数调用。以上面为例:
- 在反汇编视图中,将光标放在指令上。例如
mtsprg0 r29
- 使用 Edit > Other > Decompile as call…
- 输入函数原型,考虑输入 / 输出寄存器。这里是
void __usercall mtsgpr0(unsigned int value@<r29>);
- 按 F5 反汇编
另外一种是反汇编器优化导致常量传播 (Constant Propagation),造成函数缺失。
常量传播是一个替代表示式中已知常量的过程,类似于解决有一个已知量的线性方程组。
常量传播在编译器中使用定义可达 (Reaching definition) 分析实现,如果一个变量的所有可达定义都是赋予相同的数值(即这个赋值将同一个常量赋给变量),那么这个变量将会是一个常量,而且会被常量取代。
——Constant folding
1 | mov rax, fs:0 |
IDA 会认为这两次读取 fs:0
的结果一样,反编译后只会保留第一次读取,第二次按常量传播优化掉:
1 | v62 = __readfsqword(0); |
但 fs:0
真的不会改变吗?这个程序套了 VM 虚拟机保护,而 VM 是可以在读取一次 fs:0
后修改 fs:0
的!
虚拟机可以自己定义一套指令,在程序中能有一套函数和结构解释自己定义的指令并执行功能。
VM(虚拟机保护)是一种基于虚拟机的代码保护技术。他将基于 x86 汇编系统中的可执行代码转换为字节码指令系统的代码。来达到不被轻易篡改和逆向的目的。
这里的虚拟机并不是指 VMWare / VirtualBox 之类的虚拟机,更像是一个用于解释系统函数的一个小型模拟器。
因此我们需要让 IDA 知道这是两次返回数不同的读取。
选中第一行的 mov rax, fs:0
,用 Decompile as call… 重定义为 __int64 getKey()
。对下一句如法炮制,按 F5 就可以了。
一个综合性的例子来自强网杯,同样用了 VM 保护:
1 | ; ... |
在 Linux 中,内核提供一些操作的接口给用户态的程序使用,这就是系统调用 (system call)。对于用户态的程序,其调用相应的接口的方式,是一条汇编指令 syscall
。
比如说,创建子进程的操作,Linux
内核提供了 fork
这个系统调用作为接口。那么,如果用户态程序想调用这个内核提供的接口,其对应的汇编语句为(部分):
1 | movq $57, %rax ; AT&T style, 57 -> rax |
syscall
这个指令会先查看此时 rax
的值,然后找到系统调用号为那个值的系统调用,然后执行相应的系统调用。我们可以在系统调用列表中找到,fork
这个系统调用的系统调用号是 57。于是,我们把 57 放入 rax
寄存器中,然后使用了 syscall
指令。这就是让内核执行了 fork
。
——Linux x86_64 系统调用简介
在标准的 Linux syscall 下,传入的 rax
和返回的 rax
应该是一样的,于是 IDA 进行常量传播优化。但在 VM 的自定义环境中,syscall 就不会这么 “标准” 了。
IDA 的反编译:
1 | v58 = 0x7177625F32303231i64; |
首先,IDA 不清楚 0x1337 (4919) 对应的系统调用是什么 —— Linux 内核中的系统调用还没到 500 个呢[1]!此外,由于进行了常数传播优化,所以 IDA 把 0x1337 传播给了下面调用的 rax
。但我们可以知道 0x1337 对应的系统调用是有另外的返回值给 rax
的,所以需要让 IDA 知道这个 syscall
并不标准。
选中 syscall
,用 Decompile as call… 重定义为 int __usercall mysyscall@<rax>(int num@<rax>);
。
这里也出现了多次读取 fs
寄存器的问题,按照前面的说明修改即可。
Golang 反编译失败处理
IDA 报错 Call analysis failed
这是最令人痛苦的错误消息,但你仍然可以做些事情来解决问题。简而言之,这条消息意味着反编译器无法确定调用约定和调用参数。如果这是一个直接的、非可变参调用,你可以通过指定被调用者类型来修复它:直接跳转到被调用者并点击 Y 来指定类型。对于可变参函数,指定类型也是一个好主意,但调用分析仍然可能失败,因为反编译器必须找出调用中的实际参数数量。我们建议首先检查整个函数中的栈指针。清除任何不正确的栈指针值。其次,检查所有被调用函数的类型。如果被调用函数的类型不正确,可能会干扰其他调用并导致失败。
原因是 IDA 分析目标函数调用时函数参数分析错误,此时将目标函数定义中参数删去即可,即修改为 <type> func_X();
。
IDA 官方建议
当反编译器失败时,请检查以下事项:
-
函数边界。函数中不应存在任何无目的跳转到函数外部的分支。函数应通过返回指令或跳转到另一个函数的开始处来正确结束。如果函数在调用一个非返回函数后结束,被调用者必须被标记为非返回函数。
-
堆栈指针值。使用 Options > General > Stack pointer 命令,在反汇编视图中的地址后面以列形式显示它们。如果函数的任何位置堆栈指针值不正确,反编译可能会失败。要更正堆栈指针值,请使用 Edit > Functions > Change stack pointer 命令。
-
堆栈变量。使用 Edit > Functions > Stack variables… 命令打开堆栈帧窗口,并验证定义是否合理。在某些情况下,创建一个大的数组或一个结构变量可能会有所帮助。
-
函数类型。调用约定、参数的数量和类型必须正确。如果未指定函数类型,反编译器将尝试推断它。在某些罕见的情况下,它可能会失败。如果函数期望其输入在非标准寄存器中,或者返回结果在非标准寄存器中,您必须通知反编译器。目前它对非标准输入位置能做出很好的猜测,但无法处理非标准返回位置。
-
调用函数的类型和引用的数据项。类型错误很容易造成严重问题。使用 F 快捷键在消息窗口中显示当前项的类型。对于函数,将光标定位在开头并按下 F。如果类型不正确,使用 Edit > Functions > Set function type(快捷键是 Y)进行修改。此命令不仅适用于函数,也适用于数据和结构成员。
-
如果一个类型引用了未定义的类型,反编译可能会失败。
-
使用由最新版本 IDA 创建的数据库。
-
在某些情况下,输出可能包含红色的变量。这意味着局部变量分配失败。请阅读关于重叠变量的页面,了解可能的纠正方法。