Parcheando código con GDB

Hace un par de días, durante un reto de seguridad, nos encontramos con la situación de tener que modificar con GDB el código de un binario para que realizara las acciones que nos interesaban y no para las que había sido programado; esto suele usarse en los retos tipo “crack me” o “patch me”. Todo sea dicho, al final no era la solución al reto, pero como todo reto, se suelen probar distintas opciones.

De forma resumida tenemos un programa con la función principal y dos funciones adicionales declaradas. En la función principal se llama solo a una de las dos funciones, en este caso a la función que llamaremos “malo”, pero nosotros necesitábamos que en vez de llamar a esa función llame a la otra función, la que llamaremos “bueno”.

Un ejemplo del código en C sería el siguiente, al que denominaremos “prueba.c”:

#include <stdio.h>
#include <stdlib.h>
// Funciones auxiliares

void bueno(void) { printf("SIIII");}
void malo(void) { printf("Noooo");}

// Función principal que llama a la funcion malo
void main(void){malo();}

Nuestro objetivo era que el programa, en lugar de llamar a la función malo en el main, llamara a la función bueno. Para el reto teníamos únicamente GDB como debugger. Para la entrada usaremos el código anterior por estar más simplificado y resultar más claro.

Teniendo en cuenta que el código anterior se ha escrito en el fichero “prueba.c”, hay que seguir los siguientes pasos para compilar y cargar el binario en GDB:

$ gcc -o prueba prueba.c
$ gdb prueba

Una vez accedido a GDB le indicaremos que queremos usar el formato Intel en vez del AT&T seleccionado por defecto (me gusta más, para gustos colores o sabores):

$ set disassembly-flavor intel

El siguiente paso consistirá en ver el código del programa, teniendo encuenta que las pruebas se hacen sobre un entorno de 64 bits (por eso las direcciones son tan largas):

(gdb) disas main 
Dump of assembler code for function main: 
   0x0000000000400514 <+0>:	push   rbp 
   0x0000000000400515 <+1>:	mov    rbp,rsp
   0x0000000000400518 <+4>:	call   0x4004fc <malo>
   0x000000000040051d <+9>:	pop    rbp 
   0x000000000040051e <+10>:	ret    
End of assembler dump.
NOTA: “disas” viene de la orden “disassemble” pero se puede usar de esta forma porque no hay órdenes que empiecen por “disas” y no es necesario escribir la orden completa… es igual que ocurre en los cacharritos CISCO :)

Como vemos la llamada a la función malo, mediante CALL, está en posición “0x400518”, la cual apunta a la dirección “0x4004fc” donde se encuentra la primera instrucción de la función malo, tal como se muestra a continuación:

(gdb) disas malo 
Dump of assembler code for function malo: 
   0x00000000004004fc <+0>:	push   rbp 
   ...
   0x0000000000400512 <+22>:	pop    rbp 
   0x0000000000400513 <+23>:	ret    
End of assembler dump. 

El siguiente paso es analizar el tipo de CALL de la función principal (main), identificando el tipo de OP Code de la función; en nuestro caso se mostrarán los 8 bytes a partir del CALL:

(gdb) x/8xb 0x400518 
0x400518 >main +4<:	0xe8	0xdf	0xff	0xff	0xff	0x5d	0xc3	0x90

Vemos que el primer byte es 0xe8, que corresponde con el OP Code del CALL a dirección relativa Call32(). Dicha llamada consta de 5 bytes; el primero es el OPCode representado como “0xe8” identificativo de la llamada y los siguientes 4 bytes es la dirección donde se encuentra la función que se desea llamar. Por tanto para ver correctamente esta instrucción será necesario visualizar solo los 5 primeros bytes:

(gdb) x/5xb 0x400518
0x400518 <main +4>:	0xe8	0xdf	0xff	0xff	0xff

Donde tenemos el Op Code 0xe8 y la dirección “0xdf 0xff 0xf 0xff“. Como este ordenador es un LE hay que darle la vuelta: es decir, la dirección real es ” 0xff 0xff 0xff 0xdf”. Esto se puede realizar cambiando el carácter “b” de byte por el de word “w” e indicando la posición de memoria donde empieza la dirección que se quiere tratar, es decir, la dirección del CALL + 1 (quitamos el OP Code 0xe8):

(gdb) x/xw 0x400519
0x400519 <main +5>:	0xffffffdf

Y la pregunta que se estarán realizando: ¿de dónde sale ese dato? Pues ese dato es el valor negado de la diferencia (offset) entre el final de la llamada del CALL y la función que se quiere llamar:

Not (OFFSET) = pos pre CALL + tam instrucción call - F(x) a saltar

¿Lioso? Veámoslo por partes. Tenemos de offset este valor “0xffffffdf”, con lo que la operación NOT sería:

0xdf -> 1101 1111 (el not de esto) -> 0010 0000 -> 0x20

Por tanto el NOT de “0xffffffdf” es “0x00000020”. Si sabemos que el Call se llama en la posición “0x400518” y el tamaño de la instrucción CALL son 5 bytes, entonces sabemos que la f(x) termina en la posición “0x40051c”; cuidado porque el byte “518” ya es el primer byte del CALL:

