CVE-2016-4655

苹果在上个月紧急发布了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

Leave a Reply

Your email address will not be published. Required fields are marked *