Una de las claves para entender un programa binario es comprender el cómo funciona el stack. En este lab aprenderás a usar un debugger primitivo para visualizar algunos de los conceptos presentados en clase sobre el stack, la invocación de funciones, pase de argumentos y variables locales.
Para completar este laboratorio necesitarás una instalación de algun sabor de linux con interfaz gráfico de usuario (recomiendo LXDE) y los siguientes programas:
gcc
- compilador de lenguaje c GNUgcc-multilib
- libreria de gcc para compilar ejecutables de 32 bits.gdb
- GNU debuggerddd
- graphical display debuggerreadelf
- programa GNU para desplegar información sobre archivo ELFgedit
- solo si eres un 0xbaca1a00
como el profe y no quieres usar vim
todo el tiempo.(Al menos en Ubuntu) puedes instalar todos esos programas usando sudo apt-get install nombreDelPrograma
gdb
es un debugger estilo command line gratis, muy poderoso y popular para linux. Hoy día apoya una gran cantidad de familias de procesadores incluyendo los de la familia x86-64, IA-32 y los ARM.
ddd
es un front-end gráfico para gdb
que, entre otras cosas, permite al usuario visualizar en múltiples ventanas los resultados de gdb
.
Asegúrese que los programas mecionados en Requisitos de software está debidamente instalados.
En este paso crearemos un binario para luego depurarlo usando ddd
. Será un programa sumamente sencillo pues, como verás a continuación, hasta lo que parece sencillo en C puede parecer aterrador al ser desensamblado.
#include <stdio.h>
int main() {
int a = 42;
int *b = &a;
char st[] = "hola";
char c = 'G';
return 87;
}
Compila el código para 32 bits usando:
gcc -m32 -fno-stack-protector -o ejemplo01 ejemplo01.c
El flag -m32
hace que se genere código para procesadores de la familia IA32. El flag -fno-stack-protector
hace que no se incluya en el código instrucciones para proteger el stack (de ataques como el buffer overflow). En este lab, usamos ese flag pues ya el código es feo de por si, no hay necesidad de empeorarlo.
Asegurate de que compila sin errores. También puedes correrlo y no veras nada impreso al ejecutarlo. Sin embargo, luego de ejecutarlo puedes hacer echo $?
y debes ver el número 87
. El comando echo $?
sirve para imprimir el valor regresado por el comando más recientemente ejecutado.
En el programa observa que estamos creando 4 variables locales y no estamos invocando ninguna funcion adicional. Si todo lo que hemos explicado sobre el stack es cierto, esas variables deben vivir en el stack frame de la función main.
Respira profundo pues vamos a comenzar a usar ddd
, lo cual requiere mucha paciencia y ganas de aprender. Preparate para ver la interface de usuario más exquisita e intuitiva que jamas fue diseñada ;-)
.
Abre ddd
usando ddd ejemplo01 &
Verás una pantalla de ddd
que consiste de dos views (subventanas):
el view de arriba es el Source Window y sirve para observar el código de C mientras lo ejecutamos. Esto solo funciona si el ejecutable fue creado con símbolos de debugging (gcc -g ....
). Sin embargo, cuando hacemos reverse engineering no vamos a trabajar con binarios creados para debugging, por lo tanto no vamos a usar esta subventana. Si deseas la puedes apagar en View->Source Window.
el view de abajo es el GDB console y es un command-line interface para escribir y ejecutar comandos de gdb
. Todos las funcionalidades que puedes lograr por medio de los menus de ddd
también se pueden lograr tecleando un comando en el GDB console
.
Figura 1 Ventana inicial de ddd
.
Ya que estamos hablando de los views, vamos a prender dos adicionales que nos serán útiles para el ejercicio: Data Window y Machine Code Window.
Data Window - sirve para mostrar el contenido de registros, partes de memoria, variables, estructuras, etc.
Machine Code Window muestra el código desensamblado que se está ejecutando, las direcciones de las instrucciones y los breakpoints.
Figura 2. Ventana de ddd
con los views que usaremos: (desde arriba hasta abajo)Data Window, Machine Code Window y GDB console.
Lo que verás en la Machine Code Window es algo parecido a esto:
Dump of assembler code from 0x80483ed to 0x80484ed:
0x080483ed <main+0>: push %ebp
0x080483ee <main+1>: mov %esp,%ebp
0x080483f0 <main+3>: sub $0x20,%esp
0x080483f3 <main+6>: movl $0x2a,-0xc(%ebp)
0x080483fa <main+13>: lea -0xc(%ebp),%eax
0x080483fd <main+16>: mov %eax,-0x4(%ebp)
0x08048400 <main+19>: movl $0x616c6f68,-0x11(%ebp)
0x08048407 <main+26>: movb $0x0,-0xd(%ebp)
0x0804840b <main+30>: movb $0x47,-0x5(%ebp)
0x0804840f <main+34>: mov $0x57,%eax
0x08048414 <main+39>: leave
0x08048415 <main+40>: ret
Si señor, así se ve (parte) de tu simple programa cuando es desensamblado.
Las primeras 3 instrucciones realizan el prólogo de la función main. Se encargan de guardar el valor del registro $ebp
de la función que invocó a main. El valor de $ebp
es recuperado al final de la función, al usar la instrucción leave
.
0x080483ed <main+0>: push %ebp
Las siguientes dos instrucciones reservan espacio para el stack frame de la función main. La instrucción sub $0x20,%esp
separa separará 0x20
bytes de memoria para el stack frame de main.
0x080483ee <main+1>: mov %esp,%ebp
0x080483f0 <main+3>: sub $0x20,%esp
El cuerpo del programa son las siguientes instrucciones:
0x080483f3 <main+6>: movl $0x2a,-0xc(%ebp)
0x080483fa <main+13>: lea -0xc(%ebp),%eax
0x080483fd <main+16>: mov %eax,-0x4(%ebp)
0x08048400 <main+19>: movl $0x616c6f68,-0x11(%ebp)
0x08048407 <main+26>: movb $0x0,-0xd(%ebp)
0x0804840b <main+30>: movb $0x47,-0x5(%ebp)
0x0804840f <main+34>: mov $0x57,%eax
Las instrucciones que acabamos de listar implementan las declaraciones de nuestro programa en C. La primera de ellas escribe 0x2a
al espacio de memoria que comienza en %ebp-0xc
. Esto corresponde a la instrucción int a = 42
del código fuente.
0x080483f3 <main+6>: movl $0x2a,-0xc(%ebp)
Las siguientes dos instrucciones implementan int *b = &a;
. La primera guarda la dirección de la variable a
en el registro %eax
. La segunda asigna esa dirección al espacio de memoria que comienza en %ebp-0x4
. En otras palabras, %ebp-0x4
es la dirección en memoria de la variable puntero b
.
0x080483fa <main+13>: lea -0xc(%ebp),%eax
0x080483fd <main+16>: mov %eax,-0x4(%ebp)
Las siguientes dos logran almacenar el string "hola"
en la variable st
. Observe como lo logran. Primero escriben 0x616c6f68
(los caracteres h
o
l
a
) al espacio de memoria que comienza en %ebp - 0x11
. Luego mueven el caracter nulo (0x0
) a la dirección justo despues del caracter a
, i.e. %ebp - 0x11 + 0x4
es lo mismo que %ebp - 0xd
.
0x08048400 <main+19>: movl $0x616c6f68,-0x11(%ebp)
0x08048407 <main+26>: movb $0x0,-0xd(%ebp)
La instruccion que implementa char c = 'G'
es la siguiente:
0x0804840b <main+30>: movb $0x47,-0x5(%ebp)
Mientras que la siguiente instrucción coloca en el registro %eax
el valor de returno 0x57
(87 en decimal).
0x0804840f <main+34>: mov $0x57,%eax
(Según lo que has entendido hasta ahora, sin usar ddd) completa el diagrama del stack frame para el programa ejemplo01
, asumiendo que el %ebp
de main vale 0xfffd1580
.
0xfffd156c [ ]
0xfffd156d [ ]
0xfffd156e [ ]
0xfffd156f [ ]
0xfffd1570 [ ]
0xfffd1571 [ ]
0xfffd1573 [ ]
0xfffd1574 [0x2a] )
0xfffd1575 [0x00] |
0xfffd1576 [0x00] > variable a
0xfffd1577 [0x00] )
0xfffd1578 [ ]
0xfffd1579 [ ]
0xfffd157a [ ]
0xfffd157b [0x47] : variable c
0xfffd157c [ ]
0xfffd157d [ ]
0xfffd157e [ ]
0xfffd157f [ ]
0xfffd1580 [ ]
Ahora vamos a utilizar ddd
para validar nuestro análisis.
Nuestra primera labor en ddd
típicamente consiste en establecer uno o más breakpoints dentro del código. Un breakpoint es una forma de decirle al debugger que detenga su ejecución del programa cuando llegue a cierta instrucción. Debemos establecer al menos un breakpoint en nuestro programa pues de lo contrario al pedirle a ddd
que lo corra lo ejecutará de principio a fin sin permitir regocijarnos en su grandeza. El comando en gdb
para crear un breakpoint es: b (dirección)
o b (nombreDeFuncion)
En el GDB console escribe b main
. Acabas de crear un breakpoint al comienzo de la función main.
Crea un breakpoint adicional en la instrucción mov $0x57,%eax
.
Nuestra próxima labor es correr el programa usando el comando run
. Al correr, notarás que la consola te anuncia lo siguiente:
Starting program: /home/eip/ccom4995/ccom4995-lab02/code/ejemplo01
Breakpoint 1, 0x080483f3 in main ()
Además podrás ver en el Machine Code Window una flecha que apunta a la instrucción donde detuvimos la ejecución.
Una vez corriendo el programa podemos comenzar a realizar la mayoría de los comando interesantes a gdb
. Existen múltiples hojas de referencia rápida (cheat sheets) para gdb, por ejemplohttp://www.cs.berkeley.edu/~mavam/teaching/cs161-sp11/gdb-refcard.pdf. A continuación una lista corta de los que más utilizo:
Comando | Abreviatura | Funcionalidad |
---|---|---|
stepi |
si |
ejecuta la instrucción actual y avanzar hasta la siguiente (ejecución paso a paso). |
cont |
c |
continuar la ejecución del programa hasta el próximo breakpoint (o hasta el final si no hay más breakpoints) |
info frame |
info f |
despliega información sobre el stack frame vigente. |
info register |
i r |
despliega información sobre todos los registros |
info register (regName) |
i r (regName) |
despliega información sobre un registro particular, e.g. i r eax |
x [/Nuf] expr |
despliega el contenido de la memoria a partir de la dirección especificada. Por ejemplo, x/4wx 0xffffd138 despliega los 4 words a partir de la dirección 0xffffd138 . Vea el cheat sheet recomendado para conocer sobre los parámetros que se pueden usar con el comando x |
El comando x
también puede ser ejecutado usando el nombre de un registro en vez de la dirección de memoria. En tal caso, mostrará el contenido de la memoria a la que apunta el contenido del registro. Por ejemplo, digamos que el registro ebp
contiene el valor 0xffffd158
. El comando x /4xw $ebp
desplegaría los 4 words a partir de la dirección 0xffffd138
(nota el símbolo de dolar antes del nombre del registro). El comando x
también permite expresiones de suma o resta. Por ejemplo, x $ebp-0xc
despliega el contenido de la dirección de memoria 0xffffd158-0xc
, i.e. 0xffffd14c
.
Utilizando los comandos que acabamos de describir, corre el programa en gdb
y valida los valores del stack que ilustró anteriormente. Por ejemplo, una vez el programa ha ejecutado la instrucción movl $0x2a,-0xc(%ebp)
si das el comando x $ebp-0xc
puedes desplegar el contenido de la variable a
:
0xffffd14c: 0x0000002a
El Data Window te puede servir para monitorear el valor de variables durante la ejecución del programa. Puedes mostrar el valor de una región de memoria así:
ddd
.2. Escoges Data -> Memory y especificas la cantidad de datos y su formato.Por ejemplo, para ver los 0x20
bytes que comienzan en la dirección apuntada por el %esp
, escribes $esp
en la caja de texto y luego escoges Data -> Memory, y luego 32
, hex
, bytes
.
Figura 3. El Data Window mostrando los 32 bytes que comienzan en la dirección apuntada por el $esp
Ahora analizaremos un programa que consiste de dos funciones para ilustrar el pase de parámetros. (antes de copiar y ejecutar) Analice el siguiente programa y determine el valor de retorno de la función main
.
#include <stdio.h>
int foo(int fa, int fb, char fst[]) {
int fx;
fx = fa + fb + fst[0];
return fx;
}
int main() {
int a = 0x55;
int b = 0x11;
int z;
char st[] = "Adios";
z = foo(a,b,st);
return z;
}
Compile usando gcc -m32 -fno-stack-protector -o ejemplo02 ejemplo02.c
. Valide su contestación haciendo echo $?
.
Utilice ddd
para ejecutar y determinar el contenido de:
el stack frame de main
justo antes de invocar a foo
. Asegurese que representa el stack frame completo, incluyendo los argumentos que pasa main
a foo
.
el stack frame de foo
justo antes de hacer return.
Conteste las siguientes preguntas
¿El valor regresado por la función foo, es regresado a través del stack o a través de algun registro? Especifique la posición en el stack o el registro que es utilizado para regresar el valor.
¿Cuánto espacio es reservado por el stack frame de main
?
¿Cuánto espacio es reservado por el stack frame de foo
?
¿Cómo se pasa el tercer parámetro a foo, por valor de puntero o por valor de variable? En otras palabras, ¿se pasa el valor de Adios
o su dirección en el stack frame de main
? Muestre evidencia en ddd
para apoyar su respuesta.
Mientras se está ejecutando foo
muestre en ddd
la parte del stack que contiene el return address a la instrucción luego del call foo
en main
.
Utiliza tu conocimiento de los stacks para poner un comando printf
en el siguiente programa que imprima el valor de a
(sin pasarla como parámetro)
#include <stdio.h>
int foo() {
int fa = 99;
char fst[] = "aloha";
return fa;
}
int main() {
int a = 44;
int b = 55;
int z = foo();
return z;
}
A través de Moodle, somete un documento con las contestaciones a las preguntas de las partes 1 y 2. Por amor a tu deidad favorita, somete un documento digno de un estudiante avanzado de CCOM. Si deseas capturar pantallazos (screenshots) en Ubuntu, recomiendo el programa shutter
(sudo apt-get install shutter
) y google docs.
Debes usar tipo de letra monoespaciado (fixed width font, e.g. courier, monaco) para listados de código.