
auf Mac 10.12.2
Dieser Privilege-Escalation-Exploit nutzt eine Schwachstelle in mach_voucher_extract_attr_recipe_trap, und die Kernmethode des Exploits basiert auf MACH_MSG_OOL_PORTS_DESCRIPTOR-Nachrichten.
Einfach gesagt: Wenn eine mach_msg eine ool descriptor-Nachricht sendet, kopiert der Kernel die angegebenen Daten aus dem Benutzerraum in den Kernelraum und behält diese Daten bei, bis die Ziel-Task die Nachricht verarbeitet. Wenn die Ziel-Task eine Nachricht mit ool descriptor empfängt, kopiert der Kernel die Daten aus dem Kernelraum in den Benutzerraum (nicht unbedingt eine echte Kopie). Daher kann dieser technische Punkt genutzt werden, um Daten in den Kernel-Heap zu schreiben oder aus dem Kernel zu lesen.
Da dieser Exploit deutlich komplexer ist als Trident, werde ich ihn Schritt für Schritt analysieren – von der Entstehung der Schwachstelle bis zur vollständigen Ausnutzung. Der Code ist auf meinem GitHub verfügbar.
In iOS 10 und macOS 10.12 wurde eine neue Funktion namens mach_voucher_extract_attr_recipe_trap hinzugefügt. Es handelt sich um einen Mach trap, der innerhalb der Sandbox aufgerufen werden kann. Nachfolgend der Quellcode dieser Funktion:
kern_return_t
mach_voucher_extract_attr_recipe_trap(struct mach_voucher_extract_attr_recipe_args *args)
{
ipc_voucher_t voucher = IV_NULL;
kern_return_t kr = KERN_SUCCESS;
mach_msg_type_number_t sz = 0;
// Kopiert die Adresse von recipe_size in sz; sz enthält dann den Wert von kalloc_size
if (copyin(args->recipe_size, (void *)&sz, sizeof(sz))) <---------- (a)
return KERN_MEMORY_ERROR;
if (sz > MACH_VOUCHER_ATTR_MAX_RAW_RECIPE_ARRAY_SIZE)
return MIG_ARRAY_TOO_LARGE;
voucher = convert_port_name_to_voucher(args->voucher_name);
if (voucher == IV_NULL)
return MACH_SEND_INVALID_DEST;
mach_msg_type_number_t __assert_only max_sz = sz;
if (sz < MACH_VOUCHER_TRAP_STACK_LIMIT) {
/* keep small recipes on the stack for speed */
uint8_t krecipe[sz];
if (copyin(args->recipe, (void *)krecipe, sz)) {
kr = KERN_MEMORY_ERROR;
goto done;
}
kr = mach_voucher_extract_attr_recipe(voucher, args->key,
(mach_voucher_attr_raw_recipe_t)krecipe, &sz);
assert(sz <= max_sz);
if (kr == KERN_SUCCESS && sz > 0)
kr = copyout(krecipe, (void *)args->recipe, sz);
} else {
uint8_t *krecipe = kalloc((vm_size_t)sz); <---------- (b)
if (!krecipe) {
kr = KERN_RESOURCE_SHORTAGE;
goto done;
}
if (copyin(args->recipe, (void *)krecipe, args->recipe_size)) { <----------- (c)
kfree(krecipe, (vm_size_t)sz);
kr = KERN_MEMORY_ERROR;
goto done;
}
kr = mach_voucher_extract_attr_recipe(voucher, args->key,
(mach_voucher_attr_raw_recipe_t)krecipe, &sz);
assert(sz <= max_sz);
if (kr == KERN_SUCCESS && sz > 0)
kr = copyout(krecipe, (void *)args->recipe, sz);
kfree(krecipe, (vm_size_t)sz);
}
kr = copyout(&sz, args->recipe_size, sizeof(sz));
done:
ipc_voucher_release(voucher);
return kr;
}
args->recipe_size in sz geschrieben wird.sz zwischen MACH_VOUCHER_ATTR_MAX_RAW_RECIPE_ARRAY_SIZE (5120) und MACH_VOUCHER_TRAP_STACK_LIMIT (256) liegt, wird ein Kernel-Heap-Puffer der Größe sz allokiert.sz, sondern ein Benutzerraumzeiger. Dies führt zu einem Heap-Überlauf, den wir ausnutzen. Zudem hat die Funktion copyin die Eigenschaft, dass sie bei einer nicht gemappten Seite abbricht – diese Eigenschaft wird in unserem PoC ausgenutzt:freelist die Position neu zugewiesener Speicherblöcke nicht mehr vorhersagbar ist.Zuerst müssen wir verstehen, wie mach msg mit MACH_MSG_OOL_PORTS_DESCRIPTOR umgeht. Wenn der Kernel eine komplexe Nachricht empfängt und feststellt, dass es sich um einen ports descriptor handelt, übergibt er diese (aufgerufen durch ipc_kmsg_copyin) an die Funktion ipc_kmsg_copyin_ool_ports_descriptor, um alle Port-Objekte auszulesen. Diese Funktion ruft kalloc auf, um den benötigten Speicher zu allozieren (auf 64-Bit-Systemen ist der allozierte Speicher das Doppelte der Eingabe, da die Länge der name 4 Bytes beträgt) und konvertiert dann gültige port-name-Werte in die tatsächliche ipc_port-Objektadresse. Für Eingaben von MACH_PORT_NULL oder MACH_PORT_DEAD bleiben die Werte unverändert.
/* calculate length of data in bytes, rounding up */
if (os_mul_overflow(count, sizeof(mach_port_t), &ports_length)) {
*mr = MACH_SEND_TOO_LARGE;
return NULL;
}
if (os_mul_overflow(count, sizeof(mach_port_name_t), &names_length)) {
*mr = MACH_SEND_TOO_LARGE;
return NULL;
}
if(ports_length == 0){
return user_desc;
}
data = kalloc(ports_length); // Allokiert Speicher
...
objects = (ipc_object_t *) data;
dsc->address = data;
for ( i = 0; i < count; i++) {
mach_port_name_t name = names[i];
ipc_object_t object;
if (!MACH_PORT_VALID(name)) {
objects[i] = (ipc_object_t)CAST_MACH_NAME_TO_PORT(name);// IPC_PORT_DEAD continue;
}
...
}
Beim Angriff senden wir daher viele MACH_PORT_DEAD, um den Speicherbereich mit 0xFFFFFFFFFFFFFFFF (MACH_PORT_DEAD) zu füllen, und lösen dann die Schwachstelle aus, um einen der IPC_PORT_DEAD in einen vom Angreifer vorbereiteten Speicherbereich zu ändern. Wenn der Bereich auf eine gültige ipc port-Struktur zeigt, kann der Angreifer nach dem Empfang der OOL PORTS-Nachricht im Benutzerraum den port name erhalten, der diesem ipc_port entspricht, und den nächsten Angriffsschritt durchführen.
ipc_object-ObjektsDa wir diesen fake port bereits haben, müssen wir für die Informationsoffenlegung wissen, nach welchen Parametern der Kernel ihn unterschiedlich behandelt. Zunächst betrachten wir die Struktur von ipc_port:
struct ipc_port {
// Der Zeiger auf ipc_object befindet sich in den ersten acht Bytes – Ziel unseres Überlaufangriffs
struct ipc_object ip_object; // Typ des Port-Objekts struct ipc_mqueue,ip_messages;
struct ipc_queue ip_messages; // Nachrichtenwarteschlange
union {
struct ipc_space *receiver;
struct ipc_port *destination;
ipc_port_timestamp_t timestamp;
}data;
union {
ipc_importance_task_t imp_task;
ipc_kobject_t kobject; // Kernel-Objekt zum Port
uintptr_t alias;
}kdata;
...
} __attribute__((__packed__));
Dabei gibt es ein Kernel-Objekt, das mit dem Port verknüpft ist. Welchem Kernel-Objekttyp dieses ipc_port entspricht, wird durch die Eigenschaften von ipc_object bestimmt. Wir konstruieren also gezielt das ipc_object.
fakeport->io_bits = IO_BITS_ACTIVE | IKOT_CLOCK; // Als IKOT_CLOCK-Objekt und aktiv setzen
fakeport->io_lock_data[12] = 0x11; // Port-Sperre als aktiv setzen, um Deadlock zu vermeiden
Der Kernel betrachtet dieses ipc_port dann als Port für die Kommunikation mit einem IKOT_CLOCK-Objekt. Das nächste Ziel ist es, die Kernel-Basisadresse zu leaken: