===================
== lxulxu's blog ==
===================
Hello there

C++安全指南

c++

编程习惯

  • switch中应有default
  • 不应在debug或错误信息中提供过多内容
  • 不应该在客户端代码中硬编码对称加密秘钥
1    // Bad
2    char g_aes_key[] = {...};
3    void Foo() {
4      ....
5      AES_func(g_aes_key, input_data, output_data);
6    }
1    // Good
2    char* g_aes_key;
3    void Foo() {
4      ....
5      AES_encrypt(g_aes_key, input_data, output_data);
6    }
7    void Init() {
8      g_aes_key = get_key_from_https(user_id, ...);
9    }
  • 函数不可以返回栈上的变量的地址,而应当使用堆来传递非简单类型变量,强烈建议返回 string、vector 等类型。
1    // Bad
2    char* Foo(char* sz, int len){
3      char a[300] = {0};
4      if (len > 100) {
5        memcpy(a, sz, 100);
6      }
7      a[len] = '\0';
8      return a;  // WRONG
9    }
1    // Good
2    char* Foo(char* sz, int len) {
3        char* a = new char[300];
4        if (len > 100) {
5            memcpy(a, sz, 100);
6        }
7        a[len] = '\0';
8        return a;  // OK
9    }
  • 有逻辑联系的数组必须仔细检查
1    // Good
2    const int nWeekdays[] = {1, 2, 3, 4, 5, 6, 7};
3    const char* sWeekdays[] = {"Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"};
4    assert(ARRAY_SIZE(nWeekdays) == ARRAY_SIZE(sWeekdays));//确保有关联的nWeekdays和sWeekdays数据统一
5    for (int x = 0; x < ARRAY_SIZE(sWeekdays); x++) {
6      if (strcmp(sWeekdays[x], input) == 0) {
7        return nWeekdays[x];
8      }
9    }
  • 在头文件、源代码、文档中列举的函数声明应当一致,不应当出现定义内容错位的情况 错误示例: foo.h
1    int CalcArea(int width, int height);

foo.cc

1    int CalcArea(int height, int width) {  // Different from foo.h
2      if (height > real_height) {
3        return 0;
4      }
5      return height * width;
6    }
  • 检查复制粘贴的重复代码(相同代码通常代表错误)
  • 左右一致的重复判断/永远为真或假的判断(通常代表错误)
  • 函数每个分支都应有返回值:开启适当级别的警告(GCC 中为 -Wreturn-type 并已包含在 -Wall 中)并设置为错误,可以在编译阶段发现这类错误。
1    // Bad
2    int Foo(int bar) {
3      if (bar > 100) {
4        return 10;
5      } else if (bar > 10) {
6        return 1;
7      }
8    }

上述例子当bar<10时,其结果是未知的值。

  • 不得使用栈上未初始化的变量
  • 不得直接使用刚分配的未初始化的内存(如realloc),在 C++ 中,再次强烈推荐用 string、vector 代替手动内存分配。
1    // Bad
2    char* Foo() {
3      char* a = new char[100];
4      a[99] = '\0';
5      memcpy(a, "char", 4);
6      return a;
7    }
1    // Good
2    char* Foo() {
3      char* a = new char[100];
4      memcpy(a, "char", 4);
5      a[4] = '\0';
6      return a;
7    }
  • 与内存分配相关的函数需要检查其返回值是否正确,以防导致程序崩溃或逻辑错误
1    // Bad
2    void Foo() {
3      char* bar = mmap(0, 0x800000, .....);
4      *(bar + 0x400000) = '\x88'; // Wrong
5    }
1    // Good
2    void Foo() {
3      char* bar = mmap(0, 0x800000, .....);
4      if(bar == MAP_FAILED) {
5        return;
6      }
7      *(bar + 0x400000) = '\x88';
8    }
  • 不要在if里面赋值
  • if里,非bool类型和非bool类型的按位操作可能代表代码存在错误

文件操作

  • 避免路径穿越问题:在进行文件操作时,需要判断外部传入的文件名是否合法,如果文件名中包含 ../ 等特殊字符,则会造成路径穿越,导致任意文件的读写。
 1    void Foo() {
 2      char file_path[PATH_MAX] = "/home/user/code/";
 3      // 如果传入的文件名包含../可导致路径穿越
 4      // 例如"../file.txt",则可以读取到上层目录的file.txt文件
 5      char name[20] = "../file.txt";
 6      memcpy(file_path + strlen(file_path), name, sizeof(name));
 7      int fd = open(file_path, O_RDONLY);
 8      if (fd != -1) {
 9        char data[100] = {0};
10        int num = 0;
11        memset(data, 0, sizeof(data));
12        num = read(fd, data, sizeof(data));
13        if (num > 0) {
14          write(STDOUT_FILENO, data, num);
15        }
16        close(fd);
17      }
18    }
 1    void Foo() {
 2      char file_path[PATH_MAX] = "/home/user/code/";
 3      char name[20] = "../file.txt";
 4      // 判断传入的文件名是否非法,例如"../file.txt"中包含非法字符../,直接返回
 5      if (strstr(name, "..") != NULL){
 6        // 包含非法字符
 7        return;
 8      }
 9      memcpy(file_path + strlen(file_path), name, sizeof(name));
10      int fd = open(file_path, O_RDONLY);
11      if (fd != -1) {
12        char data[100] = {0};
13        int num = 0;
14        memset(data, 0, sizeof(data));
15        num = read(fd, data, sizeof(data));
16        if (num > 0) {
17          write(STDOUT_FILENO, data, num);
18        }
19        close(fd);
20       }
21    }
  • 避免相对路径导致的安全问题(DLL、EXE劫持等问题)
  • 文件权限控制:在创建文件时,需要根据文件的敏感级别设置不同的访问权限,以防止敏感数据被其他恶意程序读取或写入。
1    int Foo() {
2      // 不要设置为777权限,以防止被其他恶意程序操作
3      if (creat("file.txt", 0777) < 0) {
4        printf("文件创建失败!\n");
5      } else {
6        printf("文件创建成功!\n");
7      }
8      return 0;
9    }

内存操作

  • 防止各种越界写(向前/向后)
1    int a[5];
2    a[5] = 0;
  • 防止任意地址写:任意地址写会导致严重的安全隐患,可能导致代码执行。因此,在编码时必须校验写入的地址。 错误示例:
 1    void Write(MyStruct dst_struct) {
 2      char payload[10] = { 0 };
 3      memcpy(dst_struct.buf, payload, sizeof(payload));
 4    }
 5    int main() {
 6      MyStruct dst_stuct;
 7      dst_stuct.buf = (char*)user_controlled_value;
 8      Write(dst_stuct);
 9      return 0;
10    }

数字操作

  • 防止整数溢出
 1    const kMicLen = 4;
 2    // 整数溢出
 3    void Foo() {
 4      int len = 1;
 5      char payload[10] = { 0 };
 6      char dst[10] = { 0 };
 7      // Bad, 由于len小于4字节,导致计算拷贝长度时,整数溢出
 8      // len - MIC_LEN == 0xfffffffd
 9      memcpy(dst, payload, len - kMicLen);
10    }
 1    void Foo() {
 2      int len = 1;
 3      char payload[10] = { 0 };
 4      char dst[10] = { 0 };
 5      int size = len - kMicLen;
 6      // 拷贝前对长度进行判断
 7      if (size > 0 && size < 10) {
 8        memcpy(dst, payload, size);
 9        printf("memcpy good\n");
10      }
11    }
  • 防止Off-By-One:在进行计算或者操作时,如果使用的最大值或最小值不正确,使得该值比正确值多1或少1,可能导致安全风险。
1    char firstname[20];
2    char lastname[20];
3    char fullname[40];
4    fullname[0] = '\0';
5    strncat(fullname, firstname, 20);
6    // 第二次调用strncat()可能会追加另外20个字符。如果这20个字符没有终止空字符,则存在安全问题
7    strncat(fullname, lastname, 20);
1    char firstname[20];
2    char lastname[20];
3    char fullname[40];
4    fullname[0] = '\0';
5    // 当使用像strncat()函数时,必须在缓冲区的末尾为终止空字符留下一个空字节,避免off-by-one
6    strncat(fullname, firstname, sizeof(fullname) - strlen(fullname) - 1);
7    strncat(fullname, lastname, sizeof(fullname) - strlen(fullname) - 1);
  • 避免大小端错误
  • 检查除以零异常
  • 防止数字类型的错误强转
 1    int Foo() {
 2      int len = 1;
 3      unsigned int size = 9;
 4      // 1 < 9 - 10 ? 由于运算中无符号和有符号混用,导致计算结果以无符号计算
 5      if (len < size - 10) {
 6        printf("Bad\n");
 7      } else {
 8        printf("Good\n");
 9      }
10    }
 1    void Foo() {
 2      // 统一两者计算类型为有符号
 3      int len = 1;
 4      int size = 9;
 5      if (len < size - 10) {
 6        printf("Bad\n");
 7      } else {
 8        printf("Good\n");
 9      }
10    }
  • 比较数据大小时加上最小/最大值的校验
1    void Foo(int index) {
2      int a[30] = {0};
3      // 此处index是int型,只考虑了index小于数组大小,但是并未判断是否大于0
4      if (index < 30) {
5        // 如果index为负数,则越界
6        a[index] = 1;
7      }
8    }
1    void Foo(int index) {
2      int a[30] = {0};
3      // 判断index的最大最小值
4      if (index >=0 && index < 30) {
5        a[index] = 1;
6      }
7    }

指针操作

  • 检查在pointer上使用sizeof:除了测试当前指针长度,否则一般不会在pointer上使用sizeof。 可能错误:
1    size_t structure_length = sizeof(Foo*);
1    size_t structure_length = sizeof(Foo);
  • 检查直接将数组和0比较的代码:开启足够的编译器警告(GCC 中为 -Waddress,并已包含在 -Wall 中),并设置为错误,可以在编译期间发现该问题。
  • 不应当向指针赋予写死的地址:特殊情况需要特殊对待(比如开发硬件固件时可能需要写死),但是如果是系统驱动开发之类的,写死可能会导致后续的问题。
  • 检查空指针
1    *foo = 100;
2    if (!foo) {
3      ERROR("foobar");
4    }
1    if (!foo) {
2      ERROR("foobar");
3    }
4    *foo = 100;
  • 释放完后置空指针
 1    void foo() {
 2      char* p = (char*)malloc(100);
 3      memcpy(p, "hello", 6);
 4      // 此时p所指向的内存已被释放,但是p所指的地址仍然不变
 5      printf("%s\n", p);
 6      free(p);
 7      // 未设置为NULL,可能导致UAF等内存错误
 8      if (p != NULL) {  // 没有起到防错作用
 9        printf("%s\n", p); // 错误使用已经释放的内存
10      }
11    }
 1    void foo() {
 2      char* p = (char*)malloc(100);
 3      memcpy(p, "hello", 6);
 4      // 此时p所指向的内存已被释放,但是p所指的地址仍然不变
 5      printf("%s\n", p);
 6      free(p);
 7      //释放后将指针赋值为空
 8      p = NULL;
 9      if (p != NULL)  { // 没有起到防错作用
10        printf("%s\n", p); // 错误使用已经释放的内存
11      }
12    }
  • 防止错误的类型转换
 1    const int NAME_TYPE = 1;
 2    const int ID_TYPE = 2;
 3    // 该类型根据 msg_type 进行区分,如果在对MessageBuffer进行操作时没有判断目标对象,则存在类型混淆
 4    struct MessageBuffer {
 5      int msg_type;
 6      union {
 7        const char *name;
 8        int name_id;
 9      };
10    };
11    void Foo() {
12      struct MessageBuffer buf;
13      const char* default_message = "Hello World";
14      // 设置该消息类型为 NAME_TYPE,因此buf预期的类型为 msg_type + name
15      buf.msg_type = NAME_TYPE;
16      buf.name = default_message;
17      printf("Pointer of buf.name is %p\n", buf.name);
18      // 没有判断目标消息类型是否为ID_TYPE,直接修改nameID,导致类型混淆
19      buf.name_id = user_controlled_value;
20      if (buf.msg_type == NAME_TYPE) {
21        printf("Pointer of buf.name is now %p\n", buf.name);
22        // 以NAME_TYPE作为类型操作,可能导致非法内存读写
23        printf("Message: %s\n", buf.name);
24      } else {
25        printf("Message: Use ID %d\n", buf.name_id);
26      }
27    }
 1    void Foo() {
 2      struct MessageBuffer buf;
 3      const char* default_message = "Hello World";
 4      // 设置该消息类型为 NAME_TYPE,因此buf预期的类型为 msg_type + name
 5      buf.msg_type = NAME_TYPE;
 6      buf.name = default_msessage;
 7      printf("Pointer of buf.name is %p\n", buf.name);
 8      // 判断目标消息类型是否为 ID_TYPE,不是预期类型则做对应操作
 9      if (buf.msg_type == ID_TYPE)
10        buf.name_id = user_controlled_value;
11      if (buf.msg_type == NAME_TYPE) {
12        printf("Pointer of buf.name is now %p\n", buf.name);
13        printf("Message: %s\n", buf.name);
14      } else {
15        printf("Message: Use ID %d\n", buf.name_id);
16      }
17    }
  • 智能指针使用安全
 1    class Foo {
 2     public:
 3      explicit Foo(int num) { data_ = num; };
 4      void Function() { printf("Obj is %p, data = %d\n", this, data_); };
 5     private:
 6      int data_;
 7    };
 8    std::unique_ptr<Foo> fool_u_ptr = nullptr;
 9    Foo* pfool_raw_ptr = nullptr;
10    void Risk() {
11      fool_u_ptr = make_unique<Foo>(1);
12      // 从独占智能指针中获取原始指针,<Foo>(1)
13      pfool_raw_ptr = fool_u_ptr.get();
14      // 调用<Foo>(1)的函数
15      pfool_raw_ptr->Function();
16      // 独占智能指针重新赋值后会释放内存
17      fool_u_ptr = make_unique<Foo>(2);
18      // 通过原始指针操作会导致UAF,pfool_raw_ptr指向的对象已经释放
19      pfool_raw_ptr->Function();
20    }
21    // 输出:
22    // Obj is 0000027943087B80, data = 1
23    // Obj is 0000027943087B80, data = -572662307
 1    void Safe() {
 2      fool_u_ptr = make_unique<Foo>(1);
 3      // 调用<Foo>(1)的函数
 4      fool_u_ptr->function();
 5      fool_u_ptr = make_unique<Foo>(2);
 6      // 调用<Foo>(2)的函数
 7      fool_u_ptr->function();
 8    }
 9    // 输出:
10    // Obj is 000002C7BB550830, data = 1
11    // Obj is 000002C7BB557AF0, data = 2