Voici un petit write-up, pour le challenge exploit300 de l'Olympic CTF 2014.

Le binaire se trouve ici : exploit300

Analyse du binaire

    $ checksec --file ./exploit300
    RELRO         STACK CANARY    NX            PIE           RPATH      RUNPATH      FILE
    Full RELRO    Canary found    NX enabled    PIE enabled   No RPATH   No RUNPATH   ./exploit300
    $ file ./exploit300
    ./exploit300: ELF 32-bit LSB shared object, Intel 80386, version 1 (SYSV),
    dynamically linked (uses shared libs), for GNU/Linux 2.6.24,
    BuildID[sha1]=f315607803e335f93fb56a9446862539655df265, stripped

On a donc un ELF 32 stripped, avec toutes les protections imaginable...(PIE, ASLR, SSP...)

On sent d'avance qu'il ne va pas se laisser powner facilement ;)

En analysant le binaire avec IDA, j'ai pût recoder le main() en pseudo C, ce qui donne grosso-modo :

    int main(void) {
        int i; // 0x30
        int len; // 0x34
        char *p; //0x38
        char buffer1[0x100]; //0x3c

        puts("pw?");
        read(0, buffer, 8);

        if(memcmp(buffer, "letmein\n", 8))
            return 1;

        for(i = 0; i <= 0xf; i++) {
            puts("msg?");

            bzero(buffer, 0x100);

            read(0, buffer, 0x80);
            len = strlen(buffer);

            /* [1] */
            if(strchr(buffer, 'n'))
                return 1;

            /* [5] */
            p = mmap(0x11111000, 0x1000, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANON, -1);

            if(p == MAP_FAILED || p == NULL)
                return 1;

            strcpy(p+0x84, "Your message (%d bytes): %s\n");

            /* [2] */
            *((unsigned int*)(p+0x80)) = p+0x84;

            strncpy(p, buffer, 0x80);

            /* [3] */
            p[len] = 0;

            /* [4] */
            sprintf(buffer, *((unsigned int*)(p+0x80)), len, p);
            puts(buffer);
            mmunmap(p, 0x1000);
        }

        return 0;
    }

Analyse de la vulnérabilité

  • En [2], l'adresse d'une format string est stocké dans la région mappé en [5] à un offset de 0x80 bytes.
  • En [3], nous avons un off by one qui permet d'écraser le dernier byte de la format string, ce qui fait pointer la format string vers le début de la région mappé, que nous controlons. Pour déclencher le bug, il faut envoyer un buffer d'une taille de 0x80 bytes exactement.
  • En [1], on voit que le caractère 'n' est filtré.
  • En [4], nous avons un appel à sprintf. Vu que nous controlons la chaine de format, nous pouvons aboutir à un overflow. Le buffer de destination de sprintf étant sur la pile, nous avons un beau stack overflow.

Les complications

  • Première complication : le caractère 'n' étant filtré, nous ne pouvons donc pas utiliser la format string pour écrire en mémoire.
  • Deuxième complication : il y a SSP d'activé, ce qui signifie qu'il y a un stack cookie qui protège le saved-eip.
  • Troisième complication : il y a un NUL byte dans le cookie, si on souhaites le réécrire, la copie s'arrêtera, et nous ne pourrons pas écraser saved-eip.
  • Quatrième complication : si on veut roper, il faudra éviter les NUL bytes, ou trouver une solution :)

Les solutions

  • Pour bypasser SSP, j'ai utilisé la format string pour leaker le cookie. J'ai également utliser la chaine de format pour leaker l'adresse de base du binaire, afin de bypasser PIE.
  • Pour réécrire le NUL byte du cookie, j'ai réécris la stack en deux passes, vu qu'il y a 0xF itérations sur la vulnérabilité, on peut se le permettre.
  • J'injecte mon ROP dans un premier temps, tout en réécrivant les 3 premiers bytes du cookie.
  • Puis je renvois un payload pour écrire le dernier byte du cookie.
  • Pour faire du ROP avec des NUL bytes, j'ai utilisé la même technique précedente. Il faut écrire la stack en partant des adresses hautes, en redescendant progressivement.

L'exploit

  • Dans un premier temps, je récupère les infos dont j'ai besoin : stack cookie et base address
  • Ensuite, je réécris ma stack, qui est un ROP faisant un read(0, DATA, 0xffff) puis un pop ebp; ret suivis d'un leave; ret. Cela permet de swaper la stack vers le segment DATA.
  • Puisque le troisième et le premier argument de read doit contenir des NUL bytes, je m'occupe de les "patcher"
  • Je réécris ensuite mon cookie, en s'assurant de mettre un byte non NUL à la fin du cookie
  • Je "patch" le dernier byte de mon cookie
  • Mon premier payload (le read) est maintenant exécuté, j'envois alors une fake stack frame qui s'occupe d'éxécuter :
    1. mmap(0x11111000, 0x1000, PROTALL, MAPFIXED | MAP_ANON, -1, 0)
    2. read(0, 0x11111000, 0x1000)
    3. (*)(0x11111000)()
  • Il ne me reste plus qu'à envoyer un shellcode exécutant un /bin/sh, et j'obtiens mon remote shell !

Ce challenge était vraiment fun, et le bypassing de toutes ces protections m'a beaucoup plût. Celà montre qu'il est toujours possible d'exploiter à distance de manière efficace des programmes ayant toutes les protections modernes anti-exploitation.

Vous pouvez trouver l'exploit ici : exploit300.pl

    $ perl exploit.pl
    [+] Connect to remote host 109.233.61.11:3129
    [+] Log in
    [+] Get the cookie
    [+] Cookie found : 0x9dbd7200
    [+] Get the base address
    [+] Base address found : 0xb7743000
    [+] Building your stack
    [+] Insert your 3vil stack frame
    [+] Patching argument 3 of read()
    [+] Patching argument 1 of read()
    [+] Rewrite the stack cookie
    [+] Patch the first cookie byte with 0x00
    [+] Build & send your fake stack frame
    [+] Sending your shellcode
    [+] And enjoy your shell
    ls -la
    total 20
    drwxr-xr-x  2 root root 4096 Feb  8 02:50 .
    drwxr-xr-x 23 root root 4096 Feb  8 02:49 ..
    -rw-r--r--  1 root root   44 Feb  8 02:49 flag
    -rwxr-xr-x  1 root root 5464 Feb  8 02:50 task
    id
    uid=65534(nobody) gid=0(root) groups=65534(nogroup),0(root)
    cat flag
    FLAG: CTF{c36d55681410edbba58daedde46fb5e8}