原文链接:

MEM50-CPP. Do not access freed memory


使用已经被指向被内存管理函数释放的指针——包括解引用、作为运算的操作数、类型转换和作为赋值的右值——是 未定义行为 。指向已经被释放的内存的指针被称为 空悬指针 。访问一个空悬指针会导致可用 漏洞(exploitable vulnerabilities)。

何时去重用或者回收被释放的内存却决于内存管理单元。当内存被释放了,所有的指针将成为不合法的,并且它的上下文可能被返回给操作系统、使被释放内存不可访问或者依然完好无损且可访问。结果就是,处于被释放内存区域的数据可能看起来合法但可能已经意外改变了。因此,当内存被释一旦被释放,一定不要从它当中读或写。

不合规的代码示例 (new and delete)

这个不合规的代码示例中,已经被释放的 s 被解引用了。如果这次访问导致读后释放(write-after-free),这个 漏洞(vulnerability) 可被 发掘 来用于执行任意带权限的漏洞进程代码。通常,动态内存分配和释放相距甚远,使得这类问题很难被识别和分析。

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <new>

struct S {
void f();
};

void g() noexcept(false) {
S *s = new S;
// ...
delete s;
// ...
s->f();
}

函数 g() 被标记为 noexcept(false) 以遵循 MEM52-CPP. Detect and handle memory allocation errors.

合规方案 (new and delete)

在这个合规方案中, 动态分配的内存直到不被使用后才释放。

1
2
3
4
5
6
7
8
9
10
11
12
#include <new>

struct S {
void f();
};

void g() noexcept(false) {
S *s = new S;
// ...
s->f();
delete s;
}

合规方案 (Automatic Storage Duration)

当有可能时,使用自动存储期 (Automatic Storage Duration) 来代替动态存储期 (dynamic storage duration) 。既然 sg() 范围之外不再被用到,这个合规方案利用自动存储期来限制在 g() 作用范围内的 s 的生命周期。

1
2
3
4
5
6
7
8
9
struct S {
void f();
};

void g() {
S s;
// ...
s.f();
}

不合规的代码示例 (std::unique_ptr)

在接下来这个不合规的代码示例中, buff 对象的析构函数在隐式地释放了由 buff 对象管理的动态分配的内存之后,这块内存被访问了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <iostream>
#include <memory>
#include <cstring>

