[FADB] 4. Las variables locales y la pila

FUNDAMENTOS DE LOS ATAQUES POR DESBORDAMIENTO DE BUFFER

Como ya hemos comentado, la pila también sirve para asignar el espacio para variables locales y dinámicas. Para mostrar cómo funciona, vamos a dar paso a un poco de código en C:

void f(int a, int b, int c)
{
char d;
}

void main()
{
f(1,2,3);
}

Simplemente declaramos una variable d local a la función f(a,b,c) que llamamos desde main() con sus tres parámetros. Lo guardamos como prueba0.c y lo compilamos usando la herramienta GNU gcc, -g para que incluya información de depuración y -o para especificar el fichero de salida:

[bash]$ gcc -g prueba0.c -o prueba0
prueba0.c: In function `main':
prueba0.c:2: warning: return type of `main' is not `int'
[bash]$

Pasamos de las advertencias de gcc (que nos avisa de que main( ) no devuelve un entero) y vamos a por el depurador GNU gdb (-q para que nos ahorre la licencia y demás) y ejecutamos disas main para ver el desensamblado de la función main( ):

[bash]$ gdb -q prueba0
(gdb) disas main
Dump of assembler code for function main:
0x8048314

: push %ebp
0x8048315 : mov %esp,%ebp
0x8048317 : sub $0x8,%esp
0x804831a : and $0xfffffff0,%esp
0x804831d : mov $0x0,%eax
0x8048322 : sub %eax,%esp
0x8048324 : sub $0x4,%esp
0x8048327 : push $0x3
0x8048329 : push $0x2
0x804832b : push $0x1
0x804832d : call 0x804830c
0x8048332 : add $0x10,%esp
0x8048335 : leave
0x8048336 : ret
End of assembler dump.
(gdb)

Para ir viendo la evolución del programa vamos a ordenarle la detención en la entrada a main (línea 6 del código en C) con break y luego la ejecución del programa con run.

(gdb) break 6
Breakpoint 1 at 0x8048314: file prueba0.c, line 8.
(gdb) run
Starting program: /home/m0s/Programacion/C%2fC++/prueba0
Breakpoint 1, main () at prueba0.c:7
(gdb)

Veamos el contenido de los registros EIP y ESP con info registers:

(gdb) info registers eip esp
eip 0x8048314 0x8048314
esp 0xbffff80c 0xbffff80c
(gdb)

Efectivamente EIP está esperando a ejecutar la instrucción en 0x8048314 PUSH EBP. Vayamos ahora paso a paso usando nexti.

(gdb) nexti
0x08048315
(gdb) info registers eip esp ebp
eip 0x8048315 0x8048315
esp 0xbffff808 0xbffff808
ebp 0xbffff838 0xbffff838
(gdb)

Después de le ejecución de PUSH EBP, vemos que ESP ha cambiado de 0xBFFFF80C a 0xBFFFF808, tal y como vimos en el funcionamiento de la instrucción PUSH. Veamos ahora qué hay en la dirección apuntada por ESP = 0xBFFFF808 con el comando x:

(gdb) x 0xbffff808
0xbffff808: 0xbffff838
(gdb)

Como era de esperar, el contenido de la dirección 0xBFFFF808 apuntada por ESP es el mismo que el contenido de EBP (0xBFFFF838) ya que acabamos de meterlo en la pila. Sigamos con la ejecución de MOV ESP,EBP.

(gdb) nexti
0x08048317 in main () at prueba0.c:7
7 {
(gdb) info registers eip esp ebp
eip 0x8048317 0x8048317
esp 0xbffff808 0xbffff808
ebp 0xbffff808 0xbffff808
(gdb)

Vemos que ahora EBP = ESP.

(gdb) nexti [SUB 0x8,ESP]
0x0804831a 7 {
(gdb) info registers eip esp
eip 0x804831a 0x804831a
esp 0xbffff800 0xbffff800
(gdb) nexti [AND 0xFFFFFFF0,ESP]
0x0804831d 7 {
(gdb) info registers eip esp
eip 0x804831d 0x804831d
esp 0xbffff800 0xbffff800
(gdb) nexti [MOV 0x0,EAX]
0x08048322 7 {
(gdb) nexti [SUB EAX,ESP]
8 f(1,2,3);
(gdb) nexti [SUB 0x4,ESP]
0x08048327 8 f(1,2,3);
(gdb) info registers eip esp
eip 0x8048327 0x8048327
esp 0xbffff7fc 0xbffff7fc
(gdb)

Las instrucción SUB 0x8,ESP resta 8 a ESP, 0xBFFFF800. La siguiente instrucción (AND 0xFFFFFFF0,ESP) pone a 0 los 4 bits más bajos de ESP, por lo tanto no hace nada, ya que estos bits ya están a cero. Luego mueve un cero a EAX (MOV 0x0,EAX) y resta ESP – EAX (SUB EAX,ESP). En definitiva, no hace nada tampoco. Luego ejecuta SUB 0x4,ESP, que resta 4 a ESP, que se queda pues como 0xBFFFF7FC.

Puede que algunas de estas instrucciones nos parezcan un poco absurdas y con razón, ya que están generadas por un compilador. Hasta ahora nos han servido para manejarnos un poco con gdb. No vamos a profundizar más sobre estas instrucciones y vamos a lo que nos interesa: el paso de parámetros y las variables locales.

Las tres siguientes instrucciones (PUSH 3, PUSH 2 y PUSH 1) meten en la pila los parámetros de la función. Como ya habíamos comentado, los parámetros se pasan en orden inverso.

(gdb) nexti [PUSH 3]
0x08048329 8 f(1,2,3);
(gdb) nexti [PUSH 2]
0x0804832b 8 f(1,2,3);
(gdb) nexti [PUSH 1]
0x0804832d 8 f(1, 2, 3);
(gdb) info registers eip esp
eip 0x804832d 0x804832d
esp 0xbffff7f0 0xbffff7f0
(gdb) x 0xbffff7f0
0xbffff7f0: 0x00000001
(gdb) x 0xbffff7f4
0xbffff7f4: 0x00000002
(gdb) x 0xbffff7f8
0xbffff7f8: 0x00000003
(gdb)

Vemos que efectivamente 1, 2 y 3 están metidos en la pila después de las tres PUSH y que el valor de ESP ha sido decrementado en 4 por cada PUSH (4 x 3 = 12 (0xC) en total). La siguiente instruccion (EIP = 0x804832D) es la llamada a la función CALL 0x804830C, que como bien nos indica gdb nos lleva a la función f( ). Veamos pues qué hace la función f( ).

(gdb) disas f
Dump of assembler code for function f:
0x804830c : sub $0x4,%esp
0x804830f : add $0x4,%esp
0x8048312 : ret
End of assembler dump.
(gdb)

Pongamos un punto de interrupción en la entrada a f( ) (línea 1), ya que sino gdb no nos guiará dentro de f( ), y sigamos con la ejecución.

(gdb) break 1
Breakpoint 2 at 0x804830c: file prueba0.c, line 1.
(gdb) nexti [CALL 0x804830C]
Breakpoint 2, f (a=2, b=-1073743864) at prueba0.c:2
2 {
(gdb) info registers eip esp
eip 0x804830c 0x804830c
esp 0xbffff7ec 0xbffff7ec

Vemos que la dirección de la siguiente instrucción a ejecutar es 0x804830C, precisamente la primera instrucción de la función f( ), SUB 0x4,ESP.

(gdb) nexti [SUB 0x4,ESP]
4 }
(gdb) info registers eip esp
eip 0x804830f 0x804830f
esp 0xbffff7e8 0xbffff7e8
(gdb) nexti [ADD 0x4, ESP]
0x08048312 4 }
(gdb) info registers eip esp
eip 0x8048312 0x8048312
esp 0xbffff7ec 0xbffff7ec
(gdb) x 0xbffff7ec
0xbffff7ec: 0x08048332 [Dirección de retorno]
(gdb) nexti [RET]
0x08048332 in main () at prueba0.c:8
8 f(1, 2, 3);
(gdb)

En este ejemplo vemos claramente cómo se reservan 4 bytes en la pila (SUB 0x4,ESP) para la variable local char z[4]. Recordemos que un array en C se representa como una dirección de memoria, es decir, z es un puntero de 32 bits. Antes de ejecutar la instrucción RET, y como ya hemos comentado, se devuelve el valor que tenía ESP al entrar en la función (0xBFFFF7EC) con ADD 0x4,ESP, que es donde se encuentra la dirección de retorno 0x08048332. Así RET vuelve a la función main( ) correctamente.

¿Qué cambiaría si hubiésemos declarado char z, en vez de char z[4]? Pues nada en absoluto. Recordemos que la unidad mínima de asignación en la pila son 4 bytes. Tanto si necesitamos 1, 2, 3 como 4 bytes igualmente se nos asignarán 4. Es decir, siempre se reservarán en la pila tantos bytes como el primer múltiplo de 4 superior al tamaño de la variable dinámica en cuestión.

Comments

Popular Posts