Skip to content

OOL ports array spray

Bir out-of-line ports descriptor taşıyan Mach message'lar göndermek, kernel'i ipc_port * pointer'lardan oluşan bir array'i kalloc etmeye zorlar; bu controllable allocation kalloc zone'ları groom etmek, free edilmiş objeleri reallocate etmek ve kernel pointer'larını leak/control etmek için kullanılır.

Mechanism

Bir OOL ports descriptor neden heap-shaping primitive'idir

Bir Mach message body, descriptor taşıyabilir — message user/kernel sınırını geçerken kernel'in özel olarak yorumladığı typed entry'ler. Bunlardan biri MACH_MSG_OOL_PORTS_DESCRIPTOR'dır ("out-of-line ports" descriptor, type value 2). Şöyle tanımlanır:

typedef struct {
    void*                 address;      // user array of port names
    mach_msg_size_t       count;        // number of ports
    boolean_t             deallocate;
    mach_msg_copy_options_t copy;        // MACH_MSG_PHYSICAL_COPY / VIRTUAL_COPY
    mach_msg_descriptor_type_t type;     // MACH_MSG_OOL_PORTS_DESCRIPTOR
} mach_msg_ool_ports_descriptor_t;

Message gönderildiğinde, ipc_kmsg_copyin_ool_ports_descriptor() user'ın sağladığı count adet port name'inden oluşan array'i, in-kernel count adet ipc_port * pointer'dan oluşan bir array'e çevirmek zorundadır. Bu çevrilmiş array'i tutmak için kernel bir kalloc(count * sizeof(ipc_port_t)) yapar — yani 64-bit bir cihazda count * 8 byte. Array, ipc_kmsg'e attach edilir ve message receive edilene (ki bu port'ları dışarı kopyalar ve array'i free eder) ya da destroy edilene kadar kernel heap'inde yaşar.

İki invariant bunu bir attacker için değerli kılar:

  1. Controlled size, controlled contents. count tamamen attacker'ın seçtiği bir değerdir, dolayısıyla allocation herhangi bir target kalloc zone'a düşecek şekilde boyutlandırılabilir (ör. 32 port için kalloc.256). Byte'lar opaque data değildir — her 8-byte slot, kernel'in kendisinin hesapladığı bir kernel pointer'dır (gerçek bir ipc_port'un adresi). Bu sıra dışıdır: çoğu spray primitive sana data'yı control etme imkânı verir ama pointer şeklindeki kernel değerlerini değil.

  2. Allocation tutulur, sonra istendiğinde release edilir. Array, message bir port'un queue'sunda durduğu sürece kalıcıdır ve onu mach_msg(MACH_RCV_MSG) ile aldığında deterministik olarak free edilir. Bu sana bir hole'un ne zaman açılacağı ve ne zaman yeniden doldurulacağı üzerinde hassas control verir — heap feng shui'nin temel gereksinimi.

Slot'lar canlı ipc_port pointer'ları olduğundan, bir OOL ports spray amaca göre üç farklı iş yapabilir: free edilmiş bir objenin slot'unu bilinen bir shape ile güvenilir biçimde reallocate etmek; free edilmiş bir ipc_port *'ın bulunduğu yere seçilmiş bir pointer yerleştirerek dangling bir port'u reconstruct etmek; ve message'ı geri receive edip materialize olmuş port'u okuyarak bir kernel pointer'ını leak etmek (kernel adresi, port context gibi field'larda round trip boyunca hayatta kalır).

Walkthrough

Kanonik gönderim sırası: body'si tek bir OOL ports descriptor declare eden bir message inşa et, sonra onu sahip olduğun bir port'a mach_msg ile gönder.

struct ool_ports_msg {
    mach_msg_header_t          hdr;
    mach_msg_body_t            body;        // msgh_descriptor_count = 1
    mach_msg_ool_ports_descriptor_t ool;
};

mach_port_t ports[32];                       // 32 * 8 = 256 bytes -> kalloc.256
for (int i = 0; i < 32; i++)
    ports[i] = mach_task_self();             // any send right works

struct ool_ports_msg msg = {0};
msg.hdr.msgh_bits        = MACH_MSGH_BITS(MACH_MSG_TYPE_MAKE_SEND, 0)
                         | MACH_MSGH_BITS_COMPLEX;     // complex => has descriptors
msg.hdr.msgh_size        = sizeof(msg);
msg.hdr.msgh_remote_port = target;           // a port whose queue we fill
msg.body.msgh_descriptor_count = 1;

msg.ool.address  = ports;
msg.ool.count    = 32;
msg.ool.deallocate = FALSE;
msg.ool.copy     = MACH_MSG_PHYSICAL_COPY;
msg.ool.type     = MACH_MSG_OOL_PORTS_DESCRIPTOR;

kern_return_t kr = mach_msg(&msg.hdr, MACH_SEND_MSG,
                            sizeof(msg), 0, MACH_PORT_NULL,
                            MACH_MSG_TIMEOUT_NONE, MACH_PORT_NULL);
// kr == KERN_SUCCESS: the kernel kalloc'd a 256-byte array of ipc_port*

Grooming için spray: bu tür message'ların birçoğunu queue'ya koy ki kernel art arda aynı boyutta birçok allocation üretsin; bu fragmentation'ı doldurur ve bir sonraki free edilecek slot'u öngörülebilir biçimde konumlandırır.

mach_port_t holders[1000];
for (int i = 0; i < 1000; i++) {
    mach_port_allocate(mach_task_self(), MACH_PORT_RIGHT_RECEIVE, &holders[i]);
    mach_port_insert_right(mach_task_self(), holders[i], holders[i],
                           MACH_MSG_TYPE_MAKE_SEND);
    send_ool_ports(holders[i], ports, 32);   // each call -> one kalloc.256 array
}

Free edilmiş bir objeyi reclaim etmek için, victim'i free et (ör. UAF'a uğramış bir ipc_port'a veya başka bir kalloc-zone objesine olan son reference'ı bırak), sonra hemen victim'in zone'una uygun boyutta OOL ports message'ları spray et. Yüksek olasılıkla array'lerden biri free edilmiş slot'a düşer; onun 8-byte pointer slot'ları artık victim'in field'larıyla overlap eder.

Spray'i istendiğinde free etmek ve hole'ları yeniden açmak için message'ları receive et:

mach_msg(&recv.hdr, MACH_RCV_MSG, 0, sizeof(recv),
         holders[i], MACH_MSG_TIMEOUT_NONE, MACH_PORT_NULL);
// receiving frees the in-kernel ipc_port* array for that message
Public exploit'ler bu primitive'i nasıl kullandı

Project Zero'nun iOS kernel exploit incelemesi, OOL ports (ve OOL memory) descriptor'larını standart bir grooming/reallocation aracı olarak not eder: mach_portal, dangling port page'ini host port'a işaret eden bir OOL ports array'iyle reallocate etti; voucher_swap ise dangling voucher'ı, önceden allocate edilmiş bir ipc_port'a pointer taşıyan bir OOL ports array'iyle reclaim etti. (Buna karşılık async_wake bu primitive'i kullanmaz — freed port page'ini forced zone GC ile bir ipc_kmsg'in içeriğiyle reallocate eder, OOL ports ile değil.) Her durumda heap layout'unu güvenilir kılan şey, herhangi bir tek bug değil — descriptor'ın deterministik kalloc/free davranışıdır.

Detection

  • Bunlar meşru Mach message'larıdır; syscall seviyesinde bir anomali yoktur. Detection heuristic'tir: büyük count değerleri taşıyan OOL ports descriptor'lı çok sayıda complex message yayan bir process, özellikle kendi sahip olduğu port'lara yönelikse, grooming ile tutarlıdır.
  • Kalloc-zone churn (tek bir zone'da hızlı alloc/free) üzerine kernel telemetry'si, spray-then-reclaim pattern'leriyle korele olur.

Mitigation

  • Modern XNU birçok obje tipini ayrılmış, type-segregated kalloc zone'lara izole eder (kalloc_type / data-vs-pointer separation); bu, bir port-pointer array'inin ilgisiz bir victim objesiyle aynı zone'u paylaşmasını engelleyerek "victim'in generic zone'una pointer şeklinde bir array spray et" varsayımını kırar.
  • Per-CPU cache'ler, zone randomization ve PAC-signed pointer'lar, leak/place edilen pointer'ların güvenilirliğini ve değerini azaltır.

References