ORGANIZACION DE MEMORIA VIRTUAL

SEGMENTO STACK

Este segmento es un LIFO (Last In First Out), que va aumentando partiendo desde el valor mas alto de memoria, hasta el valor mas bajo en la medida que se van ingresando valores en dicha pila, el Register ESP siempre estará apuntando al valor actual de la pila. Esta pila solamente soporta dos operaciones, las cuales son push y pop que consisten básicamente en “poner” valores y “quitar” valores desde la cima de la pila, estas operaciones determinan la posición del ESP, independiente de la función que se ejecute, siempre apuntará a la última posición de la pila.

USO PRACTICO DE LOS REGISTERS

Los registers que se usan con mayor frecuencia en la Stack son: EIP, ESP y EBP, su importancia radica en que almacenan y mantienen de forma consistente los elementos almacenados en la Stack, el EIP se encarga de almacenar la posición de memoria en la que se encuentra el programa en ejecución, por lo tanto es el encargado de llevar el flujo de ejecución, como se verá más adelante, la manipulación de este register es vital para el funcionamiento de segmentos de ejecución y funciones, desde el punto de vista del investigador, es necesario que entienda correctamente su funcionamiento para determinar vulnerabilidades en la ejecución de un programa. El ESP, es el encargado de apuntar al último elemento de la Stack, de esta forma funciones del tipo PUSH y POP utilizan este register para conocer la posición de memoria que deben utilizar para retirar o establecer elementos en la Stack. Por otro lado, el register EBX tiene la capacidad de apuntar a cualquier elemento de la pila en función a su posición de memoria en la pila, lo que quiere decir que se almacena también en la Stack. Como se verá más adelante, es posible utilizar la posición de memoria del EBX para acceder a otros elementos almacenados en la Stack.
EJEMPLO DE ORGANIZACION DE MEMORIA VIRTUAL CON UN PROGRAMA SIMPLE ESCRITO EN LENGUAJE C.

Para demostrar como funciona el modelo de memoria virtual, basta con tener un programa sencillo y trazar la información que éste nos provee en tiempo de ejecución.

Listado SimpleDemoReader.c

#include<stdio.h>#include<stdlib.h>int add(int x, int y){int z = x + y;}

main(int argc, char **argv) {

int a = atoi(argv[1]);

int b = atoi(argv[2]);

int c;

char buffer[100];

gets(buffer);

puts(buffer);

c = add(a, b);

printf(«Sum of %d+%d = %d\n»,a,b,c);

exit(0);

}

Se procede a compilar el programa:

gcc -ggdb -o SimpleDemo SimpleDemoReader.c

Este comando generará el ejecutable SimpleDemo.

Ejecución:

./SimpleDemo 20 10

demo

demo

Sum of 20+10 = 30

De momento no hay nada interesante, sin embargo podemos ver el uso de memoria de este programa antes de ingresar la entrada por teclado esperada por el programa, en este punto podemos consultar el PID del proceso al que está asociado el programa y posteriormente visualizar la organización de la memoria virtual, suponiendo que el identificador del proceso sea: 11798, es posible visualizar el mapa de la memoria virtual en el directorio /proc/11798. Dentro de dicho directorio se encontrarán diferentes directorios que permiten conocer detalles sobre la ejecución del programa:

ls /proc/11798

attr coredump_filter fdinfo mem oom_adj schedstat status

auxv cpuset io mountinfo oom_score sessionid syscall

cgroup cwd latency mounts pagemap smaps task

clear_refs environ limits mountstats personality stack wchan

cmdline exe loginuid net root stat

comm fd maps numa_maps sched statm

el fichero maps contiene la disposición de memoria del programa:

cat /proc/11798/maps

00400000-00401000 r-xp 00000000 fc:00 130696 /Simples/SimpleDemo

00600000-00601000 r–p 00000000 fc:00 130696 /Simples/SimpleDemo

00601000-00602000 rw-p 00001000 fc:00 130696 /Simples/SimpleDemo

7fab1bcfb000-7fab1be75000 r-xp 00000000 08:05 4719186 /lib/libc-2.12.1.so

7fab1be75000-7fab1c074000 —p 0017a000 08:05 4719186 /lib/libc-2.12.1.so

7fab1c074000-7fab1c078000 r–p 00179000 08:05 4719186 /lib/libc-2.12.1.so

7fab1c078000-7fab1c079000 rw-p 0017d000 08:05 4719186 /lib/libc-2.12.1.so

7fab1c079000-7fab1c07e000 rw-p 00000000 00:00 0

7fab1c07e000-7fab1c09e000 r-xp 00000000 08:05 4719194 /lib/ld-2.12.1.so

7fab1c27a000-7fab1c27d000 rw-p 00000000 00:00 0

7fab1c29b000-7fab1c29e000 rw-p 00000000 00:00 0

7fab1c29e000-7fab1c29f000 r–p 00020000 08:05 4719194 /lib/ld-2.12.1.so

