IDA Pro 9.4 改进了已编译 Swift 二进制文件的分析
IDA Pro 9.4 (macOS, Linux, Windows) - 强大的反汇编程序、反编译器和多功能调试器
A powerful disassembler, decompiler and a versatile debugger. In one tool.
请访问原文链接:https://sysin.org/blog/ida-pro/ 查看最新版。原创作品,转载请保留出处。
作者主页:sysin.org
IDA Pro
一个强大的反汇编程序、反编译器和多功能调试器。集成在一个工具中。

IDA 9.4:改进已编译 Swift 二进制文件的分析

IDA 9.4 是迈向更完善 Swift 二进制分析支持的第一步。毫不意外,我们的重点仍然是适用于 ARM64 Mach-O 的 Swift 文件,但我们的总体目标是在更多不同架构和文件格式(例如 x86-64 和 ELF)下,同样提升对 Swift 的支持。
在 9.4 中,我们重点介绍两项改进:对 Swift ABI 的正确建模,以及对 Swift 运行时函数的正确类型定义。
对 Swift ABI 的正确建模
在 IDA 9.4 中,我们为函数原型新增了三个关键字:__swiftself、__swiftthrows 和 __swiftasync。
这三个关键字都以函数被声明为 __swiftcall 为前提。__swiftcall 实际上已经存在于多个版本的 IDA 中,但现在它成为了 IDA 内部所有 Swift 相关逻辑的重要判别标志。对于 Objective-C/Swift 混合二进制文件而言,__swiftcall 是告诉 IDA 某个函数应按照 Swift 机制处理的方式。
下面分别介绍这三个新关键字 (sysin)。由于它们都与 Swift ABI 中特定的寄存器密切相关,因此结合对应的 ABI 机制进行说明会更加直观。
Self 寄存器
它大概是最重要的一个,因为长期以来一直让 Swift 逆向分析变得非常令人头疼。下面这段非常简短的 ARM64 汇编代码,就很好地展示了问题所在。
Disassembly
__text:000000010000530C ADD X20, SP, #0x70+stack_str ; self
__text:0000000100005310 MOV X0, X27 ; other
__text:0000000100005314 MOV X1, X28 ; other
__text:0000000100005318 BL _$sSS6appendyySSF ; String.append(_:)
这段代码只是将两个 Swift 字符串拼接在一起:已有字符串是位于栈上的一个临时对象(#0x70+stack_str),而需要追加的字符串则被编译器存放在寄存器对 X27 和 X28 中。关于 Swift 字符串内部实现的细节,我们以后再讨论。
这里真正的问题在于:编译器究竟如何把这两个字符串传递给 String.append。
可以看到:
- 待追加的字符串按值传递,使用的是
X0和X1; - 而已有字符串却通过位于
X20中的指针进行传递。
X0 和 X1 是普通 ARM64 调用约定中的参数寄存器,而 X20 根据标准的 AAPCS64 调用约定属于被调用者保存(callee-saved)寄存器。
IDA 9.4 之前的版本知道 Swift::String 会占用两个 8 字节寄存器,但并不了解 Swift ABI 的特殊规则,因此反编译结果会错误地显示为:
Pseudocode
Swift::String v14; // x0
// ---------------------------------------------------
v14._countAndFlagsBits = v11;
v14._object = v13;
String.append(v14);
熟悉逆向的人马上就能发现问题:String.append 看起来只接收了一个参数,也就是需要追加的字符串。
更关键的是,我们完全不知道到底是哪个字符串被追加了内容。
虽然在这个简单例子中还能根据上下文推测,但 Swift 编译器实际上大量依赖 X20 来传递对象指针,因此整个反编译输出会变得几乎难以阅读 (sysin),因为对象之间的关系无法清晰表达。
为了解决这一问题,IDA 9.4 在函数原型中新增了 __swiftself 参数注解。加入该关键字后,String.append 的函数原型变为:
Pseudocode
void __swiftcall String_append(Swift::String *__swiftself self, Swift::String other);
^~~~~~~~~~~ 新关键字
更新后的函数签名使反编译器能够按照预期显示调用代码,将被操作对象作为第一个显式指针参数传递。
Pseudocode
Swift::String v14; // x0
Swift::String stack_str; // [xsp+8h] [xbp-68h] BYREF
// ---------------------------------------------------
v14._countAndFlagsBits = v11;
v14._object = v13;
String.append(self: &stack_str, other: v14);
只有带有 __swiftcall 的函数才能拥有且仅拥有一个 __swiftself 参数。
在 x86-64 Swift 二进制文件中,同样的机制对应寄存器 r13。
Async Context(异步上下文)寄存器
下面这段代码展示了另一个类似的问题。
Disassembly
__text:0000000100003EA4 ; void __swiftcall specialized thunk for @escaping @convention(thin) @async () -> ()()
__text:0000000100003EA4 _$sIetH_yts5Error_pIegHrzo_TR10async_MainTf3npf_n
...
(汇编代码保持原样)
即使忽略 PAC 相关的函数序言,我们仍然会注意到:代码中直接访问了基于 X22 的内存,却没有任何明显的初始化过程让 X22 指向有效对象。
如果这是普通的 AAPCS64 编译代码,这种现象确实令人困惑。
但实际上,Swift 编译器保留了 X22(x86-64 对应 r14)作为异步上下文寄存器(async context register)。
异步上下文保存了 async 函数暂停与恢复所需的状态,例如 continuation、resume 信息,以及最重要的 async frame 数据,这些数据往往包含整个 Swift 对象。
由于它位于专门的 callee-saved 寄存器中,因此异步函数在跨调用访问这些状态时无需反复通过普通参数传递,大幅降低了运行成本。
正因为偏离了标准 ABI,IDA 9.4 之前再次被误导。
IDA 9.3 的伪代码如下(先忽略 Swift demangler 导致的糟糕排版):
void __swiftcall specialized thunk for @escaping @convention(thin) @async () -> ()()
{
...
}
其中最关键的一句是:
// 100003ED4: variable 'v0' is possibly undefined
在 IDA GUI 中,v0 会被标记为红色,表示该寄存器在赋值之前就已经被读取。
这种红色提示通常意味着当前伪代码存在问题。
到了 IDA 9.4,我们可以将函数标记为 __swiftasync,告诉反编译器 X22 中保存的是一个有效的异步上下文指针。
Pseudocode
void __swiftcall __swiftasync specialized thunk for @escaping @convention(thin) @async () -> ()()
{
...
}
可以看到,我们并没有把异步上下文建模为一个显式参数,而是采用新的内部 intrinsic:
__swift_get_async_context()
该 intrinsic 被标记为不可传播(non-propagating),因此始终会生成一个可由用户自行指定类型的局部变量。
这样,用户可以创建例如 my_async_ctxt 这样的结构体类型,并随着分析逐步完善异步上下文的数据结构。
Error Return(错误返回)寄存器
最后登场的是 Swift ABI 中另一个相当特殊的角色:错误返回寄存器(ARM64 为 X21,x86-64 为 r12)。
下面来看第三段代码:
Disassembly
__text:0000000100000EC0 ; Swift::Int __swiftcall callTyped(_:)(Swift::Int)
...
(汇编代码保持原样)
最值得关注的是 X21 的使用方式:
首先:
MOV X21, #0
随后调用函数:
BL ...
最后:
CMP X21, #0
令人意外的是,IDA 9.4 之前会将其反编译为:
Pseudocode
Swift::Int __swiftcall callTyped(_:)(Swift::Int a1)
{
return _s10swift_errs11typedThrowsyS2iAA7MyErrorOYKF(a1);
}
原因仍然来自 Swift ABI。
按照普通 ARM64 ABI,X21 是 callee-saved 寄存器,因此反编译器认为:
初始化 X21 → 调用函数(不会修改 X21)→ 比较 X21
于是它推断:
CMP永远成立;CSEL永远选择X0;- 整个异常判断完全可以优化掉。
因此生成了错误的伪代码。
IDA 9.4 新增了 __swiftthrows 注解来解决这一问题。
与前两个关键字不同,__swiftthrows 修改的是调用者的行为,而不是函数自身。
例如,被调用函数现在会显示为:
Pseudocode
__int64 __swiftcall __swiftthrows _s10swift_errs11typedThrowsyS2iAA7MyErrorOYKF(__int64 result);
^~~~~~~~~~~~~ 新关键字
这样调用方便知道:
X21 并非 callee-saved,
而是保存 Swift 的异常状态:
0:无异常- 非
0:发生异常
(关于 Swift Typed Exceptions 的机制,这里暂不展开。)
因此,IDA 会将后续对 X21 的访问转换为:
__swift_get_error()
最终得到正确的反编译结果:
Pseudocode
Swift::Int __swiftcall callTyped(_:)(Swift::Int a1)
{
Swift::Int result; // x0
void *error; // x21
result = _s10swift_errs11typedThrowsyS2iAA7MyErrorOYKF(result: a1);
error = __swift_get_error();
if ( error != nullptr )
return -3;
return result;
}
这一改进对于分析复杂控制流非常有帮助。
__swiftthrows 可以:
- 从反混淆(demangled)的函数原型中直接推导;
- 对于没有符号但已知属于 Swift 的函数,根据函数结构自动推断。
为了避免误判,这种自动检测设计得相当严格。
因为在很多不会抛出异常的代码中,Swift 编译器仍然会把 X21 当作普通的 callee-saved 临时寄存器使用。
这一新的关键字以及调用者机制,也取代了过去 IDA 依赖 spoils list 处理 Swift 错误返回寄存器的方法,例如:
Swift::Int __swiftcall __spoils<CF,ZF,NF,VF,X0,X1,...,X17,X21,Q0,...,Q31> Thrower(Swift::Int x, Swift::Int y, Swift::Int z);
运行时符号类型定义(Runtime Symbol Typing)
ABI 建模解决了 Swift 函数彼此之间如何交互的问题。
而可读性的另一半,则来自 Swift 二进制与运行时及标准库之间的交互。
一个典型的 Swift 应用会调用数百个 swift_* 运行时入口函数,而过去 IDA 对这些函数几乎一无所知。
这些函数并不会出现在 IDA 所解析的 __swift5 元数据中(该元数据描述的是二进制定义的类型,而不是其链接的运行时),因此 IDA 最多只能猜测出类似:
__int64 __fastcall foo(_QWORD, _QWORD, ...)
这样的函数原型。
当整个程序充满了这样的调用时,Swift 的伪代码几乎全部变成了毫无意义的 _QWORD。
IDA 9.4 内置了一套经过整理的 Swift Runtime 函数原型,并根据函数名称自动应用。
因此,这些函数现在能够获得:
__swiftcall- 正确的参数类型
- 有意义的枚举类型
例如,编译器围绕 inout 和成员变量访问生成的独占访问检查函数 swift_beginAccess。
以前:
Pseudocode
// __int64 __fastcall swift_beginAccess(_QWORD, _QWORD, _QWORD, _QWORD); weak
swift_beginAccess(v1 + 16, a1, 33, 0);
swift_beginAccess(v0 + 16, v3, 1, 0);
现在:
Pseudocode
// void __swiftcall swift_beginAccess(void *pointer, void *buffer, Swift::ExclusivityFlags flags, void *pc);
swift_beginAccess((void *)(self + 16), buffer, Tracking|Modify, nullptr);
swift_beginAccess((void *)(v0 + 16), buffer, Modify, nullptr);
现在,函数原型已经是真正的 Swift 原型,而不再是猜测出来的。
注意,第三个参数被正确地定义为 Swift::ExclusivityFlags,因此:
33会显示为Tracking|Modify(0x20|0x1)1会显示为Modify
可读性大幅提升。
展望未来
一直关注 IDA 最近几个版本更新的用户应该已经知道,上述内容只是一个开始。
未来的 IDA 版本还将继续改进 Swift 分析中的诸多不足,包括:
- Strings(字符串)
- Metadata(元数据)
- Typing(类型系统)
- Mangling(符号修饰)
- ……以及更多内容。
我们的目标,是不断缩小 Swift 二进制与其应有分析体验之间的差距,为逆向工程师提供更加准确、更加自然的分析结果。
总结
总体而言,IDA 9.4 进一步扩展了对嵌入式处理器和 DSP 平台的支持范围。
这只是未来更深入分析能力的基础工作。许多改进都来自用户反馈和客户需求,在此感谢大家的建议与支持。
下载地址
IDA Pro 9.4 for macOS arm64 (Apple 芯片)
IDA Pro 9.4 for macOS x64 (Intel 处理器)
IDA Pro 9.4 for Linux arm64
IDA Pro 9.4 for Linux x64
IDA Pro 9.4 for Windows x64
更多:HTTP 协议与安全

IDA 9.4 是迈向更完善 Swift 二进制分析支持的第一步。毫不意外,我们的重点仍然是适用于 ARM64 Mach-O 的 Swift 文件,但我们的总体目标是在更多不同架构和文件格式(例如 x86-64 和 ELF)下,同样提升对 Swift 的支持。
浙公网安备 33010602011771号