你引用的開源代碼,可能夾帶了漏洞
0. 概述
近一兩年,供應鏈攻擊已不只是白帽子的實驗試水,轉(zhuǎn)變成黑客和黑產(chǎn)的真實攻擊手段,“軟件供應鏈安全”概念,重新成為了炙手可熱的話題。
當前對供應鏈安全的探討多是關(guān)于機制的,例如企業(yè)上下游公司的攻擊面,或者各種開發(fā)語言軟件包管理引用的投毒欺騙。但是對于一線的開發(fā)實踐中的風險,目前鮮有分析。
試想,在一個多人協(xié)作開發(fā)的項目中,如果:
有一個偷懶的開發(fā)者復制很多網(wǎng)上貼的示例代碼或錯誤代碼;
或者一個新加入的開發(fā)者,復制了該項目的某些舊代碼,其中有一些帶有已修復的bug;
甚至如果有一個惡意開發(fā)者,故意寫了一個形似手誤的bug、但實際是可以被遠程利用的隱蔽后門。
那么項目的擁有者要如何分辨這些有風險的代碼?只要把這些潛在的“壞的”開發(fā)角色換為上游開源代碼供應方,一個完全可能的“開源供應鏈漏洞”場景就很好理解了。
從這一個簡單的假想出發(fā),本文將帶領(lǐng)讀者看到,設計開源代碼使用的開發(fā)實踐中的真實威脅,以及我們構(gòu)建好的一個解決方案。
1. “代碼復用”引入的深層次供應鏈問題
目前為止,受到廣泛關(guān)注的供應鏈風險,集中于兩類:以名稱易于混淆的惡意軟件包倉庫投毒,如PyPi、NPM倉庫的投毒攻擊;以及滲透軟件廠商的上下游供應商,以竊取其掌握的敏感信息。但源代碼的流動和依賴,遠非庫依賴那么簡單。
這里,我們抽象出一個開發(fā)實踐中普遍存在,但內(nèi)中安全風險并未引起廣泛注意和分析的供應鏈威脅向量——代碼復用。
1.1. 代碼復用與漏洞:Google P0分析視角
今年6月,Google Project Zero下屬的漏洞根因分析小組發(fā)出半年度在野利用漏洞報告。報告分析2022年上半年內(nèi),發(fā)現(xiàn)被在野利用的系統(tǒng)或關(guān)鍵基礎軟件0day漏洞中,50%(9/18)并非全新孤立的漏洞,而是某個歷史漏洞的關(guān)聯(lián)“變種”,主要分為三類:
歷史被修復的漏洞,補丁代碼被回滾造成漏洞重現(xiàn),如CVE-2022-22620,是WebKit引擎在2013年已經(jīng)修復的漏洞,于2016年代碼重構(gòu)中被原樣改了回去;
某個此前被修復的漏洞,在有關(guān)聯(lián)和相似關(guān)系的其它功能模塊中發(fā)現(xiàn)類似漏洞,如CVE-2021-39793,是Linux etnaviv驅(qū)動程序修補的bug,在Google Pixel Mali GPU私有驅(qū)動代碼中的同源漏洞;
對于漏洞根因,此前的修復方案不完整,造成可在新的漏洞觸發(fā)路徑利用,如Windows系統(tǒng)win32k驅(qū)動中,CVE-2022-21882是CVE-2021-1732的相同用戶模式回調(diào)bug。
這樣的觀察側(cè)面表明,自體或繼承的同源代碼,帶有復雜的風險,特別是對黑客黑產(chǎn)而言,挖掘利用這些“炒冷飯”的漏洞,實際是很經(jīng)濟的實踐。
1.2. 各種形式的“代碼復用”及對應的風險案例
結(jié)合開源生態(tài)以及企業(yè)級產(chǎn)品開發(fā)實踐,我們歸結(jié)除了簡單的軟件包引用形式外,至少有三個顆粒度的代碼復用形式,特別是在C/C++這類沒有統(tǒng)一的包依賴管理機制的主流開發(fā)語言中存在。
1.2.1. 開源項目子目錄或文件形式包含
某些項目依賴于特定開源組件功能,但出于維持明確的依賴關(guān)系、清晰的制品形式的目的,并不會動態(tài)鏈接(依賴)獨立的軟件包或動態(tài)庫,而是將固定版本的開源代碼,完整目錄或者裁剪的部分文件形式,直接包含在代碼目錄下,同步編譯靜態(tài)鏈接在制品二進制中。
這種情況下,開發(fā)者的訴求一般是追求“穩(wěn)定可用即可”,并規(guī)避依賴子模塊接口更新帶來的維護成本,更不會主動關(guān)注子模塊是否有安全更新并跟進,因此很容易存在有歷史漏洞的成分。
例如,Java開發(fā)的移動端通信SDK和軟件Telegram,以jni調(diào)用形式包含了boringssl,而后者是Google自OpenSSL 1.1.0歷史分支fork并維護的二次開發(fā)項目。雖然boringssl以一定機制跟進了上游的漏洞修復,但Telegram對這些依賴的更新沒有確定策略,最后一次更新boringssl停留在2020.8.15,因此近兩年多的漏洞都可能影響著Telegram(如果構(gòu)造出合理的調(diào)用路徑),并進一步影響封裝Telegram的更下游應用,如Nekogram。
1.2.2. 函數(shù)和片段級別代碼復用(復制)
開源代碼往往也成為開發(fā)實踐中取之不竭的代碼模板和材料。對于某些典型原子功能的實現(xiàn),開發(fā)者“借用”開源代碼片段并依據(jù)自己項目的上下文做適當修改變形,是較為普遍,但又無法評估存在占比的實踐。根據(jù)Synopsys今年在17個商業(yè)體超過2400個商用軟件代碼倉庫中分析后,形成的開源安全與風險分析報告認為,商業(yè)軟件中78%比例的代碼實際是開源代碼。
在我們對開源代碼的分析中,也有一定量的代碼復用,并隨著復用的舊版本開源代碼引入潛在脆弱代碼的案例。例如,某款韓國 自研 的IoT專用安全庫中的密碼算法實現(xiàn)中,即發(fā)現(xiàn)了疑似復制自OpenSSL密碼算法庫中中國SM2算法的功能實現(xiàn),且復制版本為被爆出高危漏洞CVE-2021-3711的修補前函數(shù)。
1.2.3. 數(shù)據(jù)結(jié)構(gòu)和接口調(diào)用的語法復用
另一種典型的問題是數(shù)據(jù)結(jié)構(gòu)和接口的誤用。對某些未充分文檔化的數(shù)據(jù)結(jié)構(gòu)和功能接口,項目內(nèi)部協(xié)作的開發(fā)者可能存在共通的錯誤理解和誤用情況;作為SDK導出的接口被下游開發(fā)者誤用的情況更是屢見不鮮。此時就會看到針對一個漏洞根因的多處位置、語法各異但語用同源的漏洞,這樣的案例屢見不鮮。
一個很容易理解的案例是,OpenSSL漏洞CVE-2021-3712,根因在于定義的結(jié)構(gòu)體ASN1_IA5STRING,其中帶有一個非'\0'結(jié)尾的緩沖區(qū)指針,和緩沖區(qū)有效數(shù)據(jù)長度字段;但大量開源協(xié)作開發(fā)者未意識到緩沖區(qū)并非標準C樣式字符串,而使用了不安全的字符串操作函數(shù),例如sprintf, strdup, strcat, strchr等以及其在OpenSSL中的封裝版本,從而造成了多處緩沖區(qū)越界訪問或?qū)懸绯龅?。進一步地,這個數(shù)據(jù)結(jié)構(gòu)也被導出,從而被一些依賴的下游項目同樣誤用,造成漏洞的修復無法收斂。
2. 從問題到解決方案之間的鴻溝
考慮到代碼開發(fā)中人的因素的復雜性,可以預見以上代碼復用引入漏洞威脅在未來可能的增長,特別是在商業(yè)對抗領(lǐng)域。但現(xiàn)有的程序分析技術(shù),還不足以解決這類看似簡單的問題。
2.1. 軟件成分分析(SCA)的掣肘
看起來源代碼復用的問題,剛好是軟件成分分析(SCA)的目標領(lǐng)域,因為可以抽象為掃描發(fā)現(xiàn)帶有脆弱性的開源或自體代碼成分——但是當前的SCA或許還并不是我們期待的模樣。
現(xiàn)有實際投產(chǎn)的主流SCA技術(shù),主要面向有統(tǒng)一包管理機制的開發(fā)語言生態(tài),借助解析依賴關(guān)系配置,獲得直接和間接的軟件包依賴圖譜,亦即“成分”,典型如對Maven、PyPi、NPM等語言包倉庫的依賴。某些頭部軟件分析工具提供商也在將高校和科研院所的研究成果轉(zhuǎn)化為可用的產(chǎn)品,通過對開源代碼設計計算一種類似模糊哈希的指紋,或者對關(guān)鍵代碼做token化后形成模糊查找模板,從而具備對開源代碼引用的一定程度的匹配檢測能力。
但是,SCA的技術(shù)路線本身就制約了可分析問題的方向和顆粒度。顯然,以上討論的代碼復用引入的風險,均歸類為無顯式依賴關(guān)系的代碼復用,這決定難以通過當下成熟的SCA技術(shù)覆蓋到。而對于通過代碼指紋方式掃描代碼存在性,考慮到代碼搜索空間,采集計算指紋的代碼顆粒度越細,總和指紋庫越會嚴重膨脹,且指紋匹配搜索的運算量也可能提升到算力瓶頸以上,從而反向約束指紋設計的精度上限;根據(jù)了解,當前具備可用性前景的技術(shù),也僅能做文件粒度的簽名和匹配查找,距離函數(shù)粒度有難以逾越的鴻溝。
此外,如上所述,一些代碼復用,并不保證文本層面相似特征的保留。這種情況下,需要更偏向語法甚至語義層面的抽象和分析,這也是SCA所不具備的。
2.2. 靜態(tài)源碼分析測試(SAST)的缺失
考慮到成分分析方法最難覆蓋到偏向語法模式相似的領(lǐng)域,很自然的,我們需要尋求靜態(tài)源代碼安全分析測試(SAST)的幫助。針對開源代碼的測試,我們已經(jīng)有了一個得力的工具,GitHub面向開源開放使用的CodeQL。對CodeQL的用法介紹我們在此不再贅述,它以類似SQL的規(guī)則樣式對語法數(shù)據(jù)庫做查詢的形式、對多語言的支持、豐富的API接口都保證了作為SAST的靈活可用性。它對已知漏洞的覆蓋和可掃描性又如何?
2.2.1. 實例引入
為簡單地說明問題,我們不妨“臆造”一個與歷史漏洞同源的代碼bug。OpenSSL的一個高危歷史漏洞CVE-2020-1967,是典型的空指針解引用問題。我們選擇在一個由OpenSSL二次開發(fā)的開源項目BabaSSL中,將該漏洞移植到另一個上下文,移植的代碼變更為:

這里,tls1_lookup_sigalg()函數(shù)返回值為一個可能取值為NULL的指針,這里去掉了一個NULL的檢查。實際上,如果將這處改動反過來看,那么就可以當做一個預先存在的漏洞修復的patch。針對這個“漏洞”,根據(jù)patch寫ql規(guī)則覆蓋漏洞成因,關(guān)鍵點如下:
需要定位的目標是一個SIGALG_LOOKUP *類型變量和一個if塊;
變量在if同一層代碼塊中定義或賦值;
之后在if的條件語句中直接解引用其成員變量做判斷;
當前代碼塊以及if的判斷條件中,沒有對指針與NULL的比較判斷。
據(jù)此可以簡單編寫規(guī)則,覆蓋這個漏洞的上下文:

2.2.2. 問題和需求分析
CodeQL可有效用于特定編程語言中,典型漏洞(bug)成因模式的檢測。但真實的漏洞,只有一部分能夠被已有的規(guī)則覆蓋檢出;主要原因之一,是已有規(guī)則是通用問題導向的,而其實現(xiàn),又僅覆蓋特定問題場景。
例如,對于C/C++語言中,空指針解引用這個經(jīng)典問題(CWE-476),CodeQL用幾條通用規(guī)則覆蓋若干個典型問題場景,如特定指針類型變量先解引用后檢查NULL,或判斷某個返回值為指針類型的函數(shù),是否在多數(shù)調(diào)用時檢查了返回值是否為NULL而在某些地方未做檢查。而如果某指針變量先作為參數(shù)傳遞到了一個用戶函數(shù),之后再做了解引用,那么受限于過程間分析的能力,無法判斷傳遞到的函數(shù)是否是一個sanitizer,由此可能引入漏誤報。
對歷史漏洞編寫具有一定針對性的ql規(guī)則,在現(xiàn)實場景有特殊價值:
一方面,帶有漏洞的開源代碼,可能被以源碼形式包含在下游工程中,甚至是以代碼片段形式引用,但代碼結(jié)構(gòu)、符號命名可能存在重寫;而這種情況,一般沒有統(tǒng)一的代碼成分管理措施,這種陳舊代碼引入了歷史漏洞風險,卻無法有效被意識到、檢測出來;
一方面,某些漏洞的成因,可能在該項目的其它類似功能模塊中同樣存在,也可能在提供相似功能的其它開源項目里存在。此時,對于這些漏洞,我們需要有具備一定泛化能力的規(guī)則來做掃描,避免先被其它黑客在獲取0day信息之后,率先舉一反三地挖掘到。
但既然是高針對性規(guī)則,必然與漏洞具有基本一比一的數(shù)量規(guī)模,所以自動化生成,有必要性。
3. patch2ql:分析技術(shù)前瞻
patch2ql是云鼎實驗室針對上述需要自動化生成掃描歷史漏洞規(guī)則的問題,以CodeQL作為示例性質(zhì)的SAST基底,給出的從代碼補?。╬atch)自動生成規(guī)則(ql)的技術(shù)方案。該技術(shù)當前正在逐步完善和轉(zhuǎn)化中(由在途的專利申請保護),此處僅對其關(guān)鍵技術(shù)框架和節(jié)點做簡要說明。
3.1. 關(guān)鍵實現(xiàn)路徑
首先明確,嘗試用自動化的方法,“理解”漏洞的成因,特別是語義、邏輯層面的原因,現(xiàn)階段跨度過大;合理的方式是,由patch的代碼增刪改,刻畫出語法層面的代碼上下文,尋找相似代碼上下文;這種相似性的大原則是:
具備patch中未改變的必備上下文語法要素,在上文例子中,主要包括特定類型的變量,對應的初始化語句,if語句,對變量的解引用,次要包括具體解引用訪問的成員變量名,當前代碼塊上一層的代碼塊類型(如是for循環(huán)的body),當前語句下一層的語句類型(如是break);
包含patch所去掉的代碼要素,包括刪除的以及修改的原始代碼,這在上文沒有體現(xiàn);
不包含patch新增的代碼要素,對應上文中修復該處漏洞額外引入的NULL比較。
為實現(xiàn)以上目標,考慮包含三個關(guān)鍵步驟:
代碼要素的圈定,包括上述的三類要素??紤]到要有對代碼文本層面變動的抗性,這里考慮從AST層面實現(xiàn)比對;
根據(jù)所有主要和次要的AST節(jié)點要素,按照一定模式,生成對應的查詢限定條件語句集合,主要有exist()語句限定需要包含的AST節(jié)點,以及not exist()從句限定不應存在的節(jié)點;
從上述條件語句集合中組合條件形成ql,在目標項目上做類似回歸測試,保證對目標漏洞代碼上下文可檢出、盡量保證低誤報數(shù)量,反復測試從而整理得到最優(yōu)的集合。
3.2. AST層面的patch前后差異比對刻畫
選擇在AST層面圈定代碼差異,主要有兩方面考慮:一是AST是語法要素節(jié)點構(gòu)成的樹狀結(jié)構(gòu),可以有效做語法差異的比對;二是由AST節(jié)點之間的“邊”,亦即語法調(diào)用關(guān)系,可直接串連為生成規(guī)則語句的對象。
針對AST這樣的樹形圖,常規(guī)文本序列形式的diff難以符合需求,而需要考慮到層次結(jié)構(gòu),做結(jié)構(gòu)化比對。我們改進了編輯腳本生成算法,針對代碼特征,設計針對代碼顆粒度、自然的語法遷移關(guān)系的節(jié)點匹配條件,針對補丁前后目標函數(shù)的代碼AST,分別掃描得到其中的插入、刪除、更新、移動的語法樹節(jié)點和子樹。
舉例說明,對于如下漏洞修復的代碼補?。?/p>

可觀察到其中的關(guān)鍵修復邏輯,是針對非安全的緩沖區(qū)對象,增加額外NULL判斷,并使用安全的字符串操作函數(shù)。在AST層面使用算法,可得到差異的語法節(jié)點:

3.3. 關(guān)鍵語法節(jié)點的查詢條件表述
之后的任務是,將打補丁前后,差異的語法節(jié)點,以及未變動的、對函數(shù)邏輯而言起關(guān)鍵標識作用的語法節(jié)點,“翻譯”為ql查詢規(guī)則中的查詢條件,從而獲得多維度、盡可能全面的查詢條件集合。
這樣的翻譯工作需要考慮到目標語言的語法,特定節(jié)點關(guān)系,典型的補丁修補形式,以及必要的代碼上下文描述。例如,如果修補的代碼,是對一個if語句中的條件增加或改寫了條件謂詞,那么也需要對對應的then語句塊做必要的查詢說明,從而描述出來變更的條件判斷語句所“控制影響”的是對哪些關(guān)鍵變量的操作;又比如一個賦值語句的右值表達式中,將部分操作數(shù)做了更新,那么有必要進一步描述整個右值操作數(shù)、被賦值的變量以及變量之后的使用場景(數(shù)據(jù)流)。
上一節(jié)的差異AST,經(jīng)此一步,轉(zhuǎn)化為如下的繁復的查詢語句集合(所顯示的為局部):

3.4. 查詢條件篩選與規(guī)則優(yōu)化
最終在以上的規(guī)則集合上做加工,選取必要的查詢對象和條件,合并冗余語句,去除有沖突和錯誤的條件,并將某些對象根據(jù)語法關(guān)聯(lián)整合得到抽象的語法對象描述,從而可以得到兩類查詢規(guī)則:
一類精準匹配目標唯一漏洞,描述所有必要上下文和代碼語法,以檢測存在適度的代碼變形的原始歷史漏洞為目標,即準確規(guī)則;
一類嘗試通過忽略部分查詢條件,并對某些查詢條件做泛化(如從準確判斷目標變量類型,調(diào)整為描述目標類類型的父類型,或忽略類型、直接描述其訪問的成員),從而得到可能允許有一定程度的“誤報”,但具備能夠檢測同源、同語法漏洞類型的衍生、相似漏洞的能力,即泛化規(guī)則。
這樣得到的一份最精簡優(yōu)化規(guī)則如下:

4. 典型案例
借助CodeQL本地測試環(huán)境,可以利用patch2ql工具生成的特定項目的規(guī)則,方便地掃描檢測某些與該項目存在潛在關(guān)聯(lián)性的開源工程是否有引入性質(zhì)的同源漏洞。
值得注意的是:由CodeQL的許可限制,工具僅可以用于對開源代碼的掃描,請勿用于商業(yè)私有代碼的掃描和CI/CD流程的集成;此外,此前曾有的LGTM.com網(wǎng)站提供了一套在線編寫規(guī)則掃描已預置編譯后代碼數(shù)據(jù)庫的平臺,可以不必本地重新構(gòu)建數(shù)據(jù)庫,但該網(wǎng)站即將下線停止服務,因此規(guī)則的試用復現(xiàn)也請遵循指引,搭建CodeQL的本地環(huán)境。
4.1. 案例說明:子項目級靜態(tài)包含掃描
分析中首先選取C/C++項目中,將其它開源代碼某個快照版本靜態(tài)包含,并在生成時編譯為靜態(tài)庫或直接.o形式鏈接引用的開源項目。一個很好的例子是CMake,其中在Utilities子目錄下附帶二方開源代碼,基本進行代碼剪裁后添加“cm”前綴存在,并由開發(fā)者做版本的同步和維護。
其中存在有“cmcurl”目錄為靜態(tài)包含的curl,剪裁僅保留了libcurl共享庫部分代碼和編譯邏輯;由提交歷史分析,CMake開發(fā)者有一定的適配定制,采取不定期人工從上游(upstream)項目合并變動的同步策略,最后一次同步為2022.05.16。根據(jù)curlver.h頭文件指示,同步的libcurl版本為7.83.1。
使用一款具有函數(shù)/片段顆粒度相似度和版本判斷的軟件成分分析商業(yè)軟件FossID,對最新的CMake進行掃描,掃描共耗時約2小時??梢宰R別到curl的代碼成分,選取其中一個cmcurl目錄下文件,掃描成分判定結(jié)果如下:

由此可見,該商業(yè)方案確實是采用按片段相似度確定與某些歷史版本的最大似然成分的。但可能由于知識庫(KB)難以持續(xù)、細粒度更新,參與到相似度判定的代碼片段也并不完整、缺少一定的模糊性,導致判定出的最大似然度的版本也只有1.7%的相似成分比例。而漏洞的提示完全是根據(jù)判定的歷史版本號,給出對應知識庫內(nèi)容,與實際命中代碼無關(guān)。
而使用patch2ql,針對某個歷史漏洞patch,訓練生成ql的局部對應關(guān)系如下:
該份ql規(guī)則存在一定量冗余,但可觀察到其中描述了差異的if語句條件元素,以及對應的then分支語句。使用該規(guī)則,可以掃描分析得出,CMake當中包含的cmcurl代碼,存在該漏洞的同源(遺留)漏洞:

【網(wǎng)絡安全學習資料領(lǐng)取】

5. 如何獲取
當前patch2ql工具本身仍然在持續(xù)開發(fā)演進中,功能本身計劃將在進一步成熟后考慮開源。
相比工具本身,該工具生成的規(guī)則本身,能更好地服務開發(fā)者以及關(guān)注安全的白帽子。因此,patch2ql當前的中遠期目標,是針對開源生態(tài)中的基礎開源軟件和組件,生成盡可能覆蓋所有歷史CVE漏洞和bug的分析規(guī)則。
當前已開放針對curl和OpenSSL部分歷史漏洞生成的原始CodeQL規(guī)則集,感興趣的白帽子可移步騰訊云鼎實驗室該項目(https://github.com/YunDingLab/QlRules.git)查看分析。