C语言中的 va_list 可能存在缓冲区溢出漏洞?

4

I have the following code:

int ircsocket_print(char *message, ...)
{
    char buffer[512];
    int iError;
    va_list va;
    va_start(va, message);
    vsprintf(buffer, message, va);
    va_end(va);
    send(ircsocket_connection, buffer, strlen(buffer), 0);
    return 1;
}

我想知道如果提供大小>512的char数组给变量列表,这段代码是否容易受到缓冲区溢出攻击?如果是,该如何修复?

谢谢。

4个回答

10

是的,它是易受攻击的。

您可以通过以下方法实现您的函数:

int ircsocket_print(char *message, ...)
{
    char buf[512];
    char *buffer;
    int len;
    va_list va;

    buffer = buf;
    va_start(va, message);
    len = vsnprintf(buffer, 512, message, va);
    va_end(va);

    if (len >= 512)
    {
        buffer = (char*)malloc(len + 1);
        va_start(va, message);
        len = vsnprintf(buffer, len + 1, message, va);
        va_end(va);
    }

    send(ircsocket_connection, buffer, len, 0);

    if (buffer != buf)
        free(buffer);
    return 1;
}

2
+1 是因为指出 vsnprintf 返回将要打印的字符数,而不截断输出。 - Blagovest Buyukliev
1
请注意,在第一次调用vsnprintf()之后,必须再次调用va_end(va);va_start(va, message);,然后才能调用第二个vsnprintf()。这在C99标准的§7.15 <stdarg.h>中有说明:_[...]被调用的函数应声明一个类型为va_list的对象(通常在本分落中称为ap)。对象ap可以作为参数传递给另一个函数;如果该函数使用参数ap调用va_arg宏,则调用函数中ap的值是不确定的,并且应在任何进一步引用ap之前传递给va_end宏。_ - Jonathan Leffler
1
此外,严格来说,该问题标记为C(而不是C ++),new运算符(或者是new运算符?)不是C的一部分。delete也不是。 - Jonathan Leffler
感谢澄清vsnprintf返回的是将要被写入的字节数而不是实际写入的数字。非常有用的注释。 - Lefteris

8

是的,它存在漏洞。

只需使用vsnprintf即可:

vsnprintf(buffer, sizeof(buffer), message, va);

6

正如其他人所指出的那样,对于您的问题的基本答案是“是的,您容易受到缓冲区溢出的攻击”。

在C99中,您可以使用VLA:

void ircsocket_print(const char *fmt, ...)
{
    va_list args;

    va_start(args, fmt);
    int len = vsnprintf(0, 0, message, args);
    va_end(args);

    char buffer[len+1];

    va_start(args, fmt);
    int len = vsnprintf(buffer, len+1, message, args);
    va_end(args);

    send(ircsocket_connection, buffer, len, 0);
}

请注意调用 vsnprintf() 函数时,第二个参数传入 0 可以获取需要的长度,然后再调用该函数进行数据格式化。同时,每次调用 vsnprintf() 后请注意仔细重置 args,这是 C 标准所要求的:

§7.15 <stdarg.h>

如果需要访问可变参数,则被调用函数应声明一个对象(本子句中通常称为 ap)并指定其类型为 va_list。对象 ap 可作为参数传递给另外一个函数;如果该函数使用参数 ap 调用了宏 va_arg,则在调用函数中,参数 ap 的值是不确定的,并且在任何进一步引用 ap 之前,应向 va_end 宏传递该值。

此方法的缺点之一是它采用悲观的方式,在预先调用了两次 vsnprintf() 的情况下执行。如果第一次调用就足够,您可以更乐观一些(大多数情况下,512 就足够了),只有在第一次调用后显示不足时才分配更多的空间。
使用 VLA 的另一个缺点是,如果本地变量 buffer 的空间不足,则您的代码可能永远无法恢复。您需要判断这是否是一个严重的问题。如果是,建议使用显式内存分配函数(例如 malloc())。
char buffer = malloc(len+1);
if (buffer == 0)
    return;  // Report error?
...second vsnprintf() and send()...
free(buffer);

由于您的函数只返回常量 1,因此没有明显的理由将其作为返回任何内容的函数 - 如果它是返回 void 的函数,则调用代码不需要对返回值进行任何检查。 另一方面,也许您应该返回 send() 调用的结果(如果 malloc() 失败可能会返回错误指示)。我还将格式(消息)参数更改为 const char *; 既不这个函数也不调用它的函数修改格式字符串。

这是一个非常全面的答案,谢谢。如果我能给它评两次分,我一定会的 :) - Andreas Grapentin

2
如果你使用Linux,可以使用vasprintf()函数来分配正确大小的缓冲区。
如果需要可移植性,可以使用vsnprintf()函数来避免缓冲区溢出。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接