Talos Vulnerability Report

TALOS-2022-1677

Apple DCERPC call request uninitialized memory heap overflow vulnerability

July 13, 2023
CVE Number

CVE-2023-27934

SUMMARY

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.

CONFIRMED VULNERABLE VERSIONS

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

PRODUCT URLS

macOS - https://apple.com

CVSSv3 SCORE

7.5 - CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:U/C:H/I:H/A:H

CWE

CWE-457 - Use of Uninitialized Variable

DETAILS

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.

Crash Information

==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
VENDOR RESPONSE

Fixed by Apple on 2023-03-27, patch information available at: https://support.apple.com/en-us/HT213670

TIMELINE

2022-12-06 - Vendor Disclosure
2023-03-27 - Vendor Patch Release
2023-07-13 - Public Release

Credit

Discovered by Aleksandar Nikolic of Cisco Talos.