C和C++安全編碼筆記:動態內存管理

4.1 C內存管理:

C標準內存管理函數:

(1).malloc(size_t size):分配size個字節,並返回一個指向分配的內存的指針。分配的內存未被初始化爲一個已知值。

(2).aligned_alloc(size_t alignment, size_t size):爲一個對象分配size個字節的空間,此對象的對齊方式是alignment指定的。alignment的值必須是實現支持的一種有效的對齊方式,size的值必須是alignment的整數倍,否則行爲就是未定義的。此函數返回一個指向分配的空間的指針,如果分配失敗,則返回一個空指針。

(3).realloc(void* p, size_t size):將p所指向的內存塊的大小改爲size個字節。新大小和舊大小中較小的值那部分內存所包含的內容不變,新分配的內存未作初始化,因此將有不確定的值。如果內存請求不能被成功分配,那麼舊的對象保持不變,而且沒有值被改變。如果p是一個空指針,則該調用等價於malloc(size);如果size等於0,則該調用等價於free(p),但這種釋放內存的用法應該避免。

(4).calloc(size_t nmemb, size_t size):爲數組分配內存,(該數組共有nmemb個元素,每個元素的大小爲size個字節)並返回一個指向所分配的內存的指針。所分配的內存的內容全部被設置爲0。

內存分配函數返回一個指向分配的內存的指針,這塊內存是按照任何對象類型恰當地對齊的,如果請求失敗,則返回一個空指針。連續調用內存分配函數分配的存儲空間的順序和鄰接是不確定的。所分配的對象的生存期從分配開始,到釋放時結束。返回的指針指向所分配的空間的起始地址(最低字節地址)。

free(void* p):釋放由p指向的內存空間,這個p必須是先前通過調用aligned_alloc、malloc、calloc或realloc返回的。如果引用的內存不是被這些函數之一分配的或free(p)此前已經被調用過,將會導致未定義行爲。如果p是一個空指針,則不執行任何操作

由C內存分配函數分配的對象有分配存儲期限。存儲期限是一個對象的屬性,它定義了包含該對象存儲的最低潛在生存期。這些對象的生存期並不限於創建它的範圍內,因此,如果在一個函數內調用malloc,那麼在該函數返回後,已分配的內存仍然存在。

對齊:完整的對象類型有對齊(alignment)要求,這種要求對可以分配該類型對象的地址施加限制。對齊是實現定義的整數值,它表示可以在一個連續的地址之間分配給指定的對象的字節數量。對象類型規定了每一個該類型對象的對齊要求。消除對齊要求,往往需要生成代碼進行跨字邊界域訪問,或從更慢的奇數地址訪問,從而減慢內存訪問。

完整的對象(complete object):對象中可以包含其它對象,被包含的對象稱爲子對象(subobject)。子對象可以是成員子對象、基類子對象,或數組元素。如果一個對象不是任何其它對象的子對象,那麼它被稱爲一個完整的對象。

對齊有一個從較弱的對齊到較強的對象(或更嚴格的對齊)的順序。越嚴格的對齊,其對齊值越大。一個地址滿足某一種對齊要求,也滿足任何更弱的有效對齊要求。char、signed char、unsigned char類型的對齊要求最弱。對齊表示爲size_t類型的值。每個有效對齊值都是2的一個非負整數冪。有效的對齊包括基本類型的對齊,加上額外的一組可選的實現定義的值。

基本對齊(fundamental alignment)小於或等於在所有上下文中由編譯器支持的最大對齊。max_align_t類型的對齊與在所有上下文中由編譯器支持的對齊大小相同。擴展對齊(extended alignment)大於max_align_t類型的對齊。一個具有擴展對齊要求的類型也稱爲超對齊(overaligned)類型。每個超對齊類型,要麼是一個結構或聯合類型,其中一個成員已應用擴展對齊,要麼它包含這樣的類型。如果實現支持,可以使用aligned_alloc函數分配比正常更嚴格的對齊的內存。如果一個程序要求比alignof(max_align_t)更大的對齊,那麼這個程序是不可移植的,因爲對超對齊類型的支持是可選的。

