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.