1. 程式人生 > >Linux記憶體洩漏與溢位

Linux記憶體洩漏與溢位

Linux系統下真正有危害的是記憶體洩漏的堆積,這會最終消耗盡系統任何的記憶體。下面是排查和解決方案與大家一起分享。

1、Linux 記憶體監控記憶體洩漏的定義:

一般我們常說的記憶體洩漏是指堆記憶體的洩漏。堆記憶體是指程式從堆中分配的,大小任意的(記憶體塊的大小能夠在程式執行期決定),使用完後必須顯示釋放的記憶體。應用程式一般使用malloc,realloc,new等函式從堆中分配到一塊記憶體,使用完後,程式必須負責相應的呼叫free或delete釋放該記憶體塊,否則,這塊記憶體就不能被再次使用,我們就說這塊記憶體洩漏了。

2、Linux 記憶體監控記憶體洩露的危害

從使用者使用程式的角度來看,記憶體洩漏本身不會產生什麼危害,作為一般的使用者,根本感覺不到記憶體洩漏的存在。真正有危害的是記憶體洩漏的堆積,這會最終消耗盡系統任何的記憶體。從這個角度來說,一次性記憶體洩漏並沒有什麼危害,因為他不會堆積,而隱式記憶體洩漏危害性則很大,因為較之於常發性和偶發性記憶體洩漏他更難被檢測到。存在記憶體洩漏問題的程式除了會佔用更多的記憶體外,還會使程式的效能急劇下降。對於伺服器而言,假如出現這種情況,即使系統不崩潰,也會嚴重影響使用。

3、Linux 記憶體監控記憶體洩露的檢測和回收

對於記憶體溢位之類的麻煩可能大家在編寫指標比較多的複雜的程式的時候就會碰到。在 Linux 或 unix 下,C、C++語言是最使用工具。但是我們的 C++ 程式缺乏相應的手段來檢測記憶體資訊,而只能使用 top 指令觀察程序的動態記憶體總額。而且程式退出時,我們無法獲知任何記憶體洩漏資訊。

使用kill命令

使用Linux命令回收記憶體,我們能夠使用Ps、Kill兩個命令檢測記憶體使用情況和進行回收。在使用終極使用者許可權時使用命令“Ps”,他會列出任何正在執行的程式名稱,和對應的程序號(PID)。Kill命令的工作原理是:向Linux作業系統的核心送出一個系統操作訊號和程式的程序號(PID)。

應用例子:

為了高效率回收記憶體能夠使用命令ps 引數v:

[root@www ~]# ps v(或aux)

PID TTY STAT TIME MAJFL TRS DRS RSS %MEM COMMAND

2542 tty1Ss+ 0:00 08 1627 428 0.1 /sbin/mingetty tty1

2543 tty2Ss+ 0:00 08 1631 428 0.1 /sbin/mingetty tty2

2547 tty3Ss+ 0:00 08 1631 432 0.1 /sbin/mingetty tty3

2548 tty4Ss+ 0:00 08 1627 428 0.1 /sbin/mingetty tty4

2574 tty5Ss+ 0:00 08 1631 432 0.1 /sbin/mingetty tty5

2587 tty6Ss+ 0:00 08 1627 424 0.1 /sbin/mingetty tty6

2657 tty7Ss+ 1:1812 1710 29981 7040 3.0 /usr/bin/Xorg :0 -br -a

2670 pts/2 Ss0:01 2 682 6213 1496 0.6 -bash

3008 pts/4 Ss0:00 2 682 6221 1472 0.6 /bin/bash

3029 pts/4 S+0:00 2 32 1783 548 0.2 ping 192.168.1.12

3030 pts/2 R+0:00 2 73 5134 768 0.3 ps v

然後假如想回收Ping命令的記憶體的話,使用命令:

# Kill -9 3029

記憶體溢位就是你要求分配的記憶體超出了系統能給你的,系統不能滿足需求,於是產生溢位。

比方說棧,棧滿時再做進棧必定產生空間溢位,叫上溢,棧空時再做退棧也產生空間溢位,稱為下溢。

這是程式語言中的一個概念,典型的,在C語言中,在分配陣列時為其分配的長度是1024,但往其中裝入超過1024個數據時,由於C語言不會對陣列操作進行越界檢查,就會造成記憶體溢位錯誤 

