欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

詳解C語(yǔ)言之緩沖區(qū)溢出

 更新時(shí)間:2021年06月14日 11:32:48   作者:clover_toeic  
緩沖區(qū)是一塊連續(xù)的計(jì)算機(jī)內(nèi)存區(qū)域,可保存相同數(shù)據(jù)類型的多個(gè)實(shí)例。緩沖區(qū)可以是堆棧、堆和靜態(tài)數(shù)據(jù)區(qū)。在C/C++語(yǔ)言中,通常使用字符數(shù)組和malloc/new實(shí)現(xiàn)緩沖區(qū)。溢出指數(shù)據(jù)被添加到分配給該緩沖區(qū)的內(nèi)存塊之外。緩沖區(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 = &gtTestStrt;
    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 = &gtTestStrt;
    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)文章

最新評(píng)論