CVE-2023-27958
There exists a vulnerability in the fixed size array marshaling code of DCERPC library as used in Apple macOS 12.6.1 that can result in arbitrary code execution. A specially-crafted network packet can cause reuse of previously freed memory, which can lead to further memory corruption. An authenticated remote attacker can send a network request to trigger this vulnerability. A local attacker can write to a local socket to trigger this vulnerability.
The versions below were either tested or verified to be vulnerable by Talos or confirmed to be vulnerable by the vendor.
Apple macOS 12.6.1
macOS - https://apple.com
7.5 - CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:U/C:H/I:H/A:H
CWE-416 - Use After Free
DCERPC is a remote procedure call protocol that is the basis for RPC functionality on Windows. DCERPC framework on macOS implements this protocol and enables interoperability of Windows network services on macOS. For example, it is used on top of SMB, through which support for Active Directory is implemented. DCERPC framework is employed by rpcsvchost
binary, which opens a number of UNIX sockets that expose different RPC functionality.
An RPC service implementation based on DCERPC.framework
will have code that performs, for each RPC function invocation, unmarshalling of input parameters, actual function call with those parameters and then marshalling of output parameters. Unmarshalling and marshalling are handeled by the rpc_ss_ndr_unmar_interp
and rpc_ss_ndr_mar_interp
functions respectively. These two functions are guided by code generated from RPC service IDL file and, in short, are tasked with interpreting incoming data into expected types and packing results into data to be sent in a reply. The same structures are used in both marshalling and unmarshalling code, and parts of allocated memory are being reused.
Function rpc_ss_ndr_unmar_interp
has the folowing signature:
void rpc_ss_ndr_unmar_interp
(
idl_ulong_int IDL_parameter_count, /* [in] -- Number of parameters to */
/* marshall in this call to the */
/* interpreter */
idl_ulong_int IDL_type_index, /* [in] -- Offset into the type vector */
/* for the description of the type to be */
/* marshalled */
rpc_void_p_t IDL_param_vector[], /* [in,out] -- The addresses of each of */
/* the the parameters thus it's size is */
/* the number of parameters in the */
/* signature of the operation */
IDL_msp_t IDL_msp /* [in,out] -- Pointer to marshalling state */
)
Final parameter IDL_msp
holds the marshalling/unmarshalling state, as well as buffers that keep incoming and outgoing request data, which is a rather big structure. For our purposes, only the following fields are relevant:
/*
* Interpreter state block
*/
typedef struct IDL_ms_t {
....
idl_byte *IDL_mp; /* Pointer to first free location in current buffer */
...
idl_ulong_int IDL_left_in_buff; /* Unused space in curr buff (in bytes) */
...
In short, field IDL_mp
is a pointer to incoming or outgoing data, and IDL_left_in_buff
keeps track of how much of the buffer has been processed (unmarshalled) or populated (marshalled).
This vulnerability lies in the fact that there exists a path where memory pointed to by IDL_mp
is freed without updating IDL_left_in_buff
, which can lead to use-after-free conditions arising. Freeing happens in the following code near the end of rpc_ss_ndr_unmar_interp
:
if ((IDL_msp->IDL_language != IDL_lang_c_k)
&& (IDL_parameter_count != 0))
{
rpc_ss_mem_item_free(&IDL_msp->IDL_mem_handle,
(byte_p_t)conf_char_array_list);
}
if (IDL_msp->IDL_pickling_handle == NULL)
{
if (IDL_msp->IDL_elt_p->buff_dealloc
&& IDL_msp->IDL_elt_p->data_len != 0)
(*(IDL_msp->IDL_elt_p->buff_dealloc))
(IDL_msp->IDL_elt_p->buff_addr); [1]
IDL_msp->IDL_elt_p = NULL;
}
else
{
/* For pickling, buffer was rounded out to a multiple of 8 bytes */
IDL_UNMAR_ALIGN_MP(IDL_msp, 8);
}
}
At [1] above, if the buffer has a deallocation routine associated with it, it will be called. At runtime, with usual requests, IDL_msp->IDL_elt_p
will actually point to currently used buffer. In effect, this frees the buffer pointed to by IDL_mp
in the IDL_msp
structure. Since IDL_left_in_buff
is kept in sync with IDL_mp
, not IDL_msp->IDL_elt_p
, it is left unchanged. Additionaly, the IDL_msp->IDL_mp
pointer isn’t invalidated, leaving it pointing to freed memory. The reuse component of use-after-free vulnerability would come in marshalling the reply.
One possible path to trigger this vulnerability would be through invocation of an RPC method that has a fixed size array as an output argument. One candidate for such function is netr_ServerReqChallenge
, or function 0x04
of NETLOGON service (on macOS, NETLOGON is implemented in netlogon.bundle
). From IDL, specification for this function is:
[public] NTSTATUS netr_ServerReqChallenge(
[in,unique,string,charset(UTF16)] uint16 *server_name,
[in,string,charset(UTF16)] uint16 *computer_name,
[in,ref] netr_Credential *credentials,
[out,ref] netr_Credential *return_credentials
);
Output parameter is netr_Credential
structure, which is simply the following:
typedef [public, flag(NDR_PAHEX)] struct {
uint8 data[8];
} netr_Credential;
Data array of 8 bytes; a fixed length array, in other words.
The reuse part of use-after-free can be triggered in rpc_ss_ndr_marsh_by_copying
function, which is as follows:
void rpc_ss_ndr_marsh_by_copying
(
/* [in] */ idl_ulong_int element_count,
/* [in] */ idl_ulong_int element_size,
/* [in] */ rpc_void_p_t array_addr,
IDL_msp_t IDL_msp [2]
)
{
idl_ulong_int bytes_required; /* Number of bytes left to copy */
idl_ulong_int bytes_to_copy; /* Number of bytes to copy into this buffer */
bytes_required = element_count * element_size;
while (bytes_required != 0)
{
rpc_ss_ndr_marsh_check_buffer( 1, IDL_msp); [3]
if (bytes_required > IDL_msp->IDL_left_in_buff)
bytes_to_copy = IDL_msp->IDL_left_in_buff;
else
bytes_to_copy = bytes_required;
memcpy(IDL_msp->IDL_mp, array_addr, bytes_to_copy); [4]
IDL_msp->IDL_mp += bytes_to_copy;
IDL_msp->IDL_left_in_buff -= bytes_to_copy;
bytes_required -= bytes_to_copy;
array_addr = (rpc_void_p_t)((idl_byte *)arra_addr + bytes_to_copy);
}
}
In the above code, at [2] we can see the same IDL_msp
structure being used as already discussed. A check is performed on the buffer at [3], and IDL_mp
pointer is used in a call to memcpy at [4]. The check at [3] is important and rpc_ss_ndr_marsh_check_buffer
is simply a macro:
#define rpc_ss_ndr_marsh_check_buffer( datum_size, IDL_msp ) \
{ \
if (datum_size > IDL_msp->IDL_left_in_buff) \
{ \
if (IDL_msp->IDL_buff_addr != NULL) \
{ \
rpc_ss_attach_buff_to_iovec( IDL_msp ); \
rpc_ss_xmit_iovec_if_necess( idl_false, IDL_msp ); \
IDL_msp->IDL_mp_start_offset = 0; \
} \
rpc_ss_ndr_marsh_init_buffer( IDL_msp ); \ [5]
} \
}
Above macro checks if IDL_msp->IDL_left_in_buff
is large enough. If it’s not, it performs buffer initialization. With a correct RPC call, even if IDL_mp
isn’t free’d, IDL_left_in_buff
would be 0, which would cause buffer reinitialization. But, with a special RPC call packet, unmarshalling can succeed while there are still bytes left unprocessed, leaving IDL_left_in_buff
arbitrarily big. This would skip the call to initialization at [5] , leaving stale IDL_mp
pointer for reuse at a call to memcpy
at [4].
We can illustrate this vulnerability in the debugger:
* thread #15, stop reason = breakpoint 4.1
frame #0: 0x0000000100241400 DCERPC`rpc_ss_ndr_unmar_interp(IDL_parameter_count=0, IDL_type_index=0, IDL_param_vector=0x0000000000000000, IDL_msp=0x0000000000000000) at ndrui.c:1592
Target 0: (rpcsvchost) stopped.
(lldb) next
Process 52848 stopped
* thread #15, stop reason = step over
frame #0: 0x0000000100241737 DCERPC`rpc_ss_ndr_unmar_interp(IDL_parameter_count=3, IDL_type_index=1548, IDL_param_vector=0x000070000e1507e0, IDL_msp=0x000070000e1503a0) at ndrui.c:1593:5
Target 0: (rpcsvchost) stopped.
(lldb) p IDL_msp
(IDL_msp_t) $187 = 0x000070000e1503a0
(lldb)
First, we make note of addresses pointed to by IDL_msp
during request unmarshalling. Continuing execution leads us to freeing of memory pointed to by IDL_mp
:
* thread #15, stop reason = breakpoint 1.1
frame #0: 0x00000001002653b9 DCERPC`rpc_ss_ndr_unmar_interp(IDL_parameter_count=0, IDL_type_index=1548, IDL_param_vector=0x000070000e1507e0, IDL_msp=0x000070000e1503a0) at ndrui.c:2342:13
Target 0: (rpcsvchost) stopped.
(lldb) p IDL_msp->IDL_left_in_buff
(idl_ulong_int) $191 = 4
(lldb) p IDL_msp->IDL_mp
(idl_byte *) $192 = 0x00006310000288cc ""
(lldb) disassemble -s $rip
DCERPC`rpc_ss_ndr_unmar_interp:
-> 0x1002653b9 <+147385>: call rax
0x1002653bb <+147387>: mov rax, qword ptr [rbx + 0x4dc8]
0x1002653c2 <+147394>: add rax, 0x220
0x1002653c8 <+147400>: mov qword ptr [rbx + 0x78], rax
0x1002653cc <+147404>: shr rax, 0x3
(lldb) disassemble -s $rax
DCERPC`rpc__cn_fragbuf_free:
In the above output, we can observe the IDL_msp->IDL_mp
pointer to be freed via rpc__cn_fragbuf_free
is 0x00006310000288cc, and the current value of IDL_msp->IDL_left_in_buff
is 4. Continuing execution further will lead us to marshalling of reply data after the RPC function has been invoked:
Process 52848 stopped
* thread #15, stop reason = step over
frame #0: 0x00000001001af662 DCERPC`rpc_ss_ndr_marsh_interp(IDL_parameter_count=2, IDL_type_index=1600, IDL_param_vector=0x000070000e1507e0, IDL_msp=0x000070000e1503a0) at ndrmi.c:1692:5
Target 0: (rpcsvchost) stopped.
(lldb) p IDL_msp->IDL_mp
(idl_byte *) $195 = 0x00006310000288cc ""
(lldb) p IDL_msp->IDL_left_in_buff
(idl_ulong_int) $196 = 4
We can observe that IDL_msp
and IDL_left_in_buff
are still the same. Further on, positive IDL_msp->IDL_left_in_buff
will cause the check in rpc_ss_ndr_marsh_check_buffer
to fail to initialize the buffer at [5], and we will have the following just before the call to memcpy:
* thread #15, stop reason = step over
frame #0: 0x000000010018d287 DCERPC`rpc_ss_ndr_marsh_by_copying(element_count=8, element_size=1, array_addr=0x000070000e150750, IDL_msp=0x000070000e1503a0) at ndrmi.c:334:9
Target 0: (rpcsvchost) stopped.
(lldb) p IDL_msp->IDL_left_in_buff
(idl_ulong_int) $203 = 4
(lldb) p IDL_msp->IDL_mp
(idl_byte *) $204 = 0x00006310000288cc ""
The actual call to memcpy
triggers a use-after- free.
To trigger this vulnerability through netlogon
RPC service, an invalid (or valid but overly-long) RPC request targeting function 0x04 can be made to /var/rpc/ncalrpc/netlogon
endpoint directly or over SMB. Analysis was performed with Address Sanitizer, but the vulnerability is observable with reuglar debug heap via libgmalloc
.
==52848==ERROR: AddressSanitizer: heap-use-after-free on address 0x6310000288cc at pc 0x00010057bcfc bp 0x70000e14afd0 sp 0x70000e14a790
WRITE of size 4 at 0x6310000288cc thread T14
==52848==WARNING: invalid path to external symbolizer!
==52848==WARNING: Failed to use and restart external symbolizer!
#0 0x10057bcfb in wrap_memmove+0x2ab (/Users/anikolich/libclang_rt.asan_osx_dynamic.dylib:x86_64h+0x1ccfb)
#1 0x10018d32a in rpc_ss_ndr_marsh_by_copying+0x2aa (/Users/anikolich/DCERPC:x86_64+0x6e32a)
#2 0x1001a159a in rpc_ss_ndr_m_fix_or_conf_arr+0x81a (/Users/anikolich/DCERPC:x86_64+0x8259a)
#3 0x1001a0163 in rpc_ss_ndr_marsh_fixed_arr+0x313 (/Users/anikolich/DCERPC:x86_64+0x81163)
#4 0x100198648 in rpc_ss_ndr_marsh_struct+0xb218 (/Users/anikolich/DCERPC:x86_64+0x79648)
#5 0x1001b7d09 in rpc_ss_ndr_marsh_interp+0x8879 (/Users/anikolich/DCERPC:x86_64+0x98d09)
#6 0x101f89a82 in op4_ssr+0x2b1 (/usr/lib/rpcsvc/netlogon.bundle:x86_64+0x9a82)
#7 0x1003d67b6 in rpc__cn_call_executor+0x1366 (/Users/anikolich/DCERPC:x86_64+0x2b77b6)
#8 0x10033de5b in cthread_call_executor+0x4fb (/Users/anikolich/DCERPC:x86_64+0x21ee5b)
#9 0x1001248b2 in proxy_start+0x1e2 (/Users/anikolich/DCERPC:x86_64+0x58b2)
#10 0x7fff6ce82108 in _pthread_start+0x93 (/usr/lib/system/libsystem_pthread.dylib:x86_64+0x6108)
#11 0x7fff6ce7db8a in thread_start+0xe (/usr/lib/system/libsystem_pthread.dylib:x86_64+0x1b8a)
0x6310000288cc is located 204 bytes inside of 65597-byte region [0x631000028800,0x63100003883d)
freed by thread T14 here:
#0 0x1005a7639 in wrap_free+0xa9 (/Users/anikolich/libclang_rt.asan_osx_dynamic.dylib:x86_64h+0x48639)
#1 0x100304609 in rpc__mem_free+0x1c9 (/Users/anikolich/DCERPC:x86_64+0x1e5609)
#2 0x100303b4b in rpc__list_element_free+0x7db (/Users/anikolich/DCERPC:x86_64+0x1e4b4b)
#3 0x1003d6f5b in rpc__cn_fragbuf_free+0x1b (/Users/anikolich/DCERPC:x86_64+0x2b7f5b)
#4 0x1002653ba in rpc_ss_ndr_unmar_interp+0x23fba (/Users/anikolich/DCERPC:x86_64+0x1463ba)
#5 0x101f89a07 in op4_ssr+0x236 (/usr/lib/rpcsvc/netlogon.bundle:x86_64+0x9a07)
#6 0x1003d67b6 in rpc__cn_call_executor+0x1366 (/Users/anikolich/DCERPC:x86_64+0x2b77b6)
#7 0x10033de5b in cthread_call_executor+0x4fb (/Users/anikolich/DCERPC:x86_64+0x21ee5b)
#8 0x1001248b2 in proxy_start+0x1e2 (/Users/anikolich/DCERPC:x86_64+0x58b2)
#9 0x7fff6ce82108 in _pthread_start+0x93 (/usr/lib/system/libsystem_pthread.dylib:x86_64+0x6108)
#10 0x7fff6ce7db8a in thread_start+0xe (/usr/lib/system/libsystem_pthread.dylib:x86_64+0x1b8a)
previously allocated by thread T15 here:
#0 0x1005a74f0 in wrap_malloc+0xa0 (/Users/anikolich/libclang_rt.asan_osx_dynamic.dylib:x86_64h+0x484f0)
#1 0x100303c2d in rpc__mem_alloc+0x1d (/Users/anikolich/DCERPC:x86_64+0x1e4c2d)
#2 0x100302846 in rpc__list_element_alloc+0xb96 (/Users/anikolich/DCERPC:x86_64+0x1e3846)
#3 0x1003d6fc8 in rpc__cn_fragbuf_alloc+0x28 (/Users/anikolich/DCERPC:x86_64+0x2b7fc8)
#4 0x1003fb430 in receive_packet+0x4d0 (/Users/anikolich/DCERPC:x86_64+0x2dc430)
#5 0x1003f08b8 in receive_dispatch+0x7b8 (/Users/anikolich/DCERPC:x86_64+0x2d18b8)
#6 0x1003ede30 in rpc__cn_network_receiver+0x1b40 (/Users/anikolich/DCERPC:x86_64+0x2cee30)
#7 0x1001248b2 in proxy_start+0x1e2 (/Users/anikolich/DCERPC:x86_64+0x58b2)
#8 0x7fff6ce82108 in _pthread_start+0x93 (/usr/lib/system/libsystem_pthread.dylib:x86_64+0x6108)
#9 0x7fff6ce7db8a in thread_start+0xe (/usr/lib/system/libsystem_pthread.dylib:x86_64+0x1b8a)
Thread T14 created by T2 here:
#0 0x1005a167c in wrap_pthread_create+0x5c (/Users/anikolich/libclang_rt.asan_osx_dynamic.dylib:x86_64h+0x4267c)
#1 0x10012448b in dcethread_create+0x3fb (/Users/anikolich/DCERPC:x86_64+0x548b)
#2 0x100124b6c in dcethread_create_throw+0x2c (/Users/anikolich/DCERPC:x86_64+0x5b6c)
#3 0x10033d3ee in cthread_create+0x3ae (/Users/anikolich/DCERPC:x86_64+0x21e3ee)
#4 0x10033464e in cthread_pool_start+0x68e (/Users/anikolich/DCERPC:x86_64+0x21564e)
#5 0x100338586 in rpc__cthread_start_all+0x176 (/Users/anikolich/DCERPC:x86_64+0x219586)
#6 0x10035bd2b in rpc_server_listen+0x54b (/Users/anikolich/DCERPC:x86_64+0x23cd2b)
#7 0x10000316b in run_dcerpc_svc(void*)+0x1c (/usr/libexec/rpcsvchost:x86_64+0x10000316b)
#8 0x7fff6ce82108 in _pthread_start+0x93 (/usr/lib/system/libsystem_pthread.dylib:x86_64+0x6108)
#9 0x7fff6ce7db8a in thread_start+0xe (/usr/lib/system/libsystem_pthread.dylib:x86_64+0x1b8a)
Thread T2 created by T0 here:
#0 0x1005a167c in wrap_pthread_create+0x5c (/Users/anikolich/libclang_rt.asan_osx_dynamic.dylib:x86_64h+0x4267c)
#1 0x100002c1f in main+0x13ff (/usr/libexec/rpcsvchost:x86_64+0x100002c1f)
#2 0x7fff6cc7dcc8 in start+0x0 (/usr/lib/system/libdyld.dylib:x86_64+0x1acc8)
Thread T15 created by T3 here:
#0 0x1005a167c in wrap_pthread_create+0x5c (/Users/anikolich/libclang_rt.asan_osx_dynamic.dylib:x86_64h+0x4267c)
#1 0x10012448b in dcethread_create+0x3fb (/Users/anikolich/DCERPC:x86_64+0x548b)
#2 0x100124b6c in dcethread_create_throw+0x2c (/Users/anikolich/DCERPC:x86_64+0x5b6c)
#3 0x1003a15af in rpc__cn_assoc_acb_create+0x47f (/Users/anikolich/DCERPC:x86_64+0x2825af)
#4 0x100302d54 in rpc__list_element_alloc+0x10a4 (/Users/anikolich/DCERPC:x86_64+0x1e3d54)
#5 0x10038bdb7 in rpc__cn_assoc_acb_alloc+0x107 (/Users/anikolich/DCERPC:x86_64+0x26cdb7)
#6 0x100392671 in rpc__cn_assoc_listen+0x251 (/Users/anikolich/DCERPC:x86_64+0x273671)
#7 0x1003db80e in rpc__cn_network_select_dispatch+0x12ce (/Users/anikolich/DCERPC:x86_64+0x2bc80e)
#8 0x100364e1f in lthread_loop+0x65f (/Users/anikolich/DCERPC:x86_64+0x245e1f)
#9 0x100363c8c in lthread+0x28c (/Users/anikolich/DCERPC:x86_64+0x244c8c)
#10 0x1001248b2 in proxy_start+0x1e2 (/Users/anikolich/DCERPC:x86_64+0x58b2)
#11 0x7fff6ce82108 in _pthread_start+0x93 (/usr/lib/system/libsystem_pthread.dylib:x86_64+0x6108)
#12 0x7fff6ce7db8a in thread_start+0xe (/usr/lib/system/libsystem_pthread.dylib:x86_64+0x1b8a)
Thread T3 created by T2 here:
#0 0x1005a167c in wrap_pthread_create+0x5c (/Users/anikolich/libclang_rt.asan_osx_dynamic.dylib:x86_64h+0x4267c)
#1 0x10012448b in dcethread_create+0x3fb (/Users/anikolich/DCERPC:x86_64+0x548b)
#2 0x100124b6c in dcethread_create_throw+0x2c (/Users/anikolich/DCERPC:x86_64+0x5b6c)
#3 0x1003639a3 in rpc__nlsn_activate_desc+0xc3 (/Users/anikolich/DCERPC:x86_64+0x2449a3)
#4 0x10035bc5b in rpc_server_listen+0x47b (/Users/anikolich/DCERPC:x86_64+0x23cc5b)
#5 0x10000316b in run_dcerpc_svc(void*)+0x1c (/usr/libexec/rpcsvchost:x86_64+0x10000316b)
#6 0x7fff6ce82108 in _pthread_start+0x93 (/usr/lib/system/libsystem_pthread.dylib:x86_64+0x6108)
#7 0x7fff6ce7db8a in thread_start+0xe (/usr/lib/system/libsystem_pthread.dylib:x86_64+0x1b8a)
SUMMARY: AddressSanitizer: heap-use-after-free (/Users/anikolich/libclang_rt.asan_osx_dynamic.dylib:x86_64h+0x1ccfb) in wrap_memmove+0x2ab
Shadow bytes around the buggy address:
0x1c62000050c0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x1c62000050d0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x1c62000050e0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x1c62000050f0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x1c6200005100: fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd
=>0x1c6200005110: fd fd fd fd fd fd fd fd fd[fd]fd fd fd fd fd fd
0x1c6200005120: fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd
0x1c6200005130: fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd
0x1c6200005140: fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd
0x1c6200005150: fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd
0x1c6200005160: fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd
Shadow byte legend (one shadow byte represents 8 application bytes):
Addressable: 00
Partially addressable: 01 02 03 04 05 06 07
Heap left redzone: fa
Freed heap region: fd
Stack left redzone: f1
Stack mid redzone: f2
Stack right redzone: f3
Stack after return: f5
Stack use after scope: f8
Global redzone: f9
Global init order: f6
Poisoned by user: f7
Container overflow: fc
Array cookie: ac
Intra object redzone: bb
ASan internal: fe
Left alloca redzone: ca
Right alloca redzone: cb
Fixed by Apple on 2023-03-27, patch information available at: https://support.apple.com/en-us/HT213677
2023-01-25 - Vendor Disclosure
2023-03-27 - Vendor Patch Release
2023-07-13 - Public Release
Discovered by Aleksandar Nikolic of Cisco Talos.