7fab1c29f000-7fab1c2a0000 rw-p 00021000 08:05 4719194 /lib/ld-2.12.1.so

7fab1c2a0000-7fab1c2a1000 rw-p 00000000 00:00 0

7fff98de4000-7fff98e05000 rw-p 00000000 00:00 0 [stack]

7fff98e26000-7fff98e27000 r-xp 00000000 00:00 0 [vdso]

ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0 [vsyscall]

Aquí se puede apreciar la información de cada uno de los segmentos utilizados por el programa, incluyendo las librerías cargadas de las cuales el programa depende. Como se indicaba anteriormente, el segmento de ejecución de programa que es .text, tiene como valor de memoria el mas bajo posible, en este caso este es: 00400000 dependiendo de la arquitectura y la memoria física del ordenador.

Finalmente tenemos también el segmento de memoria de stack, como puede apreciarse tiene el valor 7fff98de4000-7fff98e05000 que es asignado de forma aleatoria cada vez que la ejecución de un programa es llevada a cabo.

Como también se ha indicado anteriormente, cada programa en ejecución se establece en el mismo espacio de memoria virtual o segmento, sin importar la localización física actual en memoria, sin embargo, dependiendo del sistema operativo, esto no siempre es verdadero, para demostrarlo podemos volver a ejecutar el mismo programa, consultar el nuevo PID generado y finalmente consultar el fichero maps donde se indican los valores de memoria, suponiendo que el nuevo PID generado de la nueva ejecución del programa es 12088, los valores para esta ejecución son:

cat /proc/12088/maps

00400000-00401000 r-xp 00000000 fc:00 130696 /Simples/SimpleDemo

00600000-00601000 r–p 00000000 fc:00 130696 /Simples/SimpleDemo

00601000-00602000 rw-p 00001000 fc:00 130696 /Simples/SimpleDemo

7f1bf2736000-7f1bf28b0000 r-xp 00000000 08:05 4719186 /lib/libc-2.12.1.so

7f1bf28b0000-7f1bf2aaf000 —p 0017a000 08:05 4719186 /lib/libc-2.12.1.so

7f1bf2aaf000-7f1bf2ab3000 r–p 00179000 08:05 4719186 /lib/libc-2.12.1.so

7f1bf2ab3000-7f1bf2ab4000 rw-p 0017d000 08:05 4719186 /lib/libc-2.12.1.so

7f1bf2ab4000-7f1bf2ab9000 rw-p 00000000 00:00 0

7f1bf2ab9000-7f1bf2ad9000 r-xp 00000000 08:05 4719194 /lib/ld-2.12.1.so

7f1bf2cb5000-7f1bf2cb8000 rw-p 00000000 00:00 0

7f1bf2cd6000-7f1bf2cd9000 rw-p 00000000 00:00 0

7f1bf2cd9000-7f1bf2cda000 r–p 00020000 08:05 4719194 /lib/ld-2.12.1.so

7f1bf2cda000-7f1bf2cdb000 rw-p 00021000 08:05 4719194 /lib/ld-2.12.1.so

7f1bf2cdb000-7f1bf2cdc000 rw-p 00000000 00:00 0

7fff9a0b2000-7fff9a0d3000 rw-p 00000000 00:00 0 [stack]

7fff9a1ff000-7fff9a200000 r-xp 00000000 00:00 0 [vdso]

ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0 [vsyscall]

Como puede apreciarse, el comportamiento anteriormente descrito no se ha cumplido y resulta de especial interés el segmento de memoria stack donde para la primera ejecución ha sido:

7fff98de4000-7fff98e05000

Mientras que para la segunda, ha sido:

7fff9a0b2000-7fff9a0d3000

Este comportamiento es debido a que en versiones del Kernel 2.6 el segmento de memoria ya no es estático para cada ejecución de programa, esto es principalmente para dificultar ataques relacionados con el desbordamiento de memoria en base al segmento de memoria virtual compartido por todos los programas en ejecución. Sin embargo es posible cambiarlo con simplemente editar el fichero /proc/sys/kernel/randomize_va_space. Para habilitar la generación aleatoria de segmento, el valor de este fichero debe de ser 1, para desactivarlo debe de ser 0, aunque en algunas distribuciones de GNU/Linux tales como Fedora y Ubuntu, algunas veces el valor por defecto es 2, esto también le indica al Kernel que habilite la generación aleatoria del segmento de memoria.

Si se desactiva esta característica, la ejecución de todos los programas se realizará en el mismo segmento de memoria, por lo tanto el fichero maps será igual siempre. Cabe aclarar que dicho segmento de memoria es compartido por todos los programas en ejecución (no solamente programas escritos en C) sino que cualquier programa que encuentre en ejecución en el sistema operativo, como resulta evidente, es recomendable tener habilitada esta característica.

EJEMPLO DE ORGANIZACION DE MEMORIA VIRTUAL CON UN PROGRAMA SIMPLE ESCRITO EN LENGUAJE C USANDO GDB.

Tomando como punto de partida el listado de código en el punto anterior, podemos utilizar la utilidad gdb, la cual permite depurar la ejecución de un programa y realizar diferentes tipos de actividades tales como generar el código en assembly del programa en ejecución. Se trata de una herramienta sumamente útil para depurar el comportamiento de un programa en lenguajes como C/C++ o Assembly.

Para compilar el ejecutable correspondiente al programa escrito en C, se utiliza el compilador gcc de la siguiente forma:
gcc -ggdb -o Simple SimpleDemoReader.c

La opción -ggdb le indica al compilador que debe generar símbolos validos para que el programa resultante pueda ser depurado. Una vez compilado se puede usar gdb de la siguiente forma:

gdb ./Simple

GNU gdb (GDB) 7.2-ubuntu

Copyright (C) 2010 Free Software Foundation, Inc.

License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html&gt;

This is free software: you are free to change and redistribute it.

There is NO WARRANTY, to the extent permitted by law. Type «show copying»

and «show warranty» for details.

This GDB was configured as «x86_64-linux-gnu».

For bug reporting instructions, please see:

<http://www.gnu.org/software/gdb/bugs/>&#8230;

Reading symbols from /Simples/Simple…done.

(gdb)

Algunos comandos se pueden emplear en este interprete, como por ejemplo list <numero linea> enseña el fichero fuente en la linea seleccionada, run permite ejecutar el programa con los parámetros necesarios, el comando break <numero de linea> permite definir un punto de ruptura para depurar la ejecución del programa y continue para continuar con la ejecución del programa hasta el final, para ir paso a paso por el programa, linea por linea, se hace uso de la instrucción step.

Otra característica interesante es el uso del comando x, este comando permite examinar la memoria en un momento determinado, indicando como primer parámetro el número de instrucciones en memoria junto con su correspondiente formato de visualización y como segundo parámetro la dirección de memoria a examinar. Por ejemplo, asumiendo que en un punto de interrupción examinamos los registers con el comando info registers y el valor para el register de la Stack correspondiente al rsp es:

rsp 0x7fffffffe0b0 0x7fffffffe0b0

Ejecutamos el comando x de la siguiente forma:

(gdb) x/10xb 0x7fffffffe0b0

0x7fffffffe0b0: 0x38 0xe2 0xff 0xff 0xff 0x7f 0x00 0x00

0x7fffffffe0b8: 0xad 0x03

El comando ha recibido el número 10, que es el número de instrucciones a examinar por el comando a partir de la línea de interrupción, la letra x que indica el formato en el cual se enseñarán dichas instrucciones(Hexadecimal) y la letra b que es la unidad de medida, en este caso bytes. Las opciones disponibles se pueden apreciar con el comando help x

(gdb) help x

Examine memory: x/FMT ADDRESS.

ADDRESS is an expression for the memory address to examine.

FMT is a repeat count followed by a format letter and a size letter.

Format letters are o(octal), x(hex), d(decimal), u(unsigned decimal),

t(binary), f(float), a(address), i(instruction), c(char) and s(string).

Size letters are b(byte), h(halfword), w(word), g(giant, 8 bytes).

The specified number of objects of the specified size are printed

according to the format.

Defaults for format and size letters are those previously used.

Default count is 1. Default address is following last thing printed

with this command or «print».

Por otro lado podemos generar el código en assembly haciendo uso del comando disassemble enviando como parámetro el método principal del programa (puede tratarse de cualquier método):

(gdb) disassemble main

Dump of assembler code for function main:

0x000000000040062c <+0>: push %rbp

0x000000000040062d <+1>: mov %rsp,%rbp

0x0000000000400630 <+4>: push %rbx

0x0000000000400631 <+5>: sub $0x98,%rsp

0x0000000000400638 <+12>: mov %edi,-0x94(%rbp)

0x000000000040063e <+18>: mov %rsi,-0xa0(%rbp)

0x0000000000400645 <+25>: mov %fs:0x28,%rax

0x000000000040064e <+34>: mov %rax,-0x18(%rbp)

0x0000000000400652 <+38>: xor %eax,%eax

0x0000000000400654 <+40>: mov -0xa0(%rbp),%rax

0x000000000040065b <+47>: add $0x8,%rax

0x000000000040065f <+51>: mov (%rax),%rax

0x0000000000400662 <+54>: mov %rax,%rdi

0x0000000000400665 <+57>: callq 0x400510 <atoi@plt>

0x000000000040066a <+62>: mov %eax,-0x84(%rbp)

0x0000000000400670 <+68>: mov -0xa0(%rbp),%rax

La salida del comando nos enseña la localización o segmento de memoria utilizado para la ejecución del programa, y consecuentemente la instrucción ejecutada.

Por otro lado para consultar los registers generales usando gdb, se usa el comando info registers, frecuentemente se usa en un punto de interrupción para ver como se encuentran los registers y posteriormente se puede usar el comando continue o step para finalizar con la ejecución o pasar la siguiente punto de interrupción respectivamente.