En C, no usar 'void' si una función no acepta ningún argumento es una vulnerabilidad potencial

8

En el estándar de codificación segura CERT, hay una recomendación de que " Siempre especifique void incluso si una función no acepta argumentos ". Se propone una posible vulnerabilidad de seguridad.

/* Compile using gcc4.3.3 */
   void foo() {
        /* Use asm code to retrieve i
         * implicitly from caller
         * and transfer it to a less privileged file */
       }

   /* Caller */
   foo(i); /* i is fed from user input */
  

En este ejemplo de código no compatible, un usuario con privilegios altos alimenta   alguna entrada secreta para la persona que llama que la persona que llama luego pasa a foo ().   Debido a la forma en que se define foo (), podemos suponer que no hay manera   para foo () para recuperar información de la persona que llama. Sin embargo, porque   el valor de i realmente se pasa a una pila (antes de la devolución)   dirección del llamante), un programador malintencionado puede cambiar el interno   Implementación y copia el valor manualmente en un menos privilegiado.   archivo.

¿Existe algún exploit que use esta práctica de codificación "no segura"? Si no, alguien puede explicar o dar un código de ejemplo cómo se puede utilizar. Además, si esto es realmente difícil de explotar o no es posible, ¿podría explicar por qué?

    
pregunta Jor-el 27.09.2013 - 14:41
fuente

2 respuestas

13

Lo que sucede aquí es que la función foo() usa la llamada declaración de estilo antiguo , es decir, como se hicieron las cosas en C antes de la primera normalización (también conocida como "ANSI C" de 1989) . En pre-ANSI C, una función bar() que toma dos argumentos de los tipos int y char * se definiría de esa manera:

void bar()
    int i;
    char *p;
{
    /* do some stuff */
}

y se declararía de la siguiente manera (generalmente en un archivo de encabezado):

void bar();

Esto significa que el código que usa la función incluiría el archivo de encabezado, que luego transmitiría información sobre la existencia de la función y sobre su tipo de retorno (aquí, void ), pero nada sobre el número de argumentos y sus tipos. Por lo tanto, luego del uso, la persona que llama debe proporcionar los parámetros y esperar que haya enviado el número y los tipos adecuados . Si el código de la persona que llama no hace las cosas correctamente, el compilador no emitirá advertencias.

Como una vulnerabilidad de seguridad , no es muy convincente. Sólo tiene sentido en un contexto de inspección de código. Algún auditor está revisando una gran cantidad de código fuente. Un desarrollador malvado está tratando de hacer cosas malvadas que el auditor no notará (este es el escenario que se explora en el Concurso C underhanded ) . Presumiblemente, el auditor mirará los archivos de encabezado y también el inicio de la implementación de la función. En tu ejemplo, verá:

void foo()
{
    /* some stuff */
}

entonces el auditor puede simplemente asumir que foo() no toma ningún parámetro, ya que la llave de apertura sigue inmediatamente a foo() . Sin embargo, el código del llamante (que está en otra parte) llama a foo() con algunos parámetros. El compilador de C no puede avisar: ya que la función se declara "estilo antiguo", el compilador de C no sabe, al compilar el código del llamante, que foo() no usa ningún parámetro (o eso parece). El código de la persona que llama empujará los argumentos en la pila (y los eliminará al regresar). El malvado programador luego incluye en la definición de foo() algún ensamblaje hecho a mano para recuperar los argumentos de la pila, incluso si, en el nivel de sintaxis C, no existen.

Por lo tanto, un canal de comunicación semi-oculto entre dos códigos malvados (el código del que llama y la función llamada), de una manera que no es visible desde una inspección superficial de la declaración de la función y el inicio de la definición, y, de manera crucial, advertido por el compilador de C tampoco.

Como vulnerabilidad , lo encuentro bastante débil. El escenario es bastante inverosímil.

El problema es más sobre la garantía de calidad. Las declaraciones antiguas son peligrosas , no debido a los malvados programadores, sino a los humanos , quien no puede pensar en todo y debe recibir ayuda de las advertencias del compilador. Ese es todo el punto de prototipos de función , introducido en ANSI C, que incluye información de tipo para los parámetros de la función. En nuestros ejemplos, aquí hay dos prototipos:

void foo(void);
void bar(int, char *);

Con estas declaraciones, el compilador de C notará que el código de la persona que llama está intentando enviar parámetros a una función que no usa ninguno, y abortará la compilación o, al menos, emitirá una advertencia severamente redactada.

Un problema típico con los prototipos antiguos es que no se realizan conversiones de tipos automáticas. Por ejemplo, con esta función:

void qux()
    char *p;
    int i;
{
    /* some stuff */
}

y esta llamada:

qux(0, 42);

El compilador, al ver la llamada, creerá que los dos parámetros son dos valores de int . Pero la función realmente espera un puntero y luego un int . Si la arquitectura es tal que un puntero toma el mismo tamaño en la pila como un int (y también tal que un puntero NULL se codifica de la misma manera que un entero de valor 0, que es una característica bastante común), entonces las cosas parece funcionar Luego compile eso en una arquitectura donde los punteros sean dos veces más grandes que los enteros: el código fallará porque el 42 se interpretará como parte del valor del puntero.

(Los detalles dependen de la arquitectura, pero esto sería típico del código C de 16 bits compilado en una arquitectura de 16/32 bits con una alineación de 16 bits, por ejemplo, una CPU 68000. En arquitecturas modernas de 64 bits,% Los valores de int tienden a estar alineados a 64 bits en la pila, lo que ahorra el aspecto de muchos programadores descuidados. Sin embargo, el problema es más general; existen problemas similares con los tipos de punto flotante.)

Debes usar prototipos; no porque las funciones de estilo antiguo induzcan vulnerabilidades, sino porque inducen a bugs.

Nota al margen: en C ++, los prototipos son obligatorios, por lo que la declaración void foo(); en realidad significa "ningún argumento", como lo que void foo(void); significaría en ANSI C.

    
respondido por el Tom Leek 27.09.2013 - 15:22
fuente
5

En C, la función prototipo foo(); declara una función que toma un número no especificado de parámetros. Esto difiere de muchos otros lenguajes (incluido C ++) en los que tal prototipo indicaría una función que toma parámetros no .

Los dos problemas potenciales enumerados en la página a la que te vinculas se presentan como Interfaz ambigua y Salida de información .

A partir de este último, uno podría imaginar trabajar para una organización que maneja información clasificada. Se podrían realizar auditorías de código para garantizar que la información de TOP SECRET no pueda fluir a un archivo clasificado solo como SECRET. Una auditoría de código de este tipo podría fácilmente pasar por alto un prototipo como foo(); , que en realidad permitiría que la información fluya en él en forma de un parámetro no declarado. Si foo(); es algo así como update_log(); y nuestro agente doble puede modificar el código para que update_log(); reciba un parámetro y lo escriba como una entrada de registro, puede ver cómo se puede lograr el flujo de salida.

El otro problema potencial, la interfaz ambigua, se refiere al hecho de que el prototipo foo(); se puede llamar de diferentes maneras. Es un poco exagerado, pero no imposible, que tal ambigüedad pueda llevar a una vulnerabilidad del software.

Si una implementación de foo (); está haciendo algo muy poco estándar, como el uso de la aritmética de punteros para llegar al marco de pila de las personas que llaman, y luego pasar los parámetros a foo(); rompería esa aritmética de punteros que conduce a problemas arbitrarios.     

respondido por el lynks 27.09.2013 - 15:25
fuente

Lea otras preguntas en las etiquetas