External method 0 of IOSurfaceRoot is IOSurfaceRootUserClient::create_surface. This method expects to receive an xml string which it
deserializes into an OSDictionary. It then passes that dictionary to IOSurfaceRoot::createSurface(task *,OSDictionary *)
here's the relevant code:
__text:0000000000005E13 mov rax, [rbx]
__text:0000000000005E16 lea rcx, _kIOSurfaceClassName ; "IOSurfaceClass"
__text:0000000000005E1D mov rsi, [rcx]
__text:0000000000005E20 mov rdi, rbx ; input OSDictionary - contents controlled
__text:0000000000005E23 call qword ptr [rax+208h]
__text:0000000000005E29 mov rcx, cs:off_A030
__text:0000000000005E30 mov rsi, [rcx] ; char *
__text:0000000000005E33 mov rdi, rax ; check that IOSurfaceString was an OSString
__text:0000000000005E36 call __ZN15OSMetaClassBase12safeMetaCastEPKS_PK11OSMetaClass ; OSMetaClassBase::safeMetaCast(OSMetaClassBase const*,OSMetaClass const*)
__text:0000000000005E3B test rax, rax
__text:0000000000005E3E jz short loc_5E4A ; if either there was no "IOSurfaceClass" key or the value wasn't a string then jump
; to 54ea and use "IOSurface"
__text:0000000000005E40 mov rdi, rax ; otherwise, pass the usercontrolled string to OSMetaClass::allocClassWithName
__text:0000000000005E43 call __ZN11OSMetaClass18allocClassWithNameEPK8OSString ; OSMetaClass::allocClassWithName(OSString const*)
__text:0000000000005E48 jmp short loc_5E56
__text:0000000000005E4A ; ---------------------------------------------------------------------------
__text:0000000000005E4A loc_5E4A: ; CODE XREF: IOSurfaceRoot::createSurface(task *,OSDictionary *)+4Aj
__text:0000000000005E4A lea rdi, aIosurface ; "IOSurface"
__text:0000000000005E51 call __ZN11OSMetaClass18allocClassWithNameEPKc ; OSMetaClass::allocClassWithName(char const*)
__text:0000000000005E56 loc_5E56: ; CODE XREF: IOSurfaceRoot::createSurface(task *,OSDictionary *)+54j
__text:0000000000005E56 mov r12, rax ; save reflection-allocated class pointer into r12
__text:0000000000005E59 mov rdi, [r14+0F8h]
__text:0000000000005E60 call _IORecursiveLockLock
__text:0000000000005E65 test r12, r12
__text:0000000000005E68 jz short loc_5EDD
__text:0000000000005E6A lea rax, __ZN9IOSurface9metaClassE ; IOSurface::metaClass
__text:0000000000005E71 mov rsi, [rax]
__text:0000000000005E74 mov rdi, r12 ; does that reflection-allocated class's metaclass inherit from IOSurface::metaClass
__text:0000000000005E77 call __ZN15OSMetaClassBase12safeMetaCastEPKS_PK11OSMetaClass ; OSMetaClassBase::safeMetaCast(OSMetaClassBase const*,OSMetaClass const*)
__text:0000000000005E7C test rax, rax
__text:0000000000005E7F jz short loc_5EC0 ; if it doesn't jump to 5ec0
__text:0000000000005EC0 loc_5EC0: ; CODE XREF: IOSurfaceRoot::createSurface(task *,OSDictionary *)+8Bj
__text:0000000000005EC0 ; IOSurfaceRoot::createSurface(task *,OSDictionary *)+A5j
__text:0000000000005EC0 mov rdi, [r14+0F8h]
__text:0000000000005EC7 call _IORecursiveLockUnlock
__text:0000000000005ECC mov rax, [r12] ; r12 is the pointer to the reflection-allocated class
__text:0000000000005ED0 mov rdi, r12
__text:0000000000005ED3 call qword ptr [rax+120h] ; call the virtual method at offset +0x120 in that objects vtable
; +0x120 is the offset of IOSurface::release - not OSObject::release - it's only valid for subclasses of
; IOSurface - for other types this could be anything
The code reads a user-controlled string from the input dictionary with the key "IOSurfaceClass" then passes that string to the IOKit C++ reflection API
OSMetaClass::allocClassWithName. This instantiates a completely user-controlled IOKit class, saving a pointer to the allocated object in r12.
The code then passes that pointer to safeMetaCast to determine if the newly-allocated object is in fact a subtype of IOSurface. If it isn't then the code calls the
virtual method at offset 0x120 in the controlled object - this offset is outside the vtable of the OSObject base class therefore the code probably looked something like this:
IOSurface* foo = (IOSurface*) allocClassWithName(controlledName);
foo->release(); // calls IOSurface::release which is a virtual method at +0x120 in vtable - not OSObject::release which is +0x28
Attached PoC demonstrates this by instantiating an IOAccelCommandBufferPool2 - this object has a vtable which is smaller that 0x120 clearly demonstrating that this is a bug!
Exploitation would hinge on being able to find an object with a suitably interesting pointer at that offset in its vtable - I would imagine there are almost certainly
good candidates but I haven't looked yet.
IOSurfaceRootUserClient is reachable in almost all sandboxes on OS X and iOS.