void test_aligned_alloc()
{
	const int arr_size = 11;
	// 分配16字節對齊的數據
#ifdef _MSC_VER
	float* array = (float*)_aligned_malloc(16, arr_size * sizeof(float));
#else
	float* array = (float*)aligned_alloc(16, arr_size * sizeof(float));
#endif
	auto addr = std::addressof(array);
	fprintf(stdout, "pointer addr: %p\n", addr);

	fprintf(stdout, "char alignment: %d, float alignment: %d, max_align_t alignment: %d\n",
		alignof(char), alignof(float), alignof(max_align_t));
}

在C標準中引入_Alignas關鍵字和aligned_alloc函數的主要理由是支持單指令多數據(SIMD)計算。

4.2 常見的C內存管理錯誤:常見的與內存管理相關的編程缺陷包括:初始化錯誤、未檢查返回值、對空指針或無效指針解引用、引用已釋放的內存、對同一塊內存釋放多次、內存泄漏和零長度分配。

初始化錯誤:由malloc函數返回的空間中的值是不確定的。一個常見的錯誤是不正確地假設malloc把分配的內存的所有位都初始化爲零。

// 讀取未初始化的內存
void test_memory_init_error()
{
	// 初始化大的內存塊可能會降低性能並且不總是必要的.
	// C標準委員會決定不需要malloc來初始化這個內存,而把這個決定留給程序員
	int n = 5;
	int* y = static_cast<int*>(malloc(n * sizeof(int)));
	int A[] = {1, 2, 3, 4, 5};

	for (int i = 0; i < n; ++i) {
		y[i] += A[i];
	}

	std::for_each(y, y+n, [](int v) { fprintf(stdout, "value: %d\n", v); });
	free(y);
}

不要假定內存分配函數初始化內存。不要引用未初始化的內存

清除或覆寫內存通常是通過調用C標準的memset函數來完成的。遺憾的是,如果不在寫後訪問內存,編譯器優化可能會默默地刪除對memset函數的調用。

未檢查返回值:內存分配函數的返回值表示分配失敗或成功。如果請求的內存分配失敗,那麼aligned_alloc、calloc、malloc和realloc函數返回空指針

// 檢查malloc的返回值
int* test_memory_return_value()
{
	// 如果不能分配請求的空間,那麼C內存分配函數將返回一個空指針
	int n = 5;
	int* ptr = static_cast<int*>(malloc(sizeof(int) * n));
	if (ptr != nullptr) {
		memset(ptr, 0, sizeof(int) * n);
	} else {
		fprintf(stderr, "fail to malloc\n");
		return nullptr;
	}

	return ptr;
}

Null或無效指針解引用:用一元操作符”*”解引用的指針的無效值包括:空指針、未按照指向的對象類型正確對齊的地址、生存期結束後的對象的地址。

空指針的解引用通常會導致段錯誤,但並非總是如此。許多嵌入式系統有映射到地址0處的寄存器,因此覆寫它們會產生不可預知的後果。在某些情況下,解引用空指針會導致任意代碼的執行

引用已釋放內存:除非指向某塊內存的指針已設置爲NULL或以其它方式被覆寫,否則就有可能訪問已被釋放的內存。

// 引用已釋放內存
void test_memory_reference_free()
{
	int* x = static_cast<int*>(malloc(sizeof(int)));
	*x = 100;
	free(x);
	// 從已被釋放的內存讀取是未定義的行爲
	fprintf(stderr, "x: %d\n", *x);
	// 寫入已經被釋放的內存位置,也不大可能導致內存故障,但可能會導致一些嚴重的問題
	*x = -100;
	fprintf(stderr, "x: %d\n", *x);
}

