Suspected Bug Collision: iOS/OSX Content Filter Kernel UAF Analysis + POC

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.

May 17 2019 by

Jamf Threat Labs

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.

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 ).

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.

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.

 +--------------------+ +-----------------+
| entry 2 <--------+ cfdb_only_entry |
+--------------------+ +-----------------+
| entry 1 |

Vulnerability Reproduction

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:

  1. Identical so_gencnt, which means the same socket object;
  2. Identical flowhash;
  3. 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:

  1. At least one Content Filter is attached.
  2. A NECP policy that affects UDP requests is added to the NECP database.
  3. The affected NECP policy and the attached Content Filter have the same filter_control_unit.

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.

Control_unit is an integer value that should be the same as the filter_control_unit as in the NECP policy.

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:


After the patch of MacOS 10.14.5/iOS 12.3, the db->cfdb_only_entry = NULL;(line 18), can be correctly executed

Subscribe to the Jamf Blog

Have market trends, Apple updates and Jamf news delivered directly to your inbox.

To learn more about how we collect, use, disclose, transfer, and store your information, please visit our Privacy Policy.