C语言陷阱与技巧第49节,不要使用固定长度数组,从没有固定边界的数据源接收数据

在各种编程语言中,字符串的处理都是非常重要,也是非常频繁的需求。但是在C语言中,很多编译器为了保持C语言程序的高效率和低消耗,通常都不会提供边界检查等安全保障,所以,程序员一不小心就可能写出不安全的C语言代码,本文主要讨论一种常见的安全隐患。

不要使用固定长度数组,从没有固定边界的数据源接收数据

如果使用固定字符数组从数据源(例如 stdin 标准输入)拷贝字符串,就有可能发生程序异常。下面这段C语言代码就是一个例子,请看:

void get_y_or_n(void)
{
    char response[8];

    puts("Continue? [y] n: ");
    gets(response);

    if (response[0] == 'n')
        exit(0);

    return;
}


这段C语言代码调用了 gets() 函数,从标准输入缓冲里读取数据,并使用固定长度为 8 的数组 response 接收。读者应该明白,gets() 函数会一直读取字符,直到遇到换行符或者文件结束标志(EOF)才会返回。

显然,程序使用固定长度为 8 的数组接收输入是不安全的,因为只要用户输入超过 8 个字符,程序的表现就不可预知了,因为程序必定有数据会被多出的字符粗暴覆盖。

读者应该能够想到,限制C语言程序接收字符个数,就能解决上述安全隐患。但是遗憾的是,gets() 函数并不具备这样的功能,除非遇到换行符或者 EOF,否则它会一直从标准输入缓冲里读取字符。这一点可以从 gets() 函数的C语言源码看出:

char *gets(char *dest)
 {
    int c = getchar();
    char *p = dest;

    while (c != EOF && c != '\n') {
        *p++ = c;
        c = getchar();
    }
    *p = '\0';

    return dest;
}


事实上,现在不少C语言编译器在处理上述C语言代码时,都会给出警告,因为 gets() 函数是不安全的,已经被逐步弃用了。

从一个没有确定边界的数据源(例如 stdin )读取数据是一件很能让C语言程序员头疼的事,因为我们不可能事先知道以后用户会究竟输入多少字符,所以也就不可能预先分配好恰当长度的字符数组接收用户输入。

解决上述困局的一个常用方法是:分配一个足够大(远超实际需要)的数组来接收输入。在上述C语言代码示例中,程序开发者希望用户只输入一个字符,所以申请了长度为 8 的数组 response,正常情况下,response 肯定足够接收 1 个字符。

如果用户遵守程序提示,只输入一个字符,上述C语言程序无疑可以正常工作。但是,如果用户带有恶意,固定长度的接收数组和 gets() 函数则很可能导致程序崩溃,甚至引发安全问题。所以,C语言程序员应该谨记:不要使用固定长度数组,从没有固定边界的数据源接收数据

拷贝和连接字符串

在C语言程序开发中,拷贝和连接字符串也比较容易引发安全隐患,因为这两个操作大多时候都会使用诸如 strcpy(),strcat(),sprintf() 之类的标准库函数,这几个库函数都有类似于 gets() 函数的安全问题。

下面将以 strcpy() 为例做进一步分析。

从命令行读取的参数通常存储在程序内存里,C语言程序也可以从命令行接收参数,此时它的 main() 函数常如下定义,请看:

int main(int argc, char *argv[])
{
    /** ... */
}

命令行输入的参数会被以字符串的形式传递给 main() 函数的参数 argv[0] 到 argv[argc-1]。如果 argc 大于 0,argv[0] 就是程序自己的名字。如果 argc 大于 1,那么 argv[1] 到 argv[argc-1] 就是命令行输入的实际参数了。例如:

#include <stdio.h>

int main(int argc, char *argv[])
{
    int i;

    printf("argc = %d, argv[0]: %s\n", argc, argv[0]);
    for(i=1; i<argc; i++){
        printf("argv[%d]: %s\n", i, argv[i]);
    }

    return 0;
}


编译并执行上述C语言代码,可以得到如下输出:

# gcc t.c
# ./a.out 
argc = 1, argv[0]: ./a.out
# ./a.out arg1 arg2
argc = 3, argv[0]: ./a.out
argv[1]: arg1
argv[2]: arg2

一般来说,C语言程序会保证 argv[argc] 等于 NULL,也即以字符串结束符结尾。

如果用来接收参数的内存长度不够,C语言程序就会出现漏洞。即使是程序名本身 argv[0] 也有可能导致程序漏洞的产生,以下面这段C语言代码为例:

int main(int argc, char *argv[])
{
    /* ... */
    char prog_name[128];
    strcpy(prog_name, argv[0]);
    /* ... */
}


恶意用户可以控制 argv[0] 的内容,使其超过 128 字节,造成数据溢出,覆盖其他有用数据。也可以将 argv[0] 设置为 NULL,使程序崩溃。出现这样的安全问题,其实就是 strcpy() 不检查参数边界导致的。有些C语言编译器在遇到 strcpy() 时,会给出一个“缓冲可能溢出”的警告。

更安全的做法是通过 strlen() 得到 argv 的长度,然后分配一块恰当长度的内存区域用于拷贝。当然了,strlen() 也只能接收非空指针,所以需要先判断一下 argv 的值,相关C语言代码是下面这样的:

int main(int argc, char *argv[])
{
    /* Do not assume that argv[0] cannot be null */
    const char * const name = argv[0] ? argv[0] : "";
    char *prog_name = (char *)malloc(strlen(name) + 1);
    if (prog_name != NULL) {
        strcpy(prog_name, name);
    }
    else {
        /* Failed to allocate memory - recover */
    }
    /* ... */
}

小结

要是想写出安全的C语言程序,就应该遵守“不使用固定长度数组,从没有固定边界的数据源接收数据”,也不应该使用固定长度的数组拷贝不确定长度的数据,本文通过C语言代码示例较为详细的讨论了这两点。其实稍稍思考一下也应该明白:使用一个固定大小的桶去接不确定水量的水,是不能保证水不会漏出来的。

阅读更多:   C语言
添加新评论

icon_redface.gificon_idea.gificon_cool.gif2016kuk.gificon_mrgreen.gif2016shuai.gif2016tp.gif2016db.gif2016ch.gificon_razz.gif2016zj.gificon_sad.gificon_cry.gif2016zhh.gificon_question.gif2016jk.gif2016bs.gificon_lol.gif2016qiao.gificon_surprised.gif2016fendou.gif2016ll.gif