
Exploit di Ian Beer per CVE-2017-2370 (kernel memory r/w su iOS 10.2)
// ianbeer
Exploit di lettura/scrittura arbitraria del kernel per CVE-2017-2370 per iOS 10.2
Testato solo su iPod Touch 6G 14C92 - altri dispositivi/firmware non funzioneranno così come sono!
*** il bug *** mach_voucher_extract_attr_recipe_trap è una mach trap che può essere chiamata da qualsiasi contesto. È codice nuovissimo, aggiunto in iOS 10.
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;
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;
}
Ecco la struttura degli argomenti (controllata dallo spazio utente)
struct mach_voucher_extract_attr_recipe_args { PAD_ARG_(mach_port_name_t, voucher_name); PAD_ARG_(mach_voucher_attr_key_t, key); PAD_ARG_(mach_voucher_attr_raw_recipe_t, recipe); PAD_ARG_(user_addr_t, recipe_size); };
recipe e recipe_size sono puntatori dello spazio utente.
Al punto (a) vengono letti quattro byte dal puntatore userspace recipe_size in sz.
Al punto (b) se sz era inferiore a MACH_VOUCHER_ATTR_MAX_RAW_RECIPE_ARRAY_SIZE (5120) e maggiore di MACH_VOUCHER_TRAP_STACK_LIMIT (256) sz viene utilizzato per allocare un buffer heap del kernel.
Al punto (c) copyin viene chiamato di nuovo per copiare la memoria utente in quel buffer appena allocato, ma invece di passare sz (la dimensione validata che è stata allocata) viene passato args->recipe_size come dimensione. Questo è il puntatore utente alla dimensione, non la dimensione!
Questo porta a un overflow dell'heap del kernel completamente controllato. Nota che il codice in realtà non può funzionare correttamente :)
*** l'exploit ***
Prendo di mira i buffer di messaggi mach preallocati allocati tramite kalloc. I primi 4 byte sono un campo di dimensione che viene utilizzato per determinare dove nel buffer leggere e scrivere un messaggio. Corrompendo questo campo possiamo causare la lettura e la scrittura di messaggi mach al di fuori dei limiti dell'allocazione kalloc che supporta il kmsg.
C'è una leggera complicazione: il kmsg preallocato di un port verrà utilizzato solo per gli effettivi invii di mach_msg da parte del kernel (non per le risposte ai metodi MIG, per esempio). Questo rende un po' più complicato ottenere abbastanza contenuto controllato al loro interno.
Un tipo di messaggio mach che il kernel invia con molti dati controllati dall'utente è un messaggio di eccezione, inviato quando un thread crasha.
Il file load_regs_and_crash.s contiene assembly ARM64 che carica i registri generali ARM64 con il contenuto di un buffer in modo tale che quando crasha il messaggio di eccezione contenga quel buffer di dati (circa 0x70 byte sono controllati.)
Sovrascrivendo il campo ikm_size del port per puntare all'intestazione di un altro port possiamo leggere e scrivere l'intestazione di un altro port e scoprire dove si trova in memoria. Possiamo quindi liberare quel secondo port e riallocare un user client al suo posto che possiamo anche leggere e scrivere.
Leggo il puntatore vtable degli userclients e poi uso la tecnica del gadget OSSerializer::serialize come dettagliato in [https://info.lookout.com/rs/051-ESQ-475/images/pegasus-exploits-technical-details.pdf] per chiamare una funzione arbitraria con due argomenti controllati.
Chiamo uuid_copy che chiama memmove(arg0, arg1, 0x10). Puntando arg0 o arg1 nello stesso userclient (che possiamo leggere ricevendo il messaggio di eccezione) possiamo leggere e scrivere memoria arbitraria del kernel in blocchi di 16 byte.