函数大小限制

IDA 反编译函数有一个最大大小限制(默认为 64 K),超过这个大小就会反编译失败。

在 IDA 安装目录下的 cfg > hexrays.cfg 中可以修改 MAX_FUNCSIZE 的值解决

Tip

在 IDA Pro 9.1 上,MAX_FUNCSIZE 最大为 256。

栈不平衡

花指令那篇文章已经有提到过了,IDA 报错 positive sp value

解决思路就是:检查函数中对栈进行操作的指令,并判断是否平衡。如果某些语句导致静态分析出来栈不平衡,就直接 nop 掉。

Warning

有的教程是看栈指针,然后用 Ctrl + K 来平衡栈。这种方法没有触及本质,看上去好像栈平衡了,IDA 还是报错。

步骤:

  1. 看函数头尾,判断栈平衡
    头部 pushcallsub/add 了多少寄存器,尾部就要 popretadd/sub 多少寄存器。
  2. 看函数中间,找破坏栈者
    一种是使用了栈的指令,另一个是函数调用约定。
函数调用约定

调用约定 (Calling Conventions) 描述了在特定架构和操作系统上函数传递参数以及返回值的方式。它还指定了函数名称的修饰方式。
完整的调用约定发展表可见于 x86 calling conventions

switch 无法识别

switch 是一个多路分支,对于分支数较少的情况,编译器会直接编译成一堆 if-else 语句,于是反编译器也会反编译为一堆 if-else 语句。

但如果 case 数量比较多,if-else 就显得低效起来。此时开启优化的编译器会为 switch 编制一个跳转表 (jump table),例如:

1
2
3
4
5
6
7
8
9
.L4:
.quad .L2
.quad .L10
.quad .L9
.quad .L8
.quad .L7
.quad .L6
.quad .L5
.quad .L3

跳转表中,每行放一个 case 代码段的首地址。

根据我的实验,在 GCC 15.1,开启优化的情况下,5 个 case(不计 default)就会触发多路分支优化,生成跳转表。

然后程序先判断 switch 的控制表达式是否大于 case 范围,如果大于就跳到 .L2(对应 default 或者结束 switch);如果在 case 范围内,就按照跳转表跳转到对应分区(64 位程序对应地址大小是 8 字节,还记得吗):

1
2
3
4
cmp     DWORD PTR [rsp+12], 5
ja .L2 ; default OR break
mov eax, DWORD PTR [rsp+12]
jmp [QWORD PTR .L4[0+rax*8]]

此时反编译器才会反编译为我们熟悉的 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 语句。

Note

“手工编写的汇编代码” 是准确反汇编的主要障碍之一。
——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
2
3
mov rax, fs:0
; other code
mov rax, fs:0

IDA 会认为这两次读取 fs:0 的结果一样,反编译后只会保留第一次读取,第二次按常量传播优化掉:

1
2
3
v62 = __readfsqword(0);
; other code
v60 = v62; // after optimization

fs:0 真的不会改变吗?这个程序套了 VM 虚拟机保护,而 VM 是可以在读取 fs:0 的同时修改 fs:0 的!

VM 虚拟机保护

虚拟机可以自己定义一套指令,在程序中能有一套函数和结构解释自己定义的指令并执行功能。

VM(虚拟机保护)是一种基于虚拟机的代码保护技术。他将基于 x86 汇编系统中的可执行代码转换为字节码指令系统的代码。来达到不被轻易篡改和逆向的目的。

这里的虚拟机并不是指 VMWare / VirtualBox 之类的虚拟机,更像是一个用于解释系统函数的一个小型模拟器。

—— 那 CTF,那 VMre,那些事(一)

因此我们需要让 IDA 知道这是两次返回数不同的读取。

选中第一行的 mov rax, fs:0,用 Decompile as call… 重定义为 __int64 getKey()。对下一句如法炮制,按 F5 就可以了。


©2025-Present Watermelonabc | 萌 ICP 备 20251229 号

Powered by Hexo & Stellar latest & Vercel & HUAWEI Cloud
您的访问数据将由 Vercel 和自托管的 Umami 进行隐私优先分析,以优化未来的访问体验

本博客总访问量:capoo-2

| 开往-友链接力 | 异次元之旅 | 中文独立博客列表

猫猫🐱 发表了 44 篇文章 · 总计 212.4k 字