02 - Lab 02 - El stack - 2023
Una de las claves para entender un programa desensamblado es comprender cómo funcióna el stack. En este lab aprenderás a usar un debugger primitivo (pero potente) para visualizar algunos de los conceptos presentados en clase sobre el stack, la invocacion de funciónes, pase de argumentos y variables locales.
Requisitos de software
Visita la pagina Requisitos de Software para que instales todo lo necesario.
Parte 1 - Comandos básicos en gdb
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.
Paso 0
Asegúrate que los programas mecionados en Requisitos de software está debidamente instalados.
Paso 1
En este paso crearemos un binario para luego correrlo paso a paso usando gdb
. Será un programa sumamente sencillo pues, como verás a continuacion, 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 -O0
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 en assembly es feo de por si, no hay necesidad de empeorarlo. La 0O
(una letra O mayúscula, seguida de un cero) le dice al compilador que no use optimización. Por lo general esa opción logra que la secuencia de instrucciones de código assembly se parezca bastante al código original de C.
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 función 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 gdb
, lo cual requiere mucha paciencia y ganas de aprender.
Paso 2
Carga el programa ejemplo1
en gdb
usando el comando gdb ejemplo01
.
Luego de varias lineas de información, verás el prompt de gdb >>>
.
Antes de comenzar a correr el programa debes establecer al menos un breakpoint, es decir una instrucción en el programa en donde deseas que detenga la ejecución. De lo contrario gdb
correrá el programa de principio a fin sin detenerse. Vamos a establecer un breakpoint al principio de la función main
b main
Una vez establecido el breakpoint y gracias al frontend que instalamos, podemos ejecutar el programa y veremos desplegadas muchos cuadros de información.
r
Tu pantalla ahora contendra varias secciones que describen, entre otros, el estado de los registros, disasembly, estado del stack.
Paso 3
Vamos a concentrarnos en la parte que dice code. Esto corresponde al código desensamblado de la función main
de tu programa ejemplo01
.
0x5655618d <main>: push ebp
0x5655618e <main+1>: mov ebp,esp
0x56556190 <main+3>: sub esp,0x20
=> 0x56556193 <main+6>: call 0x565561c0 <__x86.get_pc_thunk.ax>
0x56556198 <main+11>: add eax,0x2e44
0x5655619d <main+16>: mov DWORD PTR [ebp-0xc],0x2a
0x565561a4 <main+23>: lea eax,[ebp-0xc]
0x565561a7 <main+26>: mov DWORD PTR [ebp-0x4],eax
Las primeras 3 instrucciones realizan el prologo de la función main
.
-
La instrucción
push ebp
se encargan de guardar el valor del registroebp
de la función que invoco amain
. El valor deebp
es recuperado al final de la función con la instruccionleave
. -
Las siguientes dos instrucciones reservan espacio para el stack frame de la función main:
mov ebp,esp
- establece que dirección que se usaba para el top del stack frame de la función que invocó amain
, ahora será el fondo de el stack frame demain
.sub $0x20,%esp
separa0x20
bytes de memoria para el stack frame de main.
Vamos a obviar las siguientes dos instrucciones: call 0x565561c0 <__x86.get_pc_thunk.ax>
y add eax,0x2e44
pues no afectan el resto del análisis.
Usando el comando ni
(next instruction), lleva la ejecución del programa hasta la instrucción mov DWORD PTR [ebp-0xc],0x2a
. Esa instrucción en la que implementa int a = 42;
del programa en C. Observa como el valor 0x2a aparece en el stack.
Las próximas instrucciones son las que implementan la instrucción int *b = &a;
:
-
lea eax,[ebp-0xc]
: asigna al registroeax
la dirección correspondiente aebp-0xc
. Es decireax
ahora contendrá la dirección donde está guardado el 42 (la dirección de la variablea
). -
mov DWORD PTR [ebp-0x4],eax
: copiar el contenido deeax
en la direcciónebp-0x4
. Al ejecutar esta instrucción notaras que el stack ahora contiene (ademas del 0x2a) la dirección donde está el 0x2a.
El cuerpo del programa son las siguientes instrucciones:
call 0x11e4 <__x86.get_pc_thunk.ax>
add eax,0x2e20
mov DWORD PTR [ebp-0xc],0x2a
lea eax,[ebp-0xc]
mov DWORD PTR [ebp-0x4],eax
mov DWORD PTR [ebp-0x11],0x616c6f68
mov BYTE PTR [ebp-0xd],0x0
mov BYTE PTR [ebp-0x5],0x47
mov eax,0x57
leave
ret
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 instruccion int a = 42
del codigo fuente.
dashboard memory watch $esp 32
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 instruccion 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 [ ]
Paso 4
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 codigo. Un breakpoint es una forma de decirle al debugger que detenga su ejecucion del programa cuando llegue a cierta instruccion. 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 (nombreDeFunción)
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 instruccion mov $0x57,%eax
.
Nuestra proxima 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 instruccion donde detuvimos la ejecucion.
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 ejemplo
http://www.cs.berkeley.edu/~mavam/teaching/cs161-sp11/gdb-refcard.pdf. A continuacion una lista corta de los que más utilizo:
Comando | Abreviatura | Funciónalidad |
---|---|---|
stepi |
si |
ejecuta la instruccion actual y avanzar hasta la siguiente (ejecucion paso a paso). |
cont |
c |
continuar la ejecucion del programa hasta el proximo breakpoint (o hasta el final si no hay más breakpoints) |
info frame |
info f |
despliega informacion sobre el stack frame vigente. |
info register |
i r |
despliega informacion sobre todos los registros |
info register (regName) |
i r (regName) |
despliega informacion 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 ilustro anteriormente. Por ejemplo, una vez el programa ha ejecutado la instruccion movl $0x2a,-0xc(%ebp)
si das el comando x $ebp-0xc
puedes desplegar el contenido de la variable a
:
0xffffd14c: 0x0000002a
Paso 5 - Y para qué es el Data Window?
El Data Window te puede servir para monitorear el valor de variables durante la ejecucion del programa. Puedes mostrar el valor de una region de memoria así:
- escribes la dirección o expresión usando registro en la caja de texto en la parte superior izquierda de la ventana de
ddd
. - 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
Usando el comando 'si' adelanta el programa hasta justo después que se ejecuta la instrucción que asigna "hola" al arreglo st[]
. Incluye en tu informe.
Parte 2 -Función llamando a función
Ahora analizaremos un programa que consiste de dos funciónes 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;
}
Compila usando gcc -m32 -fno-stack-protector -o ejemplo02 ejemplo02.c
. Valida tu contestacion haciendo echo $?
.
Utilice ddd
para ejecutar y determinar el contenido de:
-
el stack frame de
main
justo antes de invocar afoo
. Asegurate que representas el stack frame completo, incluyendo los argumentos que pasamain
afoo
. -
el stack frame de
foo
justo antes de hacer return.
Contesta 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? Especifica la posicion 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 se pasa su dirección en el stack frame demain
? Muestra evidencia enddd
para apoyar su respuesta. -
Mientras se está ejecutando
foo
muestra enddd
la parte del stack que contiene el return address a la instrucción luego delcall foo
enmain
.
Parte 3 - Reto
Utiliza tu conocimiento de los stacks para poner un comando printf
en el siguiente programa que imprima el valor de a
desde la función foo
(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;
}
Entregables
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.