Peligros de la pila: el "buffer overflow"
Como hemos visto, los datos en la pila se van situando consecutivamente, y el orden de estos dependerá del convenio de llamadas que estemos utilizando. Independientemente de este órden, lo que es indudable es que corremos el riesgo de sobrescribir un dato si no respetamos bien las fronteras.
Supongamos un código en el que tengamos un vector de números y además, una variable que contiene un número importante (por ejemplo una clave de usuario, un código hash o algo de vital importancia). Es sencillo codificar esto en un Pascal muy básico:
var
i: integer;
importante: integer; { el valor de esta variable es de una importancia vital }
numeros: array[0..4] of integer;
una_variable: integer;
begin
i := 0;
importante := 222;
una_variable := 0;
for i := 0 to 5 do
numeros[i] := 999;
WriteLn(Format('importante vale %d', [importante]));
end;
Para que este código compile correctamente desde Delphi, debemos desactivar todas las optimizaciones de código y las opciones Range checking y Overflow Checking en la ventana Project - Options - Compiler - Runtime errors.
Una vez compilado, ¿qué se muestra por pantalla? No es muy compilado. Sólo asignamos valor una vez a la variable "importante", antes del bucle, por lo que su valor permanecerá constante, y en la pantalla aparecerá "importante vale 222".
Demasiado fácil. El código tiene pequeño bug, aunque no lo hemos apreciado hasta ahora. Para destapar este bug vamos a cambiar símplemente el orden en que declaramos las variables. Nos bastará con situar la variable "importante" por debajo de la variable "una_variable". El código queda así:
var
i: integer;
numeros: array[0..4] of integer;
una_variable: integer;
importante: integer; { el valor de esta variable es de una importancia vital }
begin
i := 0;
importante := 222;
una_variable := 0;
for i := 0 to 5 do
numeros[i] := 999;
WriteLn(Format('importante vale %d', [importante]));
end;
Y ahora... ¿qué se muestra por pantalla? Cualquiera diría que lo mismo. ¿en qué va a influir el orden de las variables al resultado final? Eso sería cierto si no hubieramos cometido el bug que os comentaba. Si nos molestamos en compilar y ejecutar este código, veremos que por pantalla aparece un "importante vale 999". ¡Pero qué está pasando aquí!
Veamos el aspecto de la pila en este último código, después de ejecutar la instrucción "importante := 222":

El esquema contiene tres columnas:
- Dirección: la dirección que ocupa en la memoria el elemento de la pila que estamos representando. Como podemos ver, las direcciones van dando saltos de 4 en 4, ya que cada uno de los elementos de la pila ocupa 4 bytes (una palabra de 32 bits). Así, en la dirección 10 se sitúa el primer elemento, 4 bytes más adelante el segundo, 4 bytes más el tercero, y así sucesivamente.
- Variable: Nombre del símbolo (es decir, la variable) que ocupa esta dirección de memoria.
- Valor: Valor que toma esa dirección de memoria en cada momento.
Como vemos, la variable "i" contiene el valor "0" (establecido en la primera línea de código), la variable "importante" contiene el valor "222", y el resto de variables (incluídos cada uno de los elementos del vector) contienen valores desconocidos o basura.
Conforme vamos ejecutando cada una de las vueltas del bucle, vamos asignando valores a los elementos del vector, desde i=0 hasta i=5:
- i=0: se asigna el número 999 al elemento numeros[0]
- i=1: se asigna el número 999 al elemento numeros[1]
- i=2: se asigna el número 999 al elemento numeros[2]
- i=3: se asigna el número 999 al elemento numeros[3]
- i=4: se asigna el número 999 al elemento numeros[4]
i=5: se asigna el número 999 al elemento numeros[5]. Este elemento en realidad no existe, ya que el vector tiene 5 elementos, desde 0 hasta 4. El compilador de Delphi es capaz de detectar esta situación, pero al haber desactivado la opción de compilación Range Checking estamos evitando este tipo de comprobaciones.
Al producirse este error, lo que ocurre es que se almacena el dato en el supuesto siguiente elemento, es decir, 4 bytes más adelante del último elemento del vector. Como podemos ver, en esa posición está situada la variable "importante", por lo que esta última vuelta del bucle asignará el valor 999 a la variable "importante".
¿Y esto por qué no ocurría con el primer código que escribimos? Pues muy sencillo: porque la situación de las variables en la pila era distinta, ya que las habíamos declarado en un orden distinto. Concretamente, la situación era la siguiente:

En rojo podemos ver que la variable "importante" está situada antes del vector, por lo que el bug, que sólo afecta a la variable que está almacenada detrás del vector, no sobrescribe el valor de la variable "importante", si no el valor que tenga la variable "una_variable" (en nuestro caso irrelevante).
A estas alturas ya tendréis una solución al bug: basta con hacer que el bucle termine una vuelta antes, concretamente en la instrucción for
begin
{ todas las instrucciones anteriores }
for i := 0 to 4 do
numeros[i] := 999;
{ todas las instrucciones posteriores }
end;
Se puede decir que este error es muy común para cualquier programador, aunque sea un experto. El ejemplo que he plateado es muy sencillo de detectar y corregir (de hecho, en situaciones normales el compilador lo detectará), pero cuando utilizamos aritmética de punteros, lo más normal es que alguno se nos vaya de madre, y acabe sobrescribiendo otras variables. Este viejo error es tan común que incluso tiene nombre: el bug del buffer overflow (desbordamiento de buffer).
Uno de los mayores problemas de este bug es que lo suelen utilizar los hackers para forzar a un programa a que haga tareas que no debería. Básicamente se trata del mismo ejemplo que he puesto arriba, pero si nos fijamos con cuidado, podremos ver como más abajo de las variables "importante" y "una_variable" se sitúa la dirección de retorno. Esta dirección la utiliza el procesador para saber a qué punto del programa debe saltar una vez que ha terminado de ejecutar la función. En nuestro caso, saltará a la instrucción situada en la dirección de memoria 358.
La técnica consiste en detectar un bug de desbordamiento de buffer que contenga el programa a hackear, y aprovecharlo para sobrescribir la dirección de retorno, estableciendo un valor que corresponde con la dirección donde el hacker a situado un código propio.
En nuestro caso tan sólo sobrescribíamos 4 bytes por detrás del vector, pero podríamos haber sobrescrito, por ejemplo, 8 bytes: los 4 primeros corresponderían a la variable que se sitúa por detrás del vector, y los 4 siguientes corresponderían a la dirección de retorno. Si en la posición de la pila correspondiente a la dirección de retorno, situamos una dirección válida (nos vale con una dirección que pertenezca al segmento de código del proceso), habremos conseguido nuestro propósito.
Un ejemplo: el pirata "Patapalo" ha escrito un código "maligno" y sabe (gracias a un disassambler) que este código comienza en la dirección de memoria 672.
La juguada está en conseguir que el programa sobrescriba la posición de memoria donde reside la dirección de retorno, sobrescribiendo el antiguo 358 por el nuevo 672.
Para conseguir esto, los hackers aprovechan puntos de entrada de datos al programa (lectura de ficheros, campos donde el usuario teclea un dato, etc.).
Si en alguno de estos puntos, el programa contiene un bug de desbordamiento de buffer, podemos introducir más datos de los que el programa espera, por lo que estaremos desbordando alguno de sus buffers internos. Si esta situación la controlamos, podemos sobrescribir hasta donde queramos, exactamente hasta la dirección de retorno, estableciendo el valor que queramos.
Una vez hecho esto, el procesador (ajeno a todo lo que ha pasado), terminará la ejecución y hará que se salte a la dirección 672, donde comenzará a ejecutarse el código maligno (por ejemplo enviar por mail el valor de una variable, almacenarlo en un archivo, cambiar valores a variables globales, etc.).
Cuando termine de ejecutarse este código maligno, el hacker habrá puesto cuidado en hacer que la ejecución continúe donde tendría que haberlo hecho en un principio: en la dirección de memoria 358.
En la siguiente imágen puede verse cómo quedaría la ejecución antes (a la izquierda) y después de la modificación del hacker (a la derecha). La caja roja representa el código que ha escrito el hacker, y la línea en rojo representa la línea que ha modificado el hacker.