CVE-2016-0799簡單分析

wyzsk發表於2020-08-19
作者: mrh · 2016/03/04 12:59

0x00 內容簡介


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

  • doapr_outch中有可能存在整數溢位導致申請記憶體大小為負數
  • doapr_outch函式在申請記憶體失敗時沒有做異常處理

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/

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

本文章來源於烏雲知識庫,此映象為了方便大家學習研究,文章版權歸烏雲知識庫!

相關文章