lunes, 4 de junio de 2007

Buffer Overflow: hackeando la pila (I)

Eliux [aorozco@infomed.sld.cu]

Stack Overflow, Buffer Overflow o "romper la pila" es una de las técnicas más usadas por los hackers profesionales en la fabricación de spam, bugs, odays, exploits, intrusiones a sistemas, etc. Un código, cuando rompe la pila, puede saltar a una zona cualquiera de memoria y/o ejecutar un código maligno que produzca un efecto indeseado en la máquina y viole la seguridad del sistema. Es una de las técnicas -sino la más poderosa- que tiene un hacker contemporáneo…

Notas del autor:
En los tiempos en los que incursioné en el Underworld como un newbie, me prometí que sería un hacker y que haría esas cosas que suelen hacer los hackers en las películas y uno no tiene ni idea de cómo lo hacen. Y la verdad es que hackers como Kevin Mitnick no llegaron a ser grandes descargando programas de una página Web, sino estudiando y programando sus propios códigos, usando sus propias técnicas. Si fuéramos a hablar de técnicas como de poderes de un mago, hoy les daré un indicio de cómo funciona mi poder favorito de mis tiempos de aprendiz de brujo, les hablo del Buffer Overflow.

Para entender bien este texto es necesario que conozcan algo de C y ensamblador, así como del funcionamiento interno de la pila. Durante el transcurso del texto seré tan breve como práctico. Este escrito es sólo una introducción al Buffer Overflow, no constituye un soporte clave para el domino del mismo. Este artículo fue creado con fines educativos; el autor no se hacer responsable del mal uso del contenido del mismo.

Introducción:

Si buscamos en Internet información sobre los Buffer Overflow, encontraremos un número elevado de vulnerabilidades que lo han usado. Ejemplo de ello son: syslog, sendmail, y los exploits.

El lector debe saber que el buffer no es más que un bloque contiguo de memoria de computadora que mantiene varias instancias del mismo tipo de datos, también conocido en el mundo de la programación como listas secuenciales, matrices o arrays. Los arrays, como todas las variables, pueden ser declarados tanto estáticos como dinámicos. Las variables estáticas son asignadas a la pila en tiempo de ejecución.

Overflow es inundar, machacar o sobrepasar el límite de algo. Por lo que Buffer Overflow se puede interpretar como el desborde o la superposición de códigos/datos en memoria, que se produce cuando se llena un segmento continuo de memoria con más elementos del requerido, provocando así la sobrescritura de códigos/datos ajenos a los permitidos en el proceso.

En este escrito nos centraremos más en el Stack Overflow o desbordamiento de pila, que de seguro es un término bastante conocido u oído por ustedes; pero lo que no saben es que éste puede producir más efectos colaterales que el sólo hecho de sobrescribir código.

Hagamos un análisis simple y general de cómo funciona la memoria en la PC.

Organización de la memoria:

Para entender cómo funcionan los Stack Overflow deberemos entender primero cómo es organizado un proceso en memoria. Los procesos son divididos en tres regiones: TEXT, DATA, y STACK. Nos centraremos en región de STACK o más conocida como pila. Pero no seamos perezosos, primero veamos una pequeña vista de las otras regiones.

TEXT:
La región TEXT es ajustada por el programa e incluye código (instrucciones). Esta región corresponde a la sección TEXT del archivo ejecutable. Cualquier intento de escribir en ella resulta una violación de segmentación, ya que sus datos son declarados sólo-lectura; algo así como protegidos por el sistema.

DATA:
La región contiene datos inicializados y sin inicializar. Las variables estáticas (static: su valor perdura durante la ejecución de todo el programa) son guardadas aquí. La región DATA corresponde a la sección data-bss de los ejecutables. Su tamaño es agrandable con la instrucción de código C: system call brk(2) y, si el crecimiento de éste o del STACK agota la memoria disponible, el proceso es repaginado para ejecutarse otra vez, pero con un espacio de memoria mayor (como se muestra en la figura). Quiere decir que esta vez hay más memoria entre los segmentos DATA y STACK para que éstos puedan seguir creciendo (El segmento DATA crece hacia abajo y el STACK hacia arriba).

STACK:
Una pila o STACK no es más que una array que tiene como propiedad principal que sus elementos se insertan del modo LIFO, que significa que el primer elemento que entra será el último en salir. Es algo que desde el punto de vista lógico usamos diariamente para colocar elementos uno encima del otro y llamamos pila.

¿Para qué se usan las pilas en los programas?

