C/C++ 恨透了 double free or corruption

ENG八戒發表於2023-03-25

*以下內容為本人的學習筆記,如需要轉載,請宣告原文連結 微信公眾號「ENG八戒」https://mp.weixin.qq.com/s/IwSVImp5cOB3gZbaf0YiPw

寫過 C/C++ 的都知道,記憶體允許程式設計師自主分配,用完了這些資源也得釋放出來,這種在系統執行過程中動態申請的記憶體,稱為動態記憶體。

常言道,借東西好借好還,下次再借也不難,但是有的人有時候還真的忘了還回去。這要是發生在程式執行時,申請的記憶體沒正常釋放,沒管理好,就避免不了會面對記憶體報錯的問題。

記憶體都允許你自由操縱了,靈活性是真的大,恰恰這也是它的弊端。

今天就來聊聊 C/C++ 的報錯 double free or corruption

怎麼分配和釋放記憶體?

C 語言提供了兩個函式用於分配和釋放記憶體 malloc 和 free,需要引用標頭檔案 <stdlib.h>。<stdlib.h> 是 C 標準庫標頭檔案 為 C 語言程式設計師提供可靠、高效的函式,以實現動態記憶體分配、資料型別轉換、偽隨機數生成、過程控制、搜尋和排序、數學以及多位元組或寬字元函式,還包括一些常用常數,目的是促進組織和平臺間的程式碼標準化。

#include <stdlib.h>
#include <stdio.h>

int main()
{
    int *ptr = malloc(sizeof(int));
    *ptr = 100;
    printf("%d", *ptr);
    free(ptr);
    return 0;
}

輸出:

100

呼叫 malloc 會分配一塊記憶體空間,並將這塊記憶體空間的首地址返回。呼叫時,需要傳入目標記憶體空間的大小,單位按照位元組(Byte)算,而返回的地址資料型別是 void*,所以,根據目標空間的具體用途轉換即可。

這塊記憶體空間在分配之後還屬於未初始化的狀態,如果對記憶體空間的使用比較複雜,建議先用 memset 初始化一下。

記憶體空間使用完,需要使用 free 釋放掉,避免閒置浪費,否則就算是記憶體洩漏了。記憶體洩露會直到程式程式結束為止。

在其它的高階語言裡,比如 Java、Python 等,出於記憶體安全的考慮,都不會允許使用者自己管理記憶體,而 C++ 是個例外,這可能來自於 C 語言的傳承。

C++ 裡同樣提供了 malloc 和 free,但是引用的標頭檔案變成了 是 <stdlib.h> 增強版,而且所有內容都在名稱空間內宣告,所以使用前必須透過名稱空間引用。

另外 C++ 還提供了兩個額外的運算子用於分配和釋放記憶體,分別是 new 和 delete。

#include <iostream>
using namespace std;

int main()
{
    int *ptr = new int;
    *ptr = 100;
    cout << *ptr << endl;
    delete ptr;
    return 0;
}

輸出:

100

關鍵詞 new 後接上一個資料型別,然後分配和資料型別 int 對應大小的記憶體空間,並返回首地址。對應地,new 申請的記憶體空間被使用完不再需要時,應該使用關鍵詞 delete 釋放,delete 直接操作記憶體空間首地址。

出現 double free or corruption Error

借來的錢用得可以很爽,是的,常人都這樣。不過,每到要還錢的時候就特別不情願,要麼推三推四,要麼直接抵賴,一不留神就忘了是否有還過這事。

比如,張三本來一直在外租房將就著過日子,隨著家裡人口逐漸增多,就和老婆合計著從銀行貸了一筆資金準備買房嘛,貸了款之後,銀行貸款經理就告訴他,“張先生,你們家以後每月就得由一名代表人來還貸款,不需要幾個人同時還的,記住了哈!”

好了,這個故事給了我們什麼啟發呢?就是資金或者資源的借入借出需要有一個管理人,這樣可以避免混亂進而出錯。

同樣的,在 C/C++ 的程式設計裡邊,經常會出現一些記憶體資源管理混亂而出現的報錯甚至執行時崩潰的問題,比如 double free or corruption。

#include <iostream>
using namespace std;

int main()
{
    int *ptr = new int;
    *ptr = 100;
    cout << *ptr << endl;
    delete ptr;
    delete ptr;
    return 0;
}

執行

100
free(): double free detected in tcache 2
Aborted (core dumped)

程式執行崩潰並報錯 double free,根本原因是對同一記憶體地址呼叫了多次的 free 或 delete 執行釋放,這會導致應用的記憶體管理資料結構被損壞,甚至會允許惡意使用者在記憶體任意區域寫入資料。這類損壞會導致程式崩潰或者程式的部分執行流程被改變。如果攻擊者這個時候特意覆蓋特定的暫存器或者記憶體區域來引導執行他們的程式碼,進而可以產生提升許可權的互動式 shell,這樣就完全被破防了。

這也算是記憶體洩漏的一種,系統一旦檢測到 double free 也會終止程式繼續執行(Aborted)。

記憶體被釋放之後會發生什麼?

一塊記憶體被釋放之後,空閒的記憶體會被放入連結串列中,用於重新管理和組合不同的空閒記憶體碎片,便於將來用於分配更大的記憶體空間。這個連結串列屬於雙向連結串列,每個空閒的記憶體空間都可以往前和往後查詢其它記憶體空間。

那麼攻擊者可以利用這個過程嗎?

答案是肯定的。當 free 被呼叫時,攻擊者可以讓原本需要被連結串列管理的空閒記憶體取消連結,覆蓋暫存器值並從緩衝區載入shell程式碼,最終往記憶體寫入任意值。

常見的觸發情形

上面的示例程式碼簡單演示了 double free 的觸發,平常出現這種報錯的條件並不比上面的情形要複雜多少。比如,釋放同一塊記憶體的動作在相隔了幾百甚至更多行的位置執行,有的還發生在不同原始碼檔案,這就會讓程式設計師容易多次釋放。下面嘗試總結一下,來看一下常見的犯錯情形:

  1. 釋放前判斷的條件錯誤或者其它不常見的情況
  2. 記憶體被釋放後還在使用
  3. 記憶體釋放的管理責任方混亂

如何避免

其實,細看一下上面總結的幾種常見犯錯情形,我們也可以很好地避免低階錯誤。

有個最佳實踐是,分配的記憶體地址儲存變數 ptr 在定義宣告時就應該初始化為 NULL,記憶體被釋放後應立刻將 ptr 置為 NULL,使用這塊記憶體或者釋放前應該遵循先判斷記憶體空間是否有效的原則,簡單點可以用 (ptr != NULL)。

另外,負責釋放的管理責任方應該儘量單一,即使橫跨多個原始檔或模組。這裡有個道理就是避免”多龍治水“。

中國在過去一直是個農業大國,有著重農輕商的歷史,各種典故都有著農業的影子。

相傳,幾龍治水、幾牛耕地那是對當年農業收成的預示,不妨翻一下老黃曆看看? “龍”是管雨的神,以五龍治水可獲風調雨順,因東南西北中都有神龍,各施其職。龍少了當年就要發大水;龍多了當年將要天大旱。原因是管雨的龍神少了怕管不過來,就忙忙碌碌四處播雨以至大澇;管雨的龍神多了呢,就像“三個和尚無水吃”一樣以至大旱。至於澇到什麼程度還看治水的龍少到什麼程度,龍越少澇得越嚴重。旱的程度亦一樣。

因此就有了“龍多不下雨”的諺語。

計算機程式設計說到底還是程式設計師的思維體現,人情世故也會反映在程式碼的邏輯上。

相關文章