Depuis la fin des années 1990, le paysage de la défense contre les différents exploits en
mémoire a beaucoup changé. Sous Linux, ce changement s'est essentiellement traduit par l'introduction du patch
PaX, introduisant de nouvelles fonctionnalités au cours des années, allant la randomisation de la base de mmap()
pour le chargement des librairies, ASLR (Adress Space Layout Randomization) à la non-exécutabilité de la
pile, en passant par l'émulation de trampolines ou l'introduction dans les compilateurs de cookies, ou canaries.
Dans cette section, je vous propose un bref rappel des principales fonctionnalités et défenses apparues.
NX ou la pile non exécutable
Dans l'exploitation classique des buffer-overflows, l'adresse de retour se situe dans la pile :
dans le buffer vulnérable, après l'adresse de retour, dans les arguments ou dans l'environnement. Il a donc été
naturel dans un premier temps d'empêcher les exécutions sur la pile. Plus généralement, empêcher qu'une page mémoire
puisse être à la fois en écriture et en exécution. De cette façon, il n'est plus possible d'exécuter du code injecté
au préalable. Finalement, pourquoi continuer à se servir de la pile lorsque les fonctions que nous cherchons à exécuter
dans le shellcode existent déjà dans l'espace mémoire ? En effet, que ce soit les fonctions du programme, présentes
dans la GOT (Global Offset Table) ou celles des librairies (notamment la libc, contenant toutes les fonctions
que nous cherchons, comme seteuid, system ou la famille des exec), ces fonctions prennent leurs arguments de la pile, que
nous contrôlons. Il est donc possible de reconstruire de fausses frames avec les adresses de retour pointant vers les
fonctions voulues et contenant les paramètres désirés. Une autre idée aura été d'appeller mprotect afin de modifier les
protections de la pile et donc de pouvoir ensuite exécuter ce que bon nous semble. Cependant, certaines options de PaX
permettent de désactiver mprotect() et mmap() demandant de rendre une page exécutable.
Architectures 64-bits
L'arrivée des architectures 64-bits a également changé la donne. En premier lieu, la protection des
permissions des pages ont été inclues au niveau matériel. Ainsi, la pile est non-exécutable par défaut. De plus, les paramètres ne
sont plus passés par la pile mais pas les registres rax, rbx, rcx, rdx, rsi et rdi (pour ADM64 et Intel x86_64) pour
les fonctions à 6 arguments ou moins. Enfin, l'injection des adresses de retour peut comporter beaucoup de zéros, puisque
les adresses sont codées sur 64-bits également (même si dans les exploits actuels, les bon vieux strcpy() sont relativement
désuets). Ces modifications de l'environnement d'exploitation ont supprimé du paysage
les exploits old school et notamment les return into libc et return into plt. Dans cette section, je
me contenterai d'utiliser des programmes 32-bits. Si les exploitations sont plus difficiles en 64-bits, elles ne sont pas
impossibles, en particulier lorsque l'on peut injecter des 0. Si cela vous intéresse, je vous ramène au write-up Secure FS
du Plaid CTF 2012, une épreuve comprenant un exécutable 64-bits PIE (les segments .text, .data et .bss sont également
randomizés), dans un environnement ASLR + NX d'activé.
ASLR
Le chef d'oeuvre en matière de sécurité est certainement celui-ci. En effet, bon nombre d'exploits se
basent sur la prédiction des adresses de retour, que ce soit dans la pile, dans le heap ou dans les librairies partagées.
ASLR a ainsi été conçu pour éliminer ces classes d'exploits. Le programme ldd permet d'observer ces changements. Il place
la variable d'environnement LD_TRACE_LOADED_OBJECTS à 1, qui va arrêter prématurément l'exécution d'un programme et
afficher l'adresse de chargement des librairies partagées :
Dans l'exemple précédent, on observe bien qu'entre deux exécutions successives, l'adresse de base du chargement des
librairies est différente. Les différentes pages mémoires d'un processus sont également visibles dans /proc/<pid>/maps.
On peut donc également vérifier le même comportement pour la pile et le heap :
Encore une fois, une classe entière d'exploits a pu être supprimée. Ceci dit, il existe bien sûr des moyens de passer
outre et c'est ce que nous allons tenter d'effectuer tout au long de cette section.
Stack Canary
L'utilisation de cookies ou de canaries n'est pas une protection matérielle ou du système d'exploitation
comme précedemment, mais une protection au niveau logiciel. En réalité, dès la compilation, les préludes et prologues des
fonctions sont modifiés. Au tout début de l'exécution, le canary est placé dans le segment data et initialisé. C'est un
entier aléatoire, de la taille d'un registre. Inutile de dire qu'il est initialisé avec des valeurs fortement
aléatoires (quoique des travaux ont montré que l'on pouvait fortement diminuer l'entropie des cookies). Quoiqu'il en
soit, à chaque début de fonction,
le cookie est placé soit entre le Saved Frame Pointer (valeur enregistrée de l'ebp) et l'adresse de retour, soit après la sauvegarde du contexte (donc plus haut que les variables locales, mais plus bas que le SFP, l'adresse de retour et les éventuels registres sauvegardés à l'entrée de la procédure). Au contraire, à la fin d'une fonction, avant le leave/ret, la
valeur du cookie est vérifiée et le programme se termine si la comparaison échoue.
Inutile de dire que lorsque un overflow intervient, la valeur du cookie n'est plus égale à celle spécifiée dans le segment
data, puisqu'il est a priori très difficile de deviner le cookie. Ceci dit, ces protections peuvent être détournées de
plusieurs façons selon l'implémentation : par exploitation de format strings permettant de passer outre l'écrasement
du cookie, par exploitations type off-by-one (lorsque on peut écraser le SFP, on est capable de bouger
la prochaine frame plus bas dans la pile ou dans la GOT, afin de pouvoir forcer la prochaine adresse de retour
qui sera popée) ou encore par overflows dans le heap ou le segment data (écrasement du cookie).
Ceci dit, contrairement à Windows, sous Linux ces exploitations sont très différentes selon le contexte, je ne ferais donc
pas de généralités et partirait du principe que le programme n'est pas compilé avec ce genre de protections. Certaines
distributions, comme Ubuntu, incluent par défaut cette option dans gcc. Il faut compiler avec l'argument -fno-stack-protector
pour ne pas l'inclure.
Historiquement, la première défense diffusée fû la non-exécutabilité de la pile. De ce fait, les attaquants ont très vite
cherché à sortir de celle-ci. D'ici sont nées les attaques de type return-into-libc, que nous allons maintenant détailler.
Dans les prochaines sections, nous nous
placerons exclusivement dans le cas où la base de la pile est random, la base du mapping des librairies également et la
pile n'est pas exécutable. A la fin de cette section, j'expliquerai également brièvement les modifications à
apporter aux différents exploits pour pouvoir les porter sur 64-bits, qui a notamment apporté des modifications à la
manière de passer les arguments aux fonctions.