Dirección de datos invertida en la pila

4

Como recuerdo de mi curso de microcontrolador, la pila está al final de la memoria, mientras que al principio hay algunos punteros de interrupción, código de programa y datos posteriores. Como la pila está al final de la memoria, crece en dirección a direcciones más pequeñas. Y en cada llamada de función se empuja un nuevo marco de pila. Que contiene los siguientes elementos, entre otros, en la siguiente dirección (desde una dirección pequeña a una dirección alta)

local variables
return pointer
parameters

Y, por lo que sé, la mayoría de los desbordamientos de búferes, intenta desbordar el espacio disponible en una variable local para sobrescribir, por ejemplo, el puntero de retorno.

Un pensamiento mío fue que un problema fundamental es que los datos en la pila se tratan de la misma manera, y luego en la memoria. Si un puntero a un búfer apunta a la dirección más alta del búfer, y el último elemento del búfer tiene la dirección más pequeña (por lo tanto, en orden inverso), ¿no sería de ayuda?

Así que mis preguntas son:

  • ¿Es este un pensamiento válido?
  • ¿Ha habido pensamientos similares?
  • ¿Cuáles son sus inconvenientes?
pregunta Angelo.Hannes 17.01.2014 - 17:10
fuente

2 respuestas

2

Hay algunas arquitecturas que usan una pila de "crecer". En general, esto hace que la adaptación de los sistemas operativos sea un poco más difícil; No parece ser un gran beneficio para la seguridad. La convención de hacer que las pilas crezcan viene de la era anterior a la aparición de MMU (sin MMU, RAM es un bloque; la pila crece hacia abajo desde el final del bloque, los elementos de datos se asignan hacia arriba y la memoria se agota cuando se reúnen el montón y la pila).

En el caso de desbordamientos de búfer, sucede, empíricamente , que la mayoría ocurre "en direcciones altas": el búfer tiene un tamaño 1000 pero el código con errores intenta escribir en 1001th, 1002nd. .. ranuras. Esto se relaciona con la forma en que la mayoría de los programadores implementan un bucle: cuando quieren recorrer los 1000 elementos de una matriz, arriba , con un índice de 0 a 999, en lugar de un bucle de 999 a 0 . Para la máquina, ambos son equivalentes (de hecho, en algunas computadoras antiguas de 8 bits, la velocidad de bucle hacia abajo fue ligeramente más rápida); pero los programadores son humanos con hábitos humanos, y esto incluye contar hacia arriba en lugar de hacia abajo, cuando se les da la opción.

Esto importa con la dirección de la pila porque con una pila "hacia abajo", las jugosas ranuras que el atacante quiere reescribir (en particular la dirección de retorno) están en el rango de desbordamientos "altos", que son más comunes que los desbordamientos "bajos" . Sin embargo, los desbordamientos "bajos" (también llamados "subprocesos": el código intenta acceder a las ranuras -1, -2 ...) son infrecuentes, no son desconocidos. Invertir la dirección de la pila simplemente intercambia los roles: ahora, el desbordamiento bajo se vuelve crítico.

También, como lo explica @Polynomial, los desbordamientos bajos también pueden ser interesantes. La dirección de retorno de la función es solo una de las muchas ranuras que el atacante desea sobrescribir.

En mi opinión, el problema principal con un desbordamiento de búfer es que ocurrió: el código hace cosas sin sentido y se permite que continúe sin disminuir. Es como un rinoceronte evadido de un zoológico y arrasando el centro de la ciudad; revertir la pila es como: "movamos toda la ciudad, para que el rinoceronte corra más hacia el campo, donde hará menos daño". Difícilmente es un mecanismo de seguridad "razonable". Sería una mejor idea concentrarse en no dejar que el rinoceronte se suelte.

    
respondido por el Tom Leek 17.01.2014 - 21:07
fuente
1

El principal problema con el crecimiento de la pila es que tiene que sentarse al principio de la memoria y tener un límite de tamaño superior forzado, o sentarse en la parte superior de la memoria y tener una ubicación forzada. Ambas situaciones limitan la utilización máxima de la memoria. Si tanto la pila como el montón crecen uno hacia el otro desde los extremos opuestos, solo te quedas sin memoria cuando literalmente no queda nada. En otras configuraciones, puede quedarse sin pila o montón a pesar de tener memoria libre.

La idea de "apilar hacia atrás" ha sido lanzada por bastante tiempo, y generalmente no se ve como una solución sólida. Para empezar, se encontrará con problemas si está modificando los buffers de pila que se encuentran en marcos anteriores. Inicialmente, esto no parece que sea común, pero considera que asignas buffers para muchas funciones antes que los pasas:

void foo(char* src)
{
    char dest[20];     // local allocated within frame of foo
    strcpy(dest, src); // new stack frame, so return ptr AFTER dest
    // blah blah rest of code...
    if (dest[0] == 'X')
        printf("First char is X\n");
}

Observe que el marco de pila para strcpy se ubicará numéricamente sobre el marco de pila para foo . En un sistema típico, strcpy escribiría más allá del búfer, sobre el puntero de retorno de foo , lo que llevaría al control del puntero de instrucción después de que foo devuelva. Con una pila hacia atrás, strcpy se acumularía, hasta el final del marco de pila de foo , en la parte inferior del marco de pila de strcpy , lo que provocaría una sobrescritura de la dirección de retorno de strcpy , y nuevamente dando el control del puntero de instrucción.

Una solución que muchos consideran bastante segura es utilizar una arquitectura que tenga una pila para locales y parámetros (la pila de datos) y otra para marcos de pila y punteros de retorno (la pila de control). Este aislamiento ayuda a garantizar que los punteros de retorno no se sobrescriban de forma casual cuando se desborda un búfer local. Nuevamente tiene problemas con la administración de memoria, aunque solo una pila debe tener un tamaño máximo fijo, la otra puede crecer en el montón.

Por supuesto, esta arquitectura tampoco es completamente robusta desde el punto de vista de la seguridad. Considera lo siguiente:

int int_sorter( const void *val_a, const void *val_b )
{
    // this code isn't important here
    int first = *(int*)val_a;
    int second = *(int*)val_b;
    if ( first == second )
        return 0;
    else return (first < second) ? -1 : 1;
}

void bar(char* message, int (*sorter)(const void*,const void*))
{
    int array[10];
    char dest[32];
    // do something with array
    // ...
    strcpy(dest, message);
    qsort(array, 10, sizeof( int ), sorter);
}

En este caso, asumiendo un par de pilas clásicas de crecimiento descendente en una arquitectura de doble pila, message se desborda dest en la pila de datos, copiando el puntero de función sorter que se presionó como segundo parámetro codificar%. La llamada a bar regresa normalmente (la pila de control está intacta) pero luego la llamada a strcpy contiene una instrucción para saltar a la función qsort , lo que nuevamente lleva al control sobre el puntero de la instrucción.

Al final del día, no va a encontrar una solución total al problema. Lo mejor que puedes hacer es utilizar buenas prácticas de codificación (o enseñar a tus desarrolladores a hacerlo) y habilitar protecciones como ASLR (a.k.a. PIE), DEP / NX, apilar canarios, etc.

    
respondido por el Polynomial 17.01.2014 - 18:28
fuente

Lea otras preguntas en las etiquetas