深入考察解釋型語言背后隱藏的攻擊面,Part 2(一)
接上文:
在本系列關(guān)于解釋型語言底層攻擊面的第一篇文章中,我們了解到,即使在Javascript、Python和Perl等解釋型語言的核心實現(xiàn)中,內(nèi)存安全也不是無懈可擊的。
在本文中,我們將更加深入地探討,在通過外部函數(shù)接口(Foreign Function Interface,F(xiàn)FI)將基于C/C++的庫“粘合”到解釋語言的過程中,安全漏洞是如何產(chǎn)生的。正如我們之前所討論的,F(xiàn)FI充當(dāng)用兩種不同語言編寫的代碼之間的接口。例如,使一個基于C語言的庫可用于Javascript程序。
FFI負責(zé)將編程語言A的對象翻譯成編程語言B可以使用的東西,反之亦然。為了實現(xiàn)這種翻譯,開發(fā)人員必須編寫特定于語言API的代碼,以實現(xiàn)兩種語言之間的來回轉(zhuǎn)換。這通常也被稱為編寫語言綁定。
從攻擊者的角度來看,外部語言綁定代表了一個可能的攻擊面。當(dāng)處理一個從內(nèi)存安全語言翻譯成內(nèi)存不安全語言(如C/C++)的FFI時,開發(fā)者就有可能引入內(nèi)存安全漏洞。
即使高層的語言被認為是內(nèi)存安全的,同時目標外部代碼也經(jīng)過了嚴格的安全審查,但是,在兩種語言之間架起橋梁的代碼中,仍可能潛伏著可利用的漏洞。
在這篇文章中,我們將仔細研究兩個這樣的漏洞,我們將一步步地了解攻擊者如何評估你的代碼的可利用性的。本文的目的是提高讀者對exploit開發(fā)過程的理解,而不僅僅是針對一個具體的案例,而是從概念的角度來理解。通過了解exploit開發(fā)人員如何思考您的代碼,幫您建立防御性的編程習(xí)慣,從而編寫出更安全的代碼。
在我們的案例研究中,我們將考察兩個看起來非常相似的bug,然而只有一個是bug,而另一個則是一個安全漏洞。兩者都存在于綁定Node.js包的C/C++代碼中。
node-sass
Node-sass是一個庫,它將Node.js綁定到LibSass(一款流行的樣式表預(yù)處理器Sass的C版本)。雖然node-sass最近被棄用了,但它每周仍有500萬次以上的下載量,所以,它是一個非常有價值的審計對象。
當(dāng)閱讀node-sass綁定時,我們注意到以下代碼模式:
- int indent_len = Nan::To
- Nan::Get(
- options,
- Nan::New("indentWidth").ToLocalChecked()
- ).ToLocalChecked()).FromJust();
- [1]
- ctx_w->indent = (char*)malloc(indent_len + 1);
- strcpy(ctx_w->indent, std::string(
- [2]
- indent_len,
- Nan::To
- Nan::Get(
- options,
- Nan::New("indentType").ToLocalChecked()
- ).ToLocalChecked()).FromJust() == 1 ? '\t' : ' '
在[1]處,我們注意到一個受控于用戶輸入的32位整數(shù)值被用于內(nèi)存分配。如果該用戶提供的整數(shù)為-1,則整數(shù)算術(shù)表達式indent_len + 1的值將變成0。在[2]處,原始負值用于創(chuàng)建由indent_len字符組成的制表符或空格字符串,其中indent_len值為負,現(xiàn)在將變成一個相當(dāng)大的正值,因為std::string構(gòu)造函數(shù)期望接收無符號的長度參數(shù),其類型為size_t。
在JS API級別,我們注意到indentWidth的檢索方式如下所示:
- /**
- * Get indent width
- *
- * @param {Object} options
- * @api private
- */
- function getIndentWidth(options) {
- var width = parseInt(options.indentWidth) || 2;
- return width > 10 ? 2 : width;
- }
此處的目的是確保indentWidth >= 2或 <= 10,但實際上這里僅檢查了上界,并且parseInt允許我們提供負值,例如:
- var sass = require('node-sass')
- var result = sass.renderSync({
- data: `h1 { font-size: 40px; }`,
- indentWidth: -1
- });
這將觸發(fā)一個整數(shù)溢出,從而導(dǎo)致分配的內(nèi)存不足,并進一步導(dǎo)致后續(xù)的內(nèi)存被破壞。
為了解決這個問題,node-sass應(yīng)該確保在將用戶提供的indentWidth值傳遞給底層綁定之前,先檢查該值的下界和上界。
全面地檢查輸入,并明確地將它們的取值范圍限制在對程序邏輯有意義的范圍內(nèi),這將很好地幫助您養(yǎng)成一種通用的防御性編程習(xí)慣。
所以我們來總結(jié)一下。這里的bug模式是什么?整數(shù)溢出,導(dǎo)致堆分配不足,其后的內(nèi)存填充可能會破壞相鄰的堆內(nèi)存。聽起來確實值得分配CVE,不是嗎?
然而,雖然這個整數(shù)溢出確實會導(dǎo)致堆內(nèi)存分配不足,但這個bug并不代表就是一個漏洞,因為這個樣式表輸入很可能不是攻擊者控制的,并且在任何堆破壞發(fā)生之前,都會拋出std::string異常。即使發(fā)生了堆損壞,也只是一個非常有限的控制覆蓋(借助于一個非常大的indent_len的制表符或空格字符),所以,實際被利用的可能性很低。
- anticomputer@dc1:~$ node sass.js
- terminate called after throwing an instance of 'std::length_error'
- what(): basic_string::_S_create
- Aborted (core dumped)
結(jié)論:只是一個bug。
那么,什么情況下攻擊者才會對這樣的bug感興趣呢?攻擊者能夠?qū)τ|發(fā)bug的輸入施加影響。在這種情況下,不太可能有人為node-sass綁定提供受控于攻擊者的輸入。同時,內(nèi)存破壞原語本身的控制能力也會非常有限。雖然確實存在這樣的情況:即使是非常有限的堆損壞也足以充分利用某個缺陷,但通常攻擊者會更樂于尋求具有某些控制權(quán)的情形,比如可以控制用于破壞內(nèi)存的東西,或者可以控制覆蓋的內(nèi)存數(shù)量。最好是兩者兼而有之。
在這種情況下,即使std::string構(gòu)造函數(shù)沒有退出,攻擊者也必須用空格或制表符進行大規(guī)模的覆蓋,以控制進程。雖然這并非完全不可能,但考慮到對周圍內(nèi)存布局的足夠影響和控制,可能性仍然偏低。
在這種情況下,我們通??梢酝ㄟ^回答下面的三個問題,來進行一個簡單的可利用性“嗅覺測試”:
- 攻擊者是如何觸發(fā)這個bug的?
- 攻擊者控制了哪些數(shù)據(jù),控制到什么程度?
- 哪些算法受到攻擊者控制的影響?
除此之外,可利用性主要取決于攻擊者的目標、經(jīng)驗和資源。這些我們可能一無所知。除非您花了很多時間實際編寫exploit,否則很難確定某個問題是否可利用。特別是當(dāng)您的代碼被其他軟件使用時,即您編寫的是庫代碼,或者是一個更大系統(tǒng)中的一個組件。在一個孤立的環(huán)境中,某個錯誤看起來只是bug,在更大的范圍內(nèi)可能就是安全漏洞。
雖然常識對于確定可利用性有很大的幫助,但在時間和資源允許的情況下,任何可以由用戶控制(或影響)的輸入觸發(fā)的bug都是潛在的安全漏洞,因此,將其視為安全漏洞是非常明智的做法。
png-img
對于我們的第二個案例研究,我們將考察GHSL-2020-142。這個bug存在于提供libpng綁定的node.js png-img包中。
當(dāng)加載PNG圖像進行處理時,png-img綁定將使用PNGIMG::InitStorage函數(shù)來分配用戶提供的PNG數(shù)據(jù)所需的初始內(nèi)存。
- void PngImg::InitStorage_() {
- rowPtrs_.resize(info_.height, nullptr);
- [1]
- data_ = new png_byte[info_.height * info_.rowbytes];
- [2]
- for(size_t i = 0; i < info_.height; ++i) {
- rowPtrs_[i] = data_ + i * info_.rowbytes;
- }
- }
在[1]處,我們觀察到為一個大小為info_.height * info_.rowbytes的png_byte數(shù)組分配了相應(yīng)的內(nèi)存。其中,結(jié)構(gòu)體成員height和rowbytes的類型都是png_uint_32,這意味著這里的整數(shù)算術(shù)表達式肯定是無符號32位整數(shù)運算。
info_.height可以直接作為32位整數(shù)從PNG文件提供,info_.rowbytes也可以從PNG數(shù)據(jù)派生。
這種乘法運算可能會觸發(fā)整數(shù)溢出,導(dǎo)致data_內(nèi)存區(qū)域分配不足。
例如,如果我們將info_.height設(shè)置為0x01000001,而info_.rowbytes的值為0x100,那么生成的表達式將是(0x01000001 * 0x100) & 0xffffffff ,其值為0x100。這樣的話,data_將作為一個0x100大小的png_byte數(shù)組來分配內(nèi)存,這明顯不夠用。
隨后,在[2]處,將使用行數(shù)據(jù)指針填充rowPtrs_array,這些指針指向所分配的內(nèi)存區(qū)的邊界之外,因為for循環(huán)條件是對原始的info_.height值進行操作的。
一旦實際的行數(shù)據(jù)被從PNG文件中讀取,任何與data_區(qū)域相鄰的內(nèi)存都可能被攻擊者控制的行數(shù)據(jù)覆蓋,最高可達info_.height * info_.rowbytes字節(jié),這給任何潛在的攻擊者提供了大量可控的進程內(nèi)存。
需要注意的是,根據(jù)攻擊者的意愿,可以通過不從PNG本身提供足夠數(shù)量的行數(shù)據(jù)來提前停止覆蓋,這時libpng錯誤例程就會啟動。任何后續(xù)處理錯誤路徑的程序邏輯都會在被破壞的堆內(nèi)存上運行。
這很有可能導(dǎo)致一個高度受控(無論是內(nèi)容還是大小)的堆溢出漏洞,我們的直覺是,這個bug可能是一個可利用的安全漏洞。
下面,讓我們來回答可利用性問題,以確定這個bug是否對攻擊者具有足夠的吸引力。
攻擊者是如何觸發(fā)該bug的?
這個bug是由攻擊者提供的PNG文件觸發(fā)的。攻擊者可以完全控制在png-img綁定中作用于PNG的任何數(shù)據(jù),并廢除文件格式完整性檢查所施加的任何限制。
因為攻擊者必須依賴于加載的惡意PNG文件,我們可以假設(shè)任何利用邏輯都可能必須包含在這個單一的PNG文件中。這意味著,攻擊者與目標Node.js進程反復(fù)交互的機“可能”更少,例如,實施信息泄露,以幫助后續(xù)的漏洞利用過程繞過任何系統(tǒng)級別的緩解措施,如地址空間布局隨機化(ASLR)。
我們說“可能”,是因為我們無法預(yù)測png-img的實際使用情況。換句話說,也可能存在這樣的使用情況:存在可重復(fù)的交互機會,來觸發(fā)該bug或進一步幫助利用該bug。
攻擊者能夠控制哪些數(shù)據(jù),控制到什么程度?
攻擊者可以提供所需的height和rowbytes變量,以便對整數(shù)運算和后續(xù)的整數(shù)封裝(integer wrap)進行精細控制。被封裝的值用于確定data_數(shù)組的最終分配內(nèi)存的大小。它們也可以通過PNG圖像本身提供完全受控的行數(shù)據(jù),這些數(shù)據(jù)通過rowPtrs數(shù)組中的越界指針值填充到越界內(nèi)存中。他們可以通過提前終止提供的行數(shù)據(jù),精細控制這個攻擊者提供的行數(shù)據(jù)有多少被填充到內(nèi)存中。
簡而言之,攻擊者可以通過精細控制內(nèi)容和長度來覆蓋任何與data_相鄰的堆內(nèi)存。
哪些算法會受到攻擊者控制的影響?
由于我們處理的是堆溢出,攻擊者的影響擴展到任何涉及被破壞的堆內(nèi)存的算法。這可能涉及Node.js解釋器代碼、系統(tǒng)庫代碼,當(dāng)然還有綁定代碼和任何相關(guān)庫代碼本身。
小結(jié)
在本文中,我們將深入地探討,在通過外部函數(shù)接口(Foreign Function Interface,F(xiàn)FI)將基于C/C++的庫“粘合”到解釋語言的過程中,安全漏洞是如何產(chǎn)生的。由于篇幅過長,我們將分為多篇進行介紹,更多精彩內(nèi)容,敬請期待!
本文翻譯自:https://securitylab.github.com/research/now-you-c-me-part-two