很好,既然你不小心点进来了,那么只要你不小心看完这篇文章,那么你将不小心的学会如下技能:
(1)分析 Swift 类的底层原理;
(2)逆向 Swift;
(3)使用 ιldb 脚本;
(4)使用 xcrun swift-demangle 工具;
(5)ChaCha20Poly1305 算法在 Swift 中的应用;
笔者将从 Swift String 的底层原理切入,循序渐进地带大家走进 Swift 逆向的世界,逐步理解其核心逻辑与实践思路。
首先打开 XCode 创建一个 Swift 项目,然后在入口类的构造函数中添加如下代码,并且点击 序号 15 打上断点

随后点击运行,会自动卡在断点处,然后点击步过

随后可以看到 Swift String 的内部结构了。

接下来打开 Swift 源码

可以看到 String 结构体的构造函数中接收了一个 _StringGuts 对象,继续跟进 _StringGuts 结构体

阅读源码后可以发现 _StringGuts 结构体的构造函数接收了一个 _StringObject 对象,并且这个 _StringObject 对象是通过传入 empty 参数进行构造的,所以等会可以检索 "init(empty" 来定位 _StringObject 的构造函数

该私有构造函数中,根据不同平台的指针位宽(64/32/16 位)适配空字符串的底层内存布局,保证空字符串在所有平台下的内存表示一致。
(3.1)_pointerBitWidth 编译条件
_pointerBitWidth(_64):Swift 编译器内置的私有编译标记,判断当前平台是否为 64 位架构(如 arm64/iOS、x86_64/macOS);
_pointerBitWidth(_32)/_16:对应 32 位 / 16 位架构(极少用,如老旧的 armv7 设备、嵌入式平台);
(3.2)64 位下的代码逻辑
先明确 Swift String 的底层核心字段
_countAndFlagsBits = 0:空字符串长度为 0,所有标志位清零;
Builtin.valueToBridgeObject:Swift 内置(Builtin)函数,将「空字符串的静态内存地址」转换为桥接对象指针(_object);
Nibbles.emptyString:Swift 标准库中预定义的「空字符串常量」(全局唯一,避免重复创建空字符串实例,类比 C++ 的 std::string::empty() 优化)。
Nibbles 是个枚举,源码中给它加了多个extension。进一步查看源码可以看到 Nibbles.emptyString,调用方法:small(isASCII: Bool)

通过调试也可以发现存储的地址是 0xe000000000000000

