长亭百川云 - 文章详情

Racing for everyone-descriptor describes TOCTOU 苹果内核中的新型漏洞

Flanker论安全

52

2024-07-13

# Racing for everyone: descriptor describes TOCTOU,苹果iOS/OSX内核中的新型漏洞

这篇文章是关于之前我们在苹果内核IOKit驱动中找到的一类新攻击面。之前写了个IDA脚本做了个简单扫描,发现了至少四个驱动都存在这类问题并报告给了苹果,苹果分配了3个CVE(CVE-2016-7620/4/5), 见 https://support.apple.com/kb/HT207423。 后来我和苹果的安全工程师聊天,他们告诉我他们根据这个pattern修复了十多个漏洞,包括iOS内核中多个可以利用的漏洞。

为了能更清楚地描述这类新漏洞,我们先来复习下IOKit的基础知识。

## IOKit revisited

在用户态, IOConnectCallMethod函数实现如下:

    1709 kern_return_t

    1710 IOConnectCallMethod(

    1711    mach_port_t  connection,        // In

    1712    uint32_t     selector,      // In

    1713    const uint64_t  *input,         // In

    1714    uint32_t     inputCnt,      // In

    1715    const void  *inputStruct,       // In

    1716    size_t       inputStructCnt,    // In

    1717    uint64_t    *output,        // Out

    1718    uint32_t    *outputCnt,     // In/Out

    1719    void        *outputStruct,      // Out

    1720    size_t      *outputStructCntP)  // In/Out

    1721 {

    //...

    1736     if (inputStructCnt <= sizeof(io_struct_inband_t)) {

    1737    inb_input      = (void *) inputStruct;

    1738    inb_input_size = (mach_msg_type_number_t) inputStructCnt;

    1739     }

    1740     else {

    1741    ool_input      = reinterpret_cast_mach_vm_address_t(inputStruct);

    1742    ool_input_size = inputStructCnt;

    1743     }

    1744 //...

    1770    else if (size <= sizeof(io_struct_inband_t)) {

    1771        inb_output      = outputStruct;

    1772        inb_output_size = (mach_msg_type_number_t) size;

    1773    }

    1774    else {

    1775        ool_output      = reinterpret_cast_mach_vm_address_t(outputStruct);

    1776        ool_output_size = (mach_vm_size_t)    size;

    1777    }

    1778     }

    1779 

    1780     rtn = io_connect_method(connection,         selector,

    1781                (uint64_t *) input, inputCnt,

    1782                inb_input,          inb_input_size,

    1783                ool_input,          ool_input_size,

    1784                inb_output,         &inb_output_size,

    1785                output,             outputCnt,

    1786                ool_output,         &ool_output_size);

    1787 

    //...

    1795     return rtn;

    1796 }

如果输入的inputstruct大于`sizeof(io_struct_inband_t)`, 那么传入的参数会被cast成`mach_vm_address_t`,在后面被特殊对待。

## 这里能race不?不能?那里呢?

对于每一个有好奇心的人,我们都要问,这些平常被漏洞研究者们熟视无睹的参数里有没有那些能触发条件竞争漏洞?历史上人们通常都把注意力集中在一看名字就知道有可能有race condition漏洞的`IOConnectMapMemory`中,之前包括pangu和google-project-zero的ian beer都在这里有过非常深入的研究,也导致这个人们所熟知的攻击面漏洞已经非常少了。

那回过头再来看这些IOKit的参数,这些被人们忽视的地方,究竟会不会有race呢?

我们还是需要深入了解下IOKit调用中参数是如何从用户态传递到内核态的?

在MIG trap定义和对应生成的代码中,不同的输入类型会得到不同的对待处理。http://flanker017.me:8080/source/xref/xnu-3248.50.21/osfmk/device/device.defs#602

    601

    602routine io_connect_method(

    603     connection      : io_connect_t;

    604 in  selector        : uint32_t;

    605

    606 in  scalar_input    : io_scalar_inband64_t;

    607 in  inband_input    : io_struct_inband_t;

    608 in  ool_input       : mach_vm_address_t;

    609 in  ool_input_size  : mach_vm_size_t;

    610

    611 out inband_output   : io_struct_inband_t, CountInOut;

    612 out scalar_output   : io_scalar_inband64_t, CountInOut;

    613 in  ool_output      : mach_vm_address_t;

    614 inout ool_output_size   : mach_vm_size_t

    615 );

    616

    ```

    ```

    /* Routine io_connect_method */

    mig_external kern_return_t io_connect_method

    (

        mach_port_t connection,

        uint32_t selector,

        io_scalar_inband64_t scalar_input,

        mach_msg_type_number_t scalar_inputCnt,

        io_struct_inband_t inband_input,

        mach_msg_type_number_t inband_inputCnt,

        mach_vm_address_t ool_input,

        mach_vm_size_t ool_input_size,

        io_struct_inband_t inband_output,

        mach_msg_type_number_t *inband_outputCnt,

        io_scalar_inband64_t scalar_output,

        mach_msg_type_number_t *scalar_outputCnt,

        mach_vm_address_t ool_output,

        mach_vm_size_t *ool_output_size

    )

    {

    //...

        (void)memcpy((char *) InP->scalar_input, (const char *) scalar_input, 8 * scalar_inputCnt);

    //...

        if (inband_inputCnt > 4096) {

            { return MIG_ARRAY_TOO_LARGE; }

        }

        (void)memcpy((char *) InP->inband_input, (const char *) inband_input, inband_inputCnt);

    //...

        InP->ool_input = ool_input;

        InP->ool_input_size = ool_input_size;

这段代码告诉我们,scala_input和大小小于4096的structinput是会被memcpy嵌入到传递入内核的machmsg中的,所以这里面似乎没有用户态再操作的空间。

但是,如果struct_input的大小大于4096,那么这里就有特殊对待了。它会被保留为mach_vm_address且不会被更改。

我们再继续追下去,看看这个mach_vm_address进入内核之后又会被如何处理

Now lets dive into kernel space (http://flanker017.me:8080/source/xref/xnu-3248.50.21/iokit/Kernel/IOUserClient.cpp#3701)

    3701 kern_return_t is_io_connect_method

    3702 (

    3703    io_connect_t connection,

    3704    uint32_t selector,

    3705    io_scalar_inband64_t scalar_input,

    3706    mach_msg_type_number_t scalar_inputCnt,

    3707    io_struct_inband_t inband_input,

    3708    mach_msg_type_number_t inband_inputCnt,

    3709    mach_vm_address_t ool_input,

    3710    mach_vm_size_t ool_input_size,

    3711    io_struct_inband_t inband_output,

    3712    mach_msg_type_number_t *inband_outputCnt,

    3713    io_scalar_inband64_t scalar_output,

    3714    mach_msg_type_number_t *scalar_outputCnt,

    3715    mach_vm_address_t ool_output,

    3716    mach_vm_size_t *ool_output_size

    3717 )

    3718 {

    3719     CHECK( IOUserClient, connection, client );

    3720 

    3721     IOExternalMethodArguments args;

    3722     IOReturn ret;

    3723     IOMemoryDescriptor * inputMD  = 0;

    3724     IOMemoryDescriptor * outputMD = 0;

    3725 

    //...

    3736     args.scalarInput = scalar_input;

    3737     args.scalarInputCount = scalar_inputCnt;

    3738     args.structureInput = inband_input;

    3739     args.structureInputSize = inband_inputCnt;

    3740 

    3741     if (ool_input)

    3742    inputMD = IOMemoryDescriptor::withAddressRange(ool_input, ool_input_size,

    3743                            kIODirectionOut, current_task());

    3744 

    3745     args.structureInputDescriptor = inputMD;

    //...

    3753     if (ool_output && ool_output_size)

    3754     {

    3755    outputMD = IOMemoryDescriptor::withAddressRange(ool_output, *ool_output_size,

    3756                            kIODirectionIn, current_task());

    //...

    3774     return (ret);

    3775 }

在这里我们可以看出苹果和Linux内核在处理输入上的一些不同。在Linux内核中,用户态输入倾向于被copy_from_user到一个内核allocate的空间中。而苹果内核对于大于4096的用户输入,则倾向于用一个IOMemoryDescriptor对其作一个映射,然后在内核态访问。

既然有映射存在,那么我们就要动歪脑筋了。我们能不能在IOKit调用进行的同时去在用户态修改这个映射呢?之前并没有人研究过这个问题,也没有相关的漏洞公布,似乎大家都默认,在发起调用后,这是用户态不可写的。真的是这样么?

令人吃惊的是,测试表明,这居然是可写的!后来苹果的工程师告诉我们,他们在看到我的漏洞报告的时候,才发现之前连他们都没注意到这里居然还是用户态可写的。

这就意味着,对于一个IOKit调用,如果内核处理输入的IOService接受MemoryDescriptor的话(绝大多数都接受),那么发起调用的用户态进程可以在输入被内核处理的时候去修改掉传入的参数内容,没有锁,也没有只读保护。由于连苹果的工程师都没有注意这个问题,这意味着他们在编写内核驱动的时候基本没有对这部分数据做保护处理,这不就是条件竞争漏洞的天堂么!

我迅速回忆了一下之前逆向过的几个IOKit驱动,很快就有一个漏洞pattern出现。IOReportUserClient, IOCommandQueue, IOSurface在处理用户态传进来的inputStruct的时候,在里面取出了一个长度作为后续边界处理的条件,虽然开发者肯定都先校验了这个长度,但由于这个racecondition的存在,那么用户态还是可以改掉这个长度绕过检查,自然就触发了越界。其他的pattern还有更多,就是发挥想象力的时候了。我们先来分析下IOCommandQueue这个典型例子,也就是CVE-2016-7624.

##IOCommandQueue内核服务中存在沙箱进程可以调用的越界读写漏洞

在IOCommandQueue::submit_command_buffer这个函数中,存在如上所述的条件竞争漏洞。这个函数接受structureInput或者structureInputDescriptor,其中在特定的offset存储了一个长度,虽然长度在传入的时候被校验过, 但利用这个条件竞争,攻击者依然可以控制长度,造成后续的越界读写。

### 漏洞分析

IOAccelCommandQueue::s_submit_command_buffers接受用户输入的IOExternalMethodArguments, 如果structureInputDescriptor存在,那么descriptor会被用来映射为memorymap并翻译为原始地址.

    __int64 __fastcall IOAccelCommandQueue::s_submit_command_buffers(IOAccelCommandQueue *this, __int64 a2, IOExternalMethodArguments *a3)

    {

      IOExternalMethodArguments *v3; // r12@1

      IOAccelCommandQueue *v4; // r15@1

      unsigned __int64 inputdatalen; // rsi@1

      unsigned int v6; // ebx@1

      IOMemoryDescriptor *v7; // rdi@3

      __int64 v8; // r14@3

      __int64 inputdata; // rcx@5

      v3 = a3;

      v4 = this;

      inputdatalen = (unsigned int)a3->structureInputSize;

      v6 = -536870206;

      if ( inputdatalen >= 8

        && inputdatalen - 8 == 3

                             * (((unsigned __int64)(0x0AAAAAAAAAAAAAAABLL * (unsigned __int128)(inputdatalen - 8) >> 64) >> 1) & 0x7FFFFFFFFFFFFFF8LL) )

      {

        v7 = (IOMemoryDescriptor *)a3->structureInputDescriptor;

        v8 = 0LL;

        if ( v7 )

        {

          v8 = (__int64)v7->vtbl->__ZN18IOMemoryDescriptor3mapEj(v7, 4096LL);

          v6 = -536870200;

          if ( !v8 )

            return v6;

          inputdata = (*(__int64 (__fastcall **)(__int64))(*(_QWORD *)v8 + 280LL))(v8);

          LODWORD(inputdatalen) = v3->structureInputSize;

        }

我们可以看到在offset+4, 一个DWORD被用来作为length和((unsigned \_\_int64)(0x0AAAAAAAAAAAAAAABLL * (unsigned \_\_int128)(inputdatalen - 8) >> 64) >> 1) & 0x7FFFFFFFFFFFFFF8LL)比较

随后这个length在submit_command_buffer中再次被使用. 

      if ( *((_QWORD *)this + 160) )

      {

        v5 = (IOAccelShared2 *)*((_QWORD *)this + 165);

        if ( v5 )

        {

          IOAccelShared2::processResourceDirtyCommands(v5);

          IOAccelCommandQueue::updatePriority((IOAccelCommandQueue *)v2);

          if ( *(_DWORD *)(input + 4) )

          {

            v6 = (unsigned __int64 *)(input + 24);

            v7 = 0LL;

            do

            {

              IOAccelCommandQueue::submitCommandBuffer(

                (IOAccelCommandQueue *)v2,

                *((_DWORD *)v6 - 4),//v6 based on input

                *((_DWORD *)v6 - 3),//based on input

                *(v6 - 1),//based on input

                *v6);//based on input

              ++v7;

              v6 += 3;

            }

            while ( v7 < *(unsigned int *)(input + 4) ); //NOTICE HERE

          }

注意在23行*(input+4)又被作为循环的边界使用. 但就像前面说的一样,如果用户态传进来一个descriptor, 他可以在这个时候在用户态改变这个值,绕过`s_submit_command_buffers`的检查,造成oob。

在`IOAccelCommandQueue::submitCommandBuffer`中:

        IOGraphicsAccelerator2::sendBlockFenceNotification(

          *((IOGraphicsAccelerator2 **)this + 166),

          (unsigned __int64 *)(*((_QWORD *)this + 160) + 16LL),

          data_from_input_add_24_minus_8,

          0LL,

          v13);

        result = IOGraphicsAccelerator2::sendBlockFenceNotification(

                   *((IOGraphicsAccelerator2 **)this + 166),

                   (unsigned __int64 *)(*((_QWORD *)this + 160) + 16LL),

                   data_from_input_add_24,

                   0LL,

                   v13);

我们可以看到内存内容以notification callback的返回给用户态,那么攻击者通过前面控制长度并在越界的部分精心布置内存,那么就可以造成这部分越界的内存被返回给用户态,导致内核内存泄漏甚至代码执行。

具体触发步骤为

*   用户态mmap内存, 通过IOKit调用的structureInputDescriptor传递进去

*   内核中s_submit_command_buffer函数在+4偏移校验这个长度和传入的内容长度是否匹配

*   submit_command_buffer遍历用户态传入的descriptor内存,以+4偏移的内容为长度边界。读出的内容在submitCommandBuffer中经过变换通过asyncNotificationPort返回给用户态。

*   用户态通过条件竞争更改map内存的长度,造成越界读写

### POC代码

The POC代码比较长,完整版见https://github.com/flankerhqd/descriptor-describes-racing,或者直接阅读原文。

两个关键的参数是4088 and 0xaa, 这两个整数主要为了通过内核中

     inputdatalen - 8 == 3

                             * (((unsigned __int64)(0x0AAAAAAAAAAAAAAABLL * (unsigned __int128)(inputdatalen - 8) >> 64) >> 1) & 0x7FFFFFFFFFFFFFF8LL) )

       if ( *(_DWORD *)(inputdata + 4) == (unsigned int)((unsigned __int64)(0x0AAAAAAAAAAAAAAABLL

                                                                           * (unsigned __int128)((unsigned __int64)(unsigned int)inputdatalen

                                                                                               - 8) >> 64) >> 4) )

的检查。

在POC运行之后,内核相邻内存的内容就会被leak回用户态。如果边界没有映射内存的话,就会触发一个内核panic。

在这个崩溃中,rbx寄存器已经出现了越界,意味了内核在读取一个没有映射的内存内容,触发越界。

在 10.11.5 Macbook Airs, Macbook Pros 中测试复现:

    while true; do ./cmdqueue1 ; done

#苹果的修复

苹果在10.12.2和iOS 10.2种将descriptor在内核态映射的方式改为COW,看起来从根源上消灭了这类问题,但是真的么?We will see.

相关推荐
关注或联系我们
添加百川云公众号,移动管理云安全产品
咨询热线:
4000-327-707
百川公众号
百川公众号
百川云客服
百川云客服

Copyright ©2024 北京长亭科技有限公司
icon
京ICP备 2024055124号-2