6

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

 3 years ago
source link: https://blog.popkx.com/c%E8%AF%AD%E8%A8%80%E9%99%B7%E9%98%B1%E4%B8%8E%E6%8A%80%E5%B7%A7%E7%AC%AC49%E8%8A%82-%E4%B8%8D%E8%A6%81%E4%BD%BF%E7%94%A8%E5%9B%BA%E5%AE%9A%E9%95%BF%E5%BA%A6%E6%95%B0%E7%BB%84/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

在各种编程语言中,字符串的处理都是非常重要,也是非常频繁的需求。但是在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;
}
6fcf50a97289f2f194f25c82ea200efa.png

这段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;
}
17e3ce37bd0acf8f74915c9fcd8e3e51.png

事实上,现在不少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;
}
9b19b837f4fca5eef2ae5994da68c92f.png

编译并执行上述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]);
    /* ... */
}

81379c7d4fac622707a045bc99d5727c.png
恶意用户可以控制 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 */
    }
    /* ... */
}
adf60d135d04200b335fcb04510f267f.png

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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK