Más de uno habrá oído hablar del desbordamiento de pila (buffer overflow en inglés), un error a la hora de desarrollar software que puede ser aprovechado para que un programa realice funciones u operaciones para las cuales no fue programado. Este tipo de vulnerabilidad es esencial para llevar a cabo las pruebas de penetración.

Antes de hacer el salto cuántico y empezar a desbordar pilas a diestra y siniestra, es necesario entender conceptos que quizá no se relacionen a primera vista con el tema de seguridad, sino con cuestiones de programación y de cómo la computadora gestiona la memoria. Ya que se haya entendido esto, se podrá comprender cómo es que se produce un desbordamiento de pila, qué lo ocasiona y cómo es que se toma ventaja de él. Claro, siempre es factible saltar conceptos y obviarlos, tomarlos como si fueran dogmas de fe para simplemente realizar pruebas de seguridad corriendo herramientas automatizadas y esperar que funcionen. Sin embargo, resulta importante, en nuestro esfuerzo de difusión, expandir el panorama.

¿Qué pasa cuando las herramientas no funcionan?, ¿solo levantamos los hombros y decimos “no funcionó”? ¡No! Lo que debemos hacer es  verlo de esta manera: sabemos que en la computadora hay una aplicación que es vulnerable, pero que por alguna razón la herramienta que tiene el script que explota la vulnerabilidad no funcionó. Tenemos en nuestro poder la información necesaria que puede ayudar (tipo de sistema operativo, la aplicación, el puerto a la escucha, etcétera);  tomemos todo esto, un poco de procesamiento de nuestro cerebro y desarrollemos el script que permita tomar ventaja de la vulnerabilidad que, al final de cuentas, sabemos existe y que simplemente debemos pensar y hallar la manera de explotar.

Para llegar a ese punto primero se requiere entender los conceptos implicados y no solo saber cómo correr una herramienta. Es eso, y no otra cosa, lo que separa a un hacker de un simple mortal que sabe como teclear comandos.

Variables, el poder de variar

Cuando se elabora un programa puede surgir la necesidad de esperar por datos que sean proporcionados desde una fuente externa, para lograrlo se hace uso de algo que en el argot de programación se conoce como “variable”. Una variable de programación, al igual que una variable en una función matemática, puede tomar cualquier valor. Tomemos justamente el ejemplo matemático para entenderlo y pensemos en la función: y = x + 5.

Supongamos ahora que deseamos hacer un programa que nos diga cuánto vale “y” una vez que determinemos el valor de “x”. Dicho programa diría algo como: “Hola, tengo un severo problema y necesito de tu ayuda. Debo saber el valor de “y” que resulta de la función “x + 5” pero yo no sé cuánto vale “x” ¿Podrías indicarlo?”

Así pues, sabemos que un programa puede recibir información de una fuente ajena al programa (el usuario, en el caso de nuestro ejemplo) y que esa información será almacenada en variables, pero, ¿cómo se hace esto? Sencillo, el programa, al ser ejecutado toma espacio de memoria, de todo ese espacio que tomó deja una parte pequeña de memoria para los valores que desconoce (las variables) y que está a la espera de que alguna fuente externa se los proporcione.

 .

Arreglos

Dentro de la programación existen varios tipos de variables. Están las que llegan a almacenar valores llamados enteros, las que llegan a almacenar valores flotantes o las que almacenan solo un carácter alfanumérico, entre otras. Pero hay un tipo de variable a la que le debemos prestar especial atención: los arreglos.

 ¿Qué es un arreglo? Imaginemos un largo estante, tan largo como lo deseemos, que tiene cajones y dentro de cada uno podemos guardar lo que sea, lo que queramos, y a ese estante le asignamos un nombre con el cual lo identificamos fácil y rápidamente. Un arreglo es algo similar, es un gran estante que llevará un nombre y que usaremos para guardar valores ya sea de tipo entero, flotante, caracteres, etcétera.  Cuando necesitemos un valor en específico solo bastará ir al “cajón” indicado del estante y tomarlo.

La manera en que un programa maneja un arreglo que no ha sido inicializado (es decir, que no tiene valores) es similar a la manera que maneja las variables, les deja un espacio de memoria, pero esta vez la deja de manera continua: si el arreglo es de cinco elementos dejará el espacio para cinco variables, si es de diez, dejará diez espacios, de acuerdo a lo que haya sido definido por el programador.

El error que se comete al momento de programar

Imaginemos que es necesario hacer una aplicación que guarde cinco números proporcionados de manera externa. Existen dos opciones, una es declarar cinco variables o hacer uso de un arreglo que conste de cinco elementos. Después de una no tan difícil deliberación decidimos usar el arreglo y corremos la aplicación para probarla. Llegamos al punto en donde el programa solicita introducir los cinco datos y empezamos a proporcionar uno por uno hasta llegar al quinto dato, llegado este punto lo normal sería que ya no se pudieran introducir más datos, sin embargo el programa permite un sexto dato, es decir, meter más datos de los que debería guardar. Es justo ahí donde hemos cometido un error al momento de desarrollar el programa, que más que un error es un descuido, ya que no se tomaron las debidas precauciones para evitar introducir más datos de los que el programa puede manejar de acuerdo con su diseño y es esto justamente la esencia de la vulnerabilidad llamada desbordamiento de buffer.

Cuando un programa nos permite introducir más datos de los que espera y puede manejar, sucede que existe la posibilidad de escribir en zonas de memoria donde se ubica información que ayuda al control y flujo del programa. Ahora entonces, la pregunta lógica es ¿Qué es y cómo funciona esa zona de memoria que controla la ejecución del programa?

El stack

La cosa es muy simple, un programa que es inicializado necesita tomar un espacio de memoria que usará para, llegado el momento, poner en ese espacio el valor de las variables que no fueron declaradas desde el código del programa y que será conocido una vez que el programa esté corriendo. Este espacio de memoria se conoce con el nombre de stack, o “pila” si se prefiere la traducción al español.

¿Y cómo funciona el stack?, para ilustrar este concepto hay un sinfín de objetos con los cuales se pude ejemplificar, pero tomemos uno de mis favoritos, los libros. Imaginemos un libro sobre una mesa, y sobre ese libro ponemos otro, y sobre ese libro otro más, y así hasta que tenemos una columna de cincuenta libros. Si eventualmente necesitamos tomar el primero, aquel que está justo entre la mesa y los cuarenta y nueve restantes,  no podremos hacerlo sin antes quitar los que están encima de este. De hecho, no podemos tomar el libro cuarenta y nueve sin antes haberle quitado de encima el libro cincuenta. Entonces, para llegar al de hasta abajo tengo que retirar el libro cincuenta, luego el cuarenta y nueve, luego el cuarenta y ocho, y así hasta llegar al primero.

El stack es algo muy similar, es una gran columna (virtual) de memoria donde se almacenará información, ya sea de control o de variables, y, a diferencia del ejemplo de los libros, no es que exista la posición uno, la dos o la tres; lo que existe es una dirección de memoria para cada elemento.

En el ejemplo se señaló que si quería llegar al primer libro, deberían quitarse uno por uno los cuarenta y nueve libros que estaban arriba. Este concepto de ir quitando uno por uno empezando desde el último tiene un nombre y se llama LIFO (last in, first out), que quiere decir que el último que entra será el primero  en salir.

El stack es una sección de memoria que suele utilizar el concepto LIFO (aunque no todos los stack operan así) para introducir datos que pueden ser de control y flujo del programa o el valor de variables que no fueron declaradas en el código del programa. Ahora, ¿cómo sabe la computadora en qué sección de memoria debe introducir información?  La respuesta tiene un acrónimo y es SP.

SP es el acrónimo para Stack Pointer y su función, como lo dice su nombre, es apuntar a una dirección de memoria de la pila, de hecho la del último elemento. Es importante entender que el SP siempre se encontrará apuntando al último elemento, porque justamente así es como se determinan dos operaciones que se utilizan para manipular datos en la pila: push para introducirle datos y pop para sacar.

Por ahora dejemos el asunto, en la siguiente entrega veremos cómo operan push/pop, cómo se construye una función en el stack una vez que un programa ha sido ejecutado, y qué sucede cuando se hace un desbordamiento de pila.

Continuará…

[email protected]