CVE-2023-27934
A heap overflow vulnerability exists in the request processing functionality of DCERPC library as used in Apple macOS 12.6.1 that can lead to use of uninitialized memory. A specially-crafted network packet can cause use of uninitialized memory which can lead to heap overflow and arbitrary code execution. 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-457 - Use of Uninitialized Variable
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.
There exists a vulnerability in a way that DCERPC framework processes request packets with MAYBE
semantics. In DCERPC specification, a packet coming from a client that is marked with a MAYBE
flag signifies that a client doesn’t expect a reply. In DCERPC framework implementation, this changes the codepath taken when sending out a call fault which can result in access of an otherwise uninitialized memory. To reach the vulnerable codepath, a bind request
packet is sent first, to get to the proper state of the state machine. Regardless of the validity of the bind request, a subsequent rpc call request can trigger the vulnerable code path. When responding to a call request, function rpc__cn_call_end
is called and the following code is reached:
/*
* If there are iovector elements in the call_rep,
* free them all. There would not be any in the case
* of a maybe call.
*/
if (RPC_CN_CREP_IOVLEN (call_rep) > 0) [1]
{
for (cur_iov_index = 0;
cur_iov_index < RPC_CN_CREP_IOVLEN (call_rep);
cur_iov_index++)
{
if (RPC_CN_CREP_IOV (call_rep) [cur_iov_index].buff_dealloc != [2]
NULL)
{
(RPC_CN_CREP_IOV (call_rep) [cur_iov_index].buff_dealloc)
(RPC_CN_CREP_IOV (call_rep) [cur_iov_index].buff_addr);
}
RPC_CN_CREP_IOV (call_rep) [cur_iov_index].buff_addr = NULL;
}
}
The above code is responsible for freeing chunks of fragmented packets, and as the comment specifies, it expects that a maybe
call wouldn’t have any. A check at [1] simply translates to the following:
#define RPC_CN_CREP_IOVLEN(cp) (((cp)->buffered_output).iov.num_elt)
If num_elt
of IO vector isn’t 0 (that is, there are additional buffers allocated), it proceeds to iterate over them to perform cleanup. Conveniently, each IO vector element can contain a pointer to a function that is to be called to deallocate it, otherwise its associated buffer address is simply set to NULL
. From this, we can conclude that if num_elt
is somehow bigger than the actual number of elements, a heap overflow would result during dereference at [2].
If we consider the codepaths that lead us to here, one of them goes through function handle_first_frag_action_rtn
in which we can find the following code:
/*
* Copy the opnum field into the local call rep.
*/
call_rep->opnum = RPC_CN_PKT_OPNUM (request_header_p);
if (!(RPC_CN_PKT_FLAGS (request_header_p) & RPC_C_CN_FLAGS_MAYBE)) [3]
{
/*
* Fill in the fields of the response header if this is not
* a maybe call.
*/
RPC_CN_CREP_SIZEOF_HDR (call_rep) = RPC_CN_PKT_SIZEOF_RESP_HDR;
response_header_p = (rpc_cn_packet_p_t) RPC_CN_CREP_SEND_HDR (call_rep);
...
...
...
/*
* Initialize the iovector in the call_rep to contain only
* one initial element, pointing to the protocol header.
* Also, update pointers to show that we can copy data into
* the stub data area.
*/
RPC_CN_CREP_IOVLEN (call_rep) = 1; [4]
RPC_CN_CREP_CUR_IOV_INDX (call_rep) = 0;
RPC_CN_CREP_FREE_BYTES (call_rep) =
RPC_C_CN_SMALL_FRAG_SIZE - RPC_CN_PKT_SIZEOF_RESP_HDR;
RPC_CN_CREP_ACC_BYTCNT (call_rep) = RPC_CN_PKT_SIZEOF_RESP_HDR;
RPC_CN_CREP_FREE_BYTE_PTR(call_rep) =
RPC_CN_PKT_RESP_STUB_DATA (response_header_p);
(RPC_CN_CREP_IOV (call_rep)[0]).data_len = RPC_CN_PKT_SIZEOF_RESP_HDR;
At [3], true branch is taken only if the packet is not a MAYBE
call, and at [4] num_elt
is properly initialized to 1. No initialization happens if the request has a RPC_C_CN_FLAGS_MAYBE
flag set, in which case num_elt
remains uninitialized. If the uninitialized value happens to be non-zero, this can lead to a heap buffer overflow at [2].
This can be observed in a debugger with heap debugging enabled (with address sanitizer and ASAN_OPTIONS=malloc_fill_byte=65
for better visibility):
Process 81985 stopped
* thread #16, stop reason = step over
frame #0: 0x00000001003af2a1 DCERPC`rpc__cn_call_end(call_r=0x000070000b584f40, st=0x000070000b584f60) at cncall.c:1807:5
Target 0: (rpcsvchost) stopped.
(lldb) p *call_rep
(rpc_cn_call_rep_t) $79 = {
common = {
.....
}
call_state = {
.... }
cn_call_status = 469762076
binding_rep = 0x0000611000001a80
assoc = 0x000061600000ff80
prot_header = 0x0000612000001cc0
prot_tlr = 0x0000000000000000
max_seg_size = 4280
alloc_hint = 1094795585
buffered_output = {
iov = {
num_elt = 16705 [5]
elt = {
[0] = {
buff_dealloc = 0x0000000000000000
flags = 'A'
pad1 = 'A'
pad2 = 'A'
pad3 = 'A'
buff_addr = 0x0000612000001cc0 ""
buff_len = 256
data_addr = 0x0000612000001cf0 "\x05\x01
data_len = 0
}
}
}
iov_elmts = {
[0] = {
buff_dealloc = 0x0000000000000000
flags = 'A'
pad1 = 'A'
pad2 = 'A'
pad3 = 'A'
buff_addr = 0x0000000000000000
buff_len = 1094795585
data_addr = 0x4141414141414141 "" [6]
data_len = 1094795585
}
[1] = {
buff_dealloc = 0x0000000000000000
flags = 'A'
pad1 = 'A'
pad2 = 'A'
pad3 = 'A'
buff_addr = 0x0000000000000000
buff_len = 1094795585
data_addr = 0x4141414141414141 ""
data_len = 1094795585
}
In the above excerpt of the contents of rpc_cn_call_rep_t
structure, we can see at [5] the value of num_elt
is equal to 0x4141, which is address sanitizer fill value for uninitialized memory (with ASAN_OPTIONS=malloc_fill_byte=65
, otherwise it would be 0xba
). Additionally, we can see more uninitialized memory in buffered_output
chunks. High value of num_elt
quickly results in a heap overflow and out-of-bounds memory access. Without address sanitizer, this results in the following crash:
Process 82002 exited with status = 9 (0x00000009)
Process 82008 launched: '/usr/libexec/rpcsvchost' (x86_64)
GuardMalloc[rpcsvchost-82008]: Allocations will be placed on 16 byte boundaries.
GuardMalloc[rpcsvchost-82008]: - Some buffer overruns may not be noticed.
GuardMalloc[rpcsvchost-82008]: - Applications using vector instructions (e.g., SSE) should work.
GuardMalloc[rpcsvchost-82008]: version 064535.38.1
Wed Nov 16 16:46:26 2022 main [rpcsvchost.cpp:576] found supported protseq 'ncacn_ip_tcp'
Wed Nov 16 16:46:26 2022 main [rpcsvchost.cpp:576] found supported protseq 'ncalrpc'
Wed Nov 16 16:46:26 2022 main [rpcsvchost.cpp:576] found supported protseq 'ncacn_np'
Wed Nov 16 16:46:26 2022 rpcsvc_register_plugin [rpcsvchost.cpp:441] loading service plugin from /usr/lib/rpcsvc/netlogon.bundle
Wed Nov 16 16:46:26 2022 rpcsvc_register_plugin [rpcsvchost.cpp:458] loading netlogon from netlogon.bundle
Wed Nov 16 16:46:26 2022 rpcsvc_register_plugin [rpcsvchost.cpp:441] loading service plugin from /usr/lib/rpcsvc/mdssvc.bundle
Wed Nov 16 16:46:27 2022 rpcsvc_register_plugin [rpcsvchost.cpp:458] loading mdssvc from mdssvc.bundle
Wed Nov 16 16:46:27 2022 rpcsvc_register_plugin [rpcsvchost.cpp:441] loading service plugin from /usr/lib/rpcsvc/srvsvc.bundle
Wed Nov 16 16:46:27 2022 rpcsvc_register_plugin [rpcsvchost.cpp:458] loading srvsvc from srvsvc.bundle
Wed Nov 16 16:46:27 2022 main [rpcsvchost.cpp:639] sandbox_init: com.apple.msrpc.netlogon.sb succeeded
Process 82008 stopped
* thread #16, stop reason = EXC_BAD_ACCESS (code=EXC_I386_GPFLT)
frame #0: 0x00007fff4d47aa85 DCERPC`rpc__cn_call_end + 480
DCERPC`rpc__cn_call_end:
-> 0x7fff4d47aa85 <+480>: callq *%rcx
0x7fff4d47aa87 <+482>: movzwl 0x108(%r13), %eax
0x7fff4d47aa8f <+490>: movq $0x0, (%rbx)
0x7fff4d47aa96 <+497>: incq %r14
Target 0: (rpcsvchost) stopped.
(lldb) bt
* thread #16, stop reason = EXC_BAD_ACCESS (code=EXC_I386_GPFLT)
* frame #0: 0x00007fff4d47aa85 DCERPC`rpc__cn_call_end + 480
frame #1: 0x00007fff4d483a4e DCERPC`receive_dispatch + 3999
frame #2: 0x00007fff4d4826dd DCERPC`rpc__cn_network_receiver + 1155
frame #3: 0x00007fff4d42f671 DCERPC`proxy_start + 67
frame #4: 0x00007fff6d7d3109 libsystem_pthread.dylib`_pthread_start + 148
frame #5: 0x00007fff6d7ceb8b libsystem_pthread.dylib`thread_start + 15
(lldb) reg read rcx
rcx = 0xaaaaaaaaaaaaaaaa
(lldb)
The above crash happens when an out-of-bounds access results in a check at [2] passing due to access of uninitialized memory. In the true branch of if statement at [2], a function is being called via uninitialized function pointer.
With multiple simultaneous connections and calculated and timed allocations, enough control over memory layout could be achieved to precisely control the contents of the uninitialized memory, which could result in arbitrary code execution.
On macOS, vulnerable code could be reached over network, after authentication, through SMB protocol. However, it can also be triggered through access to local UNIX sockets in /var/rpc/ncalrpc/*
via a regular user.
==82016==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x62f00000c5a0 at pc 0x0001003aface bp 0x700009e62ce0 sp 0x700009e62cd8
READ of size 8 at 0x62f00000c5a0 thread T15
==82016==WARNING: invalid path to external symbolizer!
==82016==WARNING: Failed to use and restart external symbolizer!
#0 0x1003afacd in rpc__cn_call_end+0x9fd (.DCERPC:x86_64+0x290acd)
#1 0x1003f8883 in receive_dispatch+0x8653 (.DCERPC:x86_64+0x2d9883)
#2 0x1003edf60 in rpc__cn_network_receiver+0x1b40 (.DCERPC:x86_64+0x2cef60)
#3 0x1001249e2 in proxy_start+0x1e2 (.DCERPC:x86_64+0x59e2)
#4 0x7fff6d7d3108 in _pthread_start+0x93 (/usr/lib/system/libsystem_pthread.dylib:x86_64+0x6108)
#5 0x7fff6d7ceb8a in thread_start+0xe (/usr/lib/system/libsystem_pthread.dylib:x86_64+0x1b8a)
0x62f00000c5a0 is located 24 bytes to the right of 49544-byte region [0x62f000000400,0x62f00000c588)
allocated by thread T15 here:
#0 0x1005a74f0 in wrap_malloc+0xa0 (.libclang_rt.asan_osx_dynamic.dylib:x86_64h+0x484f0)
#1 0x100303d5d in rpc__mem_alloc+0x1d (.DCERPC:x86_64+0x1e4d5d)
#2 0x100302976 in rpc__list_element_alloc+0xb96 (.DCERPC:x86_64+0x1e3976)
#3 0x1003f65b3 in receive_dispatch+0x6383 (.DCERPC:x86_64+0x2d75b3)
#4 0x1003edf60 in rpc__cn_network_receiver+0x1b40 (.DCERPC:x86_64+0x2cef60)
#5 0x1001249e2 in proxy_start+0x1e2 (.DCERPC:x86_64+0x59e2)
#6 0x7fff6d7d3108 in _pthread_start+0x93 (/usr/lib/system/libsystem_pthread.dylib:x86_64+0x6108)
#7 0x7fff6d7ceb8a in thread_start+0xe (/usr/lib/system/libsystem_pthread.dylib:x86_64+0x1b8a)
Thread T15 created by T4 here:
#0 0x1005a167c in wrap_pthread_create+0x5c (.libclang_rt.asan_osx_dynamic.dylib:x86_64h+0x4267c)
#1 0x1001245bb in dcethread_create+0x3fb (.DCERPC:x86_64+0x55bb)
#2 0x100124c9c in dcethread_create_throw+0x2c (.DCERPC:x86_64+0x5c9c)
#3 0x1003a16df in rpc__cn_assoc_acb_create+0x47f (.DCERPC:x86_64+0x2826df)
#4 0x100302e84 in rpc__list_element_alloc+0x10a4 (.DCERPC:x86_64+0x1e3e84)
#5 0x10038bee7 in rpc__cn_assoc_acb_alloc+0x107 (.DCERPC:x86_64+0x26cee7)
#6 0x1003927a1 in rpc__cn_assoc_listen+0x251 (.DCERPC:x86_64+0x2737a1)
#7 0x1003db93e in rpc__cn_network_select_dispatch+0x12ce (.DCERPC:x86_64+0x2bc93e)
#8 0x100364f4f in lthread_loop+0x65f (.DCERPC:x86_64+0x245f4f)
#9 0x100363dbc in lthread+0x28c (.DCERPC:x86_64+0x244dbc)
#10 0x1001249e2 in proxy_start+0x1e2 (.DCERPC:x86_64+0x59e2)
#11 0x7fff6d7d3108 in _pthread_start+0x93 (/usr/lib/system/libsystem_pthread.dylib:x86_64+0x6108)
#12 0x7fff6d7ceb8a in thread_start+0xe (/usr/lib/system/libsystem_pthread.dylib:x86_64+0x1b8a)
Thread T4 created by T2 here:
#0 0x1005a167c in wrap_pthread_create+0x5c (.libclang_rt.asan_osx_dynamic.dylib:x86_64h+0x4267c)
#1 0x1001245bb in dcethread_create+0x3fb (.DCERPC:x86_64+0x55bb)
#2 0x100124c9c in dcethread_create_throw+0x2c (.DCERPC:x86_64+0x5c9c)
#3 0x100363ad3 in rpc__nlsn_activate_desc+0xc3 (.DCERPC:x86_64+0x244ad3)
#4 0x10035bd8b in rpc_server_listen+0x47b (.DCERPC:x86_64+0x23cd8b)
#5 0x10000316b in run_dcerpc_svc(void*)+0x1c (/usr/libexec/rpcsvchost:x86_64+0x10000316b)
#6 0x7fff6d7d3108 in _pthread_start+0x93 (/usr/lib/system/libsystem_pthread.dylib:x86_64+0x6108)
#7 0x7fff6d7ceb8a 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 (.libclang_rt.asan_osx_dynamic.dylib:x86_64h+0x4267c)
#1 0x100002c1f in main+0x13ff (/usr/libexec/rpcsvchost:x86_64+0x100002c1f)
#2 0x7fff6d5cecc8 in start+0x0 (/usr/lib/system/libdyld.dylib:x86_64+0x1acc8)
SUMMARY: AddressSanitizer: heap-buffer-overflow (.DCERPC:x86_64+0x290acd) in rpc__cn_call_end+0x9fd
Shadow bytes around the buggy address:
0x1c5e00001860: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x1c5e00001870: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x1c5e00001880: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x1c5e00001890: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x1c5e000018a0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
=>0x1c5e000018b0: 00 fa fa fa[fa]fa fa fa fa fa fa fa fa fa fa fa
0x1c5e000018c0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x1c5e000018d0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x1c5e000018e0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x1c5e000018f0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x1c5e00001900: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
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/HT213670
2022-12-06 - Vendor Disclosure
2023-03-27 - Vendor Patch Release
2023-07-13 - Public Release
Discovered by Aleksandar Nikolic of Cisco Talos.