指针键数据结构导致的指针泄露技术分析

本文详细分析了通过指针键数据结构泄露内存地址的创新技术。该技术利用NSKeyedArchiver反序列化机制,通过精心构造的哈希碰撞模式,在不违反内存安全的情况下远程泄露共享缓存地址。文章包含完整的技术实现细节和数学推导过程。

指针泄露通过指针键数据结构

引言

在2024年的一次Project Zero团队讨论中,我们谈到远程ASLR泄露对于利用某些类型的内存破坏漏洞会有帮助或必要,特别是在苹果设备的背景下。从"哪里是寻找远程ASLR泄露的好起点"的角度出发,这导致我们发现了一种技巧,可以在某些场景下远程泄露指针,而无需任何内存安全违规或时序攻击。

该技术要求攻击面能够接收攻击者提供的数据,反序列化生成对象,重新序列化结果对象,并将重新序列化的数据发送回攻击者。团队进行了头脑风暴,虽然我们没有进行广泛分析来测试是否存在这样的攻击面,但未能立即找到macOS/iOS上具有这种行为的具体攻击面。

我没有针对真实的攻击面,而是在macOS上使用了一个人工测试用例来测试本文描述的技术,该测试用例使用NSKeyedArchiver序列化作为目标。由于缺乏实际影响的证明,我向苹果报告了这个问题,但没有在我们的bugtracker中归档。该问题在2025年3月31日的安全更新中修复。

背景技术树

hashDoS攻击

这个故事对我来说始于2011年,当时在28C3上展示了hashDoS攻击。本质上,hashDoS是对服务的拒绝服务攻击,特别是针对使用大量攻击者控制键填充哈希表的Web服务器。

该攻击基于这样一个观察:许多哈希表实现在平均情况下每次插入/查找操作具有O(1)复杂度,但在最坏情况下相同操作具有O(n)复杂度。如果攻击者知道用于键的哈希函数,通过构造所有键映射到同一哈希桶的请求,攻击者可以使服务器花费O(n²)时间处理此类请求。

作为时序攻击的hashDoS

从稍微不同的角度来看,hashDoS的核心观察是:如果攻击者可以向哈希表插入大量选定键,并知道这些键哈希到哪个哈希桶,那么攻击者可以减慢对未来选定哈希桶的访问。

当攻击者可以导致其他哈希值保密的键插入同一哈希表时,这就变得有趣了。在实践中,这可以发生在支持混合多种键类型的哈希表中,比如JavaScript的Map。

Linux:通过指针键树的有序列表泄露对象顺序

在Linux上,非特权用户空间可以通过读取/proc/self/fdinfo/<epoll fd>来发现struct file实例在内核虚拟内存中的存储顺序。该文件通过迭代红黑树列出epoll实例监视的所有文件,该树基本上按引用struct file的虚拟地址排序。

序列化攻击

序列化对象图有各种方法。在序列化频谱的一端是基于模式的序列化,理想情况下:

  • 可序列化类型及其成员与其他类型分开声明
  • 字段明确声明它们可以指向哪些其他类型
  • 反序列化从特定起始类型开始

在序列化频谱的另一端是经典Java序列化之类的东西,任何标记为Serializable的类都可以被反序列化,序列化字段通常可以灵活指向许多不同类型。

在这个频谱的中间是根本上构建得像不安全反序列化的序列化,但添加了一些粗粒度过滤器,只允许反序列化对象具有允许列表中的类型。

人工测试用例

本文描述技术的目标是通过单次执行泄露指向"共享缓存"的指针。测试用例使用NSKeyedUnarchiver.unarchivedObjectOfClasses反序列化攻击者提供的对象图,重新序列化结果,并写回结果序列化数据。

构建模块

NSNull/CFNull单例

CFNull类型很特殊:只有一个单例实例kCFNull,存储在共享缓存中。当反序列化NSNull对象时,这实际上不会创建新对象,而是使用单例。

在CFNull的CFRuntimeClass中,没有提供哈希处理程序。当在像__CFNullClass这样没有实现->hash处理程序的类型的对象上调用CFHash时,使用对象的地址作为哈希码。

NSNumber

NSNumber类型封装数字并支持几种数字类型;其哈希处理程序__CFNumberHash使用_CFHashInt对32位整数进行哈希处理。

NSDictionary

NSDictionary类型的实例是不可变哈希表,可以包含任意类型的键。键哈希通过简单取模操作映射到哈希表桶:hash_code % num_buckets。

攻击

基本思路:通过序列化NSDictionary中的键顺序进行信息泄露

如果目标进程使用攻击者选择的NSNumber键填充NSDictionary,攻击者可以通过使用数字控制将使用哪些哈希桶。如果目标进程然后插入NSNull键,并序列化结果NSDictionary,NSNull键在字典序列化键中的位置将泄露关于NSNull哈希的信息。

扩展:泄露整个桶索引

通过使用占用奇数编号桶并留空偶数编号桶的模式重复攻击,可以获取关于hash_code % num_buckets的更多信息。

通过结合使用偶数桶占用模式和奇数桶占用模式的NSDictionary的信息,可以确定hash_code % num_buckets的确切值。

数学:泄露整个哈希码

通过使用不同的哈希表大小重复此过程,可以泄露关于哈希码的更多信息。基于计算哈希码模一堆不同质数的结果值,可以使用扩展欧几里得算法计算哈希码模所有这些质数乘积的值。

组合起来

要泄露共享缓存中NSNull单例的地址,攻击者必须发送包含一个大容器的序列化数据,该容器为每个感兴趣的质数包含两个具有偶数和奇数索引模式的NSDictionary实例。

目标进程然后必须反序列化此数据,重新序列化,并将其发送回攻击者。之后,攻击者可以确定NSNull在每个NSDictionary中存储在哪个桶中,使用NSDictionary对中的桶索引确定每个哈希表大小的hash_code % num_buckets,然后使用扩展欧几里得算法获取哈希码,即NSNull单例的地址。

重现器

我为此问题编写了重现器,包括在目标机器上运行的我自己的受害者程序,以及向目标机器提供序列化数据并从目标接收重新序列化数据的攻击者程序。

结论

这是一个相当理论性的攻击;但我认为它表明,如果一切条件合适,在键数据结构中使用指针作为对象哈希可能导致指针泄露,即使不使用时序攻击。

我的示例依赖于受害者重新序列化数据;但这种技术的时序攻击版本也可能实现,需要更多请求和足够精确的测量。

针对此问题最稳健的缓解措施是避免使用对象地址作为查找键,或者使用键控哈希函数对它们进行哈希处理。然而,这可能会带来负面性能影响。

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计