在 C 中,如果函数不接受任何参数,则不使用 'void' 是一个潜在的漏洞

信息安全 C 安全编码
2021-09-04 03:49:15

在 CERT 安全编码标准中,建议“即使函数不接受任何参数,也始终指定 void ”。其中提出了一个可能的安全漏洞。

/* 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 */

在这个不合规的代码示例中,具有高权限的用户将一些秘密输入提供给调用者,然后调用者将其传递给 foo()。由于 foo() 的定义方式,我们可能会假设 foo() 无法从调用者那里检索信息。然而,因为 i 的值确实被传递到堆栈中(在调用者的返回地址之前),恶意程序员可以更改内部实现并将值手动复制到特权较低的文件中。

是否有任何利用这种“不安全”编码实践的漏洞。如果没有,有人可以解释或提供示例代码如何使用它。另外,如果这真的很难利用或不可能,你能解释一下为什么吗?

2个回答

这里发生的是该foo()函数使用所谓的旧式声明,即在第一次规范化之前在 C 中完成的事情(也就是 1989 年的“ANSI C”)。在 ANSI C 之前的版本中,一个函数bar()接受两个类型的参数intchar *以这种方式定义

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 中的含义一样。

在 C 中,函数原型foo();声明了一个函数,该函数接受未指定数量的参数。这与许多其他语言(包括 C++)不同,在这些语言中,这样的原型将指示一个带参数的函数。

您链接到的页面上列出的两个潜在问题是Ambiguous InterfaceInformation Outflow

从后者开始,人们可能会想象为一个处理机密信息的组织工作。可以执行代码审计以确保绝密信息不会流入仅归类为机密的文件中。这样的代码审计很容易错过一个原型,例如foo();,这实际上会允许信息以未声明参数的形式流入其中。如果foo();是类似的,update_log();并且我们的双重代理能够更改代码update_log();以接收参数并将其写入日志条目,您可能会看到如何实现流出。

另一个潜在的问题,不明确的接口,是指原型foo();可以以不同的方式调用的事实。这种模糊性可能导致软件漏洞有点牵强,但并非不可能。

如果 foo() 的实现;正在做一些非常不标准的事情,例如使用指针算术进入它的调用者堆栈帧,然后将参数传递给foo();会破坏导致任意问题的指针算术。