-
-
[翻译]Pegasus内核漏洞分析与利用(上)(CVE-2016-4655/CVE-2016-4656)
-
2016-10-18 14:39 5297
-
Pegasus内核漏洞分析与利用(CVE-2016-4655/CVE-2016-4656)
译者:rodster(看雪ID:leixyou)
完整PDF版本: 002.Pegasus内核漏洞分析与利用(CVE-2016-4655 4656).pdf
原文地址:http://jndok.github.io/2016/10/04/pegasus-writeup/
原文作者:Jndok
0x01.前言(Introduction)
大家好!在本文章中我决定谈谈最近被Pegasus间谍软件所使用的OS X/IOS 的两个内核漏洞,漏洞影响范围OS X 10.11.6版本和IOS 9.3.4版本。我还会对bug原理和漏洞利用技术手段做些深入分析。
因为这是我发表了第一篇文章,所以文章不可避免有些错误和粗心的地方,请各位看官多点耐心。如果你发现任何错误或者对有些事有疑惑等待,请发邮件给我me@jndok.net,我会尽我所能帮你解决问题。
在阅读之前的最后一件需要注意的事:我们仅仅关注OS X内核。这是因为在IOS实际环境中由于采用的安全措施,利用这两个漏洞更加复杂。这篇文章旨在入门,因此我们会让文章更加简单。
这是本文章结构:
0x01.前言 (18日发布)
0x02.OSUnserializeBinary的概述:数据格式、细节及运作方式 (18日发布)
0x03.两个CVE漏洞分析 (20日发布)
0x04.两个CVE漏洞攻击——最有趣的地方! (20日发布)
0x05.总结 (20日发布)
0x02.Overview(概述) of OSUnserializeBinary
XNU内核实现了一个叫做OSUnserializeXML的程序,被用来并行化一个XML格式输入到基本内核数据对象中。
最近,添加了一个新功能OSUnserializeBinary。它的目的是和XML一模一样,但是这种格式处理是不同的。OSUnserializeBinary转化一个二进制格式为基本内核数据对象。尽管没有正式文件规定,但这个格式很简单。在分析功能代码之前我们会描述这种格式。
OSUnserializeBinary’s Binary Format
二进制数据是OSUnserializeBinary过程是一个简单的uint32(32位整数)数据流的连续值。也许一个32位整数数组会更好代表这个概念。一连串数字一个接一个,每个数值描述一些东西。第一个数值所要求的有效的数据流是一个独特的签名(0x000000d3)。其他每个数值使用自己的一些比特位来描述它的数据类型和它的大小。数字能代表纯粹的原始数据。
如你所见第31个比特位被用来指示当前集合是否结束。第30位->24比特位被用来储存目前的数据类型,并且比特位23 ->0被用来存储目前元素长度。
因为一个例子通常能够让一些事情更清楚,例子拿好:
上面的二进制数据对应:
如你所见,我们标记了字典 (dict)作为第一个集合(0x81000000)的上层一个元素,并且boolean元素作为第二个集合 (0x8b000001)的上层一个元素。然后我们编码了字符数据 (AAA) 直接内联,包括null结束字节(0x00414141)。最后,对于boolean元素,没必要编码内联数据,因为它的大小(最后一个二进制数据)决定它是TRUE还是FALSE。
需要注意的一件重要的事情是,集合的概念和标记集合的结束。一个集合基本上与一组对象在同一层级。例如,一个字典里面的元素全部属于同一集合。当为了OSUnserializeBinary制作二进制目录时,标记集合的结束是一件很重要的事情,即设置第一个比特位(在枚举中标志kOSSerializeEndCollection)。为了更好的阐述概念,下面有个XML示例:
你可以看到这里有不同的层级或者集合。你可以看到我是如何标记在每个层级/集合的每个最新。如果你忘了做这个,OSUnserializeBinary会退出并返回一个非法参数错误,所以请记在心上!也请记住在字典为外层空间情况下,我标记它作为上层元素,因为它是唯一一个level 0元素。现在希望你会更好地明白二进制格式!现在我们准备开始分析OSUnserializeBinary的代码。
OSUnserializeBinary Analysis
OSUnserializeBinary仅仅在OSUnserializeXML中被调用。如果这个函数在开始输入数据就探测到独特的二进制信号(0x000000d3),函数就会知道数据是二进制格式的,不是XML,并且传递一切给OSUnserializeBinary。
OSUnserializeBinary目前的代码更新是最新的OS X漏洞版本10.11.6,可以在这儿获得。
简单的说,代码所做的是每次迭代完缓冲区所包含的数据—一个uint32_t结构并且解析它。在解析期间,它会创建一个OSObject*对象返回给调用者。返回的对象必须是一个容器对象,这意味着一个对象可以包含其他的对象。实际上来说,无论是一个字典、一个数组还是一个集合,因为这些仅仅在格式上实现的容器对象。
这也就意味着它只是level 0上的一个对象(也叫做第一个集合),并且对象必须是一个容器。另一方面,所以你提供的二进制数据在任何字典、数组或者集合中都必须是闭合的。在第一个合法容器之前或之后任何level0上的其他对象将会被忽略。
在这个基本前提下,让我们来看看代码吧。
...
...
在做了一些初始化和基本的检查后,函数开始了它的while(ok)循环。这是一个迭代二进制数据的反序列循环,它将数字除以某个数字和并行化数据对象。定位到循环增量代码处,在这个代码片段的开始,它将目前的数值读入到了key。目前数据的长度被计算并且存入了len变量。最后如果kOSSerializeEndCollecton标志(也就是第三十一个比特位)在当前key中被设置,那么布尔变量end也就被设置。
然后根据key的数据类型被转接(switch结构),每个case都适当地分配了一个对象对应它的格式数据类型。比如,看看这个kOSSerializeDictionary case:
kOSSerializeDictionary是一个对象指针,它因为当前循环指向了当前反序列化对象,并且设置了里面的每一个case。
实际上这是代码一个很重要的部分,因为我们的一个bug就是与此相关。我们之后将会描述这个bug,所以请细心阅读接下来的部分!
基本上这段代码所说的就是,如果反序列化对象不是一个引用(也就是在我们的格式数据中的一个指向其他对象的指针,你可以通过kOSSerializeObject创建),就把对象放入objsArray数组。这是被OSUnserializeBinary创建的一个数组,用来保持记录每一个反序列化对象,除了我们已经说过的引用(引用不放入数组)。
让我们看看setAtIndex宏:
如果尝试存储超过数组大小的索引,数组会增大。否则,直接存储到数组。现在让我们回到主循环代码。
If-else语句实际上是负责将个反序列化对象存储到我们早先谈到的容器中。记住那三种变量(字典,数组和集合)在首次循环的时候为空,并且保持这样,直到字典、数组或集合在数据流中被发现。
这意味着result指针(返回的对象)在数据中会前移直到一个特有的容器对象被找到。因此,每个level0上的对象在特有的对象之前和之后完全被忽略。
现在关注if(dict)分支,因为这对于我们的use-after-free bug也是很重要的。因为你可能知道一个字典必须包含两个可选对象,要么一个键和一个值。因为OSUnserializeBinary格式特殊,所以键必须是OSString或者OSSymbol。如果是一个OSString,就会被自动转换成一个OSSymbol,正如你上面所见代码。
现在,代码是为了维持已说过的在键和值之间可选。Sym会以空值开始首次循环,所以当前执行会进入else分支。第一个元素预期值是一个key,那么将变成OSSymbol或者OSString并且之后将继承OSSymbol。在接下来的迭代中,我们将会处理这个键的值。因为sym被设置,那么将被带入if(sym)分支,并且dict->setObject(sym, o, true)将在字典中适当地设置键值对。
Sym会再次被设置成空,因为在接下来的迭代中,我们期望一个键,然后是一个值等等。
我们几乎完成了OSUnserializeBinary。让我们接下去:
当一个容器对象被找到(检查switch case是不是kOSSerializeDictionary, kOSSerializeArray 和kOSSerializeSet)时,仅仅布尔变量newCollect 被设置。如果end没被设置为容器对象,这意味着在这个容器之后我们仍然有其他对象在那个层级。既然这样解析规则是“缩进”,这就意味着我们增加了一个层级。
这样做是因为在新容器中我们达到了对象的结尾,在先前容器我们必须回溯并且继续反序列化对象(因为kOSSerializeEndCollection没被设置,在新容器之后有更多的对象)
每次遇到一个新容器并且在新容器之后许多对象处理缩进的多个层级,算法仅仅把父容器压入stackArray并且开始对新容器反序列化对象。当到达新容器底部时父容器将从stackArray弹出并且从这儿进行反序列化。
你可以看到父指针(指向包含当前对象的容器对象)被压入stackArray数组,并且我们在一个对象中发现另一个的kOSSerializeEndCollecton标志,每个对象会被包含在新容器中。这三种公共变量指明被压入哪个容器(dict,array和set)然后被设置为一个新的容器。当kOSSerializeEndCollecton被找到时,如果需要,算法将进入下一个等级:
先前的容器从stackArray恢复并且再次保存到parent。然后这三个公共变量是互斥的,其中一个视情况赋值到parent,所以对象将会再次被压入先前的容器。
如果新容器是它的父容器的最后一个元素,缩进不是必需的。因为在新容器之后没有对象属于父容器,所以我们能把一切压入到新容器并且退出新容器和父容器。这里有一些XML示例:
这确实是相对简单代码做出了大量解释,但是我尝试让事情尽可能清晰。解释代码不如读代码更好,所以我建议你尝试去通过你自己阅读OSUnserializeBinary代码解决你最后的疑惑。
现在是时候看到这些bug的真正乐趣了。
【未完待续】
译者:rodster(看雪ID:leixyou)
完整PDF版本: 002.Pegasus内核漏洞分析与利用(CVE-2016-4655 4656).pdf
原文地址:http://jndok.github.io/2016/10/04/pegasus-writeup/
原文作者:Jndok
0x01.前言(Introduction)
大家好!在本文章中我决定谈谈最近被Pegasus间谍软件所使用的OS X/IOS 的两个内核漏洞,漏洞影响范围OS X 10.11.6版本和IOS 9.3.4版本。我还会对bug原理和漏洞利用技术手段做些深入分析。
因为这是我发表了第一篇文章,所以文章不可避免有些错误和粗心的地方,请各位看官多点耐心。如果你发现任何错误或者对有些事有疑惑等待,请发邮件给我me@jndok.net,我会尽我所能帮你解决问题。
在阅读之前的最后一件需要注意的事:我们仅仅关注OS X内核。这是因为在IOS实际环境中由于采用的安全措施,利用这两个漏洞更加复杂。这篇文章旨在入门,因此我们会让文章更加简单。
这是本文章结构:
0x01.前言 (18日发布)
0x02.OSUnserializeBinary的概述:数据格式、细节及运作方式 (18日发布)
0x03.两个CVE漏洞分析 (20日发布)
0x04.两个CVE漏洞攻击——最有趣的地方! (20日发布)
0x05.总结 (20日发布)
0x02.Overview(概述) of OSUnserializeBinary
XNU内核实现了一个叫做OSUnserializeXML的程序,被用来并行化一个XML格式输入到基本内核数据对象中。
最近,添加了一个新功能OSUnserializeBinary。它的目的是和XML一模一样,但是这种格式处理是不同的。OSUnserializeBinary转化一个二进制格式为基本内核数据对象。尽管没有正式文件规定,但这个格式很简单。在分析功能代码之前我们会描述这种格式。
OSUnserializeBinary’s Binary Format
二进制数据是OSUnserializeBinary过程是一个简单的uint32(32位整数)数据流的连续值。也许一个32位整数数组会更好代表这个概念。一连串数字一个接一个,每个数值描述一些东西。第一个数值所要求的有效的数据流是一个独特的签名(0x000000d3)。其他每个数值使用自己的一些比特位来描述它的数据类型和它的大小。数字能代表纯粹的原始数据。
#define kOSSerializeBinarySignature "\323\0\0" /* 0x000000d3 */ enum { kOSSerializeDictionary = 0x01000000U, kOSSerializeArray = 0x02000000U, kOSSerializeSet = 0x03000000U, kOSSerializeNumber = 0x04000000U, kOSSerializeSymbol = 0x08000000U, kOSSerializeString = 0x09000000U, kOSSerializeData = 0x0a000000U, kOSSerializeBoolean = 0x0b000000U, kOSSerializeObject = 0x0c000000U, kOSSerializeTypeMask = 0x7F000000U, kOSSerializeDataMask = 0x00FFFFFFU, kOSSerializeEndCollection = 0x80000000U, };
如你所见第31个比特位被用来指示当前集合是否结束。第30位->24比特位被用来储存目前的数据类型,并且比特位23 ->0被用来存储目前元素长度。
031000000024000000000000000000000000
因为一个例子通常能够让一些事情更清楚,例子拿好:
0x000000d3 0x81000000 0x09000004 0x00414141 0x8b000001
上面的二进制数据对应:
<dict> <string>AAA</string> <boolean>1</boolean> </dict>
如你所见,我们标记了字典 (dict)作为第一个集合(0x81000000)的上层一个元素,并且boolean元素作为第二个集合 (0x8b000001)的上层一个元素。然后我们编码了字符数据 (AAA) 直接内联,包括null结束字节(0x00414141)。最后,对于boolean元素,没必要编码内联数据,因为它的大小(最后一个二进制数据)决定它是TRUE还是FALSE。
需要注意的一件重要的事情是,集合的概念和标记集合的结束。一个集合基本上与一组对象在同一层级。例如,一个字典里面的元素全部属于同一集合。当为了OSUnserializeBinary制作二进制目录时,标记集合的结束是一件很重要的事情,即设置第一个比特位(在枚举中标志kOSSerializeEndCollection)。为了更好的阐述概念,下面有个XML示例:
<dict> <!-- dict, level 0 | END! --> <string>AAA</string> <!-- string, level 1 --> <boolean>1</boolean> <!-- bool, level 1 --> <string>BBB</string> <!-- string, level 1 --> <boolean>1</boolean> <!-- bool, level 1 --> <dict> <!-- dict, level 1 --> <string>CCC</string> <!-- string, level 2 --> <boolean>1</boolean> <!-- bool, level 2 | END! --> </dict> <string>DDD</string> <!-- string, level 1 --> <boolean>1</boolean> <!-- bool, level 1 | END! --> </dict>
你可以看到这里有不同的层级或者集合。你可以看到我是如何标记在每个层级/集合的每个最新。如果你忘了做这个,OSUnserializeBinary会退出并返回一个非法参数错误,所以请记在心上!也请记住在字典为外层空间情况下,我标记它作为上层元素,因为它是唯一一个level 0元素。现在希望你会更好地明白二进制格式!现在我们准备开始分析OSUnserializeBinary的代码。
OSUnserializeBinary Analysis
OSUnserializeBinary仅仅在OSUnserializeXML中被调用。如果这个函数在开始输入数据就探测到独特的二进制信号(0x000000d3),函数就会知道数据是二进制格式的,不是XML,并且传递一切给OSUnserializeBinary。
libkern/c++/OSUnserializeXML.cpp 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目前的代码更新是最新的OS X漏洞版本10.11.6,可以在这儿获得。
简单的说,代码所做的是每次迭代完缓冲区所包含的数据—一个uint32_t结构并且解析它。在解析期间,它会创建一个OSObject*对象返回给调用者。返回的对象必须是一个容器对象,这意味着一个对象可以包含其他的对象。实际上来说,无论是一个字典、一个数组还是一个集合,因为这些仅仅在格式上实现的容器对象。
这也就意味着它只是level 0上的一个对象(也叫做第一个集合),并且对象必须是一个容器。另一方面,所以你提供的二进制数据在任何字典、数组或者集合中都必须是闭合的。在第一个合法容器之前或之后任何level0上的其他对象将会被忽略。
在这个基本前提下,让我们来看看代码吧。
...
while (ok) { bufferPos += sizeof(*next); if (!(ok = (bufferPos <= bufferSize))) break; key = *next++; len = (key & kOSSerializeDataMask); wordLen = (len + 3) >> 2; end = (0 != (kOSSerializeEndCollecton & key)); newCollect = isRef = false; o = 0; newDict = 0; newArray = 0; newSet = 0; switch (kOSSerializeTypeMask & key) { case kOSSerializeDictionary: ... case kOSSerializeArray: ... case kOSSerializeSet: ... case kOSSerializeObject: ... case kOSSerializeNumber: ... case kOSSerializeSymbol: ... case kOSSerializeString: ... case kOSSerializeData: ... case kOSSerializeBoolean: ... default: break; }
...
在做了一些初始化和基本的检查后,函数开始了它的while(ok)循环。这是一个迭代二进制数据的反序列循环,它将数字除以某个数字和并行化数据对象。定位到循环增量代码处,在这个代码片段的开始,它将目前的数值读入到了key。目前数据的长度被计算并且存入了len变量。最后如果kOSSerializeEndCollecton标志(也就是第三十一个比特位)在当前key中被设置,那么布尔变量end也就被设置。
然后根据key的数据类型被转接(switch结构),每个case都适当地分配了一个对象对应它的格式数据类型。比如,看看这个kOSSerializeDictionary case:
case kOSSerializeDictionary: o = newDict = OSDictionary::withCapacity(len); newCollect = (len != 0); break;
kOSSerializeDictionary是一个对象指针,它因为当前循环指向了当前反序列化对象,并且设置了里面的每一个case。
case kOSSerializeData: bufferPos += (wordLen * sizeof(uint32_t)); if (bufferPos > bufferSize) break; o = OSData::withBytes(next, len); next += wordLen; break;
实际上这是代码一个很重要的部分,因为我们的一个bug就是与此相关。我们之后将会描述这个bug,所以请细心阅读接下来的部分!
基本上这段代码所说的就是,如果反序列化对象不是一个引用(也就是在我们的格式数据中的一个指向其他对象的指针,你可以通过kOSSerializeObject创建),就把对象放入objsArray数组。这是被OSUnserializeBinary创建的一个数组,用来保持记录每一个反序列化对象,除了我们已经说过的引用(引用不放入数组)。
让我们看看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;
如果尝试存储超过数组大小的索引,数组会增大。否则,直接存储到数组。现在让我们回到主循环代码。
if (dict) { if (sym) { 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(); 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-else语句实际上是负责将个反序列化对象存储到我们早先谈到的容器中。记住那三种变量(字典,数组和集合)在首次循环的时候为空,并且保持这样,直到字典、数组或集合在数据流中被发现。
这意味着result指针(返回的对象)在数据中会前移直到一个特有的容器对象被找到。因此,每个level0上的对象在特有的对象之前和之后完全被忽略。
现在关注if(dict)分支,因为这对于我们的use-after-free bug也是很重要的。因为你可能知道一个字典必须包含两个可选对象,要么一个键和一个值。因为OSUnserializeBinary格式特殊,所以键必须是OSString或者OSSymbol。如果是一个OSString,就会被自动转换成一个OSSymbol,正如你上面所见代码。
现在,代码是为了维持已说过的在键和值之间可选。Sym会以空值开始首次循环,所以当前执行会进入else分支。第一个元素预期值是一个key,那么将变成OSSymbol或者OSString并且之后将继承OSSymbol。在接下来的迭代中,我们将会处理这个键的值。因为sym被设置,那么将被带入if(sym)分支,并且dict->setObject(sym, o, true)将在字典中适当地设置键值对。
Sym会再次被设置成空,因为在接下来的迭代中,我们期望一个键,然后是一个值等等。
我们几乎完成了OSUnserializeBinary。让我们接下去:
if (newCollect) { if (!end) { stackIdx++; setAtIndex(stack, stackIdx, parent); if (!ok) break; } parent = o; dict = newDict; array = newArray; set = newSet; end = false; }
当一个容器对象被找到(检查switch case是不是kOSSerializeDictionary, kOSSerializeArray 和kOSSerializeSet)时,仅仅布尔变量newCollect 被设置。如果end没被设置为容器对象,这意味着在这个容器之后我们仍然有其他对象在那个层级。既然这样解析规则是“缩进”,这就意味着我们增加了一个层级。
这样做是因为在新容器中我们达到了对象的结尾,在先前容器我们必须回溯并且继续反序列化对象(因为kOSSerializeEndCollection没被设置,在新容器之后有更多的对象)
每次遇到一个新容器并且在新容器之后许多对象处理缩进的多个层级,算法仅仅把父容器压入stackArray并且开始对新容器反序列化对象。当到达新容器底部时父容器将从stackArray弹出并且从这儿进行反序列化。
你可以看到父指针(指向包含当前对象的容器对象)被压入stackArray数组,并且我们在一个对象中发现另一个的kOSSerializeEndCollecton标志,每个对象会被包含在新容器中。这三种公共变量指明被压入哪个容器(dict,array和set)然后被设置为一个新的容器。当kOSSerializeEndCollecton被找到时,如果需要,算法将进入下一个等级:
if (end) { if (!stackIdx) break; /* j: when there are no more levels, deserialization is done; exit */ parent = stackArray[stackIdx]; /* j: pop parent off the stackArray */ stackIdx--; set = 0; dict = 0; array = 0; /* j: cast parent to proper container and resume deserialization */ if (!(dict = OSDynamicCast(OSDictionary, parent))) { /* j: if parent can't be properly cast to a container, abort */ if (!(array = OSDynamicCast(OSArray, parent))) ok = (0 != (set = OSDynamicCast(OSSet, parent))); } }
先前的容器从stackArray恢复并且再次保存到parent。然后这三个公共变量是互斥的,其中一个视情况赋值到parent,所以对象将会再次被压入先前的容器。
如果新容器是它的父容器的最后一个元素,缩进不是必需的。因为在新容器之后没有对象属于父容器,所以我们能把一切压入到新容器并且退出新容器和父容器。这里有一些XML示例:
<dict> <string>str_1</string> <boolean>1</boolean> <string>str_2</string> <boolean>1</boolean> <dict> <!-- new level (1) --> <string>str_3</string> <boolean>1</boolean> <string>str_4</string> <boolean>1</boolean> <string>str_5</string> <boolean>1</boolean> <!-- END LEVEL 1! --> <dict> <!-- there are objects after this new container --> <!-- we have to go back a level and push str_6 inside the outer dict --> <string>str_6</string> <boolean>1</boolean> <!-- END LEVEL 0! --> </dict> <dict> <string>str_1</string> <boolean>1</boolean> <string>str_2</string> <boolean>1</boolean> <dict> <!-- END LEVEL 0! --> <!-- new level (1) --> <string>str_3</string> <boolean>1</boolean> <string>str_4</string> <boolean>1</boolean> <string>str_5</string> <boolean>1</boolean> <!-- END LEVEL 1! --> <dict> <!-- there is nothing after this dict, do not indent and finally exit --> </dict>
这确实是相对简单代码做出了大量解释,但是我尝试让事情尽可能清晰。解释代码不如读代码更好,所以我建议你尝试去通过你自己阅读OSUnserializeBinary代码解决你最后的疑惑。
现在是时候看到这些bug的真正乐趣了。
【未完待续】
阿里云助力开发者!2核2G 3M带宽不限流量!6.18限时价,开 发者可享99元/年,续费同价!
赞赏
他的文章
自我净化
4366
一道花指令标准算法SO还原题
3456
看原图