F(x) a saltar = pos pre CALL + tam instrucción call - Not (OFFSET)

F(x) a saltar = 0x400518 + 0x4 - 0x20 = 0x40051c - 0x20 = 0x4004FC

Es decir, la posición donde está la función malo que hemos identificado con anterioridad. Ahora queremos modificar el CALL para que apunte a bueno. Para ello es necesario obtener la posición de la función bueno:

(gdb) disas bueno
Dump of assembler code for function bueno: 
   0x00000000004004e4 <+0>:	push   rbp
   ...

Ya sabemos que el último byte de CALL está en la posición “0x40051c”, por tanto:

0x40051c - 0x4004e4 = 0x38

Not de 0x38 = 0xFFFFFFC7, y como es necesario ponerlo en LE -> 0xC7 0xff 0xff 0xff

Si recordamos, en el CALL de la función principal teníamos la siguiente entrada “0xe8 0xdf 0xff 0xff 0xff” la cual apuntaba a malo, y ahora queremos sustituirla por 0xc7 0xff 0xff 0xff: solo necesitamos cambiar el segundo byte del call, es decir 0xdf por 0xc7. O sea, indicarle que el byte “0x400519” valga 0xC7:

(gdb) x/xb 0x400519
0x400519 
: 0xdf

Para realizar dicha operación es necesario usar “set” indicando que lo que se quiere sustituir es un byte en la posición indicada:

(gdb) set *(unsigned char*) 0x400519 = 0xc7 
Cannot access memory at address 0x400519

Como vemos ha fallado, no tenemos acceso a dicha posición de memoria, hagamos una triquimechuela poniendo un breakpoint en la instrucción previa al call y ejecutando el programa:

(gdb) break 0x0000000000400515 
Function "0x0000000000400515" not defined. 
Make breakpoint pending on future shared library load? (y or [n]) y 
Breakpoint 1 (0x0000000000400515) pending.

Ejecutamos ahora el programa esperando a que se detenga en el break indicado, una instrucción antes del call:

(gdb) start 
Temporary breakpoint 2 at 0x400518 
Starting program: /home/moxilo/prueba

Temporary breakpoint 2, 0x0000000000400518 in main ()
(gdb) disas main
Dump of assembler code for function main: 
   0x0000000000400514 <+0>:	push   rbp 
   0x0000000000400515 <+1>:	mov    rbp,rsp 
=> 0x0000000000400518 <+4>:	call   0x4004fc <malo>
   0x000000000040051d <+9>:	pop    rbp 
   0x000000000040051e <+10>:	ret    
End of assembler dump.

Ahora ya podemos aplicar el cambio y comprobar que la modificación se ha llevado a cabo al apuntar el call a la función bueno en vez de a la función malo:

(gdb) set *(unsigned char*) 0x400519 = 0xc7
(gdb) disas main 
Dump of assembler code for function main: 
   0x0000000000400514 <+0>:	push   rbp 
   0x0000000000400515 <+1>:	mov    rbp,rsp 
=> 0x0000000000400518 <+4>:	call   0x4004e4 <bueno> 
   0x000000000040051d <+9>:	pop    rbp 
   0x000000000040051e <+10>:	ret    

End of assembler dump.

Como vemos ya apuntamos a la función bueno, por lo que si le decimos que “continue” mostrará el texto de la función buena (“SIIII”) y no el de la función mala:

(gdb) continue 
Continuing. 
SIIII[Inferior 1 (process 3897) exited with code 05]

Por tanto hemos parcheado el programa en ejecución con GDB, de tal forma que hemos obtenido la dirección de memoria de otra función, sustituyendo posteriormente la dirección de la llamada CALL por la de la función que nos ha interesado. Esto también se puede usar para sustituir instrucciones de códio que realizan ciertas comprobaciones que no nos interesa por NOPs (0x90), es decir, por “nada”.

Comments

  1. Buen post Joaquín,
    añado este enlace bastante útil (al menos para mi) donde explican los relative jump:

    http://thestarman.pcministry.com/asm/2bytejumps.htm

    S2

  2. Hola, buena entrada.

    Existe una macro para gdb que hizo hace años un desarrollador de Opera para hacer esto de forma “mas humana” (sustituyendo instrucciones ASM habitualmente desde donde apunta el EIP. http://my.opera.com/taviso/blog/show.dml/248232

    Para meter NOPs o alterar saltos Jxx esta bien (aunque siempre sera mas rapido un editor hexa que permita meter nmemonicos directamente claro). Tambien permite, por que no, alterar el valor de un registro concreto, es muy sencillo romper una proteccion que compruebe el valor devuelto por una funcion. Sabiendo que una funcion devuelve su valor en EAX, simplemente podriamos poner un br en la direccion de memoria adecuada y despues:

    (gdb) commands
    Type commands for when breakpoint 1 is hit, one per line.
    End with a line saying just “end”.
    >silent
    >set $eax=0
    >cont
    >end

    Y a tirar millas.

    Os dejo tambien algo relacionado y bastante guapo (que habria que comprobar si aun es valido en sistemas actuales):

    http://www.codeproject.com/Articles/33340/Code-Injection-into-Running-Linux-Application

    Gracias por estas entradas!