如何利用心血漏洞來獲取網站的私有 crypto 密鑰
現在OpenSSL的心血(Heartbleed) 漏洞已經是人盡皆知了: 在最流行的TLS實現之一的OpenSSL中由于缺少的一個邊界檢查導致了數以百萬計(或更多)的Web服務器泄露了內存中的各種敏感信息.這會將登錄證書, 認證cookie和網站流量泄露給攻擊者. 但它能否用于獲取站點的私鑰? 取得站點的私鑰就可以破解以前記錄的沒有達成完美的向前保密性的流量, 然后就可以在以后的TLS會話中實施中間人攻擊.
因為這是心血漏洞的更為嚴重的后果, 我決定嘗試下. 結果證明這個猜想是正確的. 經過幾天的攻關, 我能夠從一臺測試Nginx服務器上提取到私鑰. 稍后我會用我的技術來解決CloudFlare Challenge. 與另外幾名安全研究人員一起, 我們獨立地證明了RSA私鑰的確是處于危險之中. 下面我們來詳細介紹私鑰是怎樣提取的和為什么這種攻擊是可能的.
注: CloudFlare Challenge 是cloudflare.com發起的一項挑戰: 從他們搭建的測試nginx服務器(安裝了有heartbleed漏洞的OpenSSL)上竊取私鑰.
怎樣提取私鑰
不熟悉RSA的讀者可以在這了解下. 為了簡單點, 由兩個隨機生成的大的素數p和q相乘得到一個大的數(2048 bits) N. N會被公開,但p和q卻不會. 找到p或q就可以反算出私鑰. 一個通用的攻擊方式是對N做因式分解, 但這是很困難的. 然而, 通過像心血(Heartbleed)這樣的漏洞, 攻擊會變得簡單很多: 因為Web服務器需要將私鑰保存在內存中來簽名TLS的握手協議, p和q必須在內存中而且我們可以試著用heartbleed的網絡包來取得它們. 這個問題很簡單地就變成了如何從返回的數據中找到它們. 這也很簡單,因為我們知道p和q的長度是1024 bit(128字節), 并且OpenSSL在內存中以小字節序表示數據. 一個野蠻方法是將heartbleed數據包中每個連續的128字節做為一個小字節序數值然后測試這個數值是否能整除N, 這個方法足以發現潛在的漏洞. 這也是人們解決CloudFlare challenge的方法.
Coppersmith 改進
但是等等,和我們冷起動內存映像攻擊的方案不同。已經有很多通過部分消息恢復RSA的研究。最著名的是一份來自Coppersmith的論文,介紹了通過相關消息或者填充不足消息,以及在格基簡化算法幫助下因式分解部分消息來攻擊。通過 Coppersmith攻擊,只需知道P的上部或下部,N就可以有效地因式分解。基于此,和暴力破解所需的128字節相比,我們只需要上部或下部的64字節就能計算出秘鑰。在實踐中,Coppersmith的限制是計算開銷(但任然比因式分解好很多了),假設已知77字節(60%),我們可以非常迅速地梳理出“心血”包的潛在秘鑰。
回想起來,我已收集的超過10000個包(每個64KB)有242個私鑰的殘余適合Coppersmith攻擊。感謝Sage(雖然后來我發現Sage已經實現了Coppersmith攻擊)的全面計算機代數積木使Coppersmith攻擊的實現變得更容易。
我們能做的更好嗎?加入你曾經用openssl ras -text -in server.key命令查看過RSA私鑰,你會發現有相當多的數字超過兩個素數因子p和q。實際上,他們是為了優化Chinese Remainder Theorem預先計算的值。如果他們中的一些是遺漏了,他們也能被p推演出來。OpenSSL是怎樣將p和q的Montgomery representations用于快速乘積的?他們也承認Coppersmith的變體,以便局部的位也是有用的。考慮到這一點,我們開始在我的測試服務器上搜索已收集的包。但是在數據集中甚至沒有發現單獨出現的部分(大于16個字節)。這怎么可能呢?
注意:我的所有實驗和CloudFlare challenge的目標是單線程的Nginx。在一個多線程的web服務器上也是可能的,能觀察到更多的泄露。
為什么只會泄露p
當"心血"首次出現時, 有人爭辯說 RSA私鑰不會泄漏. 畢竟它們只會在Web服務器啟動時被加載, 所以它們位于較低的內存地址中. 而且隨著堆內存的向上增長, 隨后分配的被"心血"泄漏的緩沖區內存是訪問不了這些私鑰的. 這與我不能發現CRT預先計算的值是一樣的, 但不知為何p確實是泄漏了. 如果我們假設這個辯論是正確的, 問題就成了:為什么p被泄漏了?
另外, OpenSSL會清除掉所有用過的臨時BigNum. 為了減少動態分配臨時值引起的開銷, OpenSSL提供了一個以棧格式操作的BigNum池---BN_CTX. 一旦使用結束, 它的上下文會被銷毀并且所有分配的緩沖區也會被清除(scrubbed). 這意味著當創建完"心血"數據包后在內存中不會再有任何臨時數據(假設是單線程), 因為BN_CTX早就被釋放了.
我不會用我查明原因時所經歷過的痛苦來阻撓你, 所以下面我給出了答案:
當一個BigNum被擴展到一個更大的緩沖區時, 它原來的緩沖區在釋放前不會被置0. 導致p泄漏的控制流路徑鏈變得更細微了. 在初始化TLS握手期間, 服務器密鑰的交換是用私鑰簽名的. CRT簽名執行了一次modulo p操作, 這導致了p<<BN_BITS2的結果被儲存到了從BN_CTX池分配的臨時變量中. 在稍后的CRT 錯誤注入檢查中, 這個臨時變量又作為val[0]被重用了(記住BN_CTX的操作與棧類似). 一個有趣的事實是被重新分配的臨時變量只把它最低內存置0了, 所以對于p<<BN_BITS2什么都沒有被破壞(它的最低內存本身就是0). val[0]馬上接收Montgomery-reduced的值, 但因為初始緩沖區不足以儲存這個新的值, 它會擴大, 所以p又被釋放到了空閑堆空間中, 等待再次被使用. 因為這在每次TLS握手時都會發生, 它會被泄漏得到處都是.
因為很難找出是哪個BigNum會被擴大并引起靜態泄漏, 我用工具對OpenSSL做了點實驗. 結果證明了一個升級版本的p Montgomery表示也會在泄漏點被釋放, 但這只發生在Montgomery上下文初始化的首次RSA冪運算中. 它會一直存在于低內存地址中, 并且我不能在抓取的數據包中找到它.
上面的泄漏bug已經通知了OpenSSL小組. 雖然有點可怕, 但嚴格來說這并不是安全bug, 因為OpenSSL在設計時就沒想過要阻止敏感信息在堆上的泄漏.
Rubin Xu是一位劍橋大學在計算機實驗室安全組攻讀博士學位的博士生, 他的論文是關于移動安全, 并且他對密碼學也有興趣. 他是成功攻破CloudFlare Challenge的四人之一. 這篇博文首發于Light Blue Touchpaper blog. Rubin Xu感謝Joseph Bonneau對此文的建議和校對.