The iOS 12.3/MacOS 10.14.5 version was released on May 13th, 2019. This update patched a Use-After-Free vulnerability in the XNU kernel that Jamf Threat Labs team independently discovered in early May 2019. However, at the time of writing, the Jamf team is not aware whether a CVE was assigned to this vulnerability since it was patched during our preparations to disclose this vulnerability to Apple.
Based on Jamf forensics intelligence, we suspect that threat actors are exploiting this vulnerability in the wild against Mobile Device Management (MDM) users. Jamf continues the investigation to confirm this conclusively. As a precaution, Jamf advises updating iOS/OS X devices to the latest software version.
Once the initial code execution had been achieved, this potent vulnerability allows complete device takeover. Furthermore, this vulnerability can be accessed from sandboxed processes and applications on supervised devices.
Vulnerability details
The Network Extension Control Policy (NECP) is described in /bsd/net/necp.c.
“The goal of this module is to allow clients connecting via a kernel control socket to create high-level policy sessions, which are ingested into low-level kernel policies that control and tag traffic at the application, socket, and IP layers.”
Starting MacOS 10.14 and iOS 12, UDP support contains a Garbage Collection (GC) thread which is added to the Content Filter.
/*
* NECP FILTER CONTROL UNIT
*
* A user space filter agent uses the Network Extension Control Policy (NECP)
* database to specify which TCP/IP sockets need to be filtered. The NECP
* criteria may be based on a variety of properties like user ID or proc UUID.
*
* The NECP "filter control unit" is used by the socket content filter subsystem
* to deliver the relevant TCP/IP content information to the appropriate
* user space filter agent via its kernel control socket instance.
* This works as follows:
*
* 1) The user space filter agent specifies an NECP filter control unit when
* in adds its filtering rules to the NECP database.
*/
The function cfil_sock_udp_get_flow first lookups an existing entry (comment 1 in the code sample below) of a combination of Local Address/Port, and Remote Address/Port (laddr, lport, faddr, fport). If an existing entry does not exist, a new entry will be generated and inserted into the head of cfentry_link.
A cfdb_only_entry pointer always points to the latest entry (comment 2 below).
Later, the cfil_info_alloc allocates a new cfil_info object which contains a unique identifier cfil_sock_id, then inserts the cfil_info into the tail of a linked-list called cfi_link (comment 3).
The GC thread wakes up every 10 seconds, it adds the sock_id of the expired sockets into a list called expired_array (Comment [a] below), then frees the cfil_info in the expired_array in another loop (Comment ).
cfil_info_udp_expire(void *v, wait_result_t w)
{
...
TAILQ_FOREACH(cfil_info, &cfil_sock_head, cfi_link) {
if (expired_count >= UDP_FLOW_GC_MAX_COUNT)
break;
if (IS_UDP(cfil_info->cfi_so)) {
if (cfil_info_idle_timed_out(cfil_info, UDP_FLOW_GC_IDLE_TO, current_time) ||
cfil_info_action_timed_out(cfil_info, UDP_FLOW_GC_ACTION_TO) ||
cfil_info_buffer_threshold_exceeded(cfil_info)) {
expired_array[expired_count] = cfil_info->cfi_sock_id;//[a]
expired_count++;
}
}
}
cfil_rw_unlock_shared(&cfil_lck_rw);
if (expired_count == 0)
goto go_sleep;
for (uint32_t i = 0; i < expired_count; i++) {
// Search for socket (UDP only and lock so)
so = cfil_socket_from_sock_id(expired_array[i], true);//[b]
if (so == NULL) {
continue;
}
cfil_info = cfil_db_get_cfil_info(so->so_cfil_db, expired_array[i]);
...
cfil_db_delete_entry(db, hash_entry);
cfil_info_free(cfil_info);//[c]
...
}
The cfdb_only_entry should be set to NULL in function cfil_db_delete_entry. However the db->cfdb_only_entry = NULL;(line 25) is never executed.
Upon a closer look at the cfil_db_get_cfil_info function, a different path will be executed when only a single entry is left (fast path) for better performance.
struct cfil_info *
cfil_db_get_cfil_info(struct cfil_db *db, cfil_sock_id_t id)
{
struct cfil_hash_entry *hash_entry = NULL;
...
// This is an optimization for connected UDP socket which only has one flow.
// No need to do the hash lookup.
if (db->cfdb_count == 1) { //fast path
if (db->cfdb_only_entry && db->cfdb_only_entry->cfentry_cfil &&
db->cfdb_only_entry->cfentry_cfil->cfi_sock_id == id) {
return (db->cfdb_only_entry->cfentry_cfil);
}
}
hash_entry = cfil_db_lookup_entry_with_sockid(db, id);
return (hash_entry != NULL ? hash_entry->cfentry_cfil : NULL);
}
If two different cfil_info objects have the same cfil_sock_id, the following flow occurs:
In the 1st loop cfil_db_get_cfil_info returns entry2 which is the first element of the cfentry_link that will be freed in later execution; In the 2nd loop cfil_db_get_cfil_info goes into the fast path and returns the object pointed by cfdb_only_entry which is the freed entry2, so the kernel will panic in later execution as a result of a Use-After-Free vulnerability.
In order to generate the cfil_sock_id collision, we need to know how the cfil_sock_id was built.
The cfi_sock_id is calculated by so_gencnt, faddr, laddr, fport, lport.
so_gencnt is the generation count for sockets and it remains the same for a single socket. The higher 32 bits are from so_gencnt, and the lower 32 bits are an XOR operation result based on laddr, faddr, lport, and fport.
Sending two identical UDP requests will only generate one cfil_info object and at least one of the laddr, lport, faddr, fport should be different so the function cfil_sock_udp_get_flow doesn’t return immediately after cfil_db_lookup_entry.
In summary, in order to reproduce this panic, we need to send two UDP requests that meet the following prerequisites:
Identical so_gencnt, which means the same socket object;
Identical flowhash;
Different addresses or ports.
The requirements can be fulfilled by crafting the faddr, fport value.
POC Setup Environment
Running the PoC on your MacOS might not take effect unless your device has MDM enabled. To trigger the vulnerability, the device should meet the following conditions:
At least one Content Filter is attached.
A NECP policy that affects UDP requests is added to the NECP database.
The affected NECP policy and the attached Content Filter have the same filter_control_unit.
cfil_sock_udp_handle_data(bool outgoing, struct socket *so,
struct sockaddr *local, struct sockaddr *remote,
struct mbuf *data, struct mbuf *control, uint32_t flags)
{
...
if (cfil_active_count == 0) {//[a]
CFIL_LOG(LOG_DEBUG, "CFIL: UDP no active filter");
OSIncrementAtomic(&cfil_stats.cfs_sock_attach_in_vain);
return (error);
}
filter_control_unit = necp_socket_get_content_filter_control_unit(so);//[b]
if (filter_control_unit == 0) {
CFIL_LOG(LOG_DEBUG, "CFIL: UDP failed to get control unit");
return (error);
}
...
hash_entry = cfil_sock_udp_get_flow(so, filter_control_unit, outgoing, local, remote);
if (hash_entry == NULL || hash_entry->cfentry_cfil == NULL) {
CFIL_LOG(LOG_ERR, "CFIL: Falied to create UDP flow");
return (EPIPE);
}
...
}
The content filter is not activated by default and to attach it manually, we need to run Apple’s network-cmds cfilutil. Please note that cfilutil is not a pre-installed tool and you might want to compile it from the source code.
The following command activates the content filter in a way that the check in line [a] would pass.
sudo cfilutil -u [control_unit]
Control_unit is an integer value that should be the same as the filter_control_unit as in the NECP policy.
sudo cfilutil -u 100
Proof of Concept Code
The PoC code is surprisingly simple, only a few lines of Python code are required to implement it. The device will panic in a few seconds after running the PoC code. The address&port pair in the PoC is different while having the same flowhashin Content Filter.
The following panic was generated on OS X following execution of the POC:
Jamf Threat Labs is a global team of experienced threat researchers, cybersecurity experts and data scientists with skills that span penetration testing, network monitoring, malware research and app risk assessment. Jamf Threat Labs primarily monitors and explores emerging threats affecting Mac and mobile devices. The team’s research is published with the aim of raising awareness of specific threats while also improving awareness and advocacy of security practices to protect the modern workforce.