Implicaciones de seguridad de descuidar el byte extra para la terminación NULL en matrices C / C ++

20

Por favor, considere : el inglés es mi segundo idioma.

En el podcast ¡Seguridad ahora! episodio 518 ( HORNET: ¿Una solución para TOR? ), a las 27:51 marca Steve Gibson cita un ejemplo de código vulnerable en C / C ++:

  

"[...] uno de ellos [problemas con el código vulnerable] está creando una nueva matriz   de un tamaño determinado [...]. Y la solución es 'de un cierto tamaño + 1'. Asi que,   [...] [el código vulnerable] era solo un byte demasiado corto. Probablemente un   Terminador NULL, para que cuando llene la matriz con objetos de tamaño,   tendría un byte extra de NULL que garantizaría NULL   terminación, y que evitaría que esa cadena se supere . Pero   eso no es lo que hizo el programador: olvidaron el ' + 1 ' [...] "

Entiendo lo que quiere decir: cuando creas una matriz, necesitas permitir un byte extra para el byte de terminación NULL. Lo que me gustaría lograr con esta publicación es obtener un puntero para una mayor investigación sobre el impacto de tener una matriz cuyo último byte no es el terminador de bytes; No entiendo todas las implicaciones de tal negligencia, y cómo esto podría llevar a una explotación. Cuando dice que tiene la terminación NULA

  

"evitaría que esa cadena fuera invadida",

mi pregunta es "¿cómo se sobrepasa en los casos en que se descuida el carácter de terminación NULL?".

Entiendo que este es un tema enorme y, por lo tanto, no imponer a la comunidad una respuesta demasiado amplia. Pero si alguien pudiera tener la amabilidad de proporcionar algunos consejos para una lectura más profunda, estaría muy agradecido y encantado de ir y hacer la investigación yo mismo.

    
pregunta RoraΖ 29.07.2015 - 12:54
fuente

4 respuestas

21

Vulnerabilidad de terminación de cadena

Al pensar más en esto, usar strncpy() es probablemente la forma más común (en la que puedo pensar) que podría crear errores de terminación nulos. Ya que generalmente la gente piensa que la longitud del búfer no incluye strncpy(a, "0123456789abcdef", sizeof(a)); . Así que verás algo como lo siguiente:

a

Suponiendo que char a[16] se inicializa con a , la cadena strlen(a) no terminará en nulo. Entonces, ¿por qué es esto un problema? Bueno, en memoria ahora tienes algo como:

30 31 32 33 34 35 36 37 38 39 61 62 63 64 65 66 
e0 f3 3f 5a 9f 1c ff 94 49 8a 9e f5 3a 5b 64 8e

Sin un terminador nulo, las funciones de cadena estándar no sabrán la longitud del búfer. Por ejemplo, 0x00 continuará contando hasta que alcance un string.h byte. ¿Cuándo es eso, quién sabe? Pero siempre que lo encuentre, devolverá una longitud mucho mayor que su búfer; digamos 78. Veamos un ejemplo:

int main(int argc, char **argv) {
    char a[16];

    strncpy(a, "0123456789abcdef", sizeof(a));

    ... lots of code passes, functions are called...
    ... we finally come back to array a ...

    do_something_with_a(a);
}

void do_something_with_a(char *a) {
    int a_len = 0;
    char new_array[16];

    // Don't know what the length of the 'a' string is, but it's a string so lets use strlen()!
    a_len = strlen(a);

    // Gonna munge the 'a' string, so lets copy it first into new_array
    strncpy(new_array, a, a_len);
}

Acaba de escribir 78 bytes en una variable que solo tiene 16 bytes asignados.

Desbordamientos de búfer

Se produce un desbordamiento de búfer cuando se escriben más datos en un búfer que los asignados para ese búfer. Esto no es diferente para una cadena, excepto que muchas de las funciones a_len se basan en este byte nulo para señalar el final de una cadena. Como vimos arriba.

En el ejemplo, escribimos 78 bytes en un búfer que solo está asignado para 16. No solo eso, sino que es una variable local. Lo que significa que el búfer ha sido asignado en la pila. Ahora, los últimos 66 bytes que se escribieron, simplemente sobrescribieron 66 bytes de la pila.

Si escribe suficientes datos más allá del final de ese búfer, sobrescribirá la otra variable local do_something_with_a() (también no es bueno si la usa más adelante), cualquier puntero de marco de pila que se guardó en la pila, y luego Dirección de retorno de la función. Ahora realmente has ido y arruinado las cosas. Porque ahora la dirección de retorno es algo completamente equivocado. Cuando se llega al final de %code% , suceden cosas malas.

Ahora podemos agregar un poco más al ejemplo anterior.

