<span id="7ztzv"></span>
<sub id="7ztzv"></sub>

<span id="7ztzv"></span><form id="7ztzv"></form>

<span id="7ztzv"></span>

        <address id="7ztzv"></address>

            原文地址:http://drops.wooyun.org/papers/13433

            0x00 內容簡介


            最近openssl又除了一系列問題,具體可以看這里CVE-2016-0799只是其中一個比較簡單的漏洞。造成漏洞的原因主要有兩個。

            0x01 源碼分析

            首先,去github上找到了這一次漏洞修復的commit,可以看到主要修改的是doapr_outch函數。

            p1

            有了一個大致的了解之后,將代碼切換到bug修復之前的版本。函數源碼如下:

            #!cpp
            697 static void                                                     
            698 doapr_outch(char **sbuffer,
            699             char **buffer, size_t *currlen, size_t *maxlen, int c)
            700 {
            701     /* If we haven't at least one buffer, someone has doe a big booboo */
            702     assert(*sbuffer != NULL || buffer != NULL);
            703             if (*buffer == NULL) {
            704     /* |currlen| must always be <= |*maxlen| */
            705     assert(*currlen <= *maxlen);
            706 
            707     if (buffer && *currlen == *maxlen) {
            708         *maxlen += 1024;
            709         if (*buffer == NULL) {   
            710             *buffer = OPENSSL_malloc(*maxl
            711                 /* Panic! Can't really do anything sensible. Just return */
            712                 return; //這里沒有做異常處理直接返回了
            713             }           
            714             if (*currlen > 0) {
            715                 assert(*sbuffer != NULL);
            716                 memcpy(*buffer, *sbuffer, *currlen);
            717             }           
            718             *sbuffer = NULL;
            719         } else {        
            720             *buffer = OPENSSL_realloc(*buffer, *maxlen);
            721             if (!*buffer) {
            722                 /* Panic! Can't really do anything sensible. Just return */
            723                 return; //這里沒有做異常處理直接返回了
            724             }           
            725         }               
            726     }                   
            727 
            728     if (*currlen < *maxlen) {
            729         if (*sbuffer)   
            730             (*sbuffer)[(*currlen)++] = (char)c;
            731         else            
            732             (*buffer)[(*currlen)++] = (char)c;
            733     }                   
            734 
            735     return;             
            736 }
            

            我是看完了一篇國外的分析文章之后了解了整個漏洞的流程,這里我就試圖反向的思考一下這個漏洞。希望可以提高從代碼補丁中尋找重現流程的能力。

            1.1 尋找內存改寫的方式

            因為通過補丁已經知道是doapr_outch函數導致的堆腐敗問題,所以doapr_outch一定存在改寫數據的代碼段。可以看到除了728-734行代碼是對內存的改寫外,沒有其他地方操作內存的內容了。

            #!cpp
            728     if (*currlen < *maxlen) {
            729         if (*sbuffer)   
            730             (*sbuffer)[(*currlen)++] = (char)c; //這里
            731         else            
            732             (*buffer)[(*currlen)++] = (char)c; //這里
            733     }                   
            

            這里改寫內存的方式可以用偽代碼簡單總結一下:

            #!c
            base[offset]=c
            

            所以想要向指定的內存寫入數據的話需要控制baseoffset兩個參數。而寫入的數據是c。如果控制了baseoffset那么每次調用函數就可以改寫一個字節。

            如果是有經驗的開發人員可以很容易看出外部在調用的時候一定是循環調用了doapr_outch,看一看函數調用處的代碼。

            #!c
            425 static void
            426 fmtstr(char **sbuffer,
            427        char **buffer,
            428        size_t *currlen,
            429        size_t *maxlen, const char *value, int flags, int min, int max)
            430 {
            431     int padlen, strln;
            432     int cnt = 0;
            433 
            434     if (value == 0)
            435         value = "<NULL>";
            436     for (strln = 0; value[strln]; ++strln) ;
            437     padlen = min - strln;
            438     if (padlen < 0)
            439         padlen = 0;
            440     if (flags & DP_F_MINUS)
            441         padlen = -padlen;
            442 
            443     while ((padlen > 0) && (cnt < max)) {
            444         doapr_outch(sbuffer, buffer, currlen, maxlen, ' ');
            445         --padlen;
            446         ++cnt;
            447     }
            448     while (*value && (cnt < max)) {
            449         doapr_outch(sbuffer, buffer, currlen, maxlen, *value++); //這里!
            450         ++cnt;
            451     }
            452     ...
            453  }               
            

            可以看到,確實是通過循環來改寫內存的。

            1.2 副作用編程

            函數副作用會給程序設計帶來不必要的麻煩,給程序帶來十分難以查找的錯誤,并且降低程序的可讀性。嚴格的函數式語言要求函數必須無副作用。

            副作用編程帶來的不必要麻煩有一句更通俗的話可以來說明。開發一時爽,調試火葬場。這里再來看一下

            doapr_outch的函數聲明

            #!c
            static void doapr_outch(char **, char **, size_t *, size_t *, int);
            

            從聲明不難看出sbufferbuffercurrlenmaxlen這幾個參數在函數第n次運行時候如果被改變了,那么第n+1次運行的時候,這些參數將使用上次改變了的值。

            再結合代碼寫入處內存改寫的方式,就可以肯定sbufferbuffer一定有一個或者全部被改寫了,導致進入了意料之外的邏輯。

            #!c
            728     if (*currlen < *maxlen) {
            729         if (*sbuffer)   
            730             (*sbuffer)[(*currlen)++] = (char)c; //這里
            731         else            
            732             (*buffer)[(*currlen)++] = (char)c; //這里
            733     }             
            

            因為Malloc或者Realloc出來的地址一定不是可控的,而系統傳進來的sbuffer也一定不可控,再結合上面的代碼,如果sbuffer或者buffer指向NULL的話,基址就是固定的了。

            718行的代碼會將sbuffer設置為空指針。而buffer編程空指針只能是申請內存失敗的時候。

            在結合上728-733行代碼,要做到這一步一定要滿足的條件是*sbuffer*buffer都指向NULL,導致代碼進入改寫*buffer為基址的內存塊。其他任何情況都無法做到內存開始地址可控。

            所以再分代碼,看流程是否可能將*sbuffer*buffer賦值為NULL

            1.3 改寫sbuffer與buffer

            #!c
            697 static void                                                     
            698 doapr_outch(char **sbuffer,
            699             char **buffer, size_t *currlen, size_t *maxlen, int c)
            700 {
            701     /* If we haven't at least one buffer, someone has doe a big booboo */
            702     assert(*sbuffer != NULL || buffer != NULL);
            703             if (*buffer == NULL) {
            704     /* |currlen| must always be <= |*maxlen| */
            705     assert(*currlen <= *maxlen);
            706 
            707     if (buffer && *currlen == *maxlen) {
            708         *maxlen += 1024;
            709         if (*buffer == NULL) {   
            710             *buffer = OPENSSL_malloc(*maxl
            711                 /* Panic! Can't really do anything sensible. Just return */
            712                 return; //這里沒有做異常處理直接返回了
            713             }           
            714             if (*currlen > 0) {
            715                 assert(*sbuffer != NULL);
            716                 memcpy(*buffer, *sbuffer, *currlen);
            717             }           
            718             *sbuffer = NULL;//這里!
                    ...
            728     if (*currlen < *maxlen) {
            729         if (*sbuffer)   
            730             (*sbuffer)[(*currlen)++] = (char)c;
            731         else            
            732             (*buffer)[(*currlen)++] = (char)c;
            733     }                   
            734 
            735     return;             
            736 }
            

            在循環調用doapr_outch之后,當*currlen == *maxlen成立的時候就會進入內存申請模塊,因為*buffer還沒有申請過所以進入上面一個分支,申請內存后將*sbuffer設為NULL。

            還需要將*buffer設為NULL。

            #!c
            707     if (buffer && *currlen == *maxlen) {
            708         *maxlen += 1024;
            709         if (*buffer == NULL) {   
            710             *buffer = OPENSSL_malloc(*maxl
            711                 /* Panic! Can't really do anything sensible. Just return */
            712                 return; //這里沒有做異常處理直接返回了
            713             }           
            714             if (*currlen > 0) {
            715                 assert(*sbuffer != NULL);
            716                 memcpy(*buffer, *sbuffer, *currlen);
            717             }           
            718             *sbuffer = NULL;
            719         } else {        
            720             *buffer = OPENSSL_realloc(*buffer, *maxlen);
            721             if (!*buffer) {
            722                 /* Panic! Can't really do anything sensible. Just return */
            723                 return; //這里沒有做異常處理直接返回了
            724             }           
            725         }               
            726     }    
            

            再一次*currlen == *maxlen之后,又會進入內存分配階段,這次會進入Realloc的分支,那么只要realloc失敗的話,*buffer就會被賦值為NULL。

            最簡單的情況就是堆上內存用完了,這個時候buffer就是NULL了,這個時候就可以根據currlen以及后續的c來改寫目標地址的數據了。但是堆上內存用完,導致申請內存返回NULL,是一件不可控的事情。

            那么除了這種情況,還有什么情況下,realloc會返回NULL呢。

            #!c
            375    void *CRYPTO_realloc(void *str, int num, const char *file, int line)
            376    {
            377        void *ret = NULL;
            378
            379        if (str == NULL)
            380            return CRYPTO_malloc(num, file, line);
            381
            382        if (num <= 0)
            383            return NULL;
            

            可以注意到在708行,對*maxlen做了增加1024的操作,那么如果maxlen怎么1024之后超過int的范圍,就會導致realloc傳入的size是一個負數。這個時候buffer就會因為realloc的參數錯誤被設置為NULL。然后因為出錯,函數退出。

            1.3 出錯不處理

            #!c
            448     while (*value && (cnt < max)) {
            449         doapr_outch(sbuffer, buffer, currlen, maxlen, *value++); //這里!
            450         ++cnt;
            451     }
            

            從這里可以看到,*buffer被設置為NULL,返回出來了。但是外面的循環什么都沒干,又繼續執行了。

            這個時候就可以做內存改寫了。currlen與c都是與我們傳遞的字符串相關的,這個很好理解了。

            0x02 小結


            接下來要做的事情就是根據對漏洞的理解編寫一個POC來調試。這樣可以加深對漏洞的理解。在開發中也能更好的引以為戒。

            0x03 參考

            1.OpenSSL CVE-2016-0799: heap corruption via BIO_printf

            https://guidovranken.wordpress.com/2016/02/27/openssl-cve-2016-0799-heap-corruption-via-bio_printf/

            PS:

            這是我的學習分享博客http://turingh.github.io/

            歡迎大家來探討,不足之處還請指正。

            <span id="7ztzv"></span>
            <sub id="7ztzv"></sub>

            <span id="7ztzv"></span><form id="7ztzv"></form>

            <span id="7ztzv"></span>

                  <address id="7ztzv"></address>

                      亚洲欧美在线