Posted in

C 语言内存操作接口安全性归纳:从缓冲区溢出到安全替换

缓冲区溢出攻击

缓冲区溢出是指当计算机向缓冲区内填充数据位数时超过了缓冲区本身的容量,溢出的数据覆盖在合法数据上。

理想的情况是:程序会检查数据长度,而且并不允许输入超过缓冲区长度的字符。但是绝大多数程序都会假设数据长度总是与所分配的储存空间相匹配,这就为缓冲区溢出埋下隐患。

操作系统所使用的缓冲区,又被称为”堆栈”,在各个操作进程之间,指令会被临时储存在”堆栈”当中,”堆栈”也会出现缓冲区溢出。缓冲区溢出攻击是利用缓冲区溢出漏洞所进行的攻击行动,可以导致程序运行失败、系统关机、重新启动等后果。

缓冲区溢出中,最为危险的是堆栈溢出——入侵者可以利用堆栈溢出,在函数返回时改变返回程序的地址,让其跳转到任意地址,带来的危害一种是程序崩溃导致拒绝服务(Segmentation fault),另外一种就是跳转并且执行一段恶意代码。

C 语言没有提供字符串类型,字符串以字符数组的形式出现,C 标准库提供了一些操作字符串的函数:strcmpstrcpystrlenstrcatsprintf 等。因为字符串就是以 \0 结束的一段内存,这些函数实质上也就是操作内存的函数。

内存操作安全性接口分析

gets —— 危险

#include <stdio.h>
char *gets(char *buff);

从 stdin 流中读取字符串,直至接受到换行符或 EOF 时停止。但换行符会被丢弃,然后在末尾添加 \0 字符。

问题: 读入 stdin 流中的字符串超过 buff 长度造成溢出。

void main() {
    char buf[1024];
    gets(buf); // 当输入内容超过1024,直接溢出
}

fgets —— 安全

#include <stdio.h>
char *fgets(char *s, int size, FILE *stream);

从 stream 流中读取字符串,最多读取 size-1 个字符。

#define BUFSIZE 1024
void main() {
    char buf[BUFSIZE];
    fgets(buf, BUFSIZE, stdin); // 最多读入 BUFSIZE-1 个字符
}

strcpy —— 危险

char *strcpy(char *dest, const char *src);

把从 src 地址开始且含有 \0 结束符的字符串复制到以 dest 开始的地址空间。源字符串字符数目大于目的字符串 buff 长度时溢出。

// strcpy 的典型实现
char* strcpy(char *strDest, const char *strSrc) {
    assert((strDest!=NULL) && (strSrc !=NULL));
    char *address = strDest;
    while( (*strDest++ = *strSrc++) != '\0' )
        ;
    return address;
}

strncpy —— 尚可,但需注意

char *strncpy(char *dest, const char *src, size_t n);

strncpy 的行为很诡异——它不帮你保证 \0 结束。所以 strncpy 更像是特殊的 memcpy。

正确使用方法:

strncpy(dst, src, dst_size-1);
dst[dst_size-1] = '\0'; // Always do this to be safe!

安全版本宏:

#define STRNCPY(pszDst, pszSrc, iLen) \
do { \
    strncpy((pszDst), (pszSrc), (iLen)-1);\
    (pszDst)[(iLen)-1] = 0; \
} while(0)

安全版本函数:

char *sstrncpy(char *dest, const char *src, size_t n) {
    if (n == 0) return dest;
    dest[0] = 0;
    return strncat(dest, src, n - 1);
}

sprintf —— 危险

int sprintf(char *buffer, const char *format, ...);

使用 sprintf() 与使用 strcpy() 一样,都很容易对程序造成缓冲区溢出。

错误示例:

char buff[10] = {0};
char str[] = "123456789012345678";
snprintf(buff, sizeof(buff), str); // 如果 str 中有 %s 等转义字符,会继续往后读取!

正确示例:

char buff[10] = {0};
char str[] = "123456789012345678";
snprintf(buff, sizeof(buff), "%s", str); // 严格规范,结果是 "123456789"

snprintf 返回值陷阱

snprintf 的返回值与 sprintf 不同:如果输出因为 size 限制而被截断,返回值将是”如果有足够空间存储,所应能输出的字符数”——这个值可能等于或大于 size!如果返回值等于或大于 size,则表明输出字符串被截断了。

strcmp —— 危险

int strcmp(const char *s1, const char *s2);

对于 strcmp(p, "abc"),若 p 指向一个未知内存块,则会一直比较下去,直到错误。应使用 strncmp 替代。

sscanf —— 危险

// 危险
sscanf(argv[0], "%s", &buf);

// 安全 —— 加精度限制
sscanf(argv[0], "%255s", &buf);

危险性总览

函数严重性解决方案
gets最危险使用 fgets(buf, size, stdin)
strcpy很危险改为使用 strncpy
strcat很危险改为使用 strncat
sprintf很危险改为使用 snprintf
vsprintf很危险改为使用 vsnprintf
scanf/sscanf很危险使用精度说明符
getchar/fgetc/getc中等危险循环中使用时确保检查缓冲区边界
fgets低危险确保缓冲区大小正确
memcpy低危险确保缓冲区大小正确
snprintf低危险确保缓冲区大小正确
strncpy低危险确保缓冲区大小正确,手动补 \0

总结

嵌入式开发中,内存安全是基本功。记住几个原则:

  1. 永远不要用 gets、strcpy、sprintf——它们没有长度限制
  2. strncpy 不自动补 \0——用完手动加
  3. snprintf 的返回值可能大于 size——别拿返回值当实际写入长度
  4. scanf 系列加精度限制——%255s 而不是 %s
  5. 封装安全版本宏——团队统一使用 STRNCPY 等宏,从源头杜绝

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注