resueltos - pilas en c pdf
Provoca el desbordamiento de la pila en C (10)
¿Es posible hacerlo de manera confiable en el estándar C? No
¿Es posible hacerlo en al menos un compilador práctico de C sin tener que recurrir al ensamblador en línea? Sí
void * foo(char * a) {
return __builtin_return_address(0);
}
void * bar(void) {
char a[100000];
return foo(a);
}
typedef void (*baz)(void);
int main() {
void * a = bar();
((baz)a)();
}
Constrúyelo en gcc con "-O2 -fomit-frame-pointer -fno-inline"
Básicamente el flujo en este programa es el siguiente
- Barra de llamadas principales.
- la barra asigna un montón de espacio en la pila (gracias a la gran variedad),
- bar llama foo.
- foo toma una copia de la dirección de retorno (utilizando una extensión gcc). Esta dirección apunta al medio de la barra, entre la "asignación" y la "limpieza".
- foo devuelve la dirección a la barra.
- bar limpia su asignación de pila.
- bar devuelve la dirección de retorno capturada por foo a main.
- principal llama a la dirección de retorno, saltando en el medio de la barra.
- el código de limpieza de la pila de la barra se ejecuta, pero la barra actualmente no tiene un marco de pila (porque saltamos en el medio de la misma). Así que el código de limpieza de la pila se desborda de la pila.
Necesitamos -fno-inline para detener el optimizador incorporando cosas y rompiendo nuestra estructura cuidadosamente colocada. También necesitamos que el compilador libere el espacio en la pila mediante el cálculo en lugar del uso de un puntero de marco, -fomit-frame-puntero es el valor predeterminado en la mayoría de las compilaciones de gcc hoy en día, pero no está de más especificarlo explícitamente.
Creo que esta técnica debería funcionar para gcc en casi cualquier arquitectura de CPU.
Me gustaría provocar un desbordamiento de pila en una función de C para probar las medidas de seguridad en mi sistema. Podría hacer esto usando un ensamblador en línea. Pero C sería más portátil. Sin embargo, no puedo pensar en una forma de provocar un subdesbordamiento de la pila usando C, ya que la memoria de la pila se maneja de manera segura por el idioma en ese sentido.
Entonces, ¿hay una manera de provocar un desbordamiento de la pila usando C (sin usar el ensamblador en línea)?
Como se indica en los comentarios: El subdesbordamiento de la pila significa tener el puntero de la pila para apuntar a una dirección debajo del comienzo de la pila ("abajo" para las arquitecturas donde la pila crece de baja a alta).
¿Quieres decir desbordamiento de pila? ¿Poner más cosas en la pila de lo que la pila puede acomodar? Si es así, la recursión es la forma más fácil de lograrlo.
void foo();
{foo();};
Si te refieres a intentar eliminar cosas de una pila vacía, por favor, publica tu pregunta en el sitio web de la pila bajo flujo, ¡y hazme saber dónde encontraste eso! :-)
Así que hay funciones de biblioteca más antiguas en C que no están protegidas. Strcpy es un buen ejemplo de esto. Copia una cadena a otra hasta que llega a un terminador nulo. Una cosa divertida que hacer es pasar un programa que usa esta cadena con el terminador nulo eliminado. Se ejecutará mal hasta que llegue a un terminador nulo en algún lugar. O tener una copia de cadena en sí mismo. Así que volviendo a lo que estaba diciendo antes, C admite los punteros a casi cualquier cosa. Puede hacer un puntero a un elemento en la pila en el último elemento. Luego puede usar el iterador de puntero incorporado en C para disminuir el valor de la dirección, cambiar el valor de la dirección a una ubicación que preceda al último elemento de la pila. Luego pasa ese elemento al pop. Ahora, si está haciendo esto a la pila de procesos del sistema operativo que se volvería muy dependiente de la implementación del compilador y del sistema operativo. En la mayoría de los casos, un puntero a la función principal y un decremento deberían funcionar para desbordar la pila. No he probado esto en C. Solo lo he hecho en lenguaje ensamblador, se debe tener mucho cuidado al trabajar así. La mayoría de los sistemas operativos han logrado detener esto, ya que fue durante mucho tiempo un vector de ataque.
Esta suposición:
C sería más portátil
no es verdad. C no dice nada sobre una pila y cómo la utiliza la implementación. En su plataforma x86
típica, el siguiente código ( horriblemente no válido ) accederá a la pila fuera del marco de pila válido (hasta que sea detenido por el sistema operativo), pero en realidad no "saldrá" de ella:
#include <stdarg.h>
#include <stdio.h>
int underflow(int dummy, ...)
{
va_list ap;
va_start(ap, dummy);
int sum = 0;
for(;;)
{
int x = va_arg(ap, int);
fprintf(stderr, "%d/n", x);
sum += x;
}
return sum;
}
int main(void)
{
return underflow(42);
}
Por lo tanto, dependiendo de a qué se refiere exactamente con "desbordamiento de pila", este código hace lo que quiere en alguna plataforma. Pero desde el punto de vista de C, esto solo expone un comportamiento indefinido , no sugeriría usarlo. No es "portátil" en absoluto.
Hay una buena razón por la que es difícil provocar un desbordamiento de pila en C. La razón es que los estándares que cumplen C no tienen una pila.
Si lees el estándar C11, descubrirás que se trata de ámbitos, pero no de pilas. La razón de esto es que el estándar intenta, en la medida de lo posible, evitar forzar cualquier decisión de diseño en las implementaciones. Es posible que pueda encontrar una manera de provocar el desbordamiento de la pila en C pura para una implementación particular, pero dependerá de un comportamiento indefinido o de extensiones específicas de la implementación y no será portátil.
Hay una manera de desbordar la pila, pero es muy complicado. La única forma en que puedo pensar es definir un puntero al elemento inferior y luego disminuir su valor de dirección. Ie * (ptr) -. Es posible que mis paréntesis estén desactivados, pero desea disminuir el valor del puntero y luego eliminar la referencia del puntero.
En general, el sistema operativo solo va a ver el error y se bloquea. No estoy seguro de lo que estás probando. Espero que esto ayude. C te permite hacer cosas malas, pero trata de cuidar al programador. La mayoría de las formas de evitar esta protección es a través de la manipulación de punteros.
No es posible provocar un desbordamiento de la pila en C. Para provocar un subdesbordamiento, el código generado debería tener más instrucciones de apertura que instrucciones de inserción, y esto significaría que el compilador / intérprete no está en buenas condiciones.
En la década de 1980 hubo implementaciones de C que corrían C por interpretación, no por compilación. Realmente algunos de ellos usaron vectores dinámicos en lugar de la pila provista por la arquitectura.
la memoria de pila es manejada de forma segura por el idioma
La memoria de pila no es manejada por el lenguaje, sino por la implementación. Es posible ejecutar el código C y no usar pila en absoluto.
Ni la norma ISO 9899 ni K&R especifican nada sobre la existencia de una pila en el lenguaje.
Es posible hacer trucos y destruir la pila, pero no funcionará en ninguna implementación, solo en algunas implementaciones. La dirección de retorno se mantiene en la pila y usted tiene permisos de escritura para modificarla, pero esto no es un flujo insuficiente ni portátil.
No puede hacer esto en C, simplemente porque C deja el manejo de la pila a la implementación (compilador). De manera similar, no puede escribir un error en C donde se empuja algo en la pila, pero se olvida de hacerlo, o viceversa.
Por lo tanto, es imposible producir un "desbordamiento de pila" en C pura. No puede saltar de la pila en C, ni puede establecer el puntero de pila desde C. El concepto de pila es algo en un nivel incluso más bajo que el C idioma. Para acceder y controlar directamente el puntero de pila, debe escribir el ensamblador.
Lo que puede hacer en C es escribir deliberadamente fuera de los límites de la pila. Supongamos que sabemos que la pila comienza en 0x1000 y crece hacia arriba. Entonces podemos hacer esto:
volatile uint8_t* const STACK_BEGIN = (volatile uint8_t*)0x1000;
for(volatile uint8_t* p = STACK_BEGIN; p<STACK_BEGIN+n; p++)
{
*p = garbage; // write outside the stack area, at whatever memory comes next
}
Por qué necesitaría probar esto en un programa de C puro que no usa ensamblador, no tengo idea.
En caso de que alguien haya captado incorrectamente la idea de que el código anterior invoca un comportamiento indefinido, esto es lo que realmente dice el estándar C, texto normativo C11 6.5.3.2/4 (énfasis mío):
El operador unario * denota indirección. Si el operando apunta a una función, el resultado es un designador de función; si apunta a un objeto, el resultado es un lvalue que designa el objeto. Si el operando tiene el tipo "puntero a tipo", el resultado tiene el tipo "tipo". Si se ha asignado un valor no válido al puntero, el comportamiento del operador unario * es indefinido 102)
La pregunta es, entonces, cuál es la definición de un "valor inválido", ya que este no es un término formal definido por el estándar. La nota al pie 102 (informativa, no normativa) proporciona algunos ejemplos:
Entre los valores no válidos para la eliminación de referencias de un puntero por el operador unario * se encuentran un puntero nulo, una dirección alineada de manera inadecuada para el tipo de objeto al que se apunta y la dirección de un objeto después del final de su vida útil.
En el ejemplo anterior, claramente no estamos tratando con un puntero nulo, ni con un objeto que haya pasado el final de su vida útil. De hecho, el código puede causar un acceso desalineado, ya sea que este sea un problema o no, lo determina la implementación, no el estándar C.
Y el caso final de "valor inválido" sería una dirección que no es compatible con el sistema específico. Obviamente, esto no es algo que el estándar C menciona, porque los diseños de memoria de sistemas específicos no están cubiertos por el estándar C.
Por definición, un desbordamiento de pila es un tipo de comportamiento indefinido y, por lo tanto, cualquier código que active dicha condición debe ser UB. Por lo tanto, no puede causar de manera confiable un desbordamiento de pila.
Dicho esto, el siguiente abuso de matrices de longitud variable (VLA) provocará un flujo de flujo de pila controlable en muchos entornos (probado con x86, x86-64, ARM y AArch64 con Clang y GCC), configurando el puntero de pila para apuntar por encima de su valor inicial:
#include <stdint.h>
#include <stdio.h>
#include <string.h>
int main(int argc, char **argv) {
uintptr_t size = -((argc+1) * 0x10000);
char oops[size];
strcpy(oops, argv[0]);
printf("oops: %s/n", oops);
}
Esto asigna un VLA con un tamaño "negativo" (muy, muy grande), que envolverá el puntero de la pila y hará que el puntero de la pila se mueva hacia arriba. argc
y argv
se utilizan para evitar que las optimizaciones eliminen la matriz. Suponiendo que la pila crezca hacia abajo (por defecto en las arquitecturas listadas), esto será un desbordamiento de pila.
strcpy
activará una escritura en una dirección con flujo insuficiente cuando se realice la llamada, o cuando se escriba la cadena si strcpy
está en línea. El printf
final no debe ser alcanzable.
Por supuesto, todo esto supone un compilador que no solo hace que el VLA sea un tipo de asignación de pila temporal, que el compilador es completamente gratuito. Debe verificar el ensamblaje generado para verificar que el código anterior haga lo que realmente espera que haga. Por ejemplo, en ARM ( gcc -O
):
8428: e92d4800 push {fp, lr}
842c: e28db004 add fp, sp, #4, 0
8430: e1e00000 mvn r0, r0 ; -argc
8434: e1a0300d mov r3, sp
8438: e0433800 sub r3, r3, r0, lsl #16 ; r3 = sp - (-argc) * 0x10000
843c: e1a0d003 mov sp, r3 ; sp = r3
8440: e1a0000d mov r0, sp
8444: e5911004 ldr r1, [r1]
8448: ebffffc6 bl 8368 <strcpy@plt> ; strcpy(sp, argv[0])
Respecto a las respuestas ya existentes: no creo que sea apropiado hablar de comportamiento indefinido en el contexto de las técnicas de mitigación de la explotación.
Claramente, si una implementación proporciona una mitigación contra los desbordamientos de pila, se proporciona una pila. En la práctica, void foo(void) { char crap[100]; ... }
void foo(void) { char crap[100]; ... }
terminará teniendo la matriz en la pila.
Una nota solicitada por los comentarios a esta respuesta: el comportamiento indefinido es una cosa y, en principio, cualquier código que lo ejerza puede ser compilado para absolutamente cualquier cosa, incluyendo algo que no se parece en nada al código original. Sin embargo, el tema de las técnicas de mitigación de la explotación está estrechamente relacionado con el entorno objetivo y lo que sucede en la práctica . En la práctica, el código siguiente debe "funcionar" bien. Al tratar con este tipo de cosas, siempre debe verificar el ensamblaje generado para estar seguro.
Lo que me lleva a lo que en la práctica dará un desbordamiento (agregado volátil para evitar que el compilador la optimice):
static void
underflow(void)
{
volatile char crap[8];
int i;
for (i = 0; i != -256; i--)
crap[i] = ''A'';
}
int
main(void)
{
underflow();
}
Valgrind informa muy bien el problema.