La técnica más empleada por los lenguajes de alto nivel de la actualidad es la función o procedimiento. Desde el punto de vista interno de un programa, una llamada a una función (procedure call), altera el flujo de control tal como un jump (salto) lo hace; pero a diferencia de éste, la función retorna el control a la posición siguiente desde donde fue llamado, conservando los valores de los registros de la máquina (AX, BX, ZF, etc.)

Este cambio sólo es perceptible cuando se trabaja a bajo nivel -o sea, en ensamblador-, donde cuando se hace un salto a una función primero se hace PUSH a los registros y estos se guardan en la pila y se recobran cuando se sale del procedimiento, y éste antes de salir hace POP. Además de registros en la programación de alto nivel, el STACK es usado en tiempo de ejecución para pasar parámetros a las funciones y devolver valores.

La región STACK:

Como ya dijimos, un STACK es, entre otras cosas, un bloque contiguo de memoria conteniendo datos. Existe un registro llamado STACK POINTER (puntero de pila) cuyo nombre clave es SP, que apunta al tope del STACK. El fondo de la pila está en una dirección ajustada a su tamaño, es ajustado dinámicamente por el Sistema Operativo en tiempo de ejecución, cuando la CPU implementa instrucciones para hacer PUSH (introducir) y POP (sacar) al STACK.

El STACK consiste en frames lógicos de STACK (cuadros), que son "pusheados" cuando se llama a una función y "popeados" cuando se sale de ésta. Un STACK FRAME contiene los parámetros para una función, sus variables locales y los datos necesarios para recuperar el STACK FRAME previo, incluyendo el valor del puntero de instrucción (IP: apunta a la zona de memoria donde está la instrucción a ejecutar por el microprocesador) que permite ubicarnos en la posición siguiente a la llamada (IP+1).

Dependiendo de la implementación, el STACK podrá decrecer (detrás de las direcciones más altas de memoria) o crecer. En nuestros ejemplos usaremos un STACK que decrece, ya que suponemos que estamos trabajando con un procesador Intel Pentium y así es cómo procede la pila en este, así como en otros como SPARC, MIPS y MOTOROLA.

El STACK POINTER (SP) puede apuntar a cualquier zona de la pila, e incluso a la siguiente dirección libre después de ésta. Durante el texto tomaremos como punto de referencia un SP que apunta a la última dirección del STACK (donde está el inicio de la pila).

Además del SP, necesitaremos un FRAME POINTER (FP), o puntero de cuadro, que como su nombre lo indica, permite a partir de él, acceder a un frame que contenga un elemento "pusheado" (parámetros y variables locales de la función), mediante un desplazamiento. Es por ello que es llamado en algunas referencias bibliográficas como LOCAL BASE POINTER (LB) o puntero base local. Su importancia radica en que  las variables locales pueden ser referenciadas mediante sus offsets, ya que sus distancias a FP no cambian con POPs y PUSHs. Muchos de ustedes que conoce del lenguaje de ensamblador de los procesadores Intel x86 pensarán: ¿dónde está ese registro FP, que yo no lo conozco? Pues no lo busquen, puesto que aquí FP es un registro que usaremos para hacer referencia a su funcionamiento; podríamos llamarlo un registro lógico, por lo que para almacenar su valor, entre otras cosas, usaremos otro registro que sí existe y es muy usado en el mundo del ensamblador, un registro auxiliar  para referenciar a datos en memoria, como son variables, arreglos y parámetros, muy similar a un puntero común de C. Me refiero al BASE POINTER (BP). Este puntero se usará principalmente para acceder a las variables locales y procedimientos porque, a diferencia del SP, sus offsets no varían con respecto a él con PUSHs y POPs.

La primera cosa que hace la máquina cuando se  llama a un procedimiento es guardar el FP (muchas veces como en los ejemplos que usaremos usan el BP) previo, ¡piensa por qué! Pues para ser restaurado a la salida del procedimiento. Luego copia el actual SP hacia el BP (haciéndolo el nuevo puntero FP) para apuntar a las variables locales de la función, detrás de el quedaría el SP guardado y los parámetros de la función. Luego se asigna espacio para las variables locales restando a SP el tamaño total de las variables. A la salida del procedimiento, el STACK debe poder recuperar su FP guardado y las variables locales de la función ya no estarán en él.

Muchos de estos registros, en vez de registros, los verán escritos con una e delante (ej: ebp, esp, etc.) porque sin la e se refieren más bien a las versiones de 16 bits de los procesadores, y la e se refiere a registros de 32 bits.



Artículos relacionados


No hay comentarios: