简介
这是三篇系列文章中的第一篇,将详细介绍如何在iOS 12.4上远程利用iMessage中的漏洞,无需任何用户交互,它是我在2019年12月的36C3会议上演讲的一个更详细的版本。
本系列的第一部分对该漏洞进行了深入的分析,第二部分介绍了一种远程破坏ASLR的技术,第三部分介绍了如何获得远程代码执行(RCE)。
本系列中介绍的攻击使攻击者能够在几分钟内远程控制用户的iOS设备,攻击者仅拥有用户的Apple ID(移动电话号码或电子邮件地址)。之后,攻击者可以窃取文件、密码、2FA代码、短信和电子邮件以及其他用户APP数据。还可以远程激活麦克风和摄像头。所有这些都是可能的,而无需向用户显示任何交互(打开攻击者发送的URL或者消息通知)。首先攻击利用CVE-2019-8641漏洞绕过ASLR,然后在目标设备的沙盒外执行代码。
这项研究的主要原因是:仅给出一个远程内存破坏漏洞,是否有可能在iPhone上实现远程代码执行而没有其他漏洞,并且无需任何形式的用户交互?这个系列文章表明这实际上是可能的。
这个漏洞是与娜塔莉·西尔瓦诺维奇(Natalie Silvanovich)合作的漏洞研究项目中的一部分,已于2019年7月29日报告给Apple,这个漏洞首先在iOS 12.4.1中得到了缓解,于8月26日发布,使易受攻击的代码无法通过iMessage访问,在2019年10月28日发布的iOS 13.2中完全修复。根据在exploit开发过程中获得的见解,提出了进一步的强化措施,如果实施这些措施,将来会使类似的漏洞利用变得更加困难。由于其中一些强化措施与其他messenger服务和手机操作系统相关,因此在本系列文章中会提到它们,并在最后进行总结。
对于安全研究人员来说,这里提供了一个针对iPhone XS上iOS 12.4的POC。为了防止滥用,它在整个利用过程中显示多个通知,提醒受害者正在遭受攻击。而且,它不能实现本地代码执行(即执行shellcode),因此它很难与现有的特权提升漏洞相结合。这些限制都不需要进一步的漏洞来弥补,只需要有能力的攻击者重新设计exploit。内置的限制和通知仅仅是为了阻止非研究人员滥用而设计的。需要注意的是,专业的研究员很可能已经具备了利用已公布的代码的能力,无论是自己发现漏洞还是通过补丁对比来发现漏洞,或者通过CVE-2019-8646与其他任何内存破坏漏洞相结合,进行1-day的漏洞利用。
和之前一样,我希望这项研究能够帮助其他安全研究人员和软件开发人员,介绍如何绕过缓解措施,并分享一些我的想法,以确保我所发现的漏洞能够得到缓解。
iMessage架构
在最终向用户显示通知并将消息写入messages数据库之前,传入的iMessages会经过多个服务和框架。下图描述了在iOS 12.4上处理iMessage而不需要与用户交互的主要服务。(红色边框表示该进程存在沙盒)
在以下截图中可以看到:
Natalie Silvanovich在之前的一篇文章中介绍了远程可访问的攻击面,其中包括众所周知的NSKeyedUnarchiver API以及iMessage数据格式。在本系列文章中,必须知道到NSKeyedUnarchiver API中的漏洞通常可以在两种不同的上下文中触发:在沙盒的imagent和非沙盒SpringBoard进程(后者管理主iOS UI,包括主屏幕),两种情况在漏洞利用方面都各有利弊。例如,虽然SpringBoard已经在沙箱外部运行,似乎是更好的目标,但它也是一个比较重要的系统进程,当崩溃时会导致设备明显的“注销重启桌面”,屏幕突然变黑,并且在锁定屏幕出现之前加载图标会出现几秒。
从iOS13开始,NSKeyedUnarchiver
数据的解码不再发生在SpringBoard
内部,而是发生在沙箱IMDPersistenceAgent
中,这大大减少了iMessage的非沙箱攻击面。
iMessage 传递
为了通过iMessage传递exploit,需要能够向目标发送自定义iMessage。这需要与Apple的服务器进行交互,并处理iMessage的end2end
加密。用于这项研究的一种简单的方法是通过使用FRIDA这样的工具将imagent
中处理iMessage的代码HOOK到现有代码中。这样,可以通过以下方式在macOS上运行的脚本发送自定义iMessage:
- 1.生成所需的payload(例如触发NSKeyedUnarchiver错误)并将其存储到磁盘
- 2.调用一个小的Apple脚本,该脚本显示Messages.app将带有占位符内容(例如“ REPLACEME”)的消息发送给目标
- 3.使用frida来Hook imagent,并将包含占位符的iMessages的内容替换为payload文件的内容
同样,也可以通过使用frida来Hook imagent中的receiver
函数。
例如,当从Messages.app发送“REPLACEME”消息时,将向receiver发送以下iMessage(编码为二进制plist):
{
gid = "008412B9-A4F7-4B96-96C3-70C4276CB2BE";
gv = 8;
p = (
"mailto:sender@foo.bar",
"mailto:receiver@foo.bar"
);
pv = 0;
r = "6401430E-CDD3-4BC7-A377-7611706B431F";
t = "REPLACEME";
v = 1;
x = "<html><body>REPLACEME</body></html>";
}
然后,frida钩子会在消息被序列化、加密发送到Apple服务器之前对其进行修改:
{
gid = "008412B9-A4F7-4B96-96C3-70C4276CB2BE";
gv = 8;
p = (
"mailto:sender@foo.bar",
"mailto:receiver@foo.bar”
);
pv = 0;
r = "6401430E-CDD3-4BC7-A377-7611706B431F";
t = "REPLACEME";
v = 1;
x = "<html><body>REPLACEME</body></html>";
ati = <content of /private/var/tmp/com.apple.messages/payload>;
}
这将导致接收设备上的imagent使用NSKeyedUnarchiver
API解码ati
字段中的数据。可以在这里找到允许发送自定义消息和转储传入消息的示例代码。
CVE-2019-8641
用于此研究的漏洞是CVE-2019-8641,对应于Project Zero issue 1917。这是NSKeyedUnarchiver
组件中的另一个bug, Natalie已经对此进行了深入的讨论。虽然可能以类似的方式在本研究的NSKeyedUnarchiver
组件中发现的其他内存破坏漏洞,但该漏洞似乎最容易利用。
这个BUG发生在NSSharedKeyDictionary
的解档(unarchiving)过程中,NSSharedKeyDictionary
是一种特殊的NSDictionary
。其中,在NSSharedKeySet
中预先声明了keys
,以允许更快地访问元素。此外,NSSharedKeySet
本身具有subSharedKeySet
,基本上是可以构建一个KeySet的链表。
要理解这个漏洞,首先需要了解NSKeyedUnarchiver
序列化格式。下面是一个简单的存档文件,其中包含了由plutil(1)打印的序列化NSSharedKeyDictionary(NSKeyedArchiver将对象图编码为plist),并添加了一些注释:
{
"$archiver" => "NSKeyedArchiver"
# The objects contained in the archive are stored in this array
# and can be referenced during decoding using their index
"$objects" => [
# Index 0 always contains the nil value
0 => "$null"
# The serialized NSSharedKeyDictionary
1 => {
"$class" => <CFKeyedArchiverUID>{value = 7}
"NS.count" => 0
"NS.sideDic" => <CFKeyedArchiverUID>{value = 0}
"NS.skkeyset" => <CFKeyedArchiverUID>{value = 2}
}
# The NSSharedKeySet associated with the dictionary
2 => {
"$class" => <CFKeyedArchiverUID>{value = 6}
"NS.algorithmType" => 1
"NS.factor" => 3
"NS.g" => <00>
"NS.keys" => <CFKeyedArchiverUID>{value = 3}
"NS.M" => 6
"NS.numKey" => 1
"NS.rankTable" => <00000000 0001>
"NS.seed0" => 361949685
"NS.seed1" => 2328087422
"NS.select" => 0
"NS.subskset" => <CFKeyedArchiverUID>{value = 0}
}
# The keys of the NSSharedKeySet
3 => {
"$class" => <CFKeyedArchiverUID>{value = 5}
"NS.objects" => [
0 => <CFKeyedArchiverUID>{value = 4}
]
}
# The value of the first (and only) key
4 => "the_key"
# ObjC classes are stored in this format
5 => {
"$classes" => [
0 => "NSArray"
1 => "NSObject"
]
"$classname" => "NSArray"
}
6 => {
"$classes" => [
0 => "NSSharedKeySet"
1 => "NSObject"
]
"$classname" => "NSSharedKeySet"
}
7 => {
"$classes" => [
0 => "NSSharedKeyDictionary"
1 => "NSMutableDictionary"
2 => "NSDictionary"
3 => "NSObject"
]
"$classname" => "NSSharedKeyDictionary"
}
]
# A reference to the root object in the archive
"$top" => {
"root" => <CFKeyedArchiverUID>{value = 1}
}
"$version" => 100000
}
这里需要注意的一点是,这种序列化格式支持通过CFKeyedArchiverUID
值引用其中包含的对象。在解档(unarchiving)期间,NSKeyedUnarchiver
将保留UID与对象的映射,以便可以从同一归档中的多个其他对象引用单个对象。此外,在调用对象的initWithCoder
方法(在解档期间使用的构造函数)之前,会将引用添加到映射中。这样,可以对对象图进行解码,在这种情况下,可以在当前调用堆栈中进一步解档时引用该对象。有趣的是,在这种情况下,第一个对象在被引用时可能尚未完全初始化。这会产生潜在的BUG,而CVE-2019-8641正是这种情况。
下面是Objective-C (ObjC)伪代码,由NSSharedKeyDictionary
和NSSharedKeySet
类的initWithCoder
实现。不熟悉ObjC的读者可以想到以下代码结构:
[obj doXWith:y and:z];
作为对象的方法调用,类似于
obj->doX(y, z);
在C++中
-[NSSharedKeyDictionary initWithCoder:coder] {
self->_keyMap = [coder decodeObjectOfClass:[NSSharedKeySet class]
forKey:"NS.skkeyset"];
// ... decode values etc.
}
-[NSSharedKeySet initWithCoder:coder] {
self->_numKey = [coder decodeInt64ForKey:@"NS.numKey"];
self->_rankTable = [coder decodeBytesForKey:@"NS.rankTable"];
// ... copy more fields from the archive
self->_subSharedKeySet = [coder
decodeObjectOfClass:[NSSharedKeySet class]
forKey:@"NS.subskset"]];
NSArray* keys = [coder decodeObjectOfClasses:[...]
forKey:@"NS.keys"]];
if (self->_numKey != [keys count]) {
return fail(“Inconsistent archive);
}
self->_keys = calloc(self->_numKey, 8);
// copy keys into _keys
// Verify that all keys can be looked up
for (id key in keys) {
if ([self indexForKey:key] == -1) {
NSMutableArray* allKeys = [NSMutableArray arrayWithArray:keys];
[allKeys addObjectsFromArray:[self->_subSharedKeySet allKeys]];
return [NSSharedKeySet keySetWithKeys:allKeys];
}
}
}
indexForKey
的routine实现在下面的initWithCoder(第20行)末尾调用。
它使用键的hash来索引到_rankTable
,并使用结果作为索引到_keys
。它递归搜索subSharedKeySet
中的key,直到找到它或不再有subSharedKeySet
可用:
-[NSSharedKeySet indexForKey:] {
NSSharedKeySet* current = self;
uint32_t prevLength = 0;
while (current) {
// Compute a hash from the key and other internal values of
// the KeySet. Convert the hash to an index and ensure that it
// is within the bounds of rankTable
uint32_t rankTableIndex = ...;
uint32_t index = self->_rankTable[rankTableIndex];
if (index < self->_numKey) {
id candidate = self->_keys[index];
if (candidate != nil) {
if ([key isEqual:candidate]) {
return prevLength + index;
}
}
prevLength += self->_numKey;
current = self->_subSharedKeySet;
}
return -1;
}
根据上面的逻辑,下面的对象图在反序列化时会导致内存损坏:
取消存档期间会发生以下情况:
- 1.未对
NSSharedKeyDictionary
进行存档,然后对SharedKeySet
进行解档 - 2.SharedKeySet1的
initWithCoder
运行到解压它的subSharedKeySet(第6行)的位置-
_numKey
受到攻击者的完全控制,因为尚未对其进行任何检查(只有在取消存档_keys
之后才会发生此检查) -
_rankTable
也被完全控制 -
_keys
仍然是nullptr(因为ObjC对象是通过calloc分配的)
-
- 3.SharedKeySet2完全未存档。它的
subSharedKeySet
是对SharedKeySet1的引用(仍在未归档中)。最后,它为_keys
数组中的所有键调用indexForKey
:(第20行) - 4.由于SharedKeySet2的
rankTable
都为零,所以只有第一个key可以自己查找(请参阅indexForKey:中的第8至15行)。然后在SharedKeySet1上查找第二个key,在这里,由于_numKey
和_rankTable
的内容被完全控制,代码将在第10行使用受的控索引对_keys
(为nullptr)进行索引,从而导致崩溃。
下图显示了崩溃时简化的调用堆栈:
因此,现在取消了大部分任意地址的引用(在本例中是0x414140,因为索引乘以8,即元素大小),并将结果用作ObjC对象指针(“id”)。
但是,对于可以使用此BUG访问的地址有两个限制:
- 1.该地址必须被8整除(因为_keys数组存储指针大小的值)
- 2.必须小于32G,因为索引是4字节无符号整数
幸运的是,在iOS上,内存中最有用的Things都位于0x800000000 (32G)以下,因此可以使用这个原语(primitive)进行访问。
事实证明,在适当的情况下,即使是看似无用的空指针解引用,也可以变为功能强大的exploit原语。
此时,没有相关目标进程的地址空间信息。因此,必须要先构造某种信息泄漏。这将是下一篇文章的主题。
本文翻译自Project Zero Blog