CVE-2016-0799簡單分析
0x00 內容簡介
最近openssl又除了一系列問題,具體可以看這裡。CVE-2016-0799只是其中一個比較簡單的漏洞。造成漏洞的原因主要有兩個。
doapr_outch
中有可能存在整數溢位導致申請記憶體大小為負數doapr_outch
函式在申請記憶體失敗時沒有做異常處理
0x01 原始碼分析
首先,去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 }
我是看完了一篇國外的分析文章之後瞭解了整個漏洞的流程,這裡我就試圖反向的思考一下這個漏洞。希望可以提高從程式碼補丁中尋找重現流程的能力。
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
所以想要向指定的記憶體寫入資料的話需要控制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 }
可以看到,確實是透過迴圈來改寫記憶體的。
1.2 副作用程式設計
函式副作用會給程式設計帶來不必要的麻煩,給程式帶來十分難以查詢的錯誤,並且降低程式的可讀性。嚴格的函式式語言要求函式必須無副作用。
副作用程式設計帶來的不必要麻煩有一句更通俗的話可以來說明。開發一時爽,除錯火葬場。這裡再來看一下
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。
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/
歡迎大家來探討,不足之處還請指正。
相關文章
- mr原理簡單分析2020-08-23
- SSRF漏洞簡單分析2020-07-16
- 簡單陰影分析2020-12-27
- HDLC報文簡單分析2024-07-05
- 簡單的UrlDns鏈分析2024-04-16DNS
- js熱更新簡單分析2019-04-06JS
- MediaScanner原始碼簡單分析2019-03-04原始碼
- 骷髏病毒簡單分析2018-04-10
- CVE-2016-1757簡單分析2020-08-19
- redux簡單實現與分析2018-04-12Redux
- 一隻android簡訊控制馬的簡單分析2020-08-19Android
- SpringBoot2.0原始碼分析(一):SpringBoot簡單分析2018-09-30Spring Boot原始碼
- 伺服器系統簡單分析2022-10-18伺服器
- Java 8 ArrayList 原始碼簡單分析2020-03-09Java原始碼
- ZipperDown漏洞簡單分析及防護2018-05-18
- 編譯程式(compiler)的簡單分析2018-08-07編譯Compile
- ElasticSearch 簡單的 搜尋 聚合 分析2018-04-16Elasticsearch
- 面試官:簡單聊聊 Go 逃逸分析?2022-04-15面試Go
- 簡單分析軟體專案成本管理2022-03-18
- 簡單分析MySQL中的primary key功能2021-09-09MySql
- JavaScript簡單計算器程式碼分析2018-07-02JavaScript
- Linux SNAT/DNAT簡單理解與案例分析。2018-07-25Linux
- 金融大資料分析還不簡單,有了Smartbi簡單幾步就能搞定2022-01-20大資料
- 分析一個簡單的goroutine資源池2021-12-27Go
- CVE-2015-7547簡單分析與除錯2020-08-19除錯
- 伺服器使用系統簡單的分析2022-03-01伺服器
- Linux4.1.15核心啟動流程簡單分析2020-10-06Linux
- 簡單程式的時間複雜度分析2021-09-09時間複雜度
- 一個left join SQL 簡單優化分析2018-12-04SQL優化
- 簡單易懂的tinker熱修復原理分析2018-08-03
- openGauss核心分析2:簡單查詢的執行2022-07-12
- 簡單分析Flask 資料庫遷移詳情2021-12-06Flask資料庫
- 對 MySQL 慢查詢日誌的簡單分析2020-07-08MySql
- 簡單分析synchronized不會鎖洩漏的原因2019-06-05synchronized
- 瞭解Java物件,簡單聊聊JVM調優分析2020-12-15Java物件JVM
- 依存句法分析器的簡單實現2018-10-17
- Python3 | 簡單爬蟲分析網頁元素2018-11-30Python爬蟲網頁
- Python運用於資料分析的簡單教程2018-07-22Python