27
Oct
2012

Linux: analizando el stack de un proceso

En este breve artículo quiero presentar un análisis básico del stack de un proceso en Linux. Se trata de un caso de ejecución sencillo, contado como un storyboard, para ir a los conceptos fundamentales del código ejecutable (visto como assembler), llamadas a funciones y manejo de memoria. La arquitectura en este caso es IA-32.

1. Tenemos un programa sencillo escrito en C

#include <stdio.h>
#include <stdlib.h>
 
int main(void){
 
    int x;
    int y;
 
    x = 4;
    y = 5;
 
    foo();
 
    return 0;
}
 
int foo(){
    int y = 1;
    return y;
}

Se declaran dos variables locales en la función main, se llama a una función foo y se declara una variable local en la función foo.

2. Ponemos a ejecutar el programa con el debugger gdb y damos el primer paso

gdb a.out

(gdb) start
Temporary breakpoint 1 at 0x8048397
Starting program: /root/c/alcance_variables/a.out 

Temporary breakpoint 1, 0x08048397 in main ()
(gdb) stepi
0x0804839a in main ()

3. Observamos el código assembler del programa para ubicar en qué instrucción estamos parados

objdump -d a.out

08048394 
: 8048394: 55 push %ebp 8048395: 89 e5 mov %esp,%ebp 8048397: 83 e4 f0 and $0xfffffff0,%esp 804839a: 83 ec 10 sub $0x10,%esp 804839d: c7 44 24 0c 04 00 00 movl $0x4,0xc(%esp) 80483a4: 00 80483a5: c7 44 24 08 05 00 00 movl $0x5,0x8(%esp) 80483ac: 00 80483ad: e8 07 00 00 00 call 80483b9 80483b2: b8 00 00 00 00 mov $0x0,%eax 80483b7: c9 leave 80483b8: c3 ret 080483b9 : 80483b9: 55 push %ebp 80483ba: 89 e5 mov %esp,%ebp 80483bc: 83 ec 10 sub $0x10,%esp 80483bf: c7 45 fc 01 00 00 00 movl $0x1,-0x4(%ebp) 80483c6: 8b 45 fc mov -0x4(%ebp),%eax 80483c9: c9 leave 80483ca: c3 ret 80483cb: 90 nop 80483cc: 90 nop 80483cd: 90 nop 80483ce: 90 nop 80483cf: 90 nop

Primera observación: las direcciones de memoria virtual (según la vista de un proceso) donde se encuentran las instrucciones para ejecución (sección .text del archivo ELF) son a partir de la 0x08048310 (0x08048000).

Segunda observación: estamos parados en la instrucción “sub $0x10,%esp”. Esta es la primera instrucción del código escrito por nosotros (las instrucciones anteriores fueron assembler que generó el compilador por sí solo).

4. Avanzamos en el programa hasta llegar a la instrucción de asignación de valor a la variable local de la función foo

(gdb) stepi
0x0804839a in main ()
(gdb) stepi
0x0804839d in main ()
(gdb) stepi
0x080483a5 in main ()
(gdb) stepi
0x080483ad in main ()
(gdb) stepi
0x080483b9 in foo ()
(gdb) stepi
0x080483ba in foo ()
(gdb) stepi
0x080483bc in foo ()
(gdb) stepi
0x080483bf in foo ()
(gdb)
(gdb) stepi
0x080483c6 in foo ()

5. Miramos el mapa de memoria del proceso que tenemos en ejecución

root@martin-laptop:~/c/alcance_variables# cat /proc/21728/maps
00110000-0012c000 r-xp 00000000 08:01 6029453    /lib/ld-2.12.1.so
0012c000-0012d000 r--p 0001b000 08:01 6029453    /lib/ld-2.12.1.so
0012d000-0012e000 rw-p 0001c000 08:01 6029453    /lib/ld-2.12.1.so
0012e000-0012f000 r-xp 00000000 00:00 0          [vdso]
0012f000-00286000 r-xp 00000000 08:01 6029746    /lib/libc-2.12.1.so
00286000-00288000 r--p 00157000 08:01 6029746    /lib/libc-2.12.1.so
00288000-00289000 rw-p 00159000 08:01 6029746    /lib/libc-2.12.1.so
00289000-0028c000 rw-p 00000000 00:00 0
08048000-08049000 r-xp 00000000 08:01 3434955    /root/c/alcance_variables/a.out
08049000-0804a000 r--p 00000000 08:01 3434955    /root/c/alcance_variables/a.out
0804a000-0804b000 rw-p 00001000 08:01 3434955    /root/c/alcance_variables/a.out
b7fe8000-b7fe9000 rw-p 00000000 00:00 0
b7ffe000-b8000000 rw-p 00000000 00:00 0
bffdf000-c0000000 rw-p 00000000 00:00 0          [stack]

Observación: el proceso es hijo del debugger (gdb). Obtenemos el PID con el comando “ps aux | grep a.out”.

En el mapeo de memoria del proceso observamos tres elementos interesantes:

1. En la parte más baja de la memoria vemos las shared libraries (cargadas dinámicamente, por el dynamic linker)
2. En la dirección 0x08048000 vemos la imagen del binario ejecutable cargada.
3. Entre las direcciones 0xbffdf000 y 0xc0000000 se encuentra el stack.

En Linux, el stack está en la parte más alta de la memoria y crece hacia abajo.

6. Hacemos un análisis estático del binario ejecutable a.out y prestamos atención a las secciones (headers)

readelf -d a.out

