Sobreescribiendo la sección .dtors de programas compilados con GCC 4.4
En el año 2000 se publicó como modificar la sección .dtors para ejecutar código arbitrario con el compilador GCC 2.95. Vamos a ver que ocurre casi 10 años después, concretamente con la versión 4.4 de GCC que es con la que viene la Fedora 13 que tengo instalada:
Primero de todo vamos a ver como definir una función que se ejecute al acabar la ejecución del programa (un destructor). Para ello deberemos añadir el atributo destructor a la función, por ejemplo:
#include <stdio.h>
static void destructor(void) __attribute__ ((destructor));
void destructor(void)
{
printf("destructor: 0x%x\n",(void *)destructor);
}
int main(void)
{
printf("dins\n");
return 0;
}
Compilamos y ejecutamos:
$ gcc -O0 dtor.c $ ./a.out dins destructor: 0x80483f4 $ objdump -s -j .dtors a.out a.out: file format elf32-i386 Contents of section .dtors: 80495b4 ffffffff f4830408 00000000 ............
Para comprobar si podemos modificar en tiempo de ejecución el destructor creamos el siguiente código con la función evil() que queremos que se ejecute y las variables necesarias, pero vacias,
#include <stdio.h>
static void destructor(void) __attribute__ ((destructor));
void evil(void)
{
printf("EVIL thing\n");
}
void destructor(void)
{
printf("destructor: 0x%x\nevil: 0x%x\n",(void *)destructor,(void *)evil);
}
int main(void)
{
int *punter=(int *)0xffffffff;
printf("dins\n");
*punter=0xffffffff;
return 0;
}
Ahora que tenemos el binario que deberá modificar la función modificamos los valores. Compilamos el código y mediante gdb obtenemos la posición de la función evil() que queremos ejecutar:
$ gcc -O0 dtor.c
$ gdb -q a.out
Reading symbols from /home/jordi/dtor/a.out...(no debugging symbols found)...done.
(gdb) print evil
$1 = {<text variable, no debug info>} 0x80483f4
(gdb)
A continuación hacemos un objdump para obtener la posición de memoria a modificar:
$ objdump -s -j .dtors a.out a.out: file format elf32-i386 Contents of section .dtors: 80495fc ffffffff 08840408 00000000 ............
A la posición que nos indica debemos sumarle 4 bytes para escribir pasado el 0xFFFFFFFF.
Así, 0x80495fc + 0×4 = 0×8049600. A continuación modificamos los valores dejando el código de la siguiente manera:
(...)
int *punter=(int *)0x08049600;
printf("dins\n");
*punter=0x080483f4;
(...)
Compilamos y ejecutamos:
$ gcc -O0 caca.c $ ./a.out dins EVIL thing
Hemos comprobado como en el caso que un binario tenga definido un destructor no hay ningún problema en modificar la función a ejecutar ya sea mediante un buffer overflow como con un format string attack
A continuación veremos el caso que el binario no tenga destructor, preparamos el siguiente código:
#include <stdio.h>
void evil(void)
{
printf("EVIL thing\n");
}
int main()
{
int *punter=(int *)0x80483f4;
printf("dins\n");
printf("fx dtor: %x\n",(void *)evil);
*punter=0xffffffff;
return 0;
}
Compilamos y comprobamos que no tiene destructor mediante objdump:
$ gcc -O0 sns.c $ objdump -s -j .dtors a.out a.out: file format elf32-i386 Contents of section .dtors: 804957c ffffffff 00000000 ........
Cogemos el valor 0x804957c + 0×4 (0×8049580) como posición a modificar y usamos gdb para obtener la posición de la función evil() como en el caso anterior:
$ gdb -q a.out
Reading symbols from /home/jordi/dtor/a.out...(no debugging symbols found)...done.
(gdb) print evil
$1 = {<text variable, no debug info>} 0x80483b4
(gdb)
A continuación modificamos lo valores dejándolos con los que hemos obtenido:
(...)
int *punter=(int *)0x08049580;
printf("dins\n");
*punter=0x080483b4;
(...)
Lo compilamos y lo ejecutamos:
$ gcc -O0 sns.c $ ./a.out dins
Podemos ver como no se ejecuta el destructor que hemos inyectado. Podemos mirar cual es el código encargado de ejecutar el destructor buscando la función __do_global_dtors_aux:
08048330 <__do_global_dtors_aux>: 8048330: 55 push ebp 8048331: 89 e5 mov ebp,esp 8048333: 53 push ebx 8048334: 8d 64 24 fc lea esp,[esp-0x4] 8048338: 80 3d 70 96 04 08 00 cmp BYTE PTR ds:0x8049670,0x0 804833f: 75 3e jne 804837f <__do_global_dtors_aux+0x4f> 8048341: bb 80 95 04 08 mov ebx,0x8049580 8048346: a1 74 96 04 08 mov eax,ds:0x8049674 804834b: 81 eb 7c 95 04 08 sub ebx,0x804957c 8048351: c1 fb 02 sar ebx,0x2 8048354: 83 eb 01 sub ebx,0x1 8048357: 39 d8 cmp eax,ebx 8048359: 73 1d jae 8048378 <__do_global_dtors_aux+0x48> 804835b: 90 nop 804835c: 8d 74 26 00 lea esi,[esi+eiz*1+0x0] 8048360: 83 c0 01 add eax,0x1 8048363: a3 74 96 04 08 mov ds:0x8049674,eax 8048368: ff 14 85 7c 95 04 08 call DWORD PTR [eax*4+0x804957c] 804836f: a1 74 96 04 08 mov eax,ds:0x8049674 8048374: 39 d8 cmp eax,ebx 8048376: 72 e8 jb 8048360 <__do_global_dtors_aux+0x30> 8048378: c6 05 70 96 04 08 01 mov BYTE PTR ds:0x8049670,0x1 804837f: 8d 64 24 04 lea esp,[esp+0x4] 8048383: 5b pop ebx 8048384: 5d pop ebp 8048385: c3 ret 8048386: 8d 76 00 lea esi,[esi+0x0] 8048389: 8d bc 27 00 00 00 00 lea edi,[edi+eiz*1+0x0]
Analizando la función vemos que que casi al principio se compara la posición 0×8049670 con 0×0:
8048338: 80 3d 70 96 04 08 00 cmp BYTE PTR ds:0x8049670,0x0
Con gdb vemos que el valor es cero:
[jprats@croscat dtor]$ gdb -q a.out Reading symbols from /home/jprats/dtor/a.out...(no debugging symbols found)...done. (gdb) break __do_global_dtors_aux Breakpoint 1 at 0x8048334 (gdb) r Starting program: /home/jprats/dtor/a.out dins Breakpoint 1, 0x08048334 in __do_global_dtors_aux () Missing separate debuginfos, use: debuginfo-install glibc-2.12-3.i686 (gdb) x/x 0x8049670 0x8049670: 0x00000000 (gdb)
Por lo tanto el jne no salta:
804833f: 75 3e jne 804837f
El código sigue linealmente hasta llegar al siguiente cmp:
8048357: 39 d8 cmp eax,ebx
Entonces tenemos que ver que tenemos en eax y ebx. Vemos que en ebx se carga el puntero (0×8049580) al puntero de la función destructor:
8048341: bb 80 95 04 08 mov ebx,0x8049580
A continuación podemos ver que en eax se carga el contenido de 0×8049674 que resulta ser 0×0:
(gdb) x/x 0x8049674 0x8049674 <dtor_idx.5965>: 0x00000000
A ebx le restamos 0x804957c
804834b: 81 eb 7c 95 04 08 sub ebx,0x804957c
El valor 0x804957c es el inicio de la sección .dtors:
$ objdump -s -j .dtors a.out sense.destructor: file format elf32-i386 Contents of section .dtors: 804957c ffffffff 00000000 ........
Quedando en ebx 0×4. A continuación hace un SAR (shift arithmetic right) que es como si fuera una división por 4 para contar los bytes de diferencia y le resta uno que es el caso que no haya nada entre las dos posiciones:
8048351: c1 fb 02 sar ebx,0x2 8048354: 83 eb 01 sub ebx,0x1
Con lo en este caso, al desplazar 0×4 (100) 2 bits a la drecha tenemos 0×1 (001) y si le restamos 0×1 tenemos 0×0 (la lista esta vacía) por lo que no se debe ejecutar ningún destructor.
El código máquina, por lo tanto, esta comparando 0×0 con 0×0, teniendo después un jae (Jump if Above or Equal)
8048359: 73 1d jae 8048378 <__do_global_dtors_aux+0x48>
Como es igual, salta a la posición 0×8048378, saliendo de ficha función.
En cambio, en el caso que sí tengamos destructor en ebx nos quedaría 0×1, ya que entre el inicio y el fin del listado habría 2 bytes (uno en medio) de diferencia, por lo que no saltaríamos y llamaríamos al destructor:
804835b: 90 nop 804835c: 8d 74 26 00 lea esi,[esi+eiz*1+0x0] 8048360: 83 c0 01 add eax,0x1 8048363: a3 74 96 04 08 mov ds:0x8049674,eax 8048368: ff 14 85 7c 95 04 08 call DWORD PTR [eax*4+0x804957c]
Como mo podemos modificar en tiempo de ejecución la sección .text dónde se encuentra el código ni la sección .bss dónde se encuentra el valor con el que comparamos ebx:
$ objdump -x sense.destructor | grep dtor_idx 08049674 l O .bss 00000004 dtor_idx.5965
Podemos concluir que, con la versión de GCC 4.4:
- Si un binario tiene definido un destructor, se puede cambiar en tiempo de ejecución mediante un buffer overflow o un format string attack
- Si el binario no tiene definido previamente un destructor, no podremos añadirle uno
Relacionados
Imprimir
18. August 2010 at 7:55 am :
Debería ser posible modificar los permisos de las páginas que quieras escribir, y posteriormente modificar el valor necesario para que la comparación lleve a la ejecución del dtor. Claro que, pudiendo inyectar ese código ya no necesitas complicarte la vida con el dtor.
18. August 2010 at 11:13 am :
Tiene gracia cuando ejecutas código gracias al dtor, si ya tienes que ejecutar código para ejecutarlo no veo que tenga mucho sentido.
Es como parchar el código de la función __do_global_dtors_aux, si lo puedes parchear entiendo que ya no tiene gracia porque puedes hacer muchas otras.
Porcierto, bonita IP