Note

Hex-Rays 官方文档:Failures and troubleshooting

函数大小限制

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

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

Tip

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

栈不平衡 & 栈过大

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

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

Warning

“指定地址处的栈指针高于初始栈指针。函数表现得如此奇怪导致无法反编译。如果您发现栈指针值不正确,请使用 IDA 中的 Alt-K(Edit > Functions > Change stack pointer)命令进行修改。”

这是 Hex-Rays 官方的说法,因此有的教程会教看栈指针,然后用 Ctrl + K 来平衡栈。但没有触及本质。看上去好像栈平衡了,IDA 还是报错。

栈指针主要是用来判断栈的偏移处于哪种状态下是平衡的。占比多的偏移值一般就是平衡状态下的偏移值。

步骤:

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

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

栈过大就是 IDA 报错 too big stack frame,本质上和栈不平衡一样,就是 add esp, <value> 时将栈改得非常大。

1
2
add rsp, 4489FFDEh ; stack point: 1C8
nop word ptr [rax+rax+00000000h] ; stack point: -4489FE16 !

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 就可以了。

一个综合性的例子来自强网杯,同样用了 VM 保护:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
; ...

loc_36:
mov eax, 1337h ; sys_X (0x1337)
syscall ; trigger system call

loc_3D:
mov r15, qword ptr fs:loc_FD+3

loc_46:
cmp r15, qword ptr fs:loc_FD+3
jmp loc_A8C

loc_A8C:
mov [rbp+var_78], rax
; ...
系统调用

在 Linux 中,内核提供一些操作的接口给用户态的程序使用,这就是系统调用 (system call)。对于用户态的程序,其调用相应的接口的方式,是一条汇编指令 syscall

比如说,创建子进程的操作,Linux 内核提供了 fork 这个系统调用作为接口。那么,如果用户态程序想调用这个内核提供的接口,其对应的汇编语句为(部分):

1
2
movq $57, %rax ; AT&T style, 57 -> rax
syscall

syscall 这个指令会先查看此时 rax 的值,然后找到系统调用号为那个值的系统调用,然后执行相应的系统调用。我们可以在系统调用列表中找到,fork 这个系统调用的系统调用号是 57。于是,我们把 57 放入 rax 寄存器中,然后使用了 syscall 指令。这就是让内核执行了 fork
——Linux x86_64 系统调用简介

在标准的 Linux syscall 下,传入的 rax 和返回的 rax 应该是一样的,于是 IDA 进行常量传播优化。但在 VM 的自定义环境中,syscall 就不会这么 “标准” 了。

IDA 的反编译:

1
2
3
4
5
v58 = 0x7177625F32303231i64;
__writefsqword(0, 0x7177625F32303231i64);
__asm {syscall}
v59 = 4919i64;
// ...

首先,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

Hex-Rays 官方

这是最令人痛苦的错误消息,但你仍然可以做些事情来解决问题。简而言之,这条消息意味着反编译器无法确定调用约定和调用参数。如果这是一个直接的、非可变参调用,你可以通过指定被调用者类型来修复它:直接跳转到被调用者并点击 Y 来指定类型。对于可变参函数,指定类型也是一个好主意,但调用分析仍然可能失败,因为反编译器必须找出调用中的实际参数数量。我们建议首先检查整个函数中的栈指针。清除任何不正确的栈指针值。其次,检查所有被调用函数的类型。如果被调用函数的类型不正确,可能会干扰其他调用并导致失败。

原因是 IDA 分析目标函数调用时函数参数分析错误,此时将目标函数定义中参数删去即可,即修改为 <type> func_X();

IDA 官方建议

当反编译器失败时,请检查以下事项:

  1. 函数边界。函数中不应存在任何无目的跳转到函数外部的分支。函数应通过返回指令或跳转到另一个函数的开始处来正确结束。如果函数在调用一个非返回函数后结束,被调用者必须被标记为非返回函数。

  2. 堆栈指针值。使用 Options > General > Stack pointer 命令,在反汇编视图中的地址后面以列形式显示它们。如果函数的任何位置堆栈指针值不正确,反编译可能会失败。要更正堆栈指针值,请使用 Edit > Functions > Change stack pointer 命令。

  3. 堆栈变量。使用 Edit > Functions > Stack variables… 命令打开堆栈帧窗口,并验证定义是否合理。在某些情况下,创建一个大的数组或一个结构变量可能会有所帮助。

  4. 函数类型。调用约定、参数的数量和类型必须正确。如果未指定函数类型,反编译器将尝试推断它。在某些罕见的情况下,它可能会失败。如果函数期望其输入在非标准寄存器中,或者返回结果在非标准寄存器中,您必须通知反编译器。目前它对非标准输入位置能做出很好的猜测,但无法处理非标准返回位置。

  5. 调用函数的类型和引用的数据项。类型错误很容易造成严重问题。使用 F 快捷键在消息窗口中显示当前项的类型。对于函数,将光标定位在开头并按下 F。如果类型不正确,使用 Edit > Functions > Set function type(快捷键是 Y)进行修改。此命令不仅适用于函数,也适用于数据和结构成员。

  6. 如果一个类型引用了未定义的类型,反编译可能会失败。

  7. 使用由最新版本 IDA 创建的数据库。

  8. 在某些情况下,输出可能包含红色的变量。这意味着局部变量分配失败。请阅读关于重叠变量的页面,了解可能的纠正方法。


  1. Linux kernel syscall tables ↩︎


©2025-Present Watermelonabc | 萌 ICP 备 20251229 号

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

本博客总访问量:capoo-2

| 开往-友链接力 | 异次元之旅

中文独立博客列表 | 博客录 随机博客

AI 参与指数(IIIA)2 级

猫猫🐱 发表了 53 篇文章 · 总计 223.8k 字