前言
在我们上周发布第一篇iOS内核漏洞分析之后(注释1)(注释2),我们又发现了新的问题:
1,我们发现在今年的五月,为了修补一个被发现的UAF漏洞,OSUnseriallzeBinary()函数已经被patch过一次了。具体详情可以见这份报告(注释3),这个漏洞的编号为CVE-2016-1828,由Brandon Azad于2016年1月11日提交。
2,我们上一篇文章中发现可以发动UAF的这段代码貌似在iOS 9和OS X 10.11之前并不存在。而据报道Pegasus是可以通吃iOS的旧版本的,所以很可能我们发现的这个漏洞并不是Pegasus所使用的这个漏洞。(真任性,随随便便就公布了一个苹果未发现的漏洞...不愧是树人)
旧的OSUnseriallzeBinary()
在iOS 9 和OS X 10.11之前的版本中 OSUnseriallzeBinary()这个函数是长酱紫的:
393 if (dict)
394 {
395 if (sym)
396 {
397 DEBG("%s = %s\n", sym->getCStringNoCopy(), o->getMetaClass()->getClassName());
398 if (o != dict) ok = dict->setObject(sym, o);
399 o->release();
400 sym->release();
401 sym = 0;
402 }
403 else
404 {
405 sym = OSDynamicCast(OSSymbol, o);
406 ok = (sym != 0);
407 }
408 }
从上文代码可以看出,在之前的代码中,字典键值必须是OSSymbol对象,并且OSString代码路径还没被添加上去。这就意味着我们分析的那个UAF漏洞在这个版本上是无效的。如果您看的更加仔细一点还可以 发现传入setObject()的参数只有两个,而不是三个,这是因为上面的这段code是在苹果修补CVE-2016-1828之前的版本。
第一个发动UAF的方法
根据这段代码,想要发动UAF的话,第一个方法是使用CVE-2016-1828中的方法:
1,第398行会把字典的k1键值赋值给对象哦o1
2,这会增加o1和k1的应用计数增加1(达到2)
3,第399行o1会被释放,引用计数减少1
4,第400行k1(引用)会被释放,引用计数减少1
5,这个时候一切都没有问题,因为字典中还是抵达对象的引用维持着
6,接下来o2反序列化并会被添加到字典中,在重用k1的时候,setObject()方法会在字典中用o2代替o1,这时候o1的引用计数就减到0了。
7,o1就会在内存中被释放掉了,如果反序列器还要创建一个到o1的引用,就是UAF了。
第二个发动UAF的方法
这个方法看上去才更像是Pegasus使用的方法(CVE-2016-4656)
1,第398行,如果我们试图插入的对象o是dict自身的链接的话,setObject()不会被调用
2,如果setObject()不被调用,那么o和sym都不会被增加
3,399行会减少o的引用计数至一个大于或等于1的值,因为o本身就是dict的引用
4,400行很有可能会把sym的引用计数降到0,如果这个符号是个...举个例子,奇怪的随机字符串(fuzz)
5,这时OSSymbol对象sym已经不存在了
6,任何指向sym的引用都可以变成UAF
从上文可以看出,iOS 9和OS X 10.11之前的版本已经出现了这两个代码位置非常接近的漏洞,如果算上我们上一篇文章指出的地方,这短短二十行代码就有三处UAF漏洞!
如何修补
五月份苹果修补CVE-2016-1828的方式就是在调用setObject()的地方加了一个参数:
if (o != dict) ok = dict->setObject(sym, o, true);
这样的话,第一个情况中如果想要覆盖一个已经设定好的词典键值的话,程序就会报错,所以就堵上了Brandon Azad报告的这个漏洞。不幸的是苹果就乐不思蜀,止步不前,固步自封了,不再重新审计一下OSUnseriallzeBinary()这个函数,要不然如果是老师傅肯定可以发现这里还有一些来自objsArray的直接引用已经释放的对象并由此导致的UAF。这样就揭露给大家仅仅给setObject()加第三个参数”true”并不是完美的修复方案。
然而苹果并没有做更多的安全审核,没人发现上文的其他两处释放代码可能导致UAF漏洞,仅仅20行代码发现了一个还有两个UAF,可以用来突破内核获取最高权限。现在Pegasus被曝光了,苹果把此事提高到了最高议程来解决20行中的其他漏洞。因为他们已经废弃了给CVE-2016-1828的补丁,并且重构了函数不再有调用release()的地方(除了有个清理error的地方和临时变量)。所有的来自objsArray对象在函数快结束前都被释放了。这样在反序列化过程中所有的引用计数都不会降到0.
可以在这里看下我们反汇编OSSerializeBinary()的代码(见注释4),或者可以看下两套代码的区别:
--- OSSerializeBinary.cpp 2016-05-09 22:28:11.000000000 +0200
+++ OSSerializeBinaryPatched.cpp 2016-09-05 16:19:03.000000000 +0200
@@ -237,19 +237,21 @@
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
-#define setAtIndex(v, idx, o) \
+#define setAtIndex(v, idx, o, max) \
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 (v##Capacity < max) { \
+ 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; \
+ } else ok = false; \
} \
if (ok) v##Array[idx] = o;
@@ -338,13 +340,12 @@
case kOSSerializeObject:
if (len >= objsIdx) break;
o = objsArray[len];
- o->retain();
isRef = true;
break;
case kOSSerializeNumber:
bufferPos += sizeof(long long);
- if (bufferPos > bufferSize) break;
+ if (bufferPos > bufferSize || ((len != 32) && (len != 64) && (len != 16) && (len != 8))) break;
value = next[1];
value <<= 32;
value |= next[0];
@@ -354,7 +355,7 @@
case kOSSerializeSymbol:
bufferPos += (wordLen * sizeof(uint32_t));
- if (bufferPos > bufferSize) break;
+ if (bufferPos > bufferSize || len < 2) break;
if (0 != ((const char *)next)[len-1]) break;
o = (OSObject *) OSSymbol::withCString((const char *) next);
next += wordLen;
@@ -386,8 +387,11 @@
if (!isRef)
{
- setAtIndex(objs, objsIdx, o);
- if (!ok) break;
+ setAtIndex(objs, objsIdx, o, 0x1000000);
+ if (!ok) {
+ o->release();
+ break;
+ }
objsIdx++;
}
@@ -395,33 +399,35 @@
{
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;
+ OSSymbol *sym2 = OSDynamicCast(OSSymbol, sym);
+ if (!sym2 && (str = OSDynamicCast(OSString, sym)))
+ {
+ sym2 = (OSSymbol *) OSSymbol::withString(str);
+ ok = (sym2 != 0);
+ if (!sym2) break;
+ }
+
+ if (o != dict) ok = dict->setObject(sym2, o);
+ if (sym2 && sym2 != sym) {
+ sym2->release();
+ }
}
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;
}
}
else if (array)
{
ok = array->setObject(o);
- o->release();
}
else if (set)
{
- ok = set->setObject(o);
- o->release();
+ ok = set->setObject(o);
+ }
+ else if (result)
+ {
+ ok = false;
}
else
{
@@ -436,7 +442,7 @@
if (!end)
{
stackIdx++;
- setAtIndex(stack, stackIdx, parent);
+ setAtIndex(stack, stackIdx, parent, 0x10000);
if (!ok) break;
}
DEBG("++stack[%d] %p\n", stackIdx, parent);
@@ -462,15 +468,19 @@
}
}
}
- DEBG("ret %p\n", result);
-
- if (objsCapacity) kfree(objsArray, objsCapacity * sizeof(*objsArray));
- if (stackCapacity) kfree(stackArray, stackCapacity * sizeof(*stackArray));
- if (!ok && result)
+ if (!ok)
{
- result->release();
result = 0;
}
+ if (objsCapacity) {
+ uint32_t i;
+ for (i = (result?1:0); i < objsIndx; i++) {
+ objsArray[i]->release();
+ }
+ kfree(objsArray, objsCapacity * sizeof(*objsArray));
+ }
+ if (stackCapacity) kfree(stackArray, stackCapacity * sizeof(*stackArray));
+
return (result);
}
Hack.Lu
我们被Hack.Lu邀请十月份去卢森堡聊一聊这个我们对于这个CVE-2016-4656的发现并且如何发动攻击
结论
过去的两周中很多专家都在夸奖苹果对于新出的Pegasus的漏洞反应神速,同时还能很好的捂住代码不泄露。所以大家理所当然地觉得Pegasus用的是全新的内核漏洞,苹果也肯定是刚刚才知道了这些个漏洞。直到现在我们把漏洞完整分析了才直到压根儿就不是这么回事。
CVE-2016-4656之所以存在,完全就是因为苹果在给CVE-2016-1828打补丁的时候,忘记做代码安全审计了。短短20行代码!三处UAF漏洞!人家报告一个,苹果就修补一个,release()的位置就隔着setObject()啊!只要稍微长点儿心苹果可以轻而易举的修补所有的这三个漏洞,我觉得苹果这回真是糗大了,Brandon Azad都把门指给你了,你都不看,你要是瞅一眼,就没CVE-2016-4656什么事儿了。
不幸的是这不是苹果第一次搞砸了,两年来我们已经不断发现苹果把安全问题搞砸过一次又一次,直接导致了我们在苹果“没朋友”并且他们还把我们的SysSecInfo安全软件给ban了,过程在这里。(注释5)
最后一点:重读CVE-2016-1828和CVE-2017-4656我们发现其实Pegasus所用的漏洞的发现和利用,比Brandon Azad发现的漏洞难上许多,所以我们有理由认为其实Pegasus的漏洞利用是在CVE-2016-1828发现的后面的,要不然Pegasus肯定会使用CVE-2016-1828的,因为这个漏洞更简单呀。所以很有可能是Pegasus集团的人,逆向了CVE-2016-1828,然后在这个地方又重新捡到了宝贝。这只是我们不负责任的猜测,各位看官看看就好。
注释1:http://bbs.pediy.com/showthread.php?t=212555
注释2:https://www.sektioneins.de/blog/16-09-02-pegasus-ios-kernel-vulnerability-explained.html
注释3:https://bazad.github.io/2016/05/mac-os-x-use-after-free/
注释4:https://www.sektioneins.de/files/OSSerializeBinary.cpp
注释5:https://www.sektioneins.de/blog/16-08-11-sysinfo-what-happened.html