Section Headers:
  [Nr] Name              Type            Addr     Off    Size   ES Flg Lk Inf Al
  [ 0]                   NULL            00000000 000000 000000 00      0   0  0
  [ 1] .interp           PROGBITS        08048134 000134 000013 00   A  0   0  1
  [ 2] .note.ABI-tag     NOTE            08048148 000148 000020 00   A  0   0  4
  [ 3] .note.gnu.build-i NOTE            08048168 000168 000024 00   A  0   0  4
  [ 4] .gnu.hash         GNU_HASH        0804818c 00018c 000020 04   A  5   0  4
  [ 5] .dynsym           DYNSYM          080481ac 0001ac 000050 10   A  6   1  4
  [ 6] .dynstr           STRTAB          080481fc 0001fc 00004c 00   A  0   0  1
  [ 7] .gnu.version      VERSYM          08048248 000248 00000a 02   A  5   0  2
  [ 8] .gnu.version_r    VERNEED         08048254 000254 000020 00   A  6   1  4
  [ 9] .rel.dyn          REL             08048274 000274 000008 08   A  5   0  4
  [10] .rel.plt          REL             0804827c 00027c 000018 08   A  5  12  4
  [11] .init             PROGBITS        08048294 000294 000030 00  AX  0   0  4
  [12] .plt              PROGBITS        080482c4 0002c4 000040 04  AX  0   0  4
  [13] .text             PROGBITS        08048310 000310 00019c 00  AX  0   0 16
  [14] .fini             PROGBITS        080484ac 0004ac 00001c 00  AX  0   0  4
  [15] .rodata           PROGBITS        080484c8 0004c8 00000c 00   A  0   0  4
  [16] .eh_frame         PROGBITS        080484d4 0004d4 000004 00   A  0   0  4
  [17] .ctors            PROGBITS        08049f14 000f14 000008 00  WA  0   0  4
  [18] .dtors            PROGBITS        08049f1c 000f1c 000008 00  WA  0   0  4
  [19] .jcr              PROGBITS        08049f24 000f24 000004 00  WA  0   0  4
  [20] .dynamic          DYNAMIC         08049f28 000f28 0000c8 08  WA  6   0  4
  [21] .got              PROGBITS        08049ff0 000ff0 000004 04  WA  0   0  4
  [22] .got.plt          PROGBITS        08049ff4 000ff4 000018 04  WA  0   0  4
  [23] .data             PROGBITS        0804a00c 00100c 000008 00  WA  0   0  4
  [24] .bss              NOBITS          0804a014 001014 000008 00  WA  0   0  4
  [25] .comment          PROGBITS        00000000 001014 00002b 01  MS  0   0  1
  [26] .shstrtab         STRTAB          00000000 00103f 0000ee 00      0   0  1
  [27] .symtab           SYMTAB          00000000 0015b8 000410 10     28  44  4
  [28] .strtab           STRTAB          00000000 0019c8 000200 00      0   0  1

Se puede constatar allí que efectivamente las secciones del binario van entre las direcciones 0x08048134 y 0x0804a014. Se puede ver también que no hemos exportado símbolos de debug al compilar. Qué significa cada sección quedará para otro artículo, ¡pero es bastante interesante!

7. Analizamos en el debugger (gdb) los valores de los registros en el punto actual de ejecución

(gdb) info all-registers
eax            0xbffff424	-1073744860
ecx            0x3a158f3b	974491451
edx            0x1	1
ebx            0x287ff4	2654196
esp            0xbffff348	0xbffff348
ebp            0xbffff358	0xbffff358
esi            0x0	0
edi            0x0	0
eip            0x80483c6	0x80483c6 
eflags         0x286	[ PF SF IF ]
...

Ahí se pueden ver algunas cosas interesantes:

1. El Instruction Pointer (eip) está en 0x80483c6. Eso indica la instrucción del binario ejecutable (sección .text) donde estamos posicionados ahora. Podemos ver la captura del objdump y ver la instrucción de assembler en concreto.

2. El Base Pointer (ebp) está en 0xbffff358. Eso significa que al entrar a la función foo, se puso la base del stack en dicho valor. Por lo tanto, todas las variables y uso del stack que haga la función foo, será de esa dirección hacia abajo (recordad que el stack crece hacia abajo).

3. El Stack Pointer (esp) está en 0xbffff348. El tope del stack está 10 más abajo que el Base Pointer (nuevamente, el stack crece hacia abajo). Por lo tanto, la llamada a la función consumió 16 bytes del stack. Entre esos límites (0xbffff348 y 0xbffff358) tenemos todo lo que está usando del stack la función foo hasta el momento.

8. Finalmente, ¡queremos ver el stack!

(gdb) x/11x 0xbffff348
0xbffff348:	0xbffff378	0x080483f9	0x00288324	0x00000001
0xbffff358:	0xbffff378	0x080483b2	0x0015e985	0x0011eb60
0xbffff368:	0x00000005	0x00000004	0x080483e0

Observaciones:

1. Tenemos en el byte 0xbffff358 el Base Pointer de la función Main. Recordad la función prólogo. Ahí precisamente comienza el stack de la función foo. Ver en la salida de objdump la instrucción assembler que escribió ese valor allí.

2. Para arriba de la dirección 0xbffff358 vemos el stack de la función Main. Dos de los valores allí son “0x00000004” y “0x00000005”, las variables locales “x” y “y” de Main.

3. Para abajo de la dirección 0xbffff358 vemos el stack de la función foo. Uno de los valores allí (el segundo) es “0x00000001”, la variable local “x” de la función foo. Otro de los valores allí presentes es “0x080483f9”, la return address de la función foo (para volver a Main).

Creo que tenemos en este punto una forma manual pero poderosa de analizar y entender qué es lo que está pasando durante la llamada a una función en la ejecución de un binario sobre Linux.

Escribir un comentario