函数大小限制
IDA 反编译函数有一个最大大小限制(默认为 64 K),超过这个大小就会反编译失败。
在 IDA 安装目录下的 cfg > hexrays.cfg 中可以修改 MAX_FUNCSIZE
的值解决
在 IDA Pro 9.1 上,MAX_FUNCSIZE
最大为 256。
栈不平衡
花指令那篇文章已经有提到过了,IDA 报错 positive sp value
。
解决思路就是:检查函数中对栈进行操作的指令,并判断是否平衡。如果某些语句导致静态分析出来栈不平衡,就直接 nop
掉。
有的教程是看栈指针,然后用 Ctrl + K 来平衡栈。这种方法没有触及本质,看上去好像栈平衡了,IDA 还是报错。
步骤:
- 看函数头尾,判断栈平衡
头部push
、call
、sub
/add
了多少寄存器,尾部就要pop
、ret
、add
/sub
多少寄存器。 - 看函数中间,找破坏栈者
一种是使用了栈的指令,另一个是函数调用约定。
调用约定 (Calling Conventions) 描述了在特定架构和操作系统上函数传递参数以及返回值的方式。它还指定了函数名称的修饰方式。
完整的调用约定发展表可见于 x86 calling conventions
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 就可以了。