首页
社区
课程
招聘
[原创] Swift 逆向 之 授人以鱼不如授人以渔
发表于: 11小时前 174

[原创] Swift 逆向 之 授人以鱼不如授人以渔

11小时前
174

很好,既然你不小心点进来了,那么只要你不小心看完这篇文章,那么你将不小心的学会如下技能:

(1)分析 Swift 类的底层原理;

(2)逆向 Swift;

(3)使用 ιldb 脚本;

(4)使用 xcrun swift-demangle 工具;

(5)ChaCha20Poly1305 算法在 Swift 中的应用;

笔者将从 Swift String 的底层原理切入,循序渐进地带大家走进 Swift 逆向的世界,逐步理解其核心逻辑与实践思路。

首先打开 XCode 创建一个 Swift 项目,然后在入口类的构造函数中添加如下代码,并且点击 序号 15 打上断点

QQ_1765422459209

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

QQ_1765422562768

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

QQ_1765422605874

接下来打开 Swift 源码

QQ_1765419592151

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

QQ_1765419705917

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

QQ_1765419759238

该私有构造函数中,根据不同平台的指针位宽(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)

QQ_1765436959302

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

QQ_1765422773012

64 位平台设计逻辑

空字符串无需动态分配内存,直接复用全局唯一的 emptyString 静态地址,_object 指向该地址,_countAndFlagsBits 置 0,实现极致的内存效率(无堆分配)。

(3.3)32 位下的代码逻辑

StringObject.swift 文件下检索 init(count:,可以看到 32 位下调用的构造函数,因为通常是 64位,故 32位不做分析,感兴趣的可以自行了解。

QQ_1765420132253

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

QQ_1765437667976

已知字符串变量至少占用 16个字节,那么在内存中是什么样的呢?

首先前往 [Mems] 下载 或者直接从 Mems.zip 中解压得到 Mems.swift 文件,然后导入到项目中,最后键入下面的代码

上面的程序运行后可以得到下面的结果

QQ_1765439113226

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

查看字符串 "123" 的内存

QQ_1765439779485

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

QQ_1765439849837

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

QQ_1765440096654

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

QQ_1765442903546

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

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

QQ_1765443276529

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

QQ_1765443744257

QQ_1765443697548

大字符串的 _object 存储的是字符串的内存地址,偏移 0x20 就可以读取到字符串内容

测试环境分为 [ Swift 源码 ][ 项目源码 ] 和 一个 [ IPA 文件 ],Swift 源码和项目源码用来理解 Swift 底层结构,ipa 文件用来辅助分析。

SwiftDemo.zip

连接上 iPhone,打开 爱思助手,然后按照下面的步骤操作。当然,如果你有巨魔,就可以跳过这个地方了。

QQ_1765961203314

QQ_1765961890582

QQ_1765961991857

IDA 中 Swift 函数符号是经过特殊处理的,形如 _$s10Foundation4UUIDV10uuidStringSSvg_$sSS21_builtinStringLiteral17utf8CodeUnitCount7isASCIISSBp_BwBi1_tcfC 这种,前者看起来还相对明了,后者看起来就稍微费劲点,此时可以使用 xcrun swift-demangle 命令进行函数符号还原即可。

首先了解一下什么是 xcrunxcrun 是 macOS 下 Xcode 提供的工具链调度工具,用于定位和执行 Xcode 安装的各种开发工具(如 swiftclldbclangnmswift-demangle 等)。

所以 xcrun swift-demangle 是 macOS 下通过 xcrun 调度的 Swift 符号还原工具,核心作用是将 Swift 编译后生成的「名字重整(Mangled)符号」(晦涩的乱码式字符串)还原为可读的 Swift 函数 / 类 / 属性名。

使用方法如下

可能遇见的问题如下,解决方法就是使用单引号将函数符号括起来

QQ_1765856031447

首先拟定核心目标:找到 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 函数的偏移地址

QQ_1766126760152

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

QQ_1766126665969

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

QQ_1766127484334

(2.4)从这可以得到 encryptWithChaCha20Poly1305 函数传入的三个参数的内容分别为

(2.5)那这样总结一下规律,便可以写出一个 ιldb 脚本,这样下次解析就十分方便了,如下所示

QQ_1766133355905

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

QQ_1766127856397

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

QQ_1766128255957

假设 encryptWithChaCha20Poly1305 函数已被混淆为 sub_ 形式,其参数也相应变为 id a1, id a2 这类格式,不过我们已经成功定位到此处的返回值正是所需数据;至于定位的具体方法,则需要一定的技术功底支撑,无论是采用 hook 手段还是静态分析方式均可实现,而这部分内容并非本节的重点。

接下来我将一步一步介绍如何通过返回值找到 Swift 的重要数据。

(3.1) 打开 IDA 找到 encryptWithChaCha20Poly1305 函数, RET 语句的位置,如下所示

QQ_1766134264267

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

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

391e09372a4b0bd4ba751c36f18305a3

继续跟

QQ_1766206238038

上图的 ② 说是 Data 对象可能无法让人信服,那么接下来,我将一步一步证明。

(3.3.1)首先打开 XCode,随便写一个 Data 对象,代码如下所示,

(3.3.2)将断点断在 testSwiftBase64Encode 函数,当断点步过 show(val: &data) 这一行时,观察下面的信息。

QQ_1766136039729

有了前面分析 Swift String 的经验,相信此处也有一定的手法,毕竟像你这样的大师 ◖⚆ᴥ⚆◗ 。

(3.3.3)此处看到了字节 “0x0c”,,这个想必是 Data 对象中字节数组的一个长度了,并且和字符串一样,可能是分为了大 Data 和小 Data,而且长度的阈值便是 "0x0e"。空口无凭,接下来继续验证。

(3.3.3.1) 首先将字符串修改为一个你喜欢的字符串,但是长度必须是 14,然后进行调试,结果如下所示:

QQ_1766196053096

可以发现那个字节确实是长度无疑了,那么接下来验证长度的阈值是 “0x0e”

(3.3.3.2)将字符串修改为一个长度为 15 的字符串,然后进行调试,结果如下所示

QQ_1766196620008

这个地方稍微复杂一点,我大概解释如下几个地方:

首先最重要的就是 ⑧ 处,可以看到 str 变量的地址是:0x16b9093b0,所以不要把这个地方当成是 Data 中字节数组的地址,而且跟 ④ 中,_bytes 的指针也对不上,所以还需要继续找;

根据前面分析大字符串的经验,合理怀疑这个地方的 "0x0f" 说的是 Data 中字节数组的长度。其次就是 0x40006000021054f0 是存储着字节数组的地址。


[培训]Windows内核深度攻防:从Hook技术到Rootkit实战!

最后于 11小时前 被αβγδεξπ编辑 ,原因:
上传的附件:
收藏
免费 5
支持
分享
最新回复 (2)
雪    币: 7
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
2
感谢分享
10小时前
0
雪    币: 5694
活跃值: (6100)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
3
感谢,先收藏。
10小时前
0
游客
登录 | 注册 方可回帖
返回