64 位平台设计逻辑
空字符串无需动态分配内存,直接复用全局唯一的 emptyString 静态地址,_object 指向该地址,_countAndFlagsBits 置 0,实现极致的内存效率(无堆分配)。
(3.3)32 位下的代码逻辑
在 StringObject.swift 文件下检索 init(count:,可以看到 32 位下调用的构造函数,因为通常是 64位,故 32位不做分析,感兴趣的可以自行了解。

经过上面的分析,可以知道一个字符串变量至少占用了16字节。用 MemoryLayout 工具进行验证也确实是16个字节。

已知字符串变量至少占用 16个字节,那么在内存中是什么样的呢?
首先前往 [Mems] 下载 或者直接从 Mems.zip 中解压得到 Mems.swift 文件,然后导入到项目中,最后键入下面的代码
上面的程序运行后可以得到下面的结果

空字符串的_object = 0xe000000000000000,字符串 "1" 的_object = 0xe100000000000000,据此可合理推测,e后四位的十六进制数值用于存储小字符串的长度,接下来将对此展开进一步验证。
查看字符串 "123" 的内存

查看字符串 "0123456789ABCDE" 的内存

查看字符串 "0123456789ABCDEF" 的内存

字符串长度小于 16 时,e 后的4位用于表示字符串长度,并且字符串内容存储在另外 15 个字节中。

通过对比 "0123456789ABCDEFG" 与 "0123456789ABCDEF" 的内存数据可推测,字符串长度不再存储于_object 中,而是由_countAndFlagsBits 字段存储;而_object 中记录的地址偏移 0x20 后,即为字符串的实际内存地址。
查看字符串 "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" 的内存

查看字符串 "泥嚎,我正在分析 swift string!" 的内存


大字符串的 _object 存储的是字符串的内存地址,偏移 0x20 就可以读取到字符串内容
测试环境分为 [ Swift 源码 ]、 [ 项目源码 ] 和 一个 [ IPA 文件 ],Swift 源码和项目源码用来理解 Swift 底层结构,ipa 文件用来辅助分析。
SwiftDemo.zip
连接上 iPhone,打开 爱思助手,然后按照下面的步骤操作。当然,如果你有巨魔,就可以跳过这个地方了。



IDA 中 Swift 函数符号是经过特殊处理的,形如 _$s10Foundation4UUIDV10uuidStringSSvg 和 _$sSS21_builtinStringLiteral17utf8CodeUnitCount7isASCIISSBp_BwBi1_tcfC 这种,前者看起来还相对明了,后者看起来就稍微费劲点,此时可以使用 xcrun swift-demangle 命令进行函数符号还原即可。
首先了解一下什么是 xcrun。xcrun 是 macOS 下 Xcode 提供的工具链调度工具,用于定位和执行 Xcode 安装的各种开发工具(如 swiftc、lldb、clang、nm、swift-demangle 等)。
所以 xcrun swift-demangle 是 macOS 下通过 xcrun 调度的 Swift 符号还原工具,核心作用是将 Swift 编译后生成的「名字重整(Mangled)符号」(晦涩的乱码式字符串)还原为可读的 Swift 函数 / 类 / 属性名。
使用方法如下
可能遇见的问题如下,解决方法就是使用单引号将函数符号括起来

首先拟定核心目标:找到 ChaCha20 算法的明文、密钥、nonceData 及密文。
明确方向后,第一步需先了解 Swift 中 ChaCha20 算法的使用方式,你可通过 AI 自行查询,也可直接参考下文的 encryptWithChaCha20Poly1305 函数;
下面是 [ 项目源码 ] 中的 encryptWithChaCha20Poly1305(message:keyHex:nonceHex:) 函数 的具体实现。
因为这里是知道 encryptWithChaCha20Poly1305 函数传入的是三个 Swift 字符串,所以可以直接通过分析寄存器:x0, x1, x2, x3, x4, x5,从而找到明文、密钥以及 nonce 是什么。接下来进入实战环节:先使用 debugserver 启动 SwiftDemo,再通过 lldb 或者 xia0lldb 完成连接(之所以不使用 XCode 自带调试器,是因为在函数中打断点后,它会跳过大量汇编代码,会对后续分析造成阻碍,最主要是 静态偏移会发生变化)。
(2.1)首先用 IDA 打开 SwiftDemo.debug.dylib 二进制,并且找到 encryptWithChaCha20Poly1305 函数的偏移地址

(2.2)使用 lldb 进行动态调试,笔者这边使用了 ιldb,这是笔者开发的一个用于 lldb 的脚本工具。

(2.3)下好断点后,直接 continue 一下,进入 encryptWithChaCha20Poly1305 函数,紧接着跟着下面的步骤进行操作

(2.4)从这可以得到 encryptWithChaCha20Poly1305 函数传入的三个参数的内容分别为
(2.5)那这样总结一下规律,便可以写出一个 ιldb 脚本,这样下次解析就十分方便了,如下所示

看到这里有人就要说了,这是我们知道函数的原本模样,所以分析起来轻轻松松,换一个 App 还是有点难以入手,比如下面这种没有函数符号的函数,那该如何判断各个参数的含义呢?

那肯定就不判断了呀,这种就找返回值,反方向往前推,如下所示。

假设 encryptWithChaCha20Poly1305 函数已被混淆为 sub_ 形式,其参数也相应变为 id a1, id a2 这类格式,不过我们已经成功定位到此处的返回值正是所需数据;至于定位的具体方法,则需要一定的技术功底支撑,无论是采用 hook 手段还是静态分析方式均可实现,而这部分内容并非本节的重点。
接下来我将一步一步介绍如何通过返回值找到 Swift 的重要数据。
(3.1) 打开 IDA 找到 encryptWithChaCha20Poly1305 函数, RET 语句的位置,如下所示

(3.2)可以打开 lldb,并加载 ιldb 脚本,将断点下到此处看看,然后尝试解析为 Swift 字符串,得到的结果如下所示

(3.3)验证成功,接下来打开 IDA,跟一下是怎么来的

继续跟

上图的 ② 说是 Data 对象可能无法让人信服,那么接下来,我将一步一步证明。
(3.3.1)首先打开 XCode,随便写一个 Data 对象,代码如下所示,
(3.3.2)将断点断在 testSwiftBase64Encode 函数,当断点步过 show(val: &data) 这一行时,观察下面的信息。

有了前面分析 Swift String 的经验,相信此处也有一定的手法,毕竟像你这样的大师 ◖⚆ᴥ⚆◗ 。
(3.3.3)此处看到了字节 “0x0c”,,这个想必是 Data 对象中字节数组的一个长度了,并且和字符串一样,可能是分为了大 Data 和小 Data,而且长度的阈值便是 "0x0e"。空口无凭,接下来继续验证。
(3.3.3.1) 首先将字符串修改为一个你喜欢的字符串,但是长度必须是 14,然后进行调试,结果如下所示:

可以发现那个字节确实是长度无疑了,那么接下来验证长度的阈值是 “0x0e”
(3.3.3.2)将字符串修改为一个长度为 15 的字符串,然后进行调试,结果如下所示

这个地方稍微复杂一点,我大概解释如下几个地方:
首先最重要的就是 ⑧ 处,可以看到 str 变量的地址是:0x16b9093b0,所以不要把这个地方当成是 Data 中字节数组的地址,而且跟 ④ 中,_bytes 的指针也对不上,所以还需要继续找;
根据前面分析大字符串的经验,合理怀疑这个地方的 "0x0f" 说的是 Data 中字节数组的长度。其次就是 0x40006000021054f0 是存储着字节数组的地址。
[培训]Windows内核深度攻防:从Hook技术到Rootkit实战!
最后于 13小时前
被αβγδεξπ编辑
,原因: