詳解C語(yǔ)言之緩沖區(qū)溢出
一、緩沖區(qū)溢出原理
棧幀結(jié)構(gòu)的引入為高級(jí)語(yǔ)言中實(shí)現(xiàn)函數(shù)或過(guò)程調(diào)用提供直接的硬件支持,但由于將函數(shù)返回地址這樣的重要數(shù)據(jù)保存在程序員可見的堆棧中,因此也給系統(tǒng)安全帶來(lái)隱患。若將函數(shù)返回地址修改為指向一段精心安排的惡意代碼,則可達(dá)到危害系統(tǒng)安全的目的。此外,堆棧的正確恢復(fù)依賴于壓棧的EBP值的正確性,但EBP域鄰近局部變量,若編程中有意無(wú)意地通過(guò)局部變量的地址偏移竄改EBP值,則程序的行為將變得非常危險(xiǎn)。
由于C/C++語(yǔ)言沒有數(shù)組越界檢查機(jī)制,當(dāng)向局部數(shù)組緩沖區(qū)里寫入的數(shù)據(jù)超過(guò)為其分配的大小時(shí),就會(huì)發(fā)生緩沖區(qū)溢出。攻擊者可利用緩沖區(qū)溢出來(lái)竄改進(jìn)程運(yùn)行時(shí)棧,從而改變程序正常流向,輕則導(dǎo)致程序崩潰,重則系統(tǒng)特權(quán)被竊取。
例如,對(duì)于下圖的棧結(jié)構(gòu):
若將長(zhǎng)度為16字節(jié)的字符串賦給acArrBuf數(shù)組,則系統(tǒng)會(huì)從acArrBuf[0]開始向高地址填充??臻g,導(dǎo)致覆蓋EBP值和函數(shù)返回地址。若攻擊者用一個(gè)有意義的地址(否則會(huì)出現(xiàn)段錯(cuò)誤)覆蓋返回地址的內(nèi)容,函數(shù)返回時(shí)就會(huì)去執(zhí)行該地址處事先安排好的攻擊代碼。最常見的手段是通過(guò)制造緩沖區(qū)溢出使程序運(yùn)行一個(gè)用戶shell,再通過(guò)shell執(zhí)行其它命令。若該程序有root或suid執(zhí)行權(quán)限,則攻擊者就獲得一個(gè)有root權(quán)限的shell,進(jìn)而可對(duì)系統(tǒng)進(jìn)行任意操作。
除通過(guò)使堆棧緩沖區(qū)溢出而更改返回地址外,還可改寫局部變量(尤其函數(shù)指針)以利用緩沖區(qū)溢出缺陷。
注意,本文描述的堆棧緩沖區(qū)溢出不同于廣義的“堆棧溢出(Stack OverFlow)”,后者除局部數(shù)組越界和內(nèi)存覆蓋外,還可能由于調(diào)用層次太多(尤其應(yīng)注意遞歸函數(shù))或過(guò)大的局部變量所導(dǎo)致。
二、緩沖區(qū)溢出實(shí)例
本節(jié)給出若干緩沖區(qū)溢出相關(guān)的示例性程序。前三個(gè)示例為手工修改返回地址或?qū)崊?,后兩個(gè)示例為局部數(shù)組越界訪問(wèn)和緩沖區(qū)溢出。更加深入的緩沖區(qū)溢出攻擊參見相關(guān)資料。
示例函數(shù)必須包含stdio.h頭文件,并按需包含string.h頭文件(如strcpy函數(shù))。
【示例1】改變函數(shù)的返回地址,使其返回后跳轉(zhuǎn)到某個(gè)指定的指令位置,而不是函數(shù)調(diào)用后緊跟的位置。實(shí)現(xiàn)原理是在函數(shù)體中修改返回地址,即找到返回地址的位置并修改它。代碼如下:
//foo.c void foo(void){ int a, *p; p = (int*)((char *)&a + 12); //讓p指向main函數(shù)調(diào)用foo時(shí)入棧的返回地址,等效于p = (int*)(&a + 3); *p += 12; //修改該地址的值,使其指向一條指令的起始地址 } int main(void){ foo(); printf("First printf call\n"); printf("Second printf call\n"); return 0; }
編譯運(yùn)行,結(jié)果輸出Second printf call,未輸出First printf call。
下面詳細(xì)介紹代碼中兩個(gè)12的由來(lái)。
編譯(gcc main.c –g)和反匯編(objdump a.out –d)后,得到匯編代碼片段如下:
從上述匯編代碼可知,foo后面的指令地址(即調(diào)用foo時(shí)壓入的返回地址)是0x80483b8,而進(jìn)入調(diào)用printf("Second printf call“)的指令地址是0x80483c4。兩者相差12,故將返回地址的值加12即可(*p += 12)。
指令<804838a>將-8(%ebp)的地址賦值給%eax寄存器(p = &a)。可知foo()函數(shù)中的變量a存儲(chǔ)在-8(%ebp)地址上,該地址向上8+4=12個(gè)單位就是返回地址((char *)&a + 12)。修改該地址內(nèi)容(*p += 12)即可實(shí)現(xiàn)函數(shù)調(diào)用結(jié)束后跳轉(zhuǎn)到第二個(gè)printf函數(shù)調(diào)用的位置。
用gdb查看匯編指令剛進(jìn)入foo時(shí)棧頂?shù)闹?%esp),如下所示:
可見%esp值的確是調(diào)用foo后main中下條待執(zhí)行指令的地址,而代碼所修改的也正是該值。%eip則指向當(dāng)前程序(foo)的指令地址。
【示例2】暫存RunAway函數(shù)的返回地址后修改其值,使函數(shù)返回后跳轉(zhuǎn)到Detour函數(shù)的地址;Detour函數(shù)內(nèi)嘗試通過(guò)之前保存的返回地址重回main函數(shù)內(nèi)。代碼如下:
//RunAway.c int gPrevRet = 0; //保存函數(shù)的返回地址 void Detour(void){ int *p = (int*)&p + 2; //p指向函數(shù)的返回地址 *p = gPrevRet; printf("Run Away!\n"); //需要回車,或打印后fflush(stdout);刷新緩沖區(qū),否則可能在段錯(cuò)誤時(shí)無(wú)法輸出 } int RunAway(void){ int *p = (int*)&p + 2; gPrevRet = *p; *p = (int)Detour; return 0; } int main(void){ RunAway(); printf("Come Home!\n"); return 0; }
編譯運(yùn)行后輸出:
Run Away!
Come Home!
Run Away!
Come Home!
Segmentation fault
運(yùn)行后出現(xiàn)段錯(cuò)誤?There must be something wrong!錯(cuò)誤原因留待讀者思考,下面給出上述代碼的另一版本,借助匯編獲取返回地址(而不是根據(jù)棧幀結(jié)構(gòu)估算)。
register void *gEbp __asm__ ("%ebp"); void Detour(void){ *((int *)gEbp + 1) = gPrevRet; printf("Run Away!\n"); } int RunAway(void){ gPrevRet = *((int *)gEbp + 1); *((int *)gEbp + 1) = Detour; return 0; }
【示例3】在被調(diào)函數(shù)內(nèi)修改主調(diào)函數(shù)指針變量,造成后續(xù)訪問(wèn)該指針時(shí)程序崩潰。代碼如下:
//Crasher.c typedef struct{ int member1; int member2; }T_STRT; T_STRT gtTestStrt = {0}; register void *gEbp __asm__ ("%ebp"); void Crasher(T_STRT *ptStrt){ printf("[%s]: ebp = %p(0x%08x)\n", __FUNCTION__, gEbp, *((int*)gEbp)); printf("[%s]: ptStrt = %p(%p)\n", __FUNCTION__, &ptStrt, ptStrt); printf("[%s]: (1) = %p(0x%08x)\n", __FUNCTION__, ((int*)&ptStrt-2), *((int*)&ptStrt-2)); printf("[%s]: (2) = %p(0x%08x)\n", __FUNCTION__, (int*)(*((int*)&ptStrt-2)-4), *(int*)(*((int*)&ptStrt-2)-4)); printf("[%s]: (3) = %p(0x%08x)\n", __FUNCTION__, (int*)(*((int*)&ptStrt-2)-8), *(int*)(*((int*)&ptStrt-2)-8)); *(int*)( *( (int*)&ptStrt - 2 ) - 8 ) = 0; //A:此句將導(dǎo)致代碼B處發(fā)生段錯(cuò)誤 } int main(void){ printf("[%s]: ebp = %p(0x%08x)\n", __FUNCTION__, gEbp, *((int*)gEbp)); T_STRT *ptStrt = >TestStrt; printf("[%s]: ptStrt = %p(%p)\n", __FUNCTION__, &ptStrt, ptStrt); Crasher(ptStrt); printf("[%s]: ptStrt = %p(%p)\n", __FUNCTION__, &ptStrt, ptStrt); ptStrt->member1 = 5; //B:需要在此處崩潰 printf("Try to come here!\n"); return 0; }
運(yùn)行結(jié)果如下所示:
根據(jù)打印出的地址及其存儲(chǔ)內(nèi)容,可得到以下堆棧布局:
&ptStrt為形參地址0xbff8f090,該地址處在main函數(shù)棧幀中。(int*)&ptStrt - 2地址存儲(chǔ)主調(diào)函數(shù)的EBP值,根據(jù)該值可直接定位到main函數(shù)棧幀底部。(*((int*)&ptStrt - 2) - 8)為主調(diào)函數(shù)中實(shí)參ptStrt的地址,而*(int*) (*((int*)&ptStrt - 2) - 4) = 0將該地址內(nèi)容置零,即實(shí)參指針ptStrt設(shè)置為NULL(不再指向全局結(jié)構(gòu)gtTestStrt)。這樣,訪問(wèn)ptStrt->member1時(shí)就會(huì)發(fā)生段錯(cuò)誤。
注意,雖然本例代碼結(jié)構(gòu)簡(jiǎn)單,但不能輕率地推斷main函數(shù)中局部變量ptStrt位于幀基指針EBP-4處(實(shí)際上本例為EBP-8處)。以下改進(jìn)版本用于自動(dòng)計(jì)算該偏移量:
static int gOffset = 0; void Crasher(T_STRT *ptStrt){ *(int*)( *(int*)gEbp - gOffset ) = 0; } int main(void){ T_STRT *ptStrt = >TestStrt; gOffset = (char*)gEbp - (char*)(&ptStrt); Crasher(ptStrt); ptStrt->member1 = 5; //在此處崩潰 printf("Try to come here!\n"); return 0; }
當(dāng)然,該版本已失去原有意義(不借助寄存器層面手段),純?yōu)槭纠?/p>
【示例4】越界訪問(wèn)造成死循環(huán)。代碼如下:
//InfinteLoop.c void InfinteLoop(void){ unsigned char ucIdx, aucArr[10]; for(ucIdx = 0; ucIdx <= 10; ucIdx++) aucArr[ucIdx] = 1; }
在循環(huán)內(nèi)部,當(dāng)訪問(wèn)不存在的數(shù)組元素aucArr[10]時(shí),實(shí)際上在訪問(wèn)數(shù)組aucArr所在地址之后的那個(gè)位置,而該位置存放著變量ucIdx。因此aucArr[10] = 1將ucIdx重置為1,然后繼續(xù)循環(huán)的條件仍然成立,最終將導(dǎo)致死循環(huán)。
【示例5】緩沖區(qū)溢出。代碼如下:
//CarelessPapa.c register int *gEbp __asm__ ("%ebp"); void NaughtyBoy(void){ printf("[2]EBP=%p(%#x), EIP=%p(%#x)\n", gEbp, *gEbp, gEbp+1, *(gEbp+1)); printf("Catch Me!\n"); } void CarelessPapa(const char *pszStr){ printf("[1]EBP=%p(%#x)\n", gEbp, *gEbp); printf("[1]EIP=%p(%#x)\n", gEbp+1, *(gEbp+1)); char szBuf[8]; strcpy(szBuf, pszStr); } int main(void){ printf("[0]EBP=%p(%#x)\n", gEbp, *gEbp); printf("Addr: CarelessPapa=%p, NaughtyBoy=%p\n", CarelessPapa, NaughtyBoy); char szArr[]="0123456789AB\xe4\x83\x4\x8\x23\x85\x4\x8"; CarelessPapa(szArr); printf("Come Home!\n"); printf("[3]EBP=%p\n", gEbp); return 0; }
編譯運(yùn)行結(jié)果如下:
可見,當(dāng)CarelessPapa函數(shù)調(diào)用結(jié)束后,并未直接執(zhí)行Come Home的輸出,而是轉(zhuǎn)而執(zhí)行NaughtyBoy函數(shù)(輸出Catch Me),然后回頭輸出Come Home。該過(guò)程重復(fù)一次后發(fā)生段錯(cuò)誤(具體原因留待讀者思考)。
結(jié)合下圖所示的棧幀布局,詳細(xì)分析本示例緩沖區(qū)溢出過(guò)程。注意,本示例中地址及其內(nèi)容由內(nèi)嵌匯編和打印輸出獲得,正常情況下應(yīng)通過(guò)gdb調(diào)試器獲得。
首先,main函數(shù)將字符數(shù)組szArr的地址作為參數(shù)(即pszStr)傳遞給函數(shù)CarelessPapa。該數(shù)組內(nèi)容為"0123456789AB\xe4\x83\x4\x8\x23\x85\x4\x8",其中轉(zhuǎn)義字符串"\xe4\x83\x4\x8"對(duì)應(yīng)NaughtyBoy函數(shù)入口地址0x080483e4(小字節(jié)序),而"\x23\x85\x4\x8"對(duì)應(yīng)調(diào)用CarelessPapa函數(shù)時(shí)的返回地址0x8048523(小字節(jié)序)。CarelessPapa函數(shù)內(nèi)部調(diào)用strcpy庫(kù)函數(shù),將pszStr所指字符串內(nèi)容拷貝至szBuf數(shù)組。因?yàn)閟trcpy函數(shù)不進(jìn)行越界檢查,會(huì)逐字節(jié)拷貝直到遇見'\0'結(jié)束符。故pszStr字符串將從szBuf數(shù)組起始地址開始向高地址覆蓋,原返回地址0x8048523被覆蓋為NaughtyBoy函數(shù)地址0x080483e4。
這樣,當(dāng)CarelessPapa函數(shù)返回時(shí),修改后的返回地址從棧中彈出到EIP寄存器中,此時(shí)棧頂指針ESP指向返回地址上方的空間(esp+4),程序跳轉(zhuǎn)到EIP所指地址(NaughtyBoy函數(shù)入口)開始執(zhí)行,首先就是EBP入?!⑽聪裾U{(diào)用那樣先壓入返回地址,故NaughtyBoy函數(shù)棧幀中EBP位置相對(duì)CarelessPapa函數(shù)上移4個(gè)字節(jié)!此時(shí),"\x23\x85\x4\x8"可將EBP上方的EIP修改為CarelessPapa函數(shù)的返回地址(0x8048523),從而保證正確返回main函數(shù)內(nèi)。
注意,返回main函數(shù)并輸出Come Home后,main函數(shù)棧幀的EBP地址被改為0x42413938("89AB"),該地址已非堆??臻g,最終產(chǎn)生段錯(cuò)誤。EBP地址會(huì)隨每次程序執(zhí)行而改變,故試圖在szArr字符串中恢復(fù)EBP是非常困難的。
從main函數(shù)return時(shí)將返回到調(diào)用它的啟動(dòng)例程(_start函數(shù))中,返回值被啟動(dòng)例程獲得并用其作為參數(shù)調(diào)用exit函數(shù)。exit函數(shù)首先做一些清理工作,然后調(diào)用_exit系統(tǒng)調(diào)用終止進(jìn)程。main函數(shù)的返回值最終傳給_exit系統(tǒng)調(diào)用,成為進(jìn)程的退出狀態(tài)。以下代碼在main函數(shù)中直接調(diào)用exit函數(shù)終止進(jìn)程而不返回到啟動(dòng)例程:
//CarelessPapa.c register int *gEbp __asm__ ("%ebp"); void NaughtyBoy(void){ printf("[2]EBP=%p(%#x), EIP=%p(%#x)\n", gEbp, *gEbp, gEbp+1, *(gEbp+1)); printf("Catch Me!\n"); } void CarelessPapa(const char *pszStr){ printf("[1]EBP=%p(%#x)\n", gEbp, *gEbp); printf("[1]EIP=%p(%#x)\n", gEbp+1, *(gEbp+1)); char szBuf[8]; strcpy(szBuf, pszStr); } int main(void){ printf("[0]EBP=%p(%#x)\n", gEbp, *gEbp); printf("Addr: CarelessPapa=%p, NaughtyBoy=%p\n", CarelessPapa, NaughtyBoy); char szArr[]="0123456789AB\x14\x84\x4\x8\x33\x85\x4\x8"; //轉(zhuǎn)義字符串稍有變化 CarelessPapa(szArr); printf("Come Home!\n"); printf("[3]EBP=%p\n", gEbp); exit(0); //#include <stdlib.h> }
編譯運(yùn)行結(jié)果如下:
這次沒有重復(fù)執(zhí)行,也未出現(xiàn)段錯(cuò)誤。
三、緩沖區(qū)溢出防范
防范緩沖區(qū)溢出問(wèn)題的準(zhǔn)則是:確保做邊界檢查(通常不必?fù)?dān)心影響程序效率)。不要為接收數(shù)據(jù)預(yù)留相對(duì)過(guò)小的緩沖區(qū),大的數(shù)組應(yīng)通過(guò)malloc/new分配堆空間來(lái)解決;在將數(shù)據(jù)讀入或復(fù)制到目標(biāo)緩沖區(qū)前,檢查數(shù)據(jù)長(zhǎng)度是否超過(guò)緩沖區(qū)空間。同樣,檢查以確保不會(huì)將過(guò)大的數(shù)據(jù)傳遞給別的程序,尤其是第三方COTS(Commercial-off-the-shelf)商用軟件庫(kù)——不要設(shè)想關(guān)于其他人軟件行為的任何事情。
若有可能,改用具備防止緩沖區(qū)溢出內(nèi)置機(jī)制的高級(jí)語(yǔ)言(Java、C#等)。但許多語(yǔ)言依賴于C庫(kù),或具有關(guān)閉該保護(hù)特性的機(jī)制(為速度而犧牲安全性)。其次,可以借助某些底層系統(tǒng)機(jī)制或檢測(cè)工具(如對(duì)C數(shù)組進(jìn)行邊界檢查的編譯器)。許多操作系統(tǒng)(包括Linux和Solaris)提供非可執(zhí)行堆棧補(bǔ)丁,但該方式不適于這種情況:攻擊者利用堆棧溢出使程序跳轉(zhuǎn)到放置在堆上的執(zhí)行代碼。此外,存在一些偵測(cè)和去除緩沖區(qū)溢出漏洞的靜態(tài)工具(檢查代碼但并不運(yùn)行)和動(dòng)態(tài)工具(執(zhí)行代碼以確定行為),甚至采用grep命令自動(dòng)搜索源代碼中每個(gè)有問(wèn)題函數(shù)的實(shí)例。
但即使采用這些保護(hù)手段,程序員自身也可能犯其他許多錯(cuò)誤,從而引入缺陷。例如,當(dāng)使用有符號(hào)數(shù)存儲(chǔ)緩沖區(qū)長(zhǎng)度或某個(gè)待讀取內(nèi)容長(zhǎng)度時(shí),攻擊者可將其變?yōu)樨?fù)值,從而使該長(zhǎng)度被解釋為很大的正值。經(jīng)驗(yàn)豐富的程序員還容易過(guò)于自信地"把玩"某些危險(xiǎn)的庫(kù)函數(shù),如對(duì)其添加自己總結(jié)編寫的檢查,或錯(cuò)誤地推論出使用潛在危險(xiǎn)的函數(shù)在某些特殊情況下是"安全"的。
本節(jié)將主要討論一些已被證明危險(xiǎn)的C庫(kù)函數(shù)。通過(guò)在C/C++程序中禁用或慎用危險(xiǎn)的函數(shù),可有效降低在代碼中引入安全漏洞的可能性。在考慮性能和可移植性的前提下,強(qiáng)烈建議在開發(fā)過(guò)程中使用相應(yīng)的安全函數(shù)來(lái)替代危險(xiǎn)的庫(kù)函數(shù)調(diào)用。
以下分析某些危險(xiǎn)的庫(kù)函數(shù),較完整的列表參見表3-1。
3.1、gets
該函數(shù)從標(biāo)準(zhǔn)輸入讀入用戶輸入的一行文本,在遇到EOF字符或換行字符前,不會(huì)停止讀入文本。即該函數(shù)不執(zhí)行越界檢查,故幾乎總有可能使任何緩沖區(qū)溢出(應(yīng)禁用)。
gcc編譯器下會(huì)對(duì)gets調(diào)用發(fā)出警告(the `gets' function is dangerous and should not be used)。
3.2、strcpy
該函數(shù)將源字符串復(fù)制到目標(biāo)緩沖區(qū),但并未指定要復(fù)制字符的數(shù)目。若源字符串來(lái)自用戶輸入且未限制其長(zhǎng)度,則可能引發(fā)危險(xiǎn)。規(guī)避的方法如下:
1) 若知道目標(biāo)緩沖區(qū)大小,則可添加明確的檢查(不建議該法):
if(strlen(szSrc) >= dwDstSize){ /* Do something appropriate, such as throw an error. */ } else{ strcpy(szDst, szSrc); }
2) 改用strncpy函數(shù):
strncpy(szDst, szSrc, dwDstSize-1); szDst[dwDstSize-1] = '\0'; //Always do this to be safe!
若szSrc比szDst大,則該函數(shù)不會(huì)返回錯(cuò)誤;當(dāng)達(dá)到指定長(zhǎng)度(dwDstSize-1)時(shí),停止復(fù)制字符。第二句將字符串結(jié)束符放在szDst數(shù)組的末尾。
3) 在源字符串上調(diào)用strlen()來(lái)為其分配足夠的堆空間:
pszDst = (char *)malloc(strlen(szSrc)); strcpy(pszDst, szSrc);
4) 某些情況下使用strcpy不會(huì)帶來(lái)潛在的安全性問(wèn)題:
strcpy(szDst, "Hello!"); //Usually by initialization, such as char szDst[] = “Hello!”;
即使該操作造成szDst溢出,但這幾個(gè)字符顯然不會(huì)造成危害——除非用其它方式覆蓋字符串“Hello”所在的靜態(tài)存儲(chǔ)區(qū)。
安全的字符串處理函數(shù)通常體現(xiàn)在如下幾個(gè)方面:
- 顯式指明目標(biāo)緩沖區(qū)大小
- 動(dòng)態(tài)校驗(yàn)
- 返回碼(以指明成功或失敗原因)
與strcpy函數(shù)具有相同問(wèn)題的還有strcat函數(shù)。
3.3、 strncpy/strncat
該對(duì)函數(shù)是strcpy/strcat調(diào)用的“安全”版本,但仍存在一些問(wèn)題:
1) strncpy和strncat要求程序員給出剩余的空間,而不是給出緩沖區(qū)的總大小。緩沖區(qū)大小一經(jīng)分配就不再變化,但緩沖區(qū)中剩余的空間量會(huì)在每次添加或刪除數(shù)據(jù)時(shí)發(fā)生變化。這意味著程序員需始終跟蹤或重新計(jì)算剩余的空間,而這種跟蹤或重新計(jì)算很容易出錯(cuò)。
2) 在發(fā)生溢出(和數(shù)據(jù)丟失)時(shí),strncpy和strncat返回結(jié)果字符串的起始地址(而不是其長(zhǎng)度)。雖然這有利于鏈?zhǔn)奖磉_(dá),但卻無(wú)法報(bào)告緩沖區(qū)溢出。
3) 若源字符串長(zhǎng)度至少和目標(biāo)緩沖區(qū)相同,則strncpy不會(huì)使用NUL來(lái)結(jié)束字符串;這可能會(huì)在以后導(dǎo)致嚴(yán)重破壞。因此,在執(zhí)行strncpy后通常需要手工終止目標(biāo)字符串。
4) strncpy還可復(fù)制源字符串的一部分到目標(biāo)緩沖區(qū),要復(fù)制的字符數(shù)目通常基于源字符串的相關(guān)信息來(lái)計(jì)算。這種操作也會(huì)產(chǎn)生未終止字符串。
5) strncpy會(huì)在源字符串結(jié)束時(shí)使用NUL來(lái)填充整個(gè)目標(biāo)緩沖區(qū),這在源字符串較短時(shí)存在性能問(wèn)題。
3.4、sprintf
該函數(shù)使用控制字符串來(lái)指定輸出格式,該字符串通常包括"%s"(字符串輸出)。若指定字符串輸出的精確指定符,則可通過(guò)指定輸出的最大長(zhǎng)度來(lái)防止緩沖區(qū)溢出(如%.10s將復(fù)制不超過(guò)10個(gè)字符)。也可以使用"*"作為精確指定符(如"%.*s"),這樣就可傳入一個(gè)最大長(zhǎng)度值。精確字段僅指定一個(gè)參數(shù)的最大長(zhǎng)度,但緩沖區(qū)需要針對(duì)組合起來(lái)的數(shù)據(jù)的最大尺寸調(diào)整大小。
注意,"字段寬度"(如"%10s",無(wú)點(diǎn)號(hào))僅指定最小長(zhǎng)度——而非最大長(zhǎng)度,從而留下緩沖區(qū)溢出隱患。
3.5、scanf
scanf系列函數(shù)具有一個(gè)最大寬度值,函數(shù)不能讀取超過(guò)最大寬度的數(shù)據(jù)。但并非所有規(guī)范都規(guī)定了這點(diǎn),也不確定是否所有實(shí)現(xiàn)都能正確執(zhí)行這些限制。若要使用這一特性,建議在安裝或初始化期間運(yùn)行小測(cè)試來(lái)確保它能正確工作。
3.6、streadd/strecpy
這對(duì)函數(shù)可將含有不可讀字符的字符串轉(zhuǎn)換成可打印的表示。其原型包含在libgen.h頭文件內(nèi),編譯時(shí)需加-lgen [library ...]選項(xiàng)。
char *strecpy(char *pszOut, const char *pszIn, const char *pszExcept);
char *streadd(char *pszOut, const char *pszIn, const char *pszExcept);
strecpy將輸入字符串pszIn(連同結(jié)束符)拷貝到輸出字符串pszOut中,并將非圖形字符展開為C語(yǔ)言中相應(yīng)的轉(zhuǎn)義字符序列(如Control-A轉(zhuǎn)為“\001”)。參數(shù)pszOut指向的緩沖區(qū)大小必須足夠容納結(jié)果字符串;輸出緩沖區(qū)大小應(yīng)為輸入緩沖區(qū)大小的四倍(單個(gè)字符可能轉(zhuǎn)換為\abc共四個(gè)字符)。出現(xiàn)在參數(shù)pszExcept字符串內(nèi)的字符不被展開。該參數(shù)可設(shè)為空串,表示擴(kuò)展所有非圖形字符。strecpy函數(shù)返回指向pszOut字符串的指針。
streadd函數(shù)與strecpy相同,只不過(guò)返回指向pszOut字符串結(jié)束符的指針。
考慮以下代碼:
#include <libgen.h> int main(void){ char szBuf[20] = {0}; streadd(szBuf, "\t\n", ""); printf(%s\n", szBuf); return 0; }
打印輸出\t\n,而不是所有空白。
3.7、strtrns
該函數(shù)將pszStr字符串中的字符轉(zhuǎn)換后復(fù)制到結(jié)果緩沖區(qū)pszResult。其原型包含在libgen.h頭文件內(nèi):
char * strtrns(const char *pszStr, const char *pszOld, const char *pszNew, char *pszResult);
出現(xiàn)在pszOld字符串中的字符被pszNew字符串中相同位置的字符替換。函數(shù)返回新的結(jié)果字符串。
如下示例將小寫字符轉(zhuǎn)換成大寫字符:
#include <libgen.h> int main(int argc,char *argv[]){ char szLower[] = "abcdefghijklmnopqrstuvwxyz"; char szUpper[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; if(argc < 2){ printf("USAGE: %s arg\n", argv[0]); exit(0); } char *pszBuf = (char *)malloc(strlen(argv[1])); strtrns(argv[1], szLower, szUpper, pszBuf); printf("%s\n", pszBuf); return 0; }
以上代碼使用malloc分配足夠空間來(lái)復(fù)制argv[1],因此不會(huì)引起緩沖區(qū)溢出。
3.8、realpath
該函數(shù)在libc 4.5.21及以后版本中提供,使用時(shí)需要limits.h和stdlib.h頭文件。其原型為:
char *realpath(const char *pszPath, char *pszResolvedPath);
該函數(shù)展開pszPath字符串中的所有符號(hào)鏈接,并解析pszPath中所引用的/./、/../和'/'字符(相對(duì)路徑),最終生成規(guī)范化的絕對(duì)路徑名。該路徑名作為帶結(jié)束符的字符串存入pszResolvedPath指向的緩沖區(qū),長(zhǎng)度最大為PATH_MAX字節(jié)。結(jié)果路徑中不含符號(hào)鏈接、/./或/../。
若pszResolvedPath為空指針,則realpath函數(shù)使用malloc來(lái)分配PATH_MAX字節(jié)的緩沖區(qū)以存儲(chǔ)解析后的路徑名,并返回指向該緩沖區(qū)的指針。調(diào)用者應(yīng)使用free函數(shù)去釋放該該緩沖區(qū)。
若執(zhí)行成功,realpath函數(shù)返回指向pszResolvedPath(規(guī)范化絕對(duì)路徑)的指針;否則返回空指針并設(shè)置errno以指示該錯(cuò)誤,此時(shí)pszResolvedPath的內(nèi)容未定義。
調(diào)用者需要確保結(jié)果緩沖區(qū)足夠大(但不應(yīng)超過(guò)PATH_MAX),以處理任何大小的路徑。此外,不可能為輸出緩沖區(qū)確定合適的長(zhǎng)度,因此POSIX.1-2001規(guī)定,PATH_MAX字節(jié)的緩沖區(qū)足夠,但PATH_MAX不必定義為常量,且可以通過(guò)pathconf函數(shù)獲得。然而,pathconf輸出的結(jié)果可能超大,以致不適合動(dòng)態(tài)分配內(nèi)存;另一方面,pathconf函數(shù)可返回-1表明結(jié)果路徑名超出PATH_MAX限制。pszResolvedPath為空指針的特性被POSIX.1-2008標(biāo)準(zhǔn)化,以避免輸出緩沖區(qū)長(zhǎng)度難以靜態(tài)確定的缺陷。
應(yīng)禁用或慎用的庫(kù)函數(shù)如下表所示:
表3-1
函數(shù) |
危險(xiǎn)性 |
解決方案 |
gets |
最高 |
禁用gets(buf),改用fgets(buf, size, stdin) |
strcpy |
高 |
檢查目標(biāo)緩沖區(qū)大小,或改用strncpy,或動(dòng)態(tài)分配目標(biāo)緩沖區(qū) |
strcat |
高 |
改用strncat |
sprintf |
高 |
改用snprintf,或使用精度說(shuō)明符 |
scanf |
高 |
使用精度說(shuō)明符,或自己進(jìn)行解析 |
sscanf |
高 |
使用精度說(shuō)明符,或自己進(jìn)行解析 |
fscanf |
高 |
使用精度說(shuō)明符,或自己進(jìn)行解析 |
vfscanf |
高 |
使用精度說(shuō)明符,或自己進(jìn)行解析 |
vsprintf |
高 |
改為使用vsnprintf,或使用精度說(shuō)明符 |
vscanf |
高 |
使用精度說(shuō)明符,或自己進(jìn)行解析 |
vsscanf |
高 |
使用精度說(shuō)明符,或自己進(jìn)行解析 |
streadd |
高 |
確保分配的目標(biāo)參數(shù)緩沖區(qū)大小是源參數(shù)大小的四倍 |
strecpy |
高 |
確保分配的目標(biāo)參數(shù)緩沖區(qū)大小是源參數(shù)大小的四倍 |
strtrns |
高 |
手工檢查目標(biāo)緩沖區(qū)大小是否至少與源字符串相等 |
getenv |
高 |
不可假定特殊環(huán)境變量的長(zhǎng)度 |
realpath |
高(或稍低,實(shí)現(xiàn)依賴) |
分配緩沖區(qū)大小為PATH_MAX字節(jié),并手工檢查參數(shù)以確保輸入?yún)?shù)和輸出參數(shù)均不超過(guò)PATH_MAX |
syslog |
高(或稍低,實(shí)現(xiàn)依賴) |
將字符串輸入傳遞給該函數(shù)之前,將所有字符串輸入截成合理大小 |
getopt |
高(或稍低,實(shí)現(xiàn)依賴) |
將字符串輸入傳遞給該函數(shù)之前,將所有字符串輸入截成合理大小 |
getopt_long |
高(或稍低,實(shí)現(xiàn)依賴) |
將字符串輸入傳遞給該函數(shù)之前,將所有字符串輸入截成合理大小 |
getpass |
高(或稍低,實(shí)現(xiàn)依賴) |
將字符串輸入傳遞給該函數(shù)之前,將所有字符串輸入截成合理大小 |
getchar |
中 |
若在循環(huán)中使用該函數(shù),確保檢查緩沖區(qū)邊界 |
fgetc |
中 |
若在循環(huán)中使用該函數(shù),確保檢查緩沖區(qū)邊界 |
getc |
中 |
若在循環(huán)中使用該函數(shù),確保檢查緩沖區(qū)邊界 |
read |
中 |
若在循環(huán)中使用該函數(shù),確保檢查緩沖區(qū)邊界 |
bcopy |
低 |
確保目標(biāo)緩沖區(qū)不小于指定長(zhǎng)度 |
fgets |
低 |
確保目標(biāo)緩沖區(qū)不小于指定長(zhǎng)度 |
memcpy |
低 |
確保目標(biāo)緩沖區(qū)不小于指定長(zhǎng)度 |
snprintf |
低 |
確保目標(biāo)緩沖區(qū)不小于指定長(zhǎng)度 |
strccpy |
低 |
確保目標(biāo)緩沖區(qū)不小于指定長(zhǎng)度 |
strcadd |
低 |
確保目標(biāo)緩沖區(qū)不小于指定長(zhǎng)度 |
strncpy |
低 |
確保目標(biāo)緩沖區(qū)不小于指定長(zhǎng)度 |
vsnprintf |
低 |
確保目標(biāo)緩沖區(qū)不小于指定長(zhǎng)度 |
以上就是詳解C語(yǔ)言之緩沖區(qū)溢出的詳細(xì)內(nèi)容,更多關(guān)于C語(yǔ)言 緩沖區(qū)溢出的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
C語(yǔ)言實(shí)現(xiàn)銀行管理系統(tǒng)(文件操作)
這篇文章主要為大家詳細(xì)介紹了C語(yǔ)言實(shí)現(xiàn)銀行管理系統(tǒng),文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-03-03C語(yǔ)言實(shí)現(xiàn)航班售票系統(tǒng) C語(yǔ)言實(shí)現(xiàn)航班管理系統(tǒng)
這篇文章主要為大家詳細(xì)介紹了C語(yǔ)言實(shí)現(xiàn)航班售票系統(tǒng),C語(yǔ)言實(shí)現(xiàn)航班管理系統(tǒng),文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-12-12Qt?timerEvent實(shí)現(xiàn)簡(jiǎn)單秒表功能
這篇文章主要為大家詳細(xì)介紹了Qt?timerEvent實(shí)現(xiàn)簡(jiǎn)單秒表功能,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-08-08VsCode安裝和配置c/c++環(huán)境小白教程(圖文)
本文主要介紹了VsCode安裝和配置c/c++環(huán)境小白教程,文中通過(guò)示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-01-01C語(yǔ)言實(shí)現(xiàn)簡(jiǎn)易井字棋游戲
這篇文章主要為大家詳細(xì)介紹了C語(yǔ)言實(shí)現(xiàn)簡(jiǎn)易井字棋游戲,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-04-04