從已被釋放的內存讀取是未定義的行爲,但在沒有內存故障時幾乎總能成功,因爲釋放的內存是被內存管理器回收的。然而,並不保證內存的內容沒有被篡改過。雖然free函數調用通常不會擦除內存,但內存管理器可能使用這個空間的一部分來管理釋放或未分配的內存。如果內存塊已被重新分配,那麼其內容可能已經被全部替換。其結果是,這些錯誤可能檢測不出來,因爲內存中的內容可能會在測試過程中被保留,但在運行過程中被修改。

寫入已經被釋放的內存位置,也不太可能導致內存故障,但可能會導致一些嚴重的問題。如果該塊內存已被重新分配,程序員就可以覆寫此內存,一個內存塊是專門(dedicated)爲一個特定的變量分配的,但在現實中,它是被共享(shared)的。在這種情況下,該變量中包含最後一次寫入的任何數據。如果那塊內存沒有被重新分配,那麼寫入已釋放的塊可能會覆寫並損壞內存管理器所使用的數據結構。

多次釋放內存:最常見的場景是兩次釋放(double-free)。這個錯誤是危險的,因爲它會以一種不會立即顯現的方式破壞內存管理器中的數據結構。

// 多次釋放內存
void test_memory_multi_free()
{
	int* x = static_cast<int*>(malloc(sizeof(int)));
	free(x);
	// 多次釋放相同的內存會導致可以利用的漏洞
	free(x);
}

內存泄漏:當動態分配的內存不再需要後卻沒有被釋放時,就會發生內存泄漏。

零長度分配:C標準規定:如果所要求的空間大小是零,其行爲是實現定義的:要麼返回一個空指針,要麼除了不得使用返回的指針來訪問對象以外,行爲與大小彷彿是某個非零值。

// 零長度分配:不要執行零長度分配
void test_memory_0_byte_malloc()
{
	char* p1 = static_cast<char*>(malloc(0));
	fprintf(stderr, "p1 pointer: %p\n", std::addressof(p1)); // 是不確定的
	free(p1);

	p1 = nullptr;
	char* p2 = static_cast<char*>(realloc(p1, 0));
	fprintf(stderr, "p2 pointer: %p\n", std::addressof(p2)); // 是不確定的
	free(p2);
	
	int nsize = 10;
	char* p3 = static_cast<char*>(malloc(nsize));
	char* p4 = nullptr;
	// 永遠不要分配0個字節
	if ((nsize == 0) || (p4 = static_cast<char*>(realloc(p3, nsize))) == nullptr) {
		free(p3);
		p3 = nullptr;
		return;
	}

	p3 = p4;
	free(p3);
}

此外,要求分配0字節時,成功調用內存分配函數分配的存儲量是不確定的。在內存分配函數返回一個非空指針的情況下,讀取或寫入分配的內存區域將導致未定義的行爲。通常情況下,指針指向一個完全由控制結構組成的零長度的內存塊。覆寫這些控制結構損害內存所使用的數據結構。

realloc函數將釋放舊對象,並返回一個指針,它指向一個具有指定大小的新對象。然而,如果不能爲新對象分配內存,那麼它就不釋放舊對象,而且舊對象的值是不變的。正如malloc(0),realloc(p, 0)的行爲是實現定義的。

不要執行零長度分配

4.3 C++的動態內存管理:在C++中,使用new表達式分配內存並使用delete表達式釋放內存。C++的new表達式分配足夠的內存來保存所請求類型的對象,並可以初始化所分配的內存中的對象。

new表達式是構造一個對象的唯一方法,因爲不可能顯示地調用構造函數。分配的對象類型必須是一個完整的對象類型,並且不可以(例如)是一個抽象類類型或一個抽象類數組。對於非數組對象,new表達式返回一個指向所創建的對象的指針;對於數組,它返回一個指向數組初始元素的指針。new表達式分配的對象有動態存儲期限(dynamic storage duration)。存儲期限定義了該對象包含的存儲的生存期。使用動態存儲的對象的生存期,不侷限於創建該對象所在的範圍。

