【翻译】 Common vulnerabilities guide for C╱C++ programmers

Common vulnerabilities guide for C programmers

原始链接:https://security.web.cern.ch/security/recommendations/en/codetools/c.shtml

《C程序员的常见漏洞指引》

引子

  C语言中的大多缺陷(vulnerabilities)是与缓冲区溢出(buffer overflow)以及字符串操作(string manipulation)有关的。在大多数情况下,这会导致一个段错误(segmentation fault), 但如果是一些精心制作的符合程序结构与环境的恶意输入,则可以致使任意代码的执行(arbitrary code execution)。你可以在下文中找到多数常见的错误与建议的修补/解决方案。

gets

  stdio中的gets()函数并不检查缓冲区的长度,因而其经常导致一个缺陷。

漏洞代码

#include 
int main () {
    char username[8];
    int allow = 0;
    printf ("Enter your username, please: ");
    gets(username); // 用户输入"malicious"
    if (grantAccess(username)) {
        allow = 1;
    }
    if (allow != 0) { // allow的值已经被username溢出的部分覆盖了
        privilegedAction();
    }
    return 0;
}

缓解方案
  尽可能地使用 fgets()函数(与动态分配内存):

#include <stdio.h>
#include <stdlib.h>
#define LENGTH 8
int main () {
    char* username, *nlptr;
    int allow = 0;
 
    username = malloc(LENGTH * sizeof(*username));
    if (!username)
        return EXIT_FAILURE;
    printf("Enter your username, please: ");
    fgets(username,LENGTH, stdin);
    // fgets 在第LENGTH-1个字符或换行符后停止接收。
    // 但它会把\n当作一个合法字符,所以你可能需要移除它:
    nlptr = strchr(username, '\n');  // strchr 检查是否包含'\n'
    if (nlptr) *nlptr = '\0';  //如果包含,必处于字符串末尾
 
    if (grantAccess(username)) {
        allow = 1;
    }
    if (allow != 0) {
        priviledgedAction();
    }
 
    free(username);
 
    return 0;
}

strcpy

  内置的strcpy()函数不检查缓冲区的长度,可以很容易地连续覆盖一个特定的内存区。事实上,这一系列的函数都是有漏洞的:strcpy, strcat 以及 strcmp.

漏洞代码

char str1[10];
char str2[]="abcdefghijklmn";
strcpy(str1,str2);

缓解方案
  对于这个问题,如果条件允许的话,最好的方式是使用strlcpy()函数(仅在BSD系统中提供)。如若不然,你也可以很容易地自己定义这个函数,如下所示:

#include <stdio.h>
 
#ifndef strlcpy
#define strlcpy(dst,src,sz) snprintf((dst), (sz), "%s", (src))
#endif
 
enum { BUFFER_SIZE = 10 };
 
int main() {
    char dst[BUFFER_SIZE];
    char src[] = "abcdefghijk";
 
    int buffer_length = strlcpy(dst, src, BUFFER_SIZE);
 
    if (buffer_length >= BUFFER_SIZE) {
        printf("String too long: %d (%d expected)\n",
                buffer_length, BUFFER_SIZE-1);
    }
 
    printf("String copied: %s\n", dst);
 
    return 0;
}

  另一个可能说得上更便捷的方法是使用strncpy()函数,它可以阻止缓冲区溢出,但是不能保证’\0’结尾。

enum { BUFFER_SIZE = 10 };
char str1[BUFFER_SIZE];
char str2[]="abcdefghijklmn";
 
strncpy(str1,str2, BUFFER_SIZE); /* 限制复制的字符数量 */
// 我们需要为BUFFER_SIZE设置一个合适的值,并将目的缓冲区中所有的字符置为'\0'。
// 当源字符串的长度比BUFFER_SIZE还要大时,所有的'\0'均会被覆盖,字符串复制操作会截断于此。
 
if (str1[BUFFER_SIZE-1] != '\0') {
    /* 缓冲区被截断了,该处理错误了? */
}

  对于其它类似的函数,也尽可能选用带’n’的变体:比如strncmp与strncat。

sprintf

  和上一个函数一样,sprintf()函数也不检查缓冲区的边界,而且很容易导致溢出。

漏洞代码

#include <stdio.h>
#include <stdlib.h>
 
enum { BUFFER_SIZE = 10 };
 
int main() {
    char buffer[BUFFER_SIZE];
    int check = 0;
 
    sprintf(buffer, "%s", "This string is too long!");
 
    printf("check: %d", check); /* 这不会打印0! */
 
    return EXIT_SUCCESS;
}

缓解方案
  尽可能使用snprintf()函数,这会带来两个好处,一个是可以阻止缓冲区溢出,另一个是可以得到整个格式化字符串在缓冲区中确切占用的大小。

#include <stdio.h>
#include <stdlib.h>
 
enum { BUFFER_SIZE = 10 };
 
int main() {
    char buffer[BUFFER_SIZE];
 
    int length = snprintf(buffer, BUFFER_SIZE, "%s%s", "long-name", "suffix");
 
    if (length >= BUFFER_SIZE) {
        /* 处理字符串截断! */
    }
 
    return EXIT_SUCCESS;
}

printf及其朋友们

  另一大类漏洞的关注点在格式化字符串攻击,它可以导致信息泄露,内存覆盖,等等等等……这种错误可以被以下任意函数利用:printf, fprintf, sprintf 与 snprintf, 即所有参数包含格式化字符串的函数。

漏洞代码

#FormatString.c
#include <stdio.h>
 
int main(int argc, char **argv) {
    char *secret = "This is a secret!\n";
 
    printf(argv[1]);
 
    return 0;
}

  这份代码,如果用”-mpreferred-stack-boundary=2”选项编译(特指32位平台,64位不太一样,但是这些代码依旧有漏洞!)可以导致有趣的结果。
如果执行./FormatString %s,它就可以把secret字符串的值输出出来。

$ gcc -mpreferred-stack-boundary=2 FormatString.c -o FormatString
$ ./FormatString %s
This is a secret!
$

注意:”-mpreferred-stack-boundary=2”选项绝不是导致内存溢出的必须品,即使不设置它也不会使你的代码在任何意义上更安全。它仅仅是提供一个个简单直接的例子。
更多信息/解释 请参考这里

缓解方案
  这真的很简单,永远写死你的格式化字符串。至少,不要让它直接从用户的输入获得。

打开文件

  对于打开文件,我们需要特别小心,很多小问题都会出现在这里。Kupsch与Miller有一份简明教程包含了大量细节。他们也写了一个lib库来实现他们的探索。文件处理有很多方面会遭到攻击,下文仅仅叙述两个简单的例子。

下文描述一些基本的陷阱。

符号链接攻击
  创建文件前检查一个文件是否存在是一个好主意。但是,一个恶意的用户可以在你检查与使用该文件的时间差里创建一个文件(甚至是链接给一个关键的系统文件的符号链接文件)。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
 
#define MY_TMP_FILE "/tmp/file.tmp"
 
 
int main(int argc, char* argv[])
{
    FILE * f;
    if (!access(MY_TMP_FILE, F_OK)) {
        printf("File exists!\n");
        return EXIT_FAILURE;
    }
    /* 在这一刻,攻击者可以创建一个符号链接从/tmp/file.tmp到/etc/passwd */
    tmpFile = fopen(MY_TMP_FILE, "w");
 
    if (tmpFile == NULL) {
        return EXIT_FAILURE;
    }
 
    fputs("Some text...\n", tmpFile);
 
    fclose(tmpFile);
    /* 你成功地覆盖了/etc/passwd文件(如果你是从root用户运行此程序的话) */
 
    return EXIT_SUCCESS;
}

缓解方案
  要避免在打开文件时被攻击者骗过,又不能覆盖已经存在的正常文件。所以,

#include <unistd.h>
#include <stdio.h>
#include <fcntl.h>
#include <stdlib.h>
 
#define MY_TMP_FILE "/tmp/file.tmp"
 
enum { FILE_MODE = 0600 };
 
int main(int argc, char* argv[])
{
    int fd;
    FILE* f;
 
    /* 移除可能的符号链接 */
    unlink(MY_TMP_FILE);
    /* 打开文件,但如果一些恶意攻击者比我们更快地恢复了那个符号链接,open依旧会失败(这是fopen(path, "w")的安全版本) */
    fd = open(MY_TMP_FILE, O_WRONLY|O_CREAT|O_EXCL, FILE_MODE);
    if (fd == -1) {
        perror("Failed to open the file");
        return EXIT_FAILURE;
    }
    /* 得到一个FILE*型指针, 这比使用fd那个int型文件描述符便捷也高效 */
    f = fdopen(fd, "w");
    if (f == NULL) {
        perror("Failed to associate file descriptor with a stream");
        return EXIT_FAILURE;
    }
    fprintf(f, "Hello, world\n");
    fclose(f);
    /* fd已经被fclose()关闭了!!! */
    return EXIT_SUCCESS;
}

Common vulnerabilities guide for C++ programmers

原文链接:https://security.web.cern.ch/security/recommendations/en/codetools/cpp.shtml

《C++程序员的常见漏洞指南》
  C++不是C。这是我们给出的第一条建议。不要再用printf、char*以及等等,用C++的方式。如果你实在强迫症难改用C的方式写代码,请再去看C程序员的指南。

内存处理

  不要使用malloc与free,使用new与delete来代替。当你的代码异常时,new申请的内存会被干净的回收。

字符串处理

  不要使用char*,使用std::string类。
  不要使用fscanf, 使用 » 操作符与std::outputstream对象。

文件处理

  不要使用fopen(),使用std::ifstream来读取。对于写入,有点复杂。需要调用C里的open与那些C的open flag,然后从文件描述符使用boost库来生成一个比较美的stream:

#include <boost/iostreams/device/file_descriptor.hpp>
#include <boost/iostreams/stream_buffer.hpp>
#include <iostream>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
 
namespace io = boost::iostreams;
 
class ex {};
 
int main ()
{
	int fd = open("/my/file", O_WRONLY|O_CREAT|O_EXCL, 0600);
	if (fd == -1)
		throw ex();
	io::stream_buffer<io::file_descriptor_sink> fp (fd);
	std::ostream out(&fp);
 
	out << "Hello, world" << std::endl;
	return 0;
}

  最后,使用”-lboost_iostreams”标识来编译,就完美了!