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