如果提供了初始化參數(即類的構造函數的參數,或原始整數類型的合法值),那麼由new操作符所分配的內存被初始化。只有一個空的new-initializer()存在時,”普通的舊數據”(POD)類型的對象是new默認初始化(清零)的。這包括所有的內置類型。

void test_memory_new_init()
{
	// 包括所有的內置類型
	int* i1 = new int(); // 已初始化
	int* i2 = new int; // 未初始化
	fprintf(stdout, "i1: %d, i2: %d\n", *i1, *i2);

	// 就地new沒有實際分配內存,所以該內存不應該被釋放
	int* i3 = new (i1) int;
	fprintf(stdout, "i3: %d\n", *i3);

	delete i1;
	delete i2;

	// 通常情況下,分配函數無法分配存儲時拋出一個異常表示失敗
	int* p1 = nullptr;
	try {
		p1 = new int;
	} catch (std::bad_alloc) {
		fprintf(stderr, "fail to new\n");
		return;
	}
	delete p1;

	// 用std::nothrow參數調用new,當分配失敗時,分配函數不會拋出一個異常,它將返回一個空指針
	int* p2 = new(std::nothrow) int;
	if (p2 == nullptr) {
		fprintf(stderr, "fail to new\n");
		return;
	}
	delete p2;
}

就地new(placement new)是另一種形式的new表達式,它允許一個對象在任意內存位置構建。就地new需要在指定的位置有足夠的內存可用。因爲就地new沒有實際分配內存,所以該內存不應該被釋放。

分配函數:必須是一個類的成員函數或全局函數,不能在全局範圍以外的命名空間範圍中聲明,並且不能把它在全局範圍內聲明爲靜態的。分配函數的返回類型是void*。

分配函數試圖分配所請求的存儲量。如果分配成功,則它返回存儲塊的起始地址,該塊的長度(以字節爲單位)至少爲所要求的大小。分配函數返回的分配的存儲空間內容沒有任何限制。連續調用分配函數分配的存儲的順序、連續性、初始值都是不確定的。返回的指針是適當地對齊的,以便它可以被轉換爲任何具有基本對齊要求的完整對象類型的指針,並在之後用於訪問所分配的存儲中的對象或數組(直到調用一個相應的釋放函數顯示釋放這塊存儲)。即使所請求的空間大小爲零,請求也可能會失敗。如果請求成功,那麼返回值是一個非空指針值。如果一個指針是大小爲零的請求返回的,那麼對它解引用的效果是未定義的。C++在發起一個爲零的請求時行爲與C不同,它返回一個非空指針

通常情況下,分配函數無法分配存儲時拋出一個異常表示失敗,這個異常將匹配類型爲std::bad_alloc的異常處理器。

如果用std::nothrow參數調用new,當分配失敗時,分配函數不會拋出一個異常。相反,它將返回一個空指針

當一個異常被拋出時,運行時機制首先在當前範圍內搜索合適的處理器。如果當前範圍內沒有這樣的處理程序存在,那麼控制權將由當前範圍轉移到調用鏈中的一個更高的塊。這個過程一直持續,直到找到一個合適的處理程序爲止。如果在任何級別中都沒有處理程序捕獲該異常,那麼std::terminate函數被自動調用。默認情況下,terminate調用標準C庫函數abort,它會突然退出程序。當abort被調用時,沒有正常的程序終止函數調用發生,這意味着全局和靜態對象的析構函數不執行。

在C++中,處理分配和分配失敗的標準慣用法是資源獲取初始化(Resource Acquisition Is Initializatiton, RAII)。RAII運用C++的對象生存期概念控制程序資源,如內存、文件句柄、網絡連接、審計跟蹤等。要保持對資源的跟蹤,只要創建一個對象,並把資源的生存期關聯到對象的生存期即可。這使你可以使用C++對象管理設施來管理資源。其最簡單的形式是,創建一個對象,它在構造函數獲得資源,並在其析構函數中釋放資源。

class intHandle {
public:
	explicit intHandle(int* anInt) : i_(anInt) {} // 獲取資源
	~intHandle() { delete i_; } // 釋放資源

	intHandle& operator=(const int i)
	{
		*i_ = i;
		return *this;
	}

	int* get() { return i_; } // 訪問資源

private:
	intHandle(const intHandle&) = delete;
	intHandle& operator=(const intHandle&) = delete;
	int* i_;
};

// 資源獲取初始化(Resource Acquisition Is Initialization, RAII)
void test_memory_arii()
{
	intHandle ih(new int);
	ih = 5;
	fprintf(stdout, "value: %d\n", *ih.get());

	// 使用std::unique_ptr能完成同樣的事情,而且更簡單
	std::unique_ptr<int> ip(new int);
	*ip = 5;
	fprintf(stdout, "value: %d\n", *ip.get());
}

如果發生下列任何情況,new表達式拋出std::bad_array_new_length異常,以報告無效的數組長度:(1).數組的長度爲負;(2).新數組的總大小超過實現定義的最大值;(3).在一個大括號初始化列表中的初始值設定子句數量超過要初始化的元素數量(即聲明的數組的大小)。只有數組的第一個維度可能會產生這個異常,除第一個維度外的維度都是常量表達式,它們在編譯時檢查。

// 拋出std::bad_array_new_length的三種情況
void test_memory_bad_array_new_length()
{
	try {
		int negative = -1;
		new int[negative]; // 大小爲負
	} catch(const std::bad_array_new_length& e) {
		fprintf(stderr, "1: %s\n", e.what());
	}

	try {
		int small = 1;
		new int[small]{1, 2, 3}; // 過多的初始化值設定
	} catch(const std::bad_array_new_length& e) {
		fprintf(stderr, "2: %s\n", e.what());
	}

	try {
		int large = INT_MAX;
		new int[large][1000000]; // 過大
	} catch(const std::bad_alloc& e) {
		fprintf(stderr, "3: %s\n", e.what());
	}
}

釋放函數:是類的成員函數或全局函數,在一個全局範圍以外的命名空間範圍聲明釋放函數或全局範圍內聲明靜態的釋放函數都是不正確的。每個釋放函數都返回void,並且它的第一個參數是void*。對於這些函數的兩個參數形式,第一個參數是一個指向需要釋放的內存塊的指針,第二個參數是要釋放的字節數。這種形式可能被用於從基類中刪除一個派生類對象。提供給一個釋放函數的第一個參數的值可以是一個空指針值,如果是這樣的話,並且如果釋放函數是標準庫提供的,那麼該調用沒有任何作用

如果提供給標準庫中的一個釋放函數的參數是一個指針,且它不是空指針值,那麼釋放函數釋放該指針所引用的存儲,引用指向已釋放存儲(deallocated storage)的任何部分的所有指針無效。使用無效的指針值(包括將它傳遞給一個釋放函數)產生的影響是未定義的。

垃圾回收:在C++中,垃圾回收(自動回收不再被引用的內存區域)是可選的,也就是說,一個垃圾回收器(Garbage Collector, GC)不是必須的。一個垃圾回收器必須能夠識別動態分配的對象的指針,以便它可以確定哪些對象可達(reachable),不應該被回收,哪些對象不可達(unreachable),並可以回收。

4.4 常見的C++內存管理錯誤:常見的與內存管理相關的編程缺陷,包括未能正確處理分配失敗、解引用空指針、寫入已經釋放的內存、對相同的內存釋放多次、不當配對的內存管理函數、未區分標量和數組,以及分配函數使用不當。

未能正確檢查分配失敗:new表達式要麼成功要麼拋出一個異常。new操作符的nothrow形式在失敗時返回一個空指針,而不是拋出一個異常。

// 未能正確檢查分配失敗
void test_memory_new_wrong_usage()
{
	// new表達式,要麼成功,要麼拋出一個異常
	// 意味着,if條件永遠爲真,而else子句永遠不會被執行
	int* ip = new int;
	if (ip) { // 條件總是爲真
		
	} else {
		// 將永遠不執行
	}
	delete ip;

	// new操作符的nothrow形式在失敗時返回一個空指針,而不是拋出一個異常
	int* p2 = new(std::nothrow)int;
	if (p2) {
		delete p2;
	} else {
		fprintf(stderr, "fail to new\n");
	}
}

不正確配對的內存管理函數:使用new和delete而不是原始的內存分配和釋放。C內存釋放函數std::free不應該被用於由C++內存分配函數分配的資源,C++內存釋放操作符和函數也不應該被用於由C內存分配函數分配的資源。C++的內存分配和釋放函數分配和釋放內存的方式可能不同於C內存分配和釋放函數分配和釋放內存的方式。因此,在同一資源上混合調用C++內存分配和釋放函數及C內存分配和釋放函數是未定義的行爲,並可能會產生災難性的後果。

class Widget {};

// 不正確配對的內存管理函數
void test_memory_new_delete_unpaired()
{
	int* ip = new int(12);
	free(ip); // 錯誤,應使用delete ip

	int* ip2 = static_cast<int*>(malloc(sizeof(int)));
	*ip2 = 12;
	delete ip2; // 錯誤,應使用free(ip2)

	// new和delete操作符用於分配和釋放單個對象
	Widget* w = new Widget();
	delete w;

	// new[]和delete[]操作符用於分配和釋放數組
	Widget* w2 = new Widget[10];
	delete [] w2;

	// operator new()分配原始內存,但不調用構造函數
	std::string* sp = static_cast<std::string*>(operator new(sizeof(std::string)));
	//delete sp; // 錯誤
	operator delete (sp); // 正確
}

在C++的new表達式分配的對象上調用free,因爲free不會調用對象的析構函數。這樣的調用可能會導致內存泄漏、不釋放鎖或其它問題,因爲析構函數負責釋放對象所使用的資源。

new和delete操作符用於分配和釋放單個對象;new[]和delete[]操作符用於分配和釋放數組。

當分配單個對象時,先調用operator new()函數來分配對象的存儲空間,然後調用其構造函數來初始化它。當一個對象被刪除時,首先調用它的析構函數,然後調用相應的operator delete()函數來釋放該對象所佔用的內存。當分配一個對象數組時,先調用operator new[]()對整個數組分配存儲空間,隨後調用對象的構造函數來初始化數組中的每個元素。當刪除一個對象數組時,首先調用數組中的每個對象的析構函數,然後調用operator delete[]()釋放整個數組所佔用的內存。要將operator delete()與operator new()一起使用,並將operator delete[]()與operator new[]()一起使用。如果試圖使用operator delete()刪除整個數組,只有數組的第一個元素所佔用的內存將被釋放,一個明顯的內存泄漏可能會導致被利用。

new和operator new():可以直接調用operator new()分配原始內存,但不調用構造函數

函數operator new()、operator new[]()、operator delete()和operator delete[]()都可以被定義爲成員函數。它們是隱藏繼承的或命名空間範圍中同名函數的靜態成員函數。與其它內存管理函數一樣,重要的是讓它們正確配對。如果對象所使用的內存不是通過調用operator new()獲得的,同時對其使用operator delete(),就可能發生內存損壞。

多次釋放內存:智能指針是一個類類型(class type),它具有重載的”->”和”*”操作符以表現得像指針。比起原始指針,智能指針往往是一種更安全的選擇,因爲它們可以提供原始指針中不存在的增強行爲,如垃圾回收、檢查空,而且防止使用在特定情況下不合適或危險的原始指針操作(如指針算術和指針複製)。引用計數智能指針對它們所引用的對象的引用計數進行維護。當引用計數爲零時,該對象就被銷燬。

