Il n'est pas possible de patcher entièrement ce type de vulnérabilité à chaud : effectivement, la seule manière de réellement corriger
la faille est de corriger le code source (ce qui a été fait sur tous les miroirs depuis) en ajoutant la validation des pointeurs iov_base par access_ok()
dans les fonctions concernées. Sur un système non patché, il convient plutôt d'interdire l'appel système correspondant, sys_vmsplice, qui est très peu utilisé
et n'est pas obligatoire puisque il peut être réalisé à l'aide d'un enchaînement d'autres appels systèmes. Ici, nous ne prenons pas une mesure aussi radicale.
Nous vérifions si le pointeur est valide ou non avant d'accepter l'appel système. Il faut être conscient que le code vulnérable est encore présent en mémoire et que
la correction n'est pas forcément complète (mais paraît equilibrée entre un système vulnérable et un appel système en moins).
Si besoin, nous fournissons plus loin dans cette page la ligne à rajouter pour compléter la correction.
Nous avons donc codé un module qui va détourner l'appel système correspondant, logguer tout appel contenant un pointeur invalide puis retourner EFAULT (Bad Address).
Voici donc le hotfix pour Intel x86 que nous vous proposons. Les parties importantes sont abondamment commentées :
/*
###############################################################
# Vmsplice HotFix par François GOICHON pour bases-hacking.org #
###############################################################
*/
// ATTENTION ! FAIRE SYNC AVANT *TOUT* INSMOD || RMMOD
//Les headers standards quand on fait du kernel module
#include <linux/kernel.h> // On travaille dans le kernel
#include <linux/module.h> // On fait un module
#include <linux/syscalls.h> //Les symboles des syscalls
#include <asm/unistd.h> // La liste des syscalls pour __NR
#include <linux/tty.h> // Pour l'utilisationd des terminaux
#include <linux/version.h> // Pour LINUX_VERSION_CODE
#include <linux/sched.h> // Besoin pour init_mm & stuff et current
#include <asm/uaccess.h> // Pour get_user()
#include <asm/cacheflush.h> // Pour global_flush_tlb() , change_page_attr()
#include <asm/page.h> // Macro virt_to_page()
#include <linux/init.h> // Pour KERNEL_PAGE
/*
################
# Infos Module #
################
*/
MODULE_LICENSE("GPL"); //Pour avoir accès complet au système, ne pas souiller le kernel
MODULE_AUTHOR("François GOICHON, www.bases-hacking.org");
MODULE_DESCRIPTION("Hotfix contre l'exploit root Vmsplice, moche et dangereux ;) (02/2008)");
[...]
//Définition de la fonction printf(char *) qui va écrire une chaîne sur le terminal
//courant ainsi que dans les logs systèmes
//Ceci ne nous intéresse pas réellement ici, ce n'est finalement que de la présentation
/*
#########################
# Réécriture du syscall #
#########################
*/
/*
* On synchronise le déchargement du module : le module ne se déchargera que quand
* plus personne n'utilisera notre syscall
*/
static int synchro = 0;
/*
* La table des appels systèmes n'est plus exportée
* dans les kernels 2.6.x, pour ça qu'il n'y a pas l'extern
* qu'on peut voir dans toutes les références
*
* Elle sera remplie par get_sct()
* Static (on ne rigole pas avec la table des sycalls..)
*/
static void **sys_call_table = NULL;
/*
* On va remplacer un appel système. On va donc garder un
* pointeur vers le syscall original (au cas uù quelqu'un l'aurait
* modifié avant nous)
*/
asmlinkage long (*vmsplice_original) (int fd, const struct iovec __user *iov, unsigned long nr_segs, unsigned int flags);
/*
* On redéfinit le syscall
*/
//Syscall de raccordement de pages utilisateurs à un tube
asmlinkage long notre_sys_vmsplice (int fd, const struct iovec __user *iov, unsigned long nr_segs, unsigned int flags) {
synchro++; //printf n'est pas atomique
if (unlikely(!access_ok(VERIFY_READ, iov->iov_base, iov->iov_len)))
{
printk(KERN_ALERT "Exploit root vmsplice sûrement tenté par %d\n",current->uid); //current pointe le contexte courant
synchro--;
return -EFAULT;
}
synchro--;
return vmsplice_original(fd,iov,nr_segs,flags);
}
static int get_sct (void) {
unsigned long *ptr;
/*
* le symbole sys_call_table n'étant plus exportée, on doit
* la retrouver manuellement, "the hackish way" comme qui dirait.
*
* La syscall table est contenue dans le segment Kernel Data
* situé à la suite de Kernel Code (entre end_code et end_data donc)
* On le repère grâce à trois appels choisis arbitrairement
* Ici, sys_close() puis confirmation avec sys_read() et sys_open
*
* Consulter asm/unistd.h pour plus de compréhension de l'algo et
* des constantes utilisées
*
* Cet algo n'est pas portable
*/
ptr=(unsigned long *)((init_mm.end_code + 4) & 0xfffffffc);
printf("Recherche de l'adresse de sys_call_table ...");
printk("Début: 0x%p Fin: 0x%p\n",(unsigned long *)init_mm.end_code,(unsigned long *)init_mm.end_data);
printk("Ptr: 0x%p\n",(unsigned long *)ptr);
/* On fouille la section des données */
while((unsigned long )ptr < (unsigned long)init_mm.end_data) {
if ((unsigned long *)*ptr == (unsigned long *)sys_close) {
printk (KERN_INFO " -> Appel sys_close() trouvé à 0x%p\n", ptr);
if ((unsigned long *)*((ptr-__NR_close)+__NR_read) == (unsigned long *) sys_read && *((ptr-__NR_close)+__NR_open) == (unsigned long) sys_open ) {
printk (" -> table des syscalls possible à 0x%p\n", ptr-__NR_close);
printk (" -> sys_write à 0x%p\n", (unsigned long *)*(ptr-__NR_close+4));
printk (" -> sys_fork à 0x%p\n", (unsigned long *)*(ptr-__NR_close+2));
sys_call_table = (void **) ((unsigned long *)(ptr-__NR_close));
break;
}
}
ptr++;
}
printk(KERN_INFO"sys_call_table trouvée à : 0x%p\n", sys_call_table);
if (sys_call_table == NULL) return 0;
else return 1;
}
static int is_address_writable(unsigned long address)
//Vérifie si on peut écrire à l'adresse address, retourne <= 0 en cas d'erreur, 1 sinon
{
/*
#########################
# Init/Cleanup du module#
#########################
*/
//Initialisation du module - remplacement de open()
int init_module(void)
{
//Attention - trop tard, mais peut-être pour la prochaine fois..
printf("Module dangereux : sync nécessaire avant chargement\n");
printf("La fin du module est encore plus dangereuse ! Sync avant rmmod si votre système de fichiers vous importe...\n");
printk(KERN_INFO "Hotfix pour l'exploit Vmsplice chargé\n");
if (!get_sct())
{ //Si le remplissage de la syscall table n'a pas marché
printf("Table des appels système introuvable. Abandon...\n");
return 1;
}
printf("Table des appels système trouvée\n");
if (!is_address_writable((unsigned long)sys_call_table))
{
printk("La table des appels système est peut-être en read-only\n");
change_page_attr(virt_to_page(sys_call_table), 1, PAGE_KERNEL);
global_flush_tlb();
}
//On garde en mémoire l'ancien syscall et on le remplace
//On pourait vérifier avant avec ls -l /boot/vmlinuz-`uname -r` que la date du fichier < 8 Février 2008
//mais l'administrateur est censé savoir si son système est vulnérable avant d'appliquer ce genre de patch
vmsplice_original = sys_call_table[__NR_vmsplice];
sys_call_table[__NR_vmsplice] = notre_sys_vmsplice;
printf("Système patché\n");
return 0;
}
//Fin du module : on nettoie nos bêtises
void cleanup_module(void)
{
printf("Retour des syscalls à la normale...\n");
//Retourner la table des syscalls à la normale
if (sys_call_table[__NR_vmsplice] != notre_sys_vmsplice) {
printf("Quelqu'un d'autre a joué avec la table des appels système\n");
printf("Le système a de fortes chances de s'en retirer dans un état instable..\n");
printk(KERN_INFO "En attente de synchronisation\n");
while(synchro);
printk(KERN_INFO "Hotfix pour l'exploit Vmsplice déchargé\n");
}
Encore une fois, le principe n'est pas si compliqué que ça : dans un premier temps, on appelle la fonction get_sct() qui, à l'aide de quelques appels systèmes
dont les positions sont standard et connues, va repérer la table des appels systèmes parmi les données du noyau. Ensuite, on vérifie que la table est accessible
en écriture avec la fonction is_address_writable() qui va lire le descripteur correspondant (dans la Page Table). Enfin, on remplace l'adresse du syscall présente
dans la table des appels à l'offset correspondant par notre fonction personnalisée.
Cette fonction loggue simplement l'utilisation de vmsplice puis retourne en état d'erreur pour indiquer que l'appel système a echoué. En réalité, le code vulnérable
demeure présent en mémoire quelque part comme nous l'avons déjà rappellé. On peut aussi
memset(vmsplice_original,0xc3,1);
afin de remplacer le premier octet du code
vulnérable par un retour à l'appelant, ce qui devrait corriger la faille de manière redoutable mais interdira l'appel système. Ceci dit, ce n'est pas très bon d'une manière générale de réécrire les appels (ce qui est différent de simplement le détourner comme nous faisons).
Enfin, la partie déchargement du module vérifie que personne n'utilise notre appel puis rétabli l'ancien appel à sa place. Afin de compiler, nous fournissons dans nos
sources un Makefile adéquat. make insmod permettra de charger le module et make rmmod de le décharger proprement. Illustration avec une tentative avant déchargement :
# make insmod
make[1]: entrant dans le répertoire « /var/www/hacking/sources/Systeme/Vmsplice/src »
make -C /lib/modules/2.6.24-1-686/build M=/var/www/hacking/sources/Systeme/Vmsplice/src modules
make[2]: entrant dans le répertoire « /usr/src/linux-headers-2.6.24-1-686 »
CC [M] /var/www/hacking/sources/Systeme/Vmsplice/src/espion.o
Building modules, stage 2.
MODPOST 1 modules
CC /var/www/hacking/sources/Systeme/Vmsplice/src/espion.mod.o
LD [M] /var/www/hacking/sources/Systeme/Vmsplice/src/espion.ko
make[2]: quittant le répertoire « /usr/src/linux-headers-2.6.24-1-686 »
make[1]: quittant le répertoire « /var/www/hacking/sources/Systeme/Vmsplice/src »
sync
insmod src/espion.ko
Module dangereux : sync nécessaire avant chargement
La fin du module est encore plus dangereuse ! Sync avant rmmod si votre système de fichiers vous importe...
Recherche de l'adresse de sys_call_table ...
Table des appels système trouvée
Détournement des syscalls afin de surveiller le système
OK.
#
On effectue une tentative avec un utilisateur normal puis on décharge :
# make rmmod
sync
rmmod src/espion.ko
Retour des syscalls à la normale...
# dmesg
[...]
Module dangereux : sync nécessaire avant chargement
La fin du module est encore plus dangereuse ! Sync avant rmmod si votre système de fichiers vous importe...
Hotfix pour l'exploit Vmsplice chargé
Recherche de l'adresse de sys_call_table ...Début: 0xc02be915 Fin: 0xc0371384
Ptr: 0xc02be918
-> Appel sys_close() trouvé à 0xc02c3718
-> table des syscalls possible à 0xc02c3700
-> sys_write à 0xc0178fb4
-> sys_fork à 0xc01021d7
sys_call_table trouvée à : 0xc02c3700
Table des appels système trouvée
Détournement des syscalls afin de surveiller le système
Exploit root vmsplice sûrement tenté par 1000
Retour des syscalls à la normale...
En attente de synchronisation
Hotfix pour l'exploit Vmsplice déchargé
#
Le module se charge correctement et capte les tentatives d'utilisation de l'appel : le hotfix paraît donc fonctionnel.