從hs_strcpy談安全——緩沖區溢出


  對於大多數的博友來說,hs_strcpy一定會很陌生,因為這個hs_strcpy這個關鍵字和我的工作有掛鈎。本來目前就職於恆生電子,hs_strcpy是中間件中公司定義的字符串拷貝方法,在工作業余之余,看過了一篇緩沖區溢出的文章,處於好奇心就看了一下公司內部的底層代碼,發現了hs_strcpy這個函數的實現,突然發現原來這個還是其實也是存在緩沖區溢出的。什么是緩沖區溢出、如何防止緩沖區溢出,是我寫這篇文章的真正目的。小編自稱菜鳥,如果有寫的不對的地方,請多多批評。

專有名字解釋

  在看下文之前,我們還是先看一下專有的名詞解釋吧,這樣可以更好的帶大家全面的了解本文的核心內容。

1.緩沖溢出(Buffer overflow),是指當計算機向緩沖區內填充數據位數時超過了緩沖區本身的容量,使得溢出的數據覆蓋在合法數據上,理想的情況是程序檢查數據長度並不允許輸入超過緩沖區長度的字符,但是絕大多數程序都會假設數據長度總是與所分配的儲存空間相匹配,這就為緩沖區溢出埋下隱患。操作系統所使用的緩沖區又被稱為"堆棧".。在各個操作進程之間,指令會被臨時儲存在"堆棧"當中,"堆棧"也會出現緩沖區溢出。[1]

2. 棧(操作系統):由編譯器自動分配釋放 ,存放函數的參數值,局部變量的值等。其操作方式類似於數據結構中的棧。[2]

3. ESP(Extended stack pointer)是指針寄存器的一種(另一種為EBP)。用於堆棧指針。[3]

4. 擴展基址指針寄存器(extended base pointer) 其內存放一個指針,該指針指向系統棧最上面一個棧幀的底部。[4]

5. 棧幀也叫過程活動記錄,是編譯器用來實現過程/函數調用的一種數據結構[5]

 

開門見山

   隨着IDE工具的越來越發達,越來越多的程序員開始疏忽緩沖區溢出,甚至一些開發程序員從未聽說過此專有名詞。當然如果真的發生了緩沖溢出,那還是非常可怕的,雖然光大的烏龍指並非緩沖溢出所為,但是緩沖區可以再次創造光大的烏龍指。所以我們開發人員不可以忽視我們程序的緩沖區溢出問題。

      那到底什么是緩沖區溢出呢?顧名思義,緩沖區溢出的含義是為緩沖區提供了多於其存儲容量的數據,打個比方,我有一個100ml的水杯,但是我倒入了120ml的水,那么20ml的水就會溢出杯子,造成一些想不到的后果。

 

strcpy和hs_strcpy

  我們先來看一下微軟為我們開發人員提供的strcpy這個api函數的實現源碼(參考百度百科):

/**********************
* C語言標准庫函數strcpy的一種典型的工業級的最簡實現
*返回值:目標串的地址。
*對於出現異常的情況ANSI-C99標准並未定義,故由實現者決定返回值,通常為NULL。
*參數:
* strDestination 目標串
* s
 /*
GNU-C中的實現(節選):
*/
char* strcpy(char *d, const char *s)
{
  char *r=d;
  while((*d++=*s++));
  return r;
}
/* while((*d++=*s++)); 的解釋:賦值表達式返回左操作數,所以在復制NULL后,循環停止ֹ */

在比較一下恆生公司自定義開發的源碼(本人在服務器的文件中找到了定義和實現的整個代碼):
int hs_strcpy(char *d_dest,const  char *s_src)
{
    if (isnull(s_src) == 0)
    {
        d_dest[0] = '\0';
        return(-1);
    }
    
    if (d_dest == NULL)
        return(-1);

    for(; *d_dest++ = *s_src++; )  /* the same as strcpy. */
        ;

    return(0);
}

  這里我們可以看出,strcpy這個函數把自己所有的字符都依次賦值給了d這個指針,然后再把r這個字符指針指向d的開始位置(d[0]的地址),並返回了r指針的地址。然而恆生自定義的hs_strcpy有着異曲同工之處,就是把src中的內容依次賦值給dest。如果正確就返回0,錯誤返回-1(其實這里我有一點不明白,就是不知道當初的設計人員為什么把返回類型定義成int類型而不是Bool類型,因為我覺得可能bool類型效率、性能都會更好);唯一不同的是設計者判斷了一下src和dest是否為空。雖然說來,公司自定義的開發的字符串拷貝函數在效率方便有了一定的提高,但是由於在strcpy中本身就存在緩沖溢出的問題,所以很遺憾的告訴開發者,hs_strcpy也存在同樣的緩沖溢出問題。

 

用代碼講話

下面我就直接把代碼粘貼出來,把這個bug重現出來,當然不同的編譯器或者是編譯的環境不同(debug和release),雖然同樣的代碼,但是執行的結果可能不一樣,如果你拷貝了我的代碼,但是問題卻不重現,先檢查一下自己的IDE環境。

IDE環境:

       開發工具:VC6.0 編譯模式(debug

IDE界面圖1

 Strcpy緩沖溢出:

#include <stdio.h>
#include <string.h>

void foo(){
    printf("哈哈,我是被strcpy緩沖溢出的地方");
}

//最簡單的緩沖溢出例子
int main(){
    char output[8];
    //char name[] = "adcdefg";         //① 我們希望輸入正確的值
//char name[] = "adcdefghijklmnop"; //② 我們用過量的字符串來填充,造成緩沖溢出,這里我們可以參考一下str_cpy_1的圖
                                 //   得到了foo的Address為0040101E 具體要怎么測溢出點的位置,最好切換到xp環境下,會有錯誤信息,根據錯誤信息查找ascill值

                           //   不知道你們注意到了沒有,我這里不用aaa...而是用abc...,是因為我既要讓程序溢出,而且要確切的知道溢出點。因為找地址會打印出來字符的ascill,比如小寫的a對應的十進制acill值為97,但是如果控制台打印出來97 97 97 97 我們就不知道到底是從那個字母開始緩沖區溢出了,但是如果我們設置成abcd… 假如提示98 99 …,我們就知道從b開始發生了緩沖區溢出
                               //  (這里的溢出點為mnop)
    char name[] = "adcdefghijkl\x1E\x10\x40\x00";//③ 重點:我們以此修改mnop的值為剛剛捕獲到的foo的地址的值,但是需要注意我們要從后面開始修改
                                                           //    我們修改好了之后再次運行,發現了什么,注意我這里是debug模式                    
    printf("foo Address : %p",&foo);
    strcpy(output,name);
    return 0;
}

 

圖2 strcpy_1

 

圖3 strcpy_2

hs_strcpy緩沖溢出:

//下面hs_strcpy 和 isnull 都是恆生公司自己設計的
#include <stdio.h>

int isnull(const char *d_str)
{
    if (d_str == NULL || d_str[0] == '\0')
    return(0);              
    
    return(1);
}

int hs_strcpy(char *d_dest,const  char *s_src)
{
    if (isnull(s_src) == 0)
    {
        d_dest[0] = '\0';
        return(-1);
    }
    
    if (d_dest == NULL)
    return(-1);

    for(; *d_dest++ = *s_src++; )  /* the same as strcpy. */
    ;

    return(0);
}

// ͬ同樣的我們定義一個foo函數,來作為攻擊的對象
void foo(){
    printf("哈哈,我是因為hs_strcpy函數中被攻擊進來的!");
}
//char name[] = "adcdefg";                      // ① 我們希望輸入正確的數據,功能正常
//char name[]="adcdefghijklmnop";              // ② 我們輸入過多的字符,造成緩沖溢出,來尋找地址,通strcpy方法,最后還是找到
//    最后我們還是找到了mnop為緩沖溢出的地址,我們來修改緩沖地址
//    得到的foo地址還是為0040101E 
//char name[] = "adcdefghijkl\x1E\x10\x40\x00"; //③ 重點,同strcpy 
char name[] = "adcdefghijkl\x05\x10\x40\x00";

int j = 0;
int main()
{
    char output[8];
    int i;
    printf("foo Address:%p ---- main Address:%p----count(j):%d\r\n",&foo,&main, ++ j);
    hs_strcpy(output,name);
    return 0;
}

 

圖4 測試foo地址進行攻擊

 

 

圖5 hs_strcpy緩沖溢出

 

一圖揭秘緩沖溢出:

 圖6 緩沖溢出解析圖

  可能你已經從圖中看明白了,為什么會發生緩沖溢出。但是我自己還想嘮叨一下,在和那些可能還未理解的人解釋一下。對於任何一個函數而言,在調用這個函數時,調用的函數的參數需要入棧,並將call function指令下一條指令的地址,並保存到棧內,然后再跳轉到function函數內部執行。每個函數定義都會有函數頭和函數尾代碼,函數內需要用ebp保存函數棧幀基地址,因此先保存ebp原來的值到棧內,然后將棧指針esp內容保存到ebp棧頂是不用保存的,因為上一個棧幀的頂部將會是調用的棧幀底部。(兩棧幀相鄰的))。函數返回前需要做相反的操作——將esp指針恢復,並彈出ebp。這樣,函數內正常情況下無論怎么使用棧,都不會使棧失去平衡。然而當我們填充的緩沖區的內容一旦超過其本身的緩沖區容量,那么我們將會一次去占用ebp、返回地址里面的內容。從而導致緩沖區溢出的問題。

 

光大烏龍指再現

  通過上面的代碼,我們可以看到程序只是簡簡單單的訪問了一個不該訪問的Addr,但是怎么會出現原本一次發包而變成多次發包呢?其實很簡單,我們來看一下在hs_strcpy函數中,有打印出來了foo的地址和main的地址,其實會有一些想法的人肯定會知道,這里不是簡簡單單的打印一下,我們是為了再次創建“烏龍指”而設下的代碼,我們原先只是通過簡簡單單的調用到foo的地址,但是我們想想看,如果我們緩沖溢出,並且修改溢出的地址為main函數地址(注意了,這里所說的main函數,他也是一個函數,但是卻是一個特殊的函數,作為函數的一開始調用的地方,當然這話不是絕對的,我們還是有辦法修改不從main開始,扯得有點遠了)。代碼就不貼了,直接看一下效果圖把:

 

圖7 “無限”循環

     這里所謂的無限循環還是有待商榷的,確切的說,可以循環的次數還是和申請到棧的大小有關系的,在win7 64位中被循環了46次之多,而在xp 32位環境中卻被循環了32次。

  可能有些人會反駁說,這個緩沖的溢出在特定的環境下和特定的運行環境下才會發生,比如說在VC6.0中debug模式下發生,但是如果我在release模式下面就不會發生了,非常遺憾的告訴你,緩沖溢出並非只是在debug下的缺陷,在release模式下面照樣可以發生,當然也有人成功制造了在linux環境下的,發生緩沖溢出。

 

緩沖區溢出無處不在

 

圖8 XP緩沖區溢出圖

   我想我們這輩人還是從xp玩轉過來吧,所以對上面的這張圖片應該不會陌生。在xp中,我們經常可以看到類似上面的錯誤。其實上面的錯誤就是由於軟件設計的不合理,引發的緩沖區溢出,從圖上我們可以看到他的返回地址被修改成為了0x00407157。當然在0x00407157不是一個函數的開始地址,所有程序不能走了,那就只能報錯了;但如果0x00407157是一個函數地址,而且是一個敏感的函數,那么會發生什么,我想大家應該心里明白了。 

 

如何避免緩沖的溢出

  前面我們已經系統的看過了緩沖發生的整個過程,那么我們有什么辦法避免呢,呵呵,我想已經有人其實有想法了,那就是我們去控制src輸入的字符串長度。我不知道開發人員是否注意到我們在開發時,大多數的情況下,hs_strcpy是長度是一個定值,比如hs_strcpy(char[8],char[8]),但是在一些代碼中我們還是可以看到不同長度的賦值如hs_strcpy(char[8],char[128])等等。那后者很容易發生所謂的緩沖溢出。還有一種情況就是我們開發的時候,前端(delphi)傳過來的值就已經緩沖溢出了,所以有的時候,我們在允許用戶輸入數據的時候,把文本框控件的maxlength設置一下比較好。寫到這里讓我想起了培訓的那個例子,每個人都只盡了自己90%的努力,程序員沒有對輸入內容做合法性檢查,測試人員沒有對敏感數據進行測試,系統框架人員疏忽了一些不安全的代碼……,最后到用戶手上的就是一個非常可怕的產品,說不一定就會發生類似光大的事件。

 

總結

  當然緩沖溢出並不是簡簡單單的strcpy這個api函數,而且在很多函數中發生,這里就例舉部分:strcpy、strcat、sprintf、scanf、sscanf、fscanf、vfscanf、vsprintf、vscanf、vsscanf、streadd、strecpy、strtrns等等(參考IBM的一篇文章)。感興趣可以看一下這篇文章或者是查相應的文章或資料。當然網上不推介使用strcpy取而代之的是strncpy。而我推薦的就是在我們前台和后台一起對安全性進行檢查。當然我寫這篇文章還是比較入門的(如果在深層次點,會涉及到匯編),緩沖溢出並非因這篇文章而結束了,還有很多的地方需要大家去學習。 

 

本文pdf 版本已經上傳到我個人網站(http://jcodes.cn),歡迎進去下載。(打開jcodes網站->博客->從hs_strcpy談安全——緩沖區溢出

 

作者:jCodes 出處: http://jcodes.cn
本文版權歸作者和博客園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文連接,否則保留追究法律責任的權利。如果覺得還有幫助的話,可以點一下右下角的 【推薦】,希望能夠持續的為大家帶來好的技術文章!想跟我一起進步么?那就 【關注】我吧。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM