最近openssl又除了一系列問題,具體可以看這里。CVE-2016-0799只是其中一個比較簡單的漏洞。造成漏洞的原因主要有兩個。
doapr_outch
中有可能存在整數溢出導致申請內存大小為負數doapr_outch
函數在申請內存失敗時沒有做異常處理首先,去github上找到了這一次漏洞修復的commit,可以看到主要修改的是doapr_outch
函數。
有了一個大致的了解之后,將代碼切換到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 }
我是看完了一篇國外的分析文章之后了解了整個漏洞的流程,這里我就試圖反向的思考一下這個漏洞。希望可以提高從代碼補丁中尋找重現流程的能力。
因為通過補丁已經知道是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
所以想要向指定的內存寫入數據的話需要控制base
與offset
兩個參數。而寫入的數據是c
。如果控制了base
與offset
那么每次調用函數就可以改寫一個字節。
如果是有經驗的開發人員可以很容易看出外部在調用的時候一定是循環調用了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 }
可以看到,確實是通過循環來改寫內存的。
函數副作用會給程序設計帶來不必要的麻煩,給程序帶來十分難以查找的錯誤,并且降低程序的可讀性。嚴格的函數式語言要求函數必須無副作用。
副作用編程帶來的不必要麻煩有一句更通俗的話可以來說明。開發一時爽,調試火葬場。這里再來看一下
doapr_outch
的函數聲明
#!c
static void doapr_outch(char **, char **, size_t *, size_t *, int);
從聲明不難看出sbuffer
,buffer
,currlen
,maxlen
這幾個參數在函數第n次運行時候如果被改變了,那么第n+1次運行的時候,這些參數將使用上次改變了的值。
再結合代碼寫入處內存改寫的方式,就可以肯定sbuffer
和buffer
一定有一個或者全部被改寫了,導致進入了意料之外的邏輯。
#!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。
#!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。然后因為出錯,函數退出。
#!c
448 while (*value && (cnt < max)) {
449 doapr_outch(sbuffer, buffer, currlen, maxlen, *value++); //這里!
450 ++cnt;
451 }
從這里可以看到,*buffer
被設置為NULL,返回出來了。但是外面的循環什么都沒干,又繼續執行了。
這個時候就可以做內存改寫了。currlen與c都是與我們傳遞的字符串相關的,這個很好理解了。
接下來要做的事情就是根據對漏洞的理解編寫一個POC來調試。這樣可以加深對漏洞的理解。在開發中也能更好的引以為戒。
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/
歡迎大家來探討,不足之處還請指正。