【问题标题】:Format strings safely when vsnprintf is not available当 vsnprintf 不可用时安全地格式化字符串
【发布时间】:2018-09-27 12:35:36
【问题描述】:

我正在编写需要格式化字符串的代码,并且我想避免缓冲区溢出。

我知道如果vsnprintf 可用(C99 及以上)我们可以这样做:

char* formatString(const char *format, ...)
{
    char* result = NULL;
    va_list ap;
    va_start(ap, format);

    /* Get the size of the formatted string by getting vsnprintf return the
     * number of remaining characters if we ask it to write 0 characters */
    int size = vsnprintf(NULL, 0, format, ap);

    if (size > 0)
    {
        /* String formatted just fine */
        result = (char *) calloc(size + 1, sizeof(char));
        vsnprintf(result, size + 1, format, ap);
    }

    va_end(ap);
    return result;
}

我想不出在 C90 中做类似事情的方法(没有vsnprintf)。如果不编写极其复​​杂的逻辑就无法实现,我很乐意为结果设置最大长度,但我不确定如何在不冒缓冲区溢出风险的情况下实现这一点。

【问题讨论】:

  • 如果移植到较新的 C 版本不是一个选项,我会为 vsnprintf 寻找开源替代品。更简单的vsprintf 实在是太破了,无法使用。
  • vsnprintf() 被添加到 C99 的主要原因是难以保护 vsprintf() 或类似内容。一种解决方法是打开/dev/null,使用vfprintf() 将数据格式化为它,注意需要多大的结果,然后决定是否可以安全继续。恶心,尤其是如果您在每次通话时都打开设备。
  • @JonathanLeffler 感谢您的提示,请随时将其作为答案发布(带有警告):)

标签: c printf c89


【解决方案1】:

Pre-C99 没有提供简单的解决方案来格式化字符串以防止缓冲区溢出。

正是那些讨厌的"%s""%[]""%f" 格式说明符需要非常仔细地考虑其潜在的长输出。因此需要这样的功能。 @Jonathan Leffler

使用这些早期编译器执行此操作要求代码分析 format 和参数以找到所需的大小。那时,代码几乎可以让您拥有完整的my_vsnprintf()。我会为此寻求现有的解决方案。 @user694733.


即使使用 C99,*printf() 也存在环境限制。

任何一次转换可以产生的字符数至少应为 4095。C11dr §7.21.6.1 15

因此,任何尝试char buf[10000]; snprintf(buf, sizeof buf, "%s", long_string); 的代码都会面临问题,即使buf[] 足够但strlen(long_string) > 4095 也是如此。

这意味着快速而肮脏的代码可以计算% 和格式长度,并合理假设所需的大小不超过:

size_t sz = 4095*percent_count + strlen(format) + 1;

当然,对说明符的进一步分析可能会导致更保守的sz。继续写path 我们end 写我们自己的my_vsnprintf()


即使使用您自己的my_vsnprintf()安全性 也只有这么好。没有运行时检查 format(可能是动态的)是否与以下参数匹配。为此需要一种新的方法。

C99 解决方案的厚颜无耻的自我宣传,以确保匹配说明符和参数:Formatted print without the need to specify type matching specifiers using _Generic

【讨论】:

    【解决方案2】:

    comments回答。

    vsnprintf() 被添加到 C99 的主要原因是难以保护 vsprintf() 或类似内容。一种解决方法是打开/dev/null,使用vfprintf() 将数据格式化为它,注意需要多大的结果,然后决定是否可以安全继续。恶心,尤其是如果您在每次通话时都打开设备。

    这意味着你的代码可能变成:

    #include <assert.h>
    #include <stdarg.h>
    #include <stdio.h>
    #include <stdlib.h>
    
    extern char *formatString(const char *format, ...);
    
    char *formatString(const char *format, ...)
    {
        static FILE *fp_null = NULL;
    
        if (fp_null == NULL)
        {
            fp_null = fopen("/dev/null", "w");
            if (fp_null == NULL)
                return NULL;
        }
    
        va_list ap;
    
        va_start(ap, format);
        int size = vfprintf(fp_null, format, ap);
        va_end(ap);
    
        if (size < 0)
            return NULL;
    
        char *result = (char *) malloc(size + 1);
        if (result == NULL)
            return NULL;
    
        va_start(ap, format);
        int check = vsprintf(result, format, ap);
        va_end(ap);
    
        assert(check == size);
    
        return result;
    }
    
    int main(void)
    {
        char *r1 = formatString("%d Dancing Pigs = %4.2f%% of annual GDP (grandiose dancing pigs!)\n",
                                34241562, 21.2963);
        char *r2 = formatString("%s [%-13.10s] %s is %d%% %s\n", "Peripheral",
                                "sub-atomic hyperdrive", "status", 99, "of normality");
    
        if (r1 != NULL)
            printf("r1 = %s", r1);
    
        if (r2 != NULL)
            printf("r2 = %s", r2);
    
        free(r1);
        free(r2);
        return 0;
    }
    

    由于在函数内部使用fp_null 编写的静态变量,文件流无法关闭。如果这很麻烦,请将其设置为文件中的变量并向if (fp_null != NULL) { fclose(fp_null); fp_null = NULL; } 提供一个函数。

    我毫无歉意地假设使用/dev/null 的类 Unix 环境;如果你在 Windows 上工作,你可以把它翻译成NUL:

    请注意,问题中的原始代码没有使用va_start()va_end()两次(与此代码不同);那会导致灾难。在我看来,最好将va_end() 尽快放在va_start() 之后——如这段代码所示。显然,如果您的函数本身正在通过va_list,那么会有比这里显示的更大的差距,但是当您只是将变量参数传递给另一个函数时,应该只有一条线.

    代码在运行 macOS 10.14 Mojave 的 Mac 上使用 GCC 8.2.0(在 macOS 10.13 High Sierra 上编译)和命令行干净地编译:

    $ gcc -O3 -g -std=c90 -Wall -Wextra -Werror -Wmissing-prototypes \
    >     -Wstrict-prototypes vsnp37.c -o vsnp37
    $
    

    运行时会产生:

    r1 = 34241562 Dancing Pigs = 21.30% of annual GDP (grandiose dancing pigs!)
    r2 = Peripheral [sub-atomic   ] status is 99% of normality
    

    【讨论】:

    • 这样的代码在安全敏感的上下文中是不合适的,因为恶意的调用者可以传递一个字符串,其内容将被另一个线程中的代码更改(并变长)。我认为处理此类函数的最佳方法是编写一个通用的 vxprintf 函数,该函数接受一个指向函数的双间接指针,该函数将传递该指针和任何要输出的数据。通用函数的调用者可以创建一个结构,它的第一个成员是指向回调的指针,并且会根据需要使用结构的其余部分。这样的功能可以...
    • ...然后被用作标准定义的所有其他类型 printf 的“核心”,以及许多不是的有用类型(例如,用于呈现文本的“图形 printf”在图形屏幕上)。我怀疑许多实现实际上将它们的所有 printf 函数链接到一个公共例程,但这样做的确切方法各不相同。
    • 我不确定您认为vsnprintf() 会如何免受您概述的恶意线程调用场景的影响。
    • 给定vsnprintf(dest, n, "(%s)", evilstring);,无论evilstring 做什么,vsnprintf 的良好实现都应将输出限制为 n-1 个字节,后跟一个终止符。如果代码使用vsnprintf一次来查找长度,分配一个缓冲区,然后再次使用vsnprintf进行输出,并且evilStringSup变为Supercalifragilisticexpialidocious" between the two calls, the second call should store (Supe`和一个零字节,并且返回 36 而不会覆盖缓冲区末尾的任何内容。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-08-29
    • 2011-01-20
    • 1970-01-01
    • 2013-03-17
    • 1970-01-01
    • 2013-02-27
    相关资源
    最近更新 更多