这里发生的是该foo()函数使用所谓的旧式声明,即在第一次规范化之前在 C 中完成的事情(也就是 1989 年的“ANSI C”)。在 ANSI C 之前的版本中,一个函数bar()接受两个类型的参数int并char *以这种方式定义:
void bar()
int i;
char *p;
{
/* do some stuff */
}
并且它将以下列方式声明(通常在头文件中):
void bar();
这意味着使用该函数的代码将包含头文件,该文件随后将传达有关该函数的存在及其返回类型(此处为void)的信息,但不包含有关参数数量及其类型的任何信息。因此,在使用时,调用者必须提供参数并希望它发送适当的数字和类型。如果调用者代码没有正确执行操作,那么编译器将不会发出警告。
作为一个安全漏洞,它不是很有说服力。它仅在代码检查的上下文中才有意义。一些审计员正在审查大量源代码。一个邪恶的开发者试图做审计员不会注意到的邪恶事情(这是在Underhanded C Contest中探索的场景)。据推测,审计员将查看头文件以及函数实现的开始。在您的示例中,他将看到:
void foo()
{
/* some stuff */
}
那么审计员可能只是假设不foo()带参数,因为左大括号紧跟在foo(). 但是,调用者代码(在别处)foo()使用一些参数进行调用。C 编译器无法发出警告:由于该函数被声明为“旧式”,因此 C 编译器在编译调用者代码时并不知道foo()它实际上没有使用任何参数(或者看起来如此)。调用者代码会将参数压入堆栈(并在返回时删除它们)。然后,邪恶的程序员在foo()一些手工组装的定义中包含从堆栈中检索参数,即使在 C 语法级别,它们不存在。
因此,两个恶意代码(调用者代码和被调用函数)之间的半隐藏通信通道,在粗略检查函数声明和定义开始时是不可见的,而且至关重要的是,没有被警告C编译器。
作为一个漏洞,我觉得它很弱。剧情相当不靠谱。
问题更多是关于质量保证。旧式声明是危险的,不是因为邪恶的程序员,而是因为人类程序员,他们无法考虑所有事情,必须得到编译器警告的帮助。这就是 ANSI C 中引入的函数原型的全部意义,其中包括函数参数的类型信息。在我们的示例中,这里有两个原型:
void foo(void);
void bar(int, char *);
通过这些声明,C 编译器会注意到调用者代码正试图将参数发送给一个不使用任何参数的函数,并将中止编译或至少发出一个措辞严厉的警告。
旧式原型的一个典型问题是无法进行自动类型转换。例如,使用此功能:
void qux()
char *p;
int i;
{
/* some stuff */
}
这个电话:
qux(0, 42);
编译器看到调用会认为这两个参数是两个int值。但是该函数确实需要一个指针,然后是一个int. 如果架构使得指针在堆栈上的大小int与. 然后在指针比整数大两倍的架构上编译它:代码将失败,因为42将被解释为指针值的一部分。
(细节取决于架构,但这将是典型的 16 位 C 代码在 16 位对齐的 16/32 位架构上编译的,例如 68000 CPU。在 64 位现代架构上,int值往往是堆栈上的 64 位对齐,这节省了许多粗心的程序员的皮肤。不过,问题更普遍;浮点类型也会出现类似的问题。)
你应该使用原型;不是因为旧式函数会导致漏洞,而是因为它们会导致错误。
旁注:在 C++ 中,原型是强制性的,因此void foo();声明实际上意味着“没有任何参数”,就像void foo(void);ANSI C 中的含义一样。