C/C++ 指针操作
8.1 【建议】检查在pointer上使用sizeof
除了测试当前指针长度,否则一般不会在pointer上使用sizeof。
正确:
size_t pointer_length = sizeof(void*);
可能错误:
size_t structure_length = sizeof(Foo*);
可能是:
size_t structure_length = sizeof(Foo);
关联漏洞:
中风险-逻辑漏洞
8.2 【必须】检查直接将数组和0比较的代码
错误:
int a[3];
...;
if (a > 0)
...;
该判断永远为真,等价于:
int a[3];
...;
if (&a[0])
...;
可能是:
int a[3];
...;
if(a[0] > 0)
...;
开启足够的编译器警告(GCC 中为 -Waddress
,并已包含在 -Wall
中),并设置为错误,可以在编译期间发现该问题。
关联漏洞:
中风险-逻辑漏洞
8.3 【必须】不应当向指针赋予写死的地址
特殊情况需要特殊对待(比如开发硬件固件时可能需要写死)
但是如果是系统驱动开发之类的,写死可能会导致后续的问题。
关联漏洞:
高风险-内存破坏
8.4 【必须】检查空指针
错误:
*foo = 100;
if (!foo) {
ERROR("foobar");
}
正确:
if (!foo) {
ERROR("foobar");
}
*foo = 100;
错误:
void Foo(char* bar) {
*bar = '\0';
}
正确:
void Foo(char* bar) {
if(bar)
*bar = '\0';
else
...;
}
关联漏洞:
低风险-拒绝服务
8.5 【必须】释放完后置空指针
在对指针进行释放后,需要将该指针设置为NULL,以防止后续free指针的误用,导致UAF等其他内存破坏问题。尤其是在结构体、类里面存储的原始指针。
错误:
void foo() {
char* p = (char*)malloc(100);
memcpy(p, "hello", 6);
// 此时p所指向的内存已被释放,但是p所指的地址仍然不变
printf("%s\n", p);
free(p);
// 未设置为NULL,可能导致UAF等内存错误
if (p != NULL) { // 没有起到防错作用
printf("%s\n", p); // 错误使用已经释放的内存
}
}
正确:
void foo() {
char* p = (char*)malloc(100);
memcpy(p, "hello", 6);
// 此时p所指向的内存已被释放,但是p所指的地址仍然不变
printf("%s\n", p);
free(p);
//释放后将指针赋值为空
p = NULL;
if (p != NULL) { // 没有起到防错作用
printf("%s\n", p); // 错误使用已经释放的内存
}
}
对于 C++ 代码,使用 string、vector、智能指针等代替原始内存管理机制,可以大量减少这类错误。
关联漏洞:
高风险-内存破坏
8.6 【必须】防止错误的类型转换(type confusion)
在对指针、对象或变量进行操作时,需要能够正确判断所操作对象的原始类型。如果使用了与原始类型不兼容的类型进行访问,则存在安全隐患。
错误:
const int NAME_TYPE = 1;
const int ID_TYPE = 2;
// 该类型根据 msg_type 进行区分,如果在对MessageBuffer进行操作时没有判断目标对象,则存在类型混淆
struct MessageBuffer {
int msg_type;
union {
const char *name;
int name_id;
};
};
void Foo() {
struct MessageBuffer buf;
const char* default_message = "Hello World";
// 设置该消息类型为 NAME_TYPE,因此buf预期的类型为 msg_type + name
buf.msg_type = NAME_TYPE;
buf.name = default_message;
printf("Pointer of buf.name is %p\n", buf.name);
// 没有判断目标消息类型是否为ID_TYPE,直接修改nameID,导致类型混淆
buf.name_id = user_controlled_value;
if (buf.msg_type == NAME_TYPE) {
printf("Pointer of buf.name is now %p\n", buf.name);
// 以NAME_TYPE作为类型操作,可能导致非法内存读写
printf("Message: %s\n", buf.name);
} else {
printf("Message: Use ID %d\n", buf.name_id);
}
}
正确(判断操作的目标是否是预期类型):
void Foo() {
struct MessageBuffer buf;
const char* default_message = "Hello World";
// 设置该消息类型为 NAME_TYPE,因此buf预期的类型为 msg_type + name
buf.msg_type = NAME_TYPE;
buf.name = default_msessage;
printf("Pointer of buf.name is %p\n", buf.name);
// 判断目标消息类型是否为 ID_TYPE,不是预期类型则做对应操作
if (buf.msg_type == ID_TYPE)
buf.name_id = user_controlled_value;
if (buf.msg_type == NAME_TYPE) {
printf("Pointer of buf.name is now %p\n", buf.name);
printf("Message: %s\n", buf.name);
} else {
printf("Message: Use ID %d\n", buf.name_id);
}
}
关联漏洞:
高风险-内存破坏
8.7 【必须】智能指针使用安全
在使用智能指针时,防止其和原始指针的混用,否则可能导致对象生命周期问题,例如 UAF 等安全风险。
错误例子:
class Foo {
public:
explicit Foo(int num) { data_ = num; };
void Function() { printf("Obj is %p, data = %d\n", this, data_); };
private:
int data_;
};
std::unique_ptr<Foo> fool_u_ptr = nullptr;
Foo* pfool_raw_ptr = nullptr;
void Risk() {
fool_u_ptr = make_unique<Foo>(1);
// 从独占智能指针中获取原始指针,<Foo>(1)
pfool_raw_ptr = fool_u_ptr.get();
// 调用<Foo>(1)的函数
pfool_raw_ptr->Function();
// 独占智能指针重新赋值后会释放内存
fool_u_ptr = make_unique<Foo>(2);
// 通过原始指针操作会导致UAF,pfool_raw_ptr指向的对象已经释放
pfool_raw_ptr->Function();
}
// 输出:
// Obj is 0000027943087B80, data = 1
// Obj is 0000027943087B80, data = -572662307
正确,通过智能指针操作:
void Safe() {
fool_u_ptr = make_unique<Foo>(1);
// 调用<Foo>(1)的函数
fool_u_ptr->function();
fool_u_ptr = make_unique<Foo>(2);
// 调用<Foo>(2)的函数
fool_u_ptr->function();
}
// 输出:
// Obj is 000002C7BB550830, data = 1
// Obj is 000002C7BB557AF0, data = 2
关联漏洞:
高风险-内存破坏