int main(int argc, const char *argv[]) {
const char *s = "";
if (argc > 1) {
enum { BufferSize = 32 };
try {
std::unique_ptr<char[]> buff(new char[BufferSize]);
std::memset(buff.get(), 0, BufferSize);
// ...
s = std::strncpy(buff.get(), argv[1], BufferSize - 1);
} catch (std::bad_alloc &) {
// Handle error
}
}

std::cout << s << std::endl;

这段代码总是构造了一个 null 结尾的字符串,即使用了 strncpy() ,因为它把缓冲区的末尾 char 设为 0。

合规方案 (std::unique_ptr)

在这个合规方案中, buff 的生命周期被延长到了访问被其管理的内存之后。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <iostream>
#include <memory>
#include <cstring>

int main(int argc, const char *argv[]) {
std::unique_ptr<char[]> buff;
const char *s = "";

if (argc > 1) {
enum { BufferSize = 32 };
try {
buff.reset(new char[BufferSize]);
std::memset(buff.get(), 0, BufferSize);
// ...
s = std::strncpy(buff.get(), argv[1], BufferSize - 1);
} catch (std::bad_alloc &) {
// Handle error
}
}

std::cout << s << std::endl;
}

合规方案

在这个合规方案中,使用了一个带由自动存储期的 std::string 类型的变量来代替 std::unique_ptr<char[]> ,这降低了方案的复杂度且提高了安全性。

1
2
3
4
5
6
7
8
9
10
11
12
#include <iostream>
#include <string>

int main(int argc, const char *argv[]) {
std::string str;

if (argc > 1) {
str = argv[1];
}

std::cout << str << std::endl;
}

不合规的代码示例 (std::string::c_str())

在这个不合规的代码示例中,在一个临时的 std::string 对象上调用了 std::string::c_str() 。一旦 std::string 对象在赋值运算结束后被销毁,存放结果的指针将指向被释放的内存。当访问了指针指向的元素时,将导致 未定义行为

1
2
3
4
5
6
7
8
9
#include <string>

std::string str_func();
void display_string(const char *);

void f() {
const char *str = str_func().c_str();
display_string(str); /* Undefined behavior */
}

这段代码总是构造了一个 null 结尾的字符串,即使用了 strncpy() ,因为它把缓冲区的末尾 char 设为 0。

合规方案 (std::string::c_str())

在这个合规方案中,构造了该字符串的局部拷贝来确保当调用 display_string() 时,字符串 str 将是可用的。

1
2
3
4
5
6
7
8
9
10
#include <string>

std::string str_func();
void display_string(const char *s);

void f() {
std::string str = str_func();
const char *cstr = str.c_str();
display_string(cstr); /* ok */
}

不合规的代码示例

在这个不合规的代码示例中,试图通过调用 operator new() 来分配零字节的内存。如果该请求成功, operator new() 应返回非空指针值。然而,根据 C++ 标准,[basic.stc.dynamic.allocation], 段落 2 [ISO/IEC 14882-2014], 试图通过这类指针来解引用将导致 未定义行为

1
2
3
4
5
6
7
8
#include <new>

void f() noexcept(false) {
unsigned char *ptr = static_cast<unsigned char *>(::operator new(0));
*ptr = 0;
// ...
::operator delete(ptr);
}

合规方案

在这个合规方案依赖程序员的意图。如果程序员试图分配单个 unsigned char 对象,这个合规方案将利用 new 来替代直接调用 operator new() ,如该合规方案所示。

1
2
3
4
5
6
void f() noexcept(false) {
unsigned char *ptr = new unsigned char;
*ptr = 0;
// ...
delete ptr;
}

合规方案

如果程序员试图分配零字节的内存(大概想得到一个不会被程序中其他指针复用的独有的指针值,除非它被正确释放),然后并不是试图去解引用这个返回的指针,建议的方案而是将 ptr 声明为 void * 类型。根据 一致性 (conforming) 实现 (implementation)., 它不能被解引用。

1
2
3
4
5
6
void f() noexcept(false) {
unsigned char *ptr = new unsigned char;
*ptr = 0;
// ...
delete ptr;
}

Risk Assessment

读取已经被释放的先前动态分配过的内存将导致 程序异常终止 abnormal program termination拒绝服务攻击 denial-of-service attacks. 对已经被释放的内存写入会导致执行提权的任意代码漏洞程序。

Rule Severity Likelihood Remediation Cost Priority Level
MEM50-CPP High Likely Medium P18 L1

Automated Detection

Tool Version Checker Description
Astrée 20.10 dangling_pointer_use
Axivion Bauhaus Suite 7.2.0 CertC++-MEM50
Clang 3.9 clang-analyzer-cplusplus.NewDeleteclang-analyzer-alpha.security.ArrayBoundV2 Checked by clang-tidy, but does not catch all violations of this rule.
CodeSonar 6.1p0 ALLOC.UAF Use after free
Compass/ROSE
Coverity v7.5.0 USE_AFTER_FREE Can detect the specific instances where memory is deallocated more than once or read/written to the target of a freed pointer
Helix QAC 2021.2 C++4303, C++4304
Klocwork 2021.1 UFM.DEREF.MIGHT UFM.DEREF.MUST UFM.FFM.MIGHT UFM.FFM.MUST UFM.RETURN.MIGHT UFM.RETURN.MUST UFM.USE.MIGHT UFM.USE.MUST
LDRA tool suite 9.7.1 483 S, 484 S** ** Partially implemented
Parasoft C/C++test 2021.1 **CERT_CPP-MEM50-a ** Do not use resources that have been freed
Parasoft Insure++ Runtime detection
Polyspace Bug Finder R2021b CERT C++: MEM50-CPP Checks for:Pointer access out of boundsDeallocation of previously deallocated pointerUse of previously freed pointerRule partially covered.
PRQA QA-C++ 4.4 4303, 4304
PVS-Studio 7.15 V586, V774
Splint 5.0

VU#623332 describes a double-free vulnerability in the MIT Kerberos 5 function krb5_recvauth() [VU# 623332].

Search for other vulnerabilities resulting from the violation of this rule on the CERT website.

SEI CERT C++ Coding Standard EXP54-CPP. Do not access an object outside of its lifetimeMEM52-CPP. Detect and handle memory allocation errors
SEI CERT C Coding Standard MEM30-C. Do not access freed memory
MITRE CWE CWE-415, Double Free CWE-416, Use After Free

Bibliography

[ISO/IEC 14882-2014] Subclause 3.7.4.1, “Allocation Functions” Subclause 3.7.4.2, “Deallocation Functions”
[Seacord 2013b] Chapter 4, “Dynamic Memory Management”