Continuamos con el siguiente capítulo de este artículo de Linux Voice en el que hace unos días descubrimos cuáles son los Elfos que habitan en el mundo de Linux. Esta vez se explica de qué forma se organizan los procesos en la memoria.
Ahora que sabemos cómo se organizan en el disco los programas que usamos a diario, veamos qué aspecto tienen en memoria. Los procesos en Linux se organizan en segmentos. Cada segmento engloba una o más secciones ELF. Por cierto, readelf ya puede volcar la estructura de segmentos:
$ readelf --segments /bin/pwd El tipo del fichero elf es EXEC (Fichero ejecutable) Punto de entrada 0x4019f7 Hay 9 encabezados de programa, empezando en el desplazamiento 64 Encabezados de Programa: [...] mapeo de Sección a Segmento: Segmento Secciones... 00 01 .interp 02 .interp .note.ABI-tag .note.gnu.build-id .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rela.dyn .rela.plt .init .plt .text .fini .rodata .eh_frame_hdr .eh_frame 03 .init_array .fini_array .jcr .data.rel.ro .dynamic .got .got.plt .data .bss 04 .dynamic 05 .note.ABI-tag .note.gnu.build-id 06 .eh_frame_hdr 07 08 .init_array .fini_array .jcr .data.rel.ro .dynamic .got
Como puedes ser, el mapeo de secciones de segmentos está lejos de tener una correspondencia exacta. Los segmentos de los que hablamos aquí no se deben confundir con los de x86, como CS o DS de ensamblador. Los segmentos que encontrarás más a menudo son text, data y stack. El primero almacena el código del programa, ‘data’ son los datos globales del programa y ‘stack’ se usa para almacenar las variables locales y para devolver llamadas de funciones.
Cada segmento tiene asociado un conjunto de permisos. Por ejemplo, ‘text’ normalmente está marcado como de solo lectura ya que hay muy pocas razones admisibles para modificar el código del programa al vuelo. Como el kernel sabe que ‘text’ es de sólo lectura, sólo puede tener una copia física de una biblioteca compartida en memoria y mapea esta sola instancia a todos los procesos que necesiten la biblioteca.
Por otra parte, ‘data’ y ‘stack’ normalmente se marcan como no-ejecutable. Esto hace que las vulnerabilidades (como el desbordamiento de pila) sean más difíciles de llevar a cabo. Sin embargo sigue siendo posible, por lo que se emplean otros mecanismos para mantener tu sistema a salvo.
Uno de esos mecanismos es el Address Space Layout Randomisation, o ALSR. Todos los procesos en Linux tienen un esquema de memoria predecible que veremos en breve. Sabiendo que por ejemplo la parte superior de la pila está en 0xbfffffff, un cracker puede realizar un ataque de una forma más simple y fiable. Desde Linux 2.6.12, el kernel añade offsets aleatorios a estas posiciones. Una vez más, esta medida por sí sola no imposibilita atacar las vulnerabilidades pero reducen el riesgo.
El espacio de direcciones del proceso se separa en espacio de usuario y espacio del kernel. En sistemas x86 de 32 bits se usa la llamada partición 3/1: de 4 Gb disponibles, 3Gb se dejan al usuario y 1Gb (compartido entre procesos) es para el kernel. La memoria del kernel es inaccesible por el código de usuario por razones de seguridad. Eso significa que un puntero inválido en el código del espacio de usuario puede almacenar direcciones a partir de 0xc000000. En sistemas x86 de 64 bits, el espacio de direcciones se divide a partes iguales. Tanto el espacio de usuario como el espacio del kernel tienen un tamaño de 128Tb y la memoria del kernel comienza en 0xffff800000000000.
Puedes ver la parte del espacio de usuario de la memoria virtual de un proceso en el diagrama. La pila ocupa la parte superior del espacio de memoria (las direcciones más altas) y crece hacia abajo. El tamaño por defecto de la pila es de 8Mb pero puedes ajustarlo con ulimit -s <nuevo valor>. En la parte inferior de la pila se almacenan el nombre del programa, los argumentos de la linea de comandos y el entorno. La función main() de tu programa recibe punteros a estas cadenas que además el kernel pone en la pila. Oficialmente, main() tiene el siguiente prototipo: main(int argc, char **argv, char **envp) (y me apuesto algo a que nunca te la has encontrado con un tercer argumento). Por convención argv[0] se trata como el nombre del programa. Así que si haces un strncpy() cuidadosamente ahí, ps(1) mostrará tu proceso con un nombre diferente.
El siguiente área se conoce por «memmap» y es donde mmap(2) pone mapas de memoria anónimos incluyendo bibliotecas compartidas. También crece de arriba a abajo en x86-64. Como sabrás, el intérprete del programa normalmente se mapea primero por lo que típicamente se ve al final de este área. Ahora miremos a la parte baja. Es mejor hacerlo con un ejemplo. Abre el terminal y ejecuta:
$ cat /proc/self/maps 00400000-0040b000 r-xp 00000000 08:05 135283 /usr/bin/cat 0060b000-0060c000 r--p 0000b000 08:05 135283 /usr/bin/cat 0060c000-0060d000 rw-p 0000c000 08:05 135283 /usr/bin/cat 009bb000-009dc000 rw-p 00000000 00:00 0 [heap]
/proc/selfes un enlace simbólico que se refiere a la entrada de procesos en /proc. Como puedes ver, el primer «habitante» aquí es el ‘text’ [segmento]. Fíjate en los permisos: text es de sólo lectura y ejecutable. En sistemas x86-64, el código del programa normalmente se mapea en la dirección 0x400000; en 32 bits usa la dirección 0x08048000.
El segmento no ejecutable ‘data’ /bin/cat es de solo lectura (constantes) y el modificable son simplemente datos globales (incluyendo BBS). Luego sigue la pila: un área donde se asigna memoria dinámica cuando cat hace malloc(3) (el cual es importado una vez más de glibc). Existen bastante asignadores de memoria pero la forma clásica es hacer una llamada de sistema brk(2) cuando necesitas dar un salto hacia adelante (crece de abajo a arriba).
Ahora por favor, échale un rato para ver cómo se organizan los distintos procesos de tu sistema en su espacio de memoria virtual. Simplemente vuelca /proc/<PID>/maps para el proceso que te interese, pero ten en cuenta que necesitarás permisos de root para hacerlo en procesos que no pertenezcan a tu usuario. Para hacer pruebas más interesantes, piensa en deshabilitar ASLR temporalmente. Para hacerlo ejecuta echo 0 >
/proc/sys/kernel/randomize_va_space. También puedes deshabilitar ASRL por procesos ejecutando setarch $(uname -m) -R programa. No olvides volver a habilitarlo cuando termines.
El sistema de archivos /proc tiene mucha más información sobre los procesos en ejecución: puedes ver archivos abiertos, argumentos de la linea de comandos o de entorno por decir una pocas cosas. Todo esto es accesible desde el directorio /proc/<PID> y está bien descrito en el manpage de proc(5).
· Primera parte: Elfos de Linuxoria
· Tercera parte: Observando la naturaleza