苹果在上个月紧急发布了9.3.5更新来封堵Pegasus攻击中使用的漏洞,不过内核信息泄露的漏洞(CVE-2016-4655)在iOS10beta8版本中仍然没有被修补。直到今日开始推送的10.0.1版本中才修补该漏洞(安全更新)。
由于iOS10是iPhone7/7p的预装系统,因此苹果可能在知晓该漏洞前已经开始生产iPhone7/7p设备,导致无法在10.0中修补该漏洞。而Pegasus攻击中使用的另一个内核UAF类型的漏洞(CVE-2016-4656)其实在iOS10beta1版本中已经被修补,猜测是苹果内部安全团队应该也发现了该漏洞。
漏洞原理
OSUnserializeBinary函数用于解析二进制格式的序列化对象,之前爆出的UAF漏洞(CVE-2016-1828)和这次的UAF漏洞(CVE-2016-4656)都存在于该函数中。我们观察OSNumber对象的创建代码。
len = (key & kOSSerializeDataMask);
wordLen = (len + 3) >> 2;
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;
switch (kOSSerializeTypeMask & key)
{
...
case kOSSerializeNumber:
bufferPos += sizeof(long long);
if (bufferPos > bufferSize) break;
value = next[1];
value <<= 32;
value |= next[0];
o = OSNumber::withNumber(value, len); // <--------- len可控
next += 2;
break;
在创建的过程中value和len都是我们可以任意指定的数据,查看OSNumber.cpp的代码可以发现len其实对应的含义是number of bits,也就是数据的位数。数据位数应该不超过64,然而整个初始化过程中没有任何额外的检查。
bool OSNumber::init(unsigned long long inValue, unsigned int newNumberOfBits)
{
if (!super::init())
return false;
size = newNumberOfBits; // <--------- size可控
value = (inValue & sizeMask);
return true;
}
OSNumber *OSNumber::withNumber(unsigned long long value,
unsigned int newNumberOfBits)
{
OSNumber *me = new OSNumber;
if (me && !me->init(value, newNumberOfBits)) {
me->release();
return 0;
}
return me;
}
我们控制的size成员会被numberOfBits和numberOfBytes函数使用,因此我们也可以任意控制这两个函数的返回值。
unsigned int OSNumber::numberOfBits() const { return size; }
unsigned int OSNumber::numberOfBytes() const { return (size + 7) / 8; }
继续观察使用这两个函数的地方,可以发现is_io_registry_entry_get_property_bytes中通过numberOfBytes函数确认OSNumber的数据长度,最终造成内核栈数据读取的漏洞。而内核栈上保存了函数调用地址以及stack cookie等数据,可以轻易计算出内核的基地址。
} else if( (off = OSDynamicCast( OSNumber, obj ))) {
offsetBytes = off->unsigned64BitValue(); // <--------- offsetBytes是栈上的一个uint64_t的变量
len = off->numberOfBytes(); // <--------- len可控
bytes = &offsetBytes; // <--------- bytes指向栈上的地址
} else
ret = kIOReturnBadArgument;
if( bytes) {
if( *dataCnt < len)
ret = kIOReturnIPCError;
else {
*dataCnt = len;
bcopy( bytes, buf, len ); // <--------- 拷贝任意长度栈上的数据返回给用户态
}
}
漏洞修补
分析10.0.1的内核可以看到苹果在创建OSNumber前对参数做了额外的校验。
v30 = (OSSet *)(*(_DWORD *)v15 & 0xFFFFFF);
v31 = *(_DWORD *)v15 & 0x7F000000;
if ( v31 <= 0x7FFFFFF )
{
if ( v31 > 0x2FFFFFF )
{
if ( v31 != 0x3000000 )
{
v16 = v27;
if ( v31 != 0x4000000
|| v108 + 12 > (unsigned __int64)v109
|| (unsigned int)((_DWORD)v30 - 8) > 0x38 // <--------- 限定len的范围是8-64
|| !((1LL << (*v15 - 8)) & 0x100000001000101LL) ) // <--------- 限定len只能是8/16/32/64
{
goto LABEL_158;
}
v107 = *(_DWORD *)v15;
v108 += 12LL;
v51 = v15;
v20 = OSNumber::withNumber(
*((unsigned int *)v15 + 1) | ((unsigned __int64)*((unsigned int *)v15 + 2) << 32),
*(_DWORD *)v15 & 0xFFFFFF);
漏洞发现
我们并非通过bindiff等复杂手段定位该漏洞,所以。。。这是一个悲伤的。。。撞洞的故事。。。
实际情况是当苹果发布9.3.5版本后,我们测试了手中的漏洞,结果发现这枚未满周岁的信息泄露漏洞不幸阵亡 TT