關於OpenSSL“心臟出血”漏洞的分析

wyzsk發表於2020-08-19
作者: Fish · 2014/04/08 14:17

0x00 背景


原作者:Sean Cassidy 原作者Twitter:@ex509 原作者部落格:http://blog.existentialize.com 來源:http://blog.existentialize.com/diagnosis-of-the-openssl-heartbleed-bug.html

當我分析GnuTLS的漏洞的時候,我曾經說過,那不會是我們看到的最後一個TLS棧上的嚴重bug。然而我沒想到這次OpenSSL的bug會如此嚴重。

OpenSSL“心臟出血”漏洞是一個非常嚴重的問題。這個漏洞使攻擊者能夠從記憶體中讀取多達64 KB的資料。一些安全研究員表示:

無需任何特權資訊或身份驗證,我們就可以從我們自己的(測試機上)偷來X.509證書的私鑰、使用者名稱與密碼、聊天工具的訊息、電子郵件以及重要的商業文件和通訊等資料。

這一切是如何發生的呢?讓我們一起從程式碼中一探究竟吧。

0x01 Bug


請看ssl/dl_both.c,漏洞的補丁從這行語句開始:

#!cpp
int            
dtls1_process_heartbeat(SSL *s)
    {          
    unsigned char *p = &s->s3->rrec.data[0], *pl;
    unsigned short hbtype;
    unsigned int payload;
    unsigned int padding = 16; /* Use minimum padding */

一上來我們就拿到了一個指向一條SSLv3記錄中資料的指標。結構體SSL3_RECORD的定義如下(譯者注:結構體SSL3_RECORD不是SSLv3記錄的實際儲存格式。一條SSLv3記錄所遵循的儲存格式請參見下文分析):

#!cpp
typedef struct ssl3_record_st
    {
        int type;               /* type of record */
        unsigned int length;    /* How many bytes available */
        unsigned int off;       /* read/write offset into 'buf' */
        unsigned char *data;    /* pointer to the record data */
        unsigned char *input;   /* where the decode bytes are */
        unsigned char *comp;    /* only used with decompression - malloc()ed */
        unsigned long epoch;    /* epoch number, needed by DTLS1 */
        unsigned char seq_num[8]; /* sequence number, needed by DTLS1 */
    } SSL3_RECORD;

每條SSLv3記錄中包含一個型別域(type)、一個長度域(length)和一個指向記錄資料的指標(data)。我們回頭去看dtls1_process_heartbeat:

#!cpp
/* Read type and payload length first */
hbtype = *p++;
n2s(p, payload);
pl = p;

SSLv3記錄的第一個位元組標明瞭心跳包的型別。宏n2s從指標p指向的陣列中取出前兩個位元組,並把它們存入變數payload中——這實際上是心跳包載荷的長度域(length)。注意程式並沒有檢查這條SSLv3記錄的實際長度。變數pl則指向由訪問者提供的心跳包資料。

這個函式的後面進行了以下工作:

#!cpp
unsigned char *buffer, *bp;
int r;

/* Allocate memory for the response, size is 1 byte
 * message type, plus 2 bytes payload length, plus
 * payload, plus padding
 */
buffer = OPENSSL_malloc(1 + 2 + payload + padding);
bp = buffer;

所以程式將分配一段由訪問者指定大小的記憶體區域,這段記憶體區域最大為 (65535 + 1 + 2 + 16) 個位元組。變數bp是用來訪問這段記憶體區域的指標。

#!cpp
/* Enter response type, length and copy payload */
*bp++ = TLS1_HB_RESPONSE;
s2n(payload, bp);
memcpy(bp, pl, payload);

宏s2n與宏n2s乾的事情正好相反:s2n讀入一個16 bit長的值,然後將它存成雙位元組值,所以s2n會將與請求的心跳包載荷長度相同的長度值存入變數payload。然後程式從pl處開始複製payload個位元組到新分配的bp陣列中——pl指向了使用者提供的心跳包資料。最後,程式將所有資料發回給使用者。那麼Bug在哪裡呢?

0x01a 使用者可以控制變數payload和pl

如果使用者並沒有在心跳包中提供足夠多的資料,會導致什麼問題?比如pl指向的資料實際上只有一個位元組,那麼memcpy會把這條SSLv3記錄之後的資料——無論那些資料是什麼——都複製出來。

很明顯,SSLv3記錄附近有不少東西。

說實話,我對發現了OpenSSL“心臟出血”漏洞的那些人的宣告感到吃驚。當我聽到他們的宣告時,我認為64 KB資料根本不足以推算出像私鑰一類的資料。至少在x86上,堆是向高地址增長的,所以我認為對指標pl的讀取只能讀到新分配的記憶體區域,例如指標bp指向的區域。儲存私鑰和其它資訊的記憶體區域的分配早於對指標pl指向的記憶體區域的分配,所以攻擊者是無法讀到那些敏感資料的。當然,考慮到現代malloc的各種神奇實現,我的推斷並不總是成立的。

當然,你也沒辦法讀取其它程式的資料,所以“重要的商業文件”必須位於當前程式的記憶體區域中、小於64 KB,並且剛好位於指標pl指向的記憶體塊附近。

研究者聲稱他們成功恢復了金鑰,我希望能看到PoC。如果你找到了PoC,請聯絡我

0x01b 漏洞修補

修復程式碼中最重要的一部分如下:

#!cpp
/* Read type and payload length first */
if (1 + 2 + 16 > s->s3->rrec.length)
    return 0; /* silently discard */
hbtype = *p++;
n2s(p, payload);
if (1 + 2 + payload + 16 > s->s3->rrec.length)
    return 0; /* silently discard per RFC 6520 sec. 4 */
pl = p;

這段程式碼幹了兩件事情:首先第一行語句拋棄了長度為0的心跳包,然後第二步檢查確保了心跳包足夠長。就這麼簡單。

0x02 前車之鑑


我們能從這個漏洞中學到什麼呢?

我是C的粉絲。這是我最早接觸的程式語言,也是我在工作中使用的第一門得心應手的語言。但是和之前相比,現在我更清楚地看到了C語言的侷限性。

GnuTLS漏洞和這個漏洞出發,我認為我們應當做到下面三條:

花錢請人對像OpenSSL這樣的關鍵安全基礎設施進行安全審計;
為這些庫寫大量的單元測試和綜合測試;
開始在更安全的語言中編寫替代品。

考慮到使用C語言進行安全程式設計的困難性,我不認為還有什麼其他的解決方案。我會試著做這些,你呢?

作者簡介:Sean是一位關於如何把事兒幹好的軟體工程師。現在他在Squadron工作。Squadron是一個專為SaaS應用程式準備的配置與釋出管理工具。

測試版本的結果以及檢測工具:

OpenSSL 1.0.1 through 1.0.1f (inclusive) are vulnerable
OpenSSL 1.0.1g is NOT vulnerable
OpenSSL 1.0.0 branch is NOT vulnerable
OpenSSL 0.9.8 branch is NOT vulnerable

http://filippo.io/Heartbleed/

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

相關文章