pre { background:#eeeeee; border:1px solid #A6B0BF; font-size:120%; line-height:100%; overflow:auto; padding:10px; color:#000000 } pre:hover { border:1px solid #efefef; } code { font-size:120%; text-align:left; margin:0;padding:0; color: #000000;} .clear { clear:both; overflow:hidden; }

Bienvenido al blog

Bienvenidos al blog de seguridad en sistemas

miércoles, 29 de febrero de 2012

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
#include
//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 “0×400518″, la cual apunta a la dirección “0×4004fc” 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 : 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 : 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 : 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 “0×00000020″. Si sabemos que el Call se llama en la posición “0×400518″ y el tamaño de la instrucción CALL son 5 bytes, entonces sabemos que la f(x) termina en la posición “0×40051c”; 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 “0×40051c”, por tanto:


0x40051c - 0x4004e4 = 0x38

Not de 0×38 = 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 “0×400519″ valga 0xC7:


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

Para realizar dicha operación es necesario usar “set” indicando que lo que se quiere sustiruir 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 (0×90), es decir, por “nada”.

PD: soy consciente de la poca actividad del foro, pero los 4 tomos de canción de hielo y fuego, el nombre del viento, el temblor de un hombre sabio, las 7 horas de inglés semanales y el GREM que lo tengo en dos semanas son los culpables.

Continuar...