void do_something_with_a(char *a, char *new_a) {
    int a_len = 0;
    char new_array[16];

    // Don't know what the length of the 'a' string is, but it's a string so
    // lets use strlen()!
    a_len = strlen(a);

    // 
    // By the way, copying anything based on a length that's not what you
    // initialized the array with is horrible horrible coding.  But it's
    // just an example.
    //
    // Gonna munge the 'a' string, so lets copy it first into new_array
    strncpy(new_array, a, a_len);

    // 'a_len' was on the stack, that we just blew away by writing 66 extra 
    // bytes to the 'new_array' buffer.  So now the first 4 bytes after 16
    // has now been written into a_len.  This can still be interpreted as
    // a signed int.  So if you use the example memory, a_len is now 0xe0f33f5a
    //
    // ... did some more munging ...
    //
    // Now I want to return the new munged string in the *new_a variable
    strncpy(new_a, new_array, a_len);

    // Everything burns

}

Creo que mis comentarios explican todo bastante. Pero al final, ahora has escrito una gran cantidad de datos en una matriz, probablemente pensando que solo estás escribiendo 16 bytes. Dependiendo de cómo se manifieste esta vulnerabilidad, esto podría conducir a la explotación a través de la ejecución remota de código.

Este es un ejemplo muy artificial de codificación deficiente, pero puede ver cómo las cosas pueden escalar rápidamente si no tiene cuidado al trabajar con la memoria y al copiar datos. La mayoría de las veces la vulnerabilidad no será tan obvia. Con los programas grandes, tiene tanto que hacer que la vulnerabilidad no sea fácil de detectar y se desencadene con el código de múltiples llamadas a funciones.

Para obtener más información sobre cómo funcionan los desbordamientos de búfer .

Y antes de que alguien lo mencione, ignoré la endianess al hacer referencia a la memoria por simplicidad

Lecturas adicionales

Descripción completa de la vulnerabilidad
Entrada de Enumeración de debilidad común (CWE)
Presentación de cadenas de codificación seguras (el PDF se descarga automáticamente)
University of Pittsburgh - Codificación segura C / C ++: Vulnerabilidades de cadenas (PDF)

    
respondido por el RoraΖ 29.07.2015 - 14:04
fuente
2

Estoy en riesgo de ser redundante agregando otra respuesta, pero creo que las respuestas existentes podrían no abordar completamente lo que estás preguntando. En una vulnerabilidad tradicional de desbordamiento de búfer (específicamente de la variedad basada en la pila), uno intenta sobrescribir el puntero del marco en la pila para hacer que la ejecución salte al código de explotación cuando la función actual intenta volver.

Obviamente, eso no va a funcionar si lo único que (el atacante) puede hacer que el programa escriba más allá del final del búfer es un byte cero. Potencialmente, puedes hacer que el programa se bloquee de esta manera haciendo que intente saltar a una dirección no válida, pero eso es solo un DoS y no una ejecución remota de código.

Sin embargo, considere que obtiene el programa para escribir una cadena de longitud 16 en un búfer de 16 bytes que llamaremos "A", de modo que el byte nulo se sobrepase. Luego, hace que el programa sobrescriba ese byte nulo con algo que no es \ 0, por lo que ahora la cadena A no termina en nulo. Si luego obtiene el programa para enviarle los contenidos de A, se leerá más allá del final de A, lo que posiblemente le dará acceso a todo tipo de información secreta. Heartbleed usó este tipo de divulgación de información para robar claves privadas, lo cual es bastante serio.

En este punto, la cadena A es en realidad más larga de lo que esperaba el programador. No es tan difícil imaginar que el programador confíe en que A sea una cadena de 16 bytes y la copie en otro lugar, lo que podría desbordar otros buffers en mucho más de un byte. Esto podría ser usado para ejecutar código arbitrario.

    
respondido por el Lexelby 06.08.2015 - 16:33
fuente
1

Como indicó en su respuesta, los desbordamientos de búfer son una vulnerabilidad probable si un programador no termina una cadena de caracteres con un byte NULO. La razón es que la mayoría de las funciones de cadena asumen esto y continuarán hasta que encuentren un cero. Si tiene suerte, el error es lo suficientemente malo como para que obtenga un error de falla de segmentación al principio del desarrollo, de modo que pueda depurar y corregir el problema. Sin embargo, con muchos errores, un error tan obvio solo ocurrirá en condiciones especiales. A menudo, un atacante puede aprovechar la forma en que se comporta el programa y, dependiendo de los detalles de la vulnerabilidad, explotarlo para leer el contenido de la memoria que debería estar oculto, o copiar los datos de las entradas del usuario en áreas de la memoria que el usuario no tenía previsto para controlar, etc. Si el búfer está ubicado en la pila, este último exploit puede usarse para inyectar código, y para sobrescribir la dirección de retorno almacenada en el marco de pila ubicado en una dirección más alta (en x86) en la pila. Algunos sistemas operativos tienen protecciones, como segmentos de memoria no ejecutables, pero esto es definitivamente algo en lo que el programador no quiere confiar :)

Los programadores que son nuevos en la programación en lenguajes como C pueden encontrar que las prácticas para evitar estos problemas son difíciles o propensas a errores, pero eventualmente se convierte en una segunda naturaleza, aunque todavía es posible cometer un error. Solo practico tanto como sea posible, he estado programando en C durante aproximadamente 7 años y todavía necesito corregir un error de vez en cuando.

Una buena forma de practicar es asignar una matriz de caracteres, ya que está configurada con un carácter ASCII no imprimible que no sea 0, 1 está bien. Use la función de biblioteca estándar de su elección para copiar alguna cadena en la matriz, obviamente si falla el programa es incorrecto. De lo contrario, simplemente use un ciclo básico para iterar sobre cada elemento de la matriz e imprima el valor numérico, verifique que el 0 esté donde debería estar, si usa printf, se detendrá en ese punto de la cadena. Me parece que esta es una buena manera de experimentar para encontrar las diferencias entre las funciones, p. Ej. strcat, strncat, strcpy, strlcpy, strlcpy, strlcat, sprint, etc. Recomendaría usar strlcpy y strlcat sobre strncpy y strncat para la mayoría de las cosas, son mucho más fáciles y menos propensas a errores.

Otro consejo, si parece difícil validar tu algoritmo mentalmente, imagina que estás haciendo lo mismo en una entrada extremadamente pequeña. Para cadenas, imagine que está realizando la operación en una cadena con solo espacio para 1 carácter más el byte NULO. Esto hace que sea fácil ver muchas propiedades de las cadenas de caracteres que de otro modo requieren más trabajo cerebral. Por ejemplo, debe asignar una matriz con 2 elementos, aunque necesita almacenar un solo carácter. El segundo elemento, str [1], obviamente necesita ser 0. strlen informará que la longitud de la cadena es 1. Ahora puede generalizar cómodamente para saber que strlen (str) es siempre el índice del byte NULL (suponiendo que NULL termina, por supuesto :), igualmente strlen (str) - 1 donde > 0 es siempre el índice del último carácter en la cadena. La cantidad de almacenamiento necesario para la cadena y el byte NULL siempre es strlen (str) + 1.

Una última cosa. Es importante tener en cuenta que las cadenas terminadas en NULL son solo una convención. Ha habido y hay muchas alternativas posibles. Solo es necesario si utiliza funciones que suponen que el byte NULO indica el punto en la memoria donde debería dejar de hacer cosas. Este es el caso de las funciones de cadena en libc. Podría escribir sus propias funciones de cadena que almacenan la longitud precedida al principio de la cadena. Al costo de una cierta complejidad adicional introducida por el tipo punning requerido para cadenas con longitudes que exceden los 255 caracteres, y manteniendo este número cada vez que se actualiza la cadena, este enfoque tiene la ventaja de encontrar la longitud de la cadena en tiempo O (1) de O (n). También puede almacenar el puntero de la cadena y el valor de la longitud en una estructura, aunque esto no se puede generalizar con precisión a las cadenas que no se encuentran en el montón. La mayoría de los programadores probablemente solo le dirán que debe atenerse a las representaciones de cadena estándar para la mayoría de las cosas, y probablemente tengan razón. Pero si es su propio código, quien debería decirle qué hacer, es su máquina de computación universal (al menos, aproximación finita), explore el panorama de computación y conviértalo en su caja de arena, ¡y diviértase!

    
respondido por el user3259161 29.07.2015 - 19:50
fuente
1

Esto está flotando en las respuestas anteriores, pero creo que debería ser explícito. El manejo de la matriz de caracteres de C / C ++ tiene una serie de posibles peligros de off-by-one ... Ejemplos:

""  // a zero length string requiring one byte of storage
    // in memory:  00

"Hi."  // a length 3 string requiring four bytes of storage
       // in memory:  48 69 2e 00
"Hi."[3]  // is the 00, the characters in a string and a string array are indexed starting at 0, to wit
"Hi."[0]  // is the 'H'.

char foo[3]  // a length three character array requiring three bytes of storage
     bar[4]  // a length four character array requiring four bytes of storage

strncpy(foo, "Hi.", 3)  // copies three characters from a length three string to a length three character array.  
                        // The result is not a string because the null is not copied.

strcpy(foo, "Hi.")  // copies four characters from a length three string to a length three character array
                    // This causes overrun of the array.
                    // It writes 00 on whatever (if anything) is allocated next in storage.

strcpy(bar, "Hi.")  // copies four characters from a length three string to a length four character array.
                    // This works/is safe (enough).

Entonces

  • una longitud de tres cadenas contiene cuatro caracteres.
  • una cadena de longitud tres no cabe en una matriz de tres caracteres de longitud
  • copiar tres caracteres de una cadena de longitud tres no copia la cadena
  • si mystring es una cadena de longitud n , mystring [ n ] es la terminación 00. En consecuencia, se puede razonar brevemente (o no hacerlo) que se copie hasta el n -th carácter copiará el 00.

O, para resumir, esto está diseñado para causar errores off-by-one.

    
respondido por el Eric Towers 30.07.2015 - 01:25
fuente

Lea otras preguntas en las etiquetas