釋放函數拋出一個異常:如果釋放函數通過拋出一個異常終止,那麼該行爲是未定義的。釋放函數,包括全局的operator delete()函數,它的數組形式與其用戶定義的重載,經常在銷燬類類型的對象時被調用,其中包括作爲某個異常結果的棧解開。允許棧解開期間拋出異常導致逃避了調用std::terminate,而導致std::abort函數調用的默認效果。這種情況可能被利用爲一種拒絕服務攻擊的機會。因此,釋放函數必須避免拋出異常

4.5 內存管理器:既管理已分配的內存,也管理已釋放的內存。在大多數操作系統中,包括POSIX系統和Windows,內存管理器作爲客戶進程的一部分運行。分配給客戶進程的內存,以及供內部使用而分配的內存,全部位於客戶進程的可尋址內存空間內。操作系統通常提供內存管理器作爲它的一部分(通常是libc的一部分)。在較不常見的情況下,編譯器也可以提供替代的內存管理器。內存管理器可以被靜態鏈接在可執行文件中,也可以在運行時確定。

4.6 Doug Lea的內存分配器:GNU C庫和大多數Linux版本(例如Red Hat、Debian)都是將Doug Lea的malloc實現(dlmalloc)作爲malloc的默認原生版本。Doug Lea獨立地發佈了dlmalloc,其他一些人對其作了修改並用作GNU libc的分配器。

動態分配的內存也可能遭遇緩衝區溢出。例如,緩衝區溢出可被用於破壞內存管理器所使用的數據結構從而能夠執行任意的代碼。

解鏈(unlink)技術:最早由Solar Designer提出。unlink技術被用於利用緩衝區溢出來操縱內存塊的邊界標誌,以欺騙unlink()宏向任意位置寫入4字節數據。

4.7 雙重釋放漏洞:Doug Lea的malloc還易於導致雙重釋放漏洞。這種類型的漏洞是由於對同一塊內存釋放兩次造成的(在這兩次釋放之間沒有對內存進行重新分配)。要成功地利用雙重釋放漏洞,有兩個條件必須滿足:被釋放的內存塊必須在內存中獨立存在(也就是說,其相鄰的內存塊必須是已分配的,這樣就不會發生合併操作了),並且該內存所被放入的筐(在dlmalloc中,空閒塊被組織成環形雙鏈表,或筐(bin))必須爲空。

寫入已釋放的內存:一個常見的安全缺陷。

RtlHeap:並非只有使用dlmalloc開發的應用程序纔可能存在基於堆的漏洞。使用微軟RtlHeap開發的應用程序在內存管理API被誤用時也有可能被利用。與大多數軟件一樣,RtlHeap也在不斷地進化,不同的Windows版本通常都有不同的RtlHeap實現,它們的行爲稍有不同。

4.8 緩解策略:有很多緩解措施可以用來消除或減少基於堆的漏洞。

空指針:一個明顯的可以減少C和C++程序中漏洞數量的技術就是在指針所引用的內存被釋放後,將此指針設置爲NULL。空懸指針(執行已釋放內存的指針)可能導致賦寫已釋放內存和雙重釋放漏洞。將指針置爲NULL後,任何企圖解引用該指針的操作都會導致致命的錯誤,這樣就增加了在實現和測試過程中發現問題的機率。並且,如果指針被設置爲NULL,內存可以被”釋放”多次而不會導致不良後果

一致的內存管理約定:(1).使用同樣的模式分配和釋放內存;(2).在同一個模塊中,在同一個抽象層次中分配和釋放內存;(3).讓分配和釋放配對。

隨機化:傳統的malloc函數調用返回的內存分配地址在很大程度上是可預測的。通過讓內存管理程序返回的內存塊地址隨機化,可以使對基於堆的漏洞利用變得更加困難。

運行時分析工具:Valgrind、Purify、Insure++、Application Verifier。

GitHubhttps://github.com/fengbingchun/Messy_Test

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章