systemadmin.es > Programación > Sobreescribiendo la sección .dtors de programas compilados con GCC 4.4

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 + 0x4 = 0x8049600. 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 + 0x4 (0x8049580) 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 0x8049670 con 0x0:

 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 (0x8049580) 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 0x8049674 que resulta ser 0x0:

(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 0x4. 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 0x4 (100) 2 bits a la drecha tenemos 0x1 (001) y si le restamos 0x1 tenemos 0x0 (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 0x0 con 0x0, 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 0x8048378, saliendo de ficha función.

En cambio, en el caso que sí tengamos destructor en ebx nos quedaría 0x1, 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

2 comments to “Sobreescribiendo la sección .dtors de programas compilados con GCC 4.4”

  1. 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.

  2. 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

Deja un comentario:

XHTML - Tags permitidos:<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>