緩沖區(qū)溢出漏洞那些事:C -gets函數(shù)
基本概念
緩沖區(qū)是在數(shù)據(jù)從一個位置傳輸?shù)搅硪粋€位置時臨時保存數(shù)據(jù)的內(nèi)存存儲區(qū)域。當數(shù)據(jù)量超過內(nèi)存緩沖區(qū)的存儲容量時,就會發(fā)生緩沖區(qū)溢出(或緩沖區(qū)溢出)。結(jié)果,試圖將數(shù)據(jù)寫入緩沖區(qū)的程序會覆蓋相鄰的內(nèi)存位置。
緩沖區(qū)溢出原指當某個數(shù)據(jù)超過了處理程序回傳堆棧地址限制的范圍時,程序出現(xiàn)的異常操作。造成此現(xiàn)象的原因有:
- 存在缺陷的程序設(shè)計
- 尤其是C語言?,不像其他一些高級語言?會自動進行數(shù)組或者指針的堆棧區(qū)塊邊界檢查,增加溢出風險。
- C語言中的C標準庫還具有一些非常危險的操作函數(shù),使用不當也為溢出創(chuàng)造條件。
什么是緩沖區(qū)溢出攻擊
攻擊者通過覆蓋應(yīng)用程序的內(nèi)存來利用緩沖區(qū)溢出問題。這會改變程序的執(zhí)行路徑,觸發(fā)損壞文件或暴露私人信息的響應(yīng)。例如,攻擊者可能會引入額外的代碼,向應(yīng)用程序發(fā)送新指令以訪問 IT 系統(tǒng)。
如果攻擊者知道程序的內(nèi)存布局,他們可以故意提供緩沖區(qū)無法存儲的輸入,并覆蓋保存可執(zhí)行代碼的區(qū)域,用他們自己的代碼替換它。例如,攻擊者可以覆蓋指針(指向內(nèi)存中另一個區(qū)域的對象)并將其指向漏洞利用負載,從而獲得對程序的控制權(quán)。
緩沖區(qū)溢出攻擊的類型
基于堆棧的緩沖區(qū)溢出更為常見,并利用僅在函數(shù)執(zhí)行期間存在的堆棧內(nèi)存。
基于堆的攻擊更難執(zhí)行,并且涉及將分配給程序的內(nèi)存空間泛濫到超出用于當前運行時操作的內(nèi)存。
哪些編程語言更容易受到攻擊?
C 和 C++ 是兩種極易受到緩沖區(qū)溢出攻擊的語言,因為它們沒有內(nèi)置的保護措施來防止覆蓋或訪問內(nèi)存中的數(shù)據(jù)。Mac OSX、Windows 和 Linux 都使用用 C 和 C++ 編寫的代碼。
PERL、Java、JavaScript 和 C# 等語言使用內(nèi)置的安全機制來最大限度地減少緩沖區(qū)溢出的可能性。
如何防止緩沖區(qū)溢出
開發(fā)人員可以通過代碼中的安全措施或使用提供內(nèi)置保護的語言來防止緩沖區(qū)溢出漏洞。
此外,現(xiàn)代操作系統(tǒng)具有運行時保護。三種常見的保護措施是:
- 地址空間隨機化 (ASLR)— 隨機移動數(shù)據(jù)區(qū)域的地址空間位置。通常,緩沖區(qū)溢出攻擊需要知道可執(zhí)行代碼的位置,而隨機化地址空間使得這幾乎不可能。
- 數(shù)據(jù)執(zhí)行預(yù)防——將某些內(nèi)存區(qū)域標記為不可執(zhí)行或可執(zhí)行,從而阻止攻擊在不可執(zhí)行區(qū)域中運行代碼。
- 結(jié)構(gòu)化異常處理程序覆蓋保護 (SEHOP)— 幫助阻止惡意代碼攻擊結(jié)構(gòu)化異常處理 (SEH),這是一種用于管理硬件和軟件異常的內(nèi)置系統(tǒng)。因此,它可以防止攻擊者利用 SEH 覆蓋利用技術(shù)。在功能級別上,使用基于堆棧的緩沖區(qū)溢出來覆蓋存儲在線程堆棧中的異常注冊記錄來實現(xiàn) SEH 覆蓋。
代碼和操作系統(tǒng)保護中的安全措施是不夠的。當組織發(fā)現(xiàn)緩沖區(qū)溢出漏洞時,它必須迅速做出反應(yīng)以修補受影響的軟件,并確保軟件用戶可以訪問補丁。
示例代碼展示
根據(jù)STACK1_VS_2017.cpp代碼進行修改;
#include <stdlib.h>
#include <stdio.h>
#include "Windows.h"
int main(int argc, char **argv) {
MessageBoxA((HWND)-0, (LPCSTR) "緩沖區(qū)溢出測試\n", (LPCSTR)"功能", (UINT)0);
int cookie;
char buf[2];
int *a = &cookie;
char * b = buf;
printf("buf: %08x cookie: %08x\n", b, a);
u_int64 p =(u_int64)a-(u_int64)b;
printf("兩變量內(nèi)存地址之差=%d\n",p);
gets(buf);
if (cookie == 0x41424344)
printf("緩沖區(qū)溢出成功!\n");
}
運行效果展示;
使用MessageBoxA函數(shù)檢測程序是否正常運行,點擊確定開始測試
使用printf()函數(shù)輸出提示信息,使用gets()函數(shù)獲取用戶輸入信息;
任意輸入兩個數(shù)值,不滿足條件,程序運行完畢;
代碼分析漏洞成因
誘因:char buf[2]; 代碼部分解析---使用char 將變量buf聲明成了一個擁有2個元素數(shù)組其中元素類型為字符.buf有了兩個自己長度,
提示:u_int64 p =(u_int64)a-(u_int64)b; 代碼部分對程序涉及變量了內(nèi)存地址進行了一個減分計算并對賦值給變量 p,(為使其運算成立還對其進行了類型轉(zhuǎn)義),結(jié)果可告知兩個變量內(nèi)存地址距離,方便溢出利用
隱患:使用gets()函數(shù)獲取輸入數(shù)據(jù),因gets()函數(shù)無限讀取數(shù)據(jù)并不檢查緩沖區(qū)的大小限制,會將超出緩沖區(qū)的數(shù)據(jù)繼續(xù)寫入堆棧,導(dǎo)致存在溢出隱患。
為方便理解此處代碼演示下:
#include <stdlib.h>
#include <stdio.h>
#include "Windows.h"
int main() {
char test[] = "test1";
printf("test1初始值為%s\n清輸入st值:",test);
char st[2];
gets(st);
printf("輸出test:%s\n",test);
printf("輸出st:%s\n",st);
}
在運行效果上可以看到超出堆??臻g的值繼續(xù)寫入堆棧導(dǎo)致覆蓋了test在堆棧內(nèi)對應(yīng)的值,導(dǎo)致其數(shù)值進行了改變:test1-3456
反匯編分析其運行過程堆棧是如何變化的
有運行得知(外加源代碼)程序初始關(guān)鍵詞:test1初始值為test1。
可通過此關(guān)鍵詞,在反匯編程序快速定位到程序相關(guān)函數(shù)運行區(qū)域。
在入口指令處下斷點方便分析。
并在實際運行發(fā)現(xiàn)運行到此處為顯示相關(guān)特征字符信息,初步判斷正確。
將此區(qū)域字符串進行反編譯與源碼對照進一步驗證。
沒匯編指令對照不太直觀換個插件與工具,進行展示。
未輸入st值時test對應(yīng)數(shù)據(jù)堆棧情況。
000000000061FDE8 000000000061FE0A "test1"。
輸入后查看。
000000000061FDE8 000000000061FE08 "123456"。
000000000061FDF0 000000000061FE0A "3456" //之前為test1。
根據(jù)此思路分析之前的示例程序。
反匯編分析
根據(jù)之前的思路定位關(guān)閉區(qū)域。
定位特征代碼:
根據(jù)特征代碼搜索;
在入口指令處下斷點;
運行看到一個變量的對應(yīng)地址;
繼續(xù)運行看到另一個變量的地址;
因剩下的只是運行用就不展示了。
輸入數(shù)值后可看到由之前空白數(shù)據(jù)已被輸入的數(shù)據(jù)覆蓋;
擴展知識:發(fā)現(xiàn)棧中對應(yīng)的值與內(nèi)存對應(yīng)的值是相反的,因為堆棧中的值遵循先進后出原則故是相反的。
擴展分析
看到此區(qū)域有個je跳轉(zhuǎn)指令,其上方有個cmp指令(功能是用來比較),其中有一值被固定為:41424344。
可以看到明顯不滿足跳轉(zhuǎn)故跳轉(zhuǎn)不會執(zhí)行,不顯示隱藏的信息。
根據(jù)此思路:輸入兩值將緩沖區(qū)堆滿,之后數(shù)據(jù)溢出覆蓋思路,并且根據(jù)提示計算出兩變量相距離為2與堆棧數(shù)據(jù)先進后出原則,故輸入如下條件即可使跳轉(zhuǎn)成立,進而輸出隱藏信息
12DCBA;
跳轉(zhuǎn)成立;
可以看到隱藏信息已顯示;
擴展知識
RSP+2C = rsp對應(yīng)的地址+2c 即000000000061FDE0+2C=61FE0C;
將16進制轉(zhuǎn)10進制計算;
IDA調(diào)試分析使其跟清晰地去查看變化;
為方便分析可在關(guān)鍵函數(shù)處下斷點單步執(zhí)行看執(zhí)行效果;
由之前可知MesssageBoxA后才開始正式運行,故可在此call后下斷點;
運行,直到到斷點暫停;
運行后發(fā)現(xiàn)rsi對應(yīng)數(shù)值無變化,正常因除了初始賦值后無其它賦值給rsi的指令;
gets函數(shù)執(zhí)行前rsi對應(yīng)數(shù)據(jù)仍未變化;
gets函數(shù)執(zhí)行后rsi對應(yīng)數(shù)據(jù)發(fā)生改變;
ush rsi
.text:00000000004078D1 push rbx
.text:00000000004078D2 sub rsp, 38h
.text:00000000004078D6 call __main
.text:00000000004078DB lea rsi, [rsp+48h+var_1E] 初始rsi值
.text:00000000004078E0 xor r9d, r9d ; uType
.text:00000000004078E3 xor ecx, ecx ; hWnd
.text:00000000004078E5 lea rbx, [rsp+48h+var_1C]
.text:00000000004078EA lea r8, Caption ; lpCaption
.text:00000000004078F1 lea rdx, Text ; lpText
.text:00000000004078F8 call cs:__imp_MessageBoxA
.text:00000000004078FE mov r8, rbx
.text:0000000000407901 mov rdx, rsi
.text:0000000000407904 sub rbx, rsi
.text:0000000000407907 lea rcx, aBuf08xCookie08 ; "buf: %08x cookie: %08x\n"
.text:000000000040790E call _Z6printfPKcz ; printf(char const*,...)
.text:0000000000407913 lea rcx, byte_40902D ; char *
.text:000000000040791A mov rdx, rbx
.text:000000000040791D call _Z6printfPKcz ; printf(char const*,...)
.text:0000000000407922 mov rcx, rsi
.text:0000000000407925 call gets
.text:000000000040792A cmp [rsp+48h+var_1C], 41424344h
.text:0000000000407932 jz short loc_40793D
根據(jù)此區(qū)域匯編代碼分析得知,除了初始運行對rsi進行賦值后均無對其賦值的指令,故可得知存在溢出漏洞,覆蓋了rsi原始數(shù)據(jù),導(dǎo)致其值改變,根據(jù)單步跟蹤執(zhí)行流程可值執(zhí)行完gets函數(shù)后其值發(fā)生變化,故可判斷使用gets函數(shù)獲取的數(shù)據(jù)導(dǎo)致堆棧數(shù)據(jù)溢出。
修復(fù)
可使用fgets或gets_s函數(shù)替換gets函數(shù)。
函數(shù)解析:
fgets()函數(shù)的第2個參數(shù)指明了讀入字符的最大數(shù)量。如果該參數(shù)的值是n,那么fgets()將讀入n-1個字符,或者讀到遇到的第一個換行符為止;
這里為3,故獲取的數(shù)值為12,共輸入6個數(shù)讀取到第2個結(jié)束
IDA附加進程并單步執(zhí)行查看執(zhí)行流程;
fgets執(zhí)行前rsi對應(yīng)數(shù)據(jù);
fgets執(zhí)行后rsi對應(yīng)數(shù)據(jù)
可以看到數(shù)據(jù)未溢出,分析到此告一段落。