Talos Vulnerability Report

TALOS-2023-1717

Apple DCERPC association groups use-after-free vulnerability

July 13, 2023
CVE Number

CVE-2023-32387

SUMMARY

A use-after-free vulnerability exists in the library supporting DCERPC functionality in Apple macOS 13.1. A series of specially crafted network packets trigger a use-after-free condition which can lead to memory corruption and 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 13.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-416 - Use After Free

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 the rpcsvchost binary, which opens a number of UNIX sockets that expose different RPC functionality.

The DCERPC specification defines an association as a utility object aiding the communication between server and client. These associations correspond to one transport connection and are logically grouped in so-called association groups. For every RPC session a new association object is allocated. In the codebase, an association object has a type of rpc_cn_assoc_s_t.

struct rpc_cn_assoc_s_t
{
    rpc_list_t                          link;   /* MUST BE 1ST */       [1]
    rpc_cn_sm_ctlblk_t                  assoc_state;
    unsigned32                          assoc_status;
    unsigned32                          assoc_local_status;
    unsigned16                          assoc_flags;
    unsigned16                          assoc_ref_count;
    unsigned16                          assoc_acb_ref_count;
    ...
}
...
typedef struct
{
    pointer_t   next;   /* next element of list                     */
    pointer_t   last;   /* last element of list in a descriptor or  */
                        /* pointer to the prior element in an element */
} rpc_list_t, *rpc_list_p_t;

The associations are logically put into association groups. Typically a server and client communication will create one association group, with each association being responsible for an RPC session. An association group is a doubly linked list holding association objects, with every association object holding a next and last pointer for the corresponding list elements at [1] above.

Through the do_assoc_req_action_rtn() and accept_add_action_rtn() RPC calls, the function rpc__cn_assoc_grp_alloc() is called, which quite predictably allocates a new association group and adds the association to the group.

PRIVATE rpc_cn_local_id_t rpc__cn_assoc_grp_alloc
(
  rpc_addr_p_t            rpc_addr,
  rpc_transport_info_p_t  transport_info,
  unsigned32              type,
  unsigned32              rem_id,
  unsigned32              *st
)
{
    ...
    /*
     * Ideally we'd like to locate an association group in the
     * existing table which is not being used. Worst case we'll
     * increase the size of the table to get a new group.
     */
    assoc_grp = rpc_g_cn_assoc_grp_tbl.assoc_grp_vector;
    for (i = 0, found_assoc_grp = false;
         i < rpc_g_cn_assoc_grp_tbl.grp_count;
         i++)
    {
        if (assoc_grp[i].grp_state.cur_state == RPC_C_ASSOC_GRP_CLOSED)         [2]
        {
            assoc_grp = &assoc_grp[i];
            found_assoc_grp = true;
            break;
        }
    }

    if (!found_assoc_grp)
    {
        /*
         * The association group table will have to be expanded to
         * get a free group.
         */
        grp_id = rpc__cn_assoc_grp_create (st);                                 [3]
        ...
    }

    ...

    /*
     * Finally send a new association event through the group state
     * machine.
     */
    RPC_CN_ASSOC_GRP_EVAL_EVENT (assoc_grp,
                                 RPC_C_ASSOC_GRP_NEW,
                                 NULL,
                                 assoc_grp->grp_status);
    ...
}

If no available association group is found at [2], the code proceeds to create a new group in rpc__cn_assoc_grp_create() at [3] above.

#define RPC_C_ASSOC_GRP_ALLOC_SIZE              10

INTERNAL rpc_cn_local_id_t rpc__cn_assoc_grp_create
(
  unsigned32              *st
)
{
    ...
    new_count = old_count + RPC_C_ASSOC_GRP_ALLOC_SIZE;                                [4]
    ...
    /*
     * First allocate a new association group table larger than the
     * existing by a fixed amount.
     */
    RPC_MEM_ALLOC (new_assoc_grp,
                   rpc_cn_assoc_grp_p_t,
                   sizeof(rpc_cn_assoc_grp_t) * new_count,
                   RPC_C_MEM_CN_ASSOC_GRP_BLK,
                   RPC_C_MEM_WAITOK);

    /*
     * If there is an old association group table copy it into the
     * new table and free it.
     */
    if (rpc_g_cn_assoc_grp_tbl.assoc_grp_vector != NULL)
    {
        memcpy (new_assoc_grp,
                rpc_g_cn_assoc_grp_tbl.assoc_grp_vector,
                (old_count * sizeof (rpc_cn_assoc_grp_t)));
        for (i = 0; i < old_count; i++)
        {
            /*
             * Relocate the "last" pointer in the head of the grp_assoc_list.
             * We don't check group's state because they must be all active.
             * Otherwise, this function never get called. (grp_assoc_list.next
             * shouldn't be NULL.)
             */
            if (new_assoc_grp[i].grp_assoc_list.next != NULL)
            {
                ((rpc_list_p_t)(new_assoc_grp[i].grp_assoc_list.next))->last =          [5]
                    (pointer_t)&new_assoc_grp[i].grp_assoc_list;
            }
        }
        ...
    }
    ...
}

At [4], it is evident that the code allocates groups in increments of 10. So at first call, memory for 10 new groups is allocated, then memory for 20 groups is allocated, 10 previous groups and 10 new groups, etc. Next, the old groups are copied to the newly allocated memory. Finally at [5], an update at the linked list is performed for each head element, setting the value of the last pointer to the head of the group linked list.

When the connection between client and server terminates, the association object is freed in rpc__cn_assoc_acb_dealloc(). Many cleanup actions are performed; however, no care is employed to make sure that the association is removed from a group. In essence, if an association is deallocated for any reason, a dangling reference of the association object may still exist in an association group.

We saw that the code allocates groups in increments of 10. After 10 association requests by a client, in the 11th association request a new association group will need to be allocated. Then at [5] the last pointer of the head element of each association group will be updated. Naturally, if the head element is a previously freed association, a use-after-free scenario can occur. Timing of free and reuse depend on network connections and are under attacker control. With precise timing and memory layout control, this use-after-free can result in memory corruption, which can ultimately lead to arbitrary code execution.

VENDOR RESPONSE

Fixed by Apple on 2023-05-18, patch information available at: https://support.apple.com/en-us/HT213760

TIMELINE

2023-02-14 - Vendor Disclosure
2023-05-18 - Vendor Patch Release
2023-07-13 - Public Release

Credit

Discovered by Dimitrios Tatsis of Cisco Talos.