在程式設計師設計的程式碼中包含的“記憶體溢位”漏洞實在太多了。本文將給大家介紹記憶體溢位問題的產生根源、巨大危害和解決途徑。
一、為什麼會出現記憶體溢位問題?
導致記憶體溢位問題的原因有很多,比如:
(1) 使用非型別安全(non-type-safe)的語言如 C/C++ 等。
(2) 以不可靠的方式存取或者複製記憶體緩衝區。
(3) 編譯器設定的記憶體緩衝區太靠近關鍵資料結構。
下面來分析這些因素:
1. 記憶體溢位問題是 C 語言或者 C++ 語言所固有的缺陷,它們既不檢查陣列邊界,又不檢查型別可靠性(type-safety)。眾所周知,用 C/C++ 語言開發的程式由於目的碼非常接近機器核心,因而能夠直接訪問記憶體和暫存器,這種特性大大提升了 C/C++ 語言程式碼的效能。只要合理編碼,C/C++ 應用程式在執行效率上必然優於其它高階語言。然而,C/C++ 語言導致記憶體溢位問題的可能性也要大許多。其他語言也存在內容溢位問題,但它往往不是程式設計師的失誤,而是應用程式的執行時環境出錯所致。
2. 當應用程式讀取使用者(也可能是惡意攻擊者)資料,試圖複製到應用程式開闢的記憶體緩衝區中,卻無法保證緩衝區的空間足夠時(換言之,假設程式碼申請了 N 位元組大小的記憶體緩衝區,隨後又向其中複製超過 N 位元組的資料)。記憶體緩衝區就可能會溢位。想一想,如果你向 12 盎司的玻璃杯中倒入 16 盎司水,那麼多出來的 4 盎司水怎麼辦?當然會滿到玻璃杯外面了!
3. 最重要的是,C/C++ 編譯器開闢的記憶體緩衝區常常鄰近重要的資料結構。現在假設某個函式的堆疊緊接在在記憶體緩衝區後面時,其中儲存的函式返回地址就會與記憶體緩衝區相鄰。此時,惡意攻擊者就可以向記憶體緩衝區複製大量資料,從而使得記憶體緩衝區溢位並覆蓋原先保存於堆疊中的函式返回地址。這樣,函式的返回地址就被攻擊者換成了他指定的數值;一旦函式呼叫完畢,就會繼續執行“函式返回地址”處的程式碼。非但如此,C++ 的某些其它資料結構,比如 v-table 、例外事件處理程式、函式指標等,也可能受到類似的攻擊。
好,閒話少說,現在來看一個具體的例子。
請思考:以下程式碼有何不妥之處?
void CopyData(char *szData) {
char cDest[32];
strcpy(cDest,szData);
// 處理 cDest
}
奇怪,這段程式碼好象沒什麼不對勁啊!確實,只有呼叫上述 CopyData() 才會出問題。例如:這樣使用 CopyData() 是安全的:
char *szNames[] = {"Michael","Cheryl","Blake"};
CopyData(szName[1]);
為什麼呢?因為陣列中的姓名("Michael"、"Cheryl"、"Blake")都是字串常量,而且長度都不超過 32 個字元,用它們做 strcpy() 的引數總是安全的。再假設 CopyData 的唯一引數 szData 來自 socket 套接字或者檔案等不可靠的資料來源。由於 strcpy 並不在乎資料來源,只要沒遇上空字元,它就會一個字元一個字元地複製 szData 的內容。此時,複製到 cDest 的字串就可能超過 32 字元,進而導致記憶體緩衝區 cDest 的溢位;溢位的字元就會取代記憶體緩衝區後面的資料。不幸的是,CopyData 函式的返回地址也在其中!於是,當 CopyData 函式呼叫完畢以後,程式就會轉入攻擊者給出的“返回地址”,從而落入攻擊者的圈套!授人以柄,慘!
前面提到的其它資料結構也可能受到類似的攻擊。假設有人利用記憶體溢位漏洞覆蓋了下列 C++ 類中的 v-table :
void CopyData(char *szData) {
char cDest[32];
CFoo foo;
strcpy(cDest,szData);
foo.Init();
}
與其它 C++ 類一樣,這裡的 CFoo 類也對應一個所謂的 v-table,即用於儲存一個類的全部方法地址的列表。若攻擊者利用記憶體溢位漏洞偷換了 v-table 的內容,則 CFoo 類中的所有方法,包括上述 Init() 方法,都會指向攻擊者給出的地址,而不是原先 v-table 中的方法地址。順便說一句,即使你在某個 C++ 類的原始碼中沒有呼叫任何方法,也不能認為這個類是安全的,因為它在執行時至少需要呼叫一個內部方法——析構器(destructor)!當然,如果真有一個類沒有呼叫任何方法,那麼它的存在意義也就值得懷疑了。
二、解決記憶體溢位問題
不要太悲觀,下面討論記憶體溢位問題的解決和預防措施。
1、改用受控程式碼
2002 年 2 月和 3 月,微軟公司展開了 Microsoft Windows Security Push 活動。在此期間,我所在的小組一共培訓了超過 8500 人,教授他們如何在設計、測試和文件編制過程中解決安全問題。在我們向所有程式設計人員提出的建議中,有一條就是:緊跟微軟公司軟體開發策略的步伐,將某些應用程式和工具軟體由原先基於本地 Win32 的 C++ 程式碼改造成基於 .NET 的受控程式碼。我們的理由很多,但其中最根本的一條,就是為了解決記憶體溢位問題。基於受控程式碼的軟體發生記憶體溢位問題的機率要小得多,因為受控程式碼無法直接存取系統指標、暫存器或者記憶體。作為開發人員,你應該考慮(至少是打算)用受控程式碼改寫某些應用程式或工具軟體。例如:企業管理工具就是很好的改寫物件之一。當然,你也應該很清楚,不可能在一夜之間把所有用 C++ 開發的軟體用 C# 之類的受控程式碼語言改寫。
2、遵守黃金規則
當你用 C/C++ 書寫程式碼時,應該處處留意如何處理來自使用者的資料。如果一個函式的資料來源不可靠,又用到記憶體緩衝區,那麼它就必須嚴格遵守下列規則:
  • 必須知道記憶體緩衝區的總長度。
  • 檢驗記憶體緩衝區。
  • 提高警惕。
