Los desbordamientos de búfer se producen debido a un error de programación: debido a circunstancias imprevistas por parte del desarrollador, pero activadas por el atacante, el código está escribiendo datos más allá del final del búfer. Que fin Bueno, hay dos : hacia direcciones altas y hacia direcciones bajas.
La forma tradicional de escribir un bucle es así:
for (i = 0; i < n; i ++)
y no como este:
for (i = n - 1; i >= 0; i --)
lo que significa que los desarrolladores tienden a aplicar algoritmos en el orden de bajo a alto. No hay una razón matemática para eso, pero los desarrolladores están entrenados para pensar así; y la evolución del hardware ha seguido (las estrategias de búsqueda previa en los chips RAM también se basan en este patrón). Los sospechosos habituales para la manipulación de cadenas ( strcat()
, sprintf()
...) también se desbordan en el extremo superior por el mismo motivo: operan de bajo a alto.
Esto implica que la mayoría de los desbordamientos de búfer también se producen en el extremo superior del búfer, no en el extremo inferior. En una pila que crece hacia abajo, esto permite que estos desbordamientos se dirijan a la dirección de retorno de la función actual: lo mejor que un explotador de desbordamiento de búfer puede esperar es redireccionar la ejecución al código elegido por el atacante, y para eso el desbordamiento sobrescribirá una ranura de memoria que contiene un puntero a código, que el código seguirá en algún punto posterior. La ranura de "dirección de retorno" en la pila es conveniente para eso. Pero ese no es el único puntero al código.
En particular, cuando se usa C ++, cualquier objeto con métodos virtuales debe contener algún tipo de referencia a una estructura que contenga punteros a estos métodos (la vtable ). Esto proporciona muchos objetivos adicionales que pueden estar dentro del rango de un desbordamiento de búfer. Los objetos de C ++ se pueden asignar a la pila.
Otro punto es que cuando se desborda un búfer, podría ubicarse en otro marco de pila. Por ejemplo, esto:
void foo(char *s)
{
char buf[10];
bar(buf, "x", s);
}
void bar(char *dst, char *s1, char *s2)
{
sprintf(dst, "%s/%s", s1, s2);
}
En este ejemplo, si el atacante puede poner una cadena sobredimensionada como parámetro en foo()
, el búfer asignado en el marco de pila para foo()
se desbordará en bar()
. Si la pila crece hacia arriba, la dirección de retorno de foo()
no se dañará, pero la de bar()
se sobrescribirá.
Para resumir:
- Una pila ascendente significa que la dirección de retorno de la función que asignó el búfer normalmente no se dañará.
- Pero otras ranuras de direcciones de retorno de funciones para otras funciones pueden estar dentro del rango (y en realidad están más dentro del rango que si la pila hubiera crecido hacia abajo).
- Y aunque son más raros, los desbordamientos de búferes en el extremo inferior (a veces llamados "subflujos") también ocurren en la práctica.
- Y la ranura de la dirección de retorno es solo uno de los objetivos jugosos para un atacante.
Por lo tanto, no podemos pensar realmente en las pilas que crecen hacia arriba como una protección contra la explotación de desbordamientos de búfer. Cambia las condiciones de ataque, pero no necesariamente hacia terrenos más seguros. La experiencia muestra que los desbordamientos de búfer se han explotado con éxito en arquitecturas con pilas en crecimiento ascendente.
(Nota: el CPU del PA-RISC, por sí mismo, no conoce pila. La pila es parte de la convención de llamada , que dice que GR 30 es ser utilizado convencionalmente como puntero de pila, y que la pila crece hacia arriba. Esto es cómo HP Lo definí , pero cualquier sistema operativo podría decidir lo contrario. Es una tendencia genérica para la mayoría de las CPU RISC: el hardware no conoce la pila, por lo que la posición y la dirección de la pila son una cuestión de convención entre el sistema operativo y las aplicaciones.)