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