讓我們來具體看看這些“黃金規則”:
(1)必須知道記憶體緩衝區的總長度。
類似這樣的程式碼就有可能導致 bug :
void Function(char *szName) {
char szBuff[MAX_NAME];
// 複製並使用 szName
strcpy(szBuff,szName);
}
它的問題出在函式並不知道 szName 的長度是多少,此時複製資料是不安全的。正確的做法是,在複製資料前首先獲取 szName 的長度:
void Function(char *szName, DWORD cbName) {
char szBuff[MAX_NAME];
// 複製並使用 szName
if (cbName < MAX_NAME)
strcpy(szBuff,szName);
}
這樣雖然有所改進,可它似乎又過於信任 cbName 了。攻擊者仍然有機會偽造 czName 和 szName 兩個引數以謊報資料長度和記憶體緩衝區長度。因此,你還得檢檢這兩個引數!
(2)檢驗記憶體緩衝區
如何知道由引數傳來的記憶體緩衝區長度是否真實呢?你會完全信任來自使用者的資料嗎?通常,答案是否定的。其實,有一種簡單的辦法可以檢驗記憶體緩衝區是否溢位。請看如下程式碼片斷:
void Function(char *szName, DWORD cbName) {
char szBuff[MAX_NAME];
// 檢測記憶體
szBuff[cbName] = 0×42;
// 複製並使用 szName
if (cbName < MAX_NAME)
strcpy(szBuff,szName);
}
這段程式碼試圖向欲檢測的記憶體緩衝區末尾寫入資料 0×42 。你也許會想:真是多此一舉,何不直接複製記憶體緩衝區呢?事實上,當記憶體緩衝區已經溢位時,一旦再向其中寫入常量值,就會導致程式程式碼出錯並中止執行。這樣在開發早期就能及時發現程式碼中的 bug 。試想,與其讓攻擊者得手,不如及時中止程式;你大概也不願看到攻擊者隨心所欲地向記憶體緩衝區複製資料吧。
(3)提高警惕
雖然檢驗記憶體緩衝區能夠有效地減小記憶體溢位問題的危害,卻不能從根本上避免記憶體溢位攻擊。只有從原始碼開始提高警惕,才能真正免除記憶體溢位攻擊的危脅。不錯,上一段程式碼已經很對使用者資料相當警惕了。它能確保複製到記憶體緩衝區的資料總長度不會超過 szBuff 的長度。然而,某些函式在使用或複製不可靠的資料時也可能潛伏著記憶體溢位漏洞。為了檢查你的程式碼是否存在記憶體溢位漏洞,你必須儘量追蹤傳入資料的流向,向程式碼中的每一個假設提出質疑。一旦這麼做了,你將會意識到其中某些假設是錯誤的;然後你還會驚訝地叫道:好多 bug 呀!
在呼叫 strcpy、strcat、gets 等經典函式時當然要保持警惕;可對於那些所謂的第 n 版 (n-versions) strcpy 或 strcat 函式 —— 比如 strncpy 或 strncat (其中 n = 1,2,3 ……) —— 也不可輕信。的確,這些改良版本的函式是安全一些、可靠一些,因為它們限制了進入記憶體緩衝區的資料長度;然而,它們也可能導致記憶體溢位問題!請看下列程式碼,你能指出其中的錯誤嗎?
#define SIZE(b) (sizeof(b))
char buff[128];
strncpy(buff,szSomeData,SIZE(buff));
strncat(buff,szMoreData,SIZE(buff));
strncat(buff,szEvenMoreData,SIZE(buff));
給點提示:請注意這些字串函式的最後一個引數。怎麼,棄權?我說啊,如果你是執意要放棄那些“不安全”的經典字串函式,並且一律改用“相對安全”的第 n 版函式的話,恐怕你這下半輩子都要為了修復這些新函式帶來的新 bug 而疲於奔命了。呵呵,開個玩笑而已。為何這麼說?首先,它們的最後一個引數並不代表記憶體緩衝區的總長度,它們只是其剩餘空間的長度;不難看出,每執行完一個 strn… 函式,記憶體緩衝區 buff 的長度就減少一些。其次,傳遞給函式的記憶體緩衝區長度難免存在“大小差一”(off-by-one)的誤差。你認為在計算 buff 的長度時包括了字串末尾的空字元嗎?當我向聽眾提出這個問題時,得到的肯定答覆和否定答覆通常是 50 比 50 ,對半開。也就是說,大約一半人認為計算了末尾的空字元,而另一半人認為忽略了該字元。最後,那些第 n 版函式所返回的字串不見得以空字元結束,所以在使用前務必要仔細閱讀它們的說明文件。
3、編譯選項 /GS
“/GS”是 Visual C++ .NET 新引入的一個編譯選項。它指示編譯器在某些函式的堆疊幀(stack-frames) 中插入特定資料,以幫助消除針對堆疊的記憶體溢位問題隱患。切記,使用該選項並不能替你清除程式碼中的記憶體溢位漏洞,也不可能消滅任何 bug 。它只是亡羊補牢,讓某些記憶體溢位問題隱患無法演變成真正的記憶體溢位問題;也就是說,它能防止攻擊者在發生記憶體溢位時向程序中插入和執行惡意程式碼。無論如何,這也算是小小的安全保障吧。請注意,在新版的本地 Win32 C++ 中使用 Win32 應用程式嚮導建立新專案時,預設設定已經打開了此選項。同樣,Windows .NET Server 環境也預設打開了此選項。關於 /GS 選項的更多資訊,請參考 Brandon Bray 的《Compiler Security Checks In Depth》一書。
三、請指出安全漏洞
最後,請大家看一段程式碼,其中存在至少一個安全漏洞。你發現了嗎?在後續文章中將會給出答案!
WCHAR g_wszComputerName[INTERNET_MAX_HOST_NAME_LENGTH + 1];
// 獲取伺服器名,再將它轉成 Unicode 字串。
BOOL GetServerName (EXTENSION_CONTROL_BLOCK *pECB) {
DWORD dwSize = sizeof(g_wszComputerName);
char szComputerName[INTERNET_MAX_HOST_NAME_LENGTH + 1];
if (pECB->GetServerVariable (pECB->ConnID,
"SERVER_NAME",
szComputerName,
&dwSize)) {

出處:https://blog.csdn.net/tiangwan2011/article/details/7904111