全境擴散:從 Windows 11 到 Libarchive 的 Half-Day 威脅與全面影響

摘要
Windows 11 在 2023 年 10 月發布的更新中,新增了對 RAR、7z 等多達 11 種壓縮格式的支援,使用者可以在原生的檔案總管內操作這些格式的檔案,大幅提升了便利性。然而,這一改進同時也引入了潛在的資安風險。Windows 11 使用老牌開源專案 libarchive 來實現多種壓縮格式的支援,該專案被廣泛使用在 Linux、BSD、macOS 等作業系統,以及 ClickHouse、Homebrew、Osquery 等等知名大型專案中。自 2016 起,Google 的 OSS-Fuzz 專案便 24 小時不間斷地對其進行模糊測試,是歷經時間考驗的函式庫。
然而,在 OSS-Fuzz 執行的模糊測試中,libarchive 的覆蓋率並不理想。除了 Microsoft Offensive Research & Security Engineering (MORSE) 在 2024 年 1 月自行揭露的兩個遠端程式碼執行漏洞(RCE)之外,我們仍透過程式碼審查與模糊測試發現了 libarchive 中的數個弱點。其中包括位於 RAR 解壓縮程式碼中的 Heap Buffer Overflow 漏洞,以及因 Windows 未對 libarchive 的執行結果進行妥善檢查,導致的任意檔案寫入和任意檔案刪除的漏洞。此外,我們也將在本文中揭露 libarchive 與 Windows 結合後產生的諸多神奇特性。
而每當 libarchive 這類廣泛使用的函式庫存在弱點時,其風險往往滲透到各個層面,影響難以估計。加上當 Microsoft 為 Windows 進行修補時,相應的 patch 並不會立即回饋到 libarchive 中,這使得攻擊者能夠通過分析 patch 找出漏洞位置,並在漏洞修補的空窗期,利用該漏洞對其他使用 libarchive 的專案進行攻擊。所以最後,我們將以 ClickHouse 為例,說明如何在 libarchive 尚未獲得修補時,在看似不受影響的 ClickHouse 中觸發尚未修補的漏洞。
Introduction
在 Windows 11 的 KB5031455 更新之前,Windows 原生僅支援 ZIP 格式的壓縮檔案。ZIP 在檔案總管中顯示的類型是「Compressed (zipped) Folder」,使用者可以直接點兩下 ZIP 來查看其包含什麼檔案,甚至可以直接開啟檔案或是加入新的檔案。
「Compressed (zipped) Folder」讓使用者可以在不解壓縮的情況下瀏覽壓縮檔案內的清單,並且對檔案點兩下就可以直接使用,例如直接點擊兩下開啟文字檔案:
這是因為當使用者點兩下壓縮檔案內的檔案時,檔案總管會將該檔案解壓縮到 %TEMP%
目錄下的一個以隨機 UUID 命名的暫存資料夾中,實際上是從該暫存位置開啟檔案。由於這是暫存檔案,稍後會自動刪除:
Compressed Archived folder
接著,Windows 11 在 2023 年 10 月的 KB5031455 更新之後支援了 11 種新的壓縮檔案格式:
此類的檔案在檔案總管中顯示的類型是「Compressed Archive Folder」:
由於非常好奇 Windows 11 是透過什麼方式來支援這 11 種新的壓縮格式,我們開始分析檔案總管以及相關的 DLL 檔案。檔案總管對 ZIP 的原生支援是由 zipfldr.dll
這支 DLL 所負責的。在 KB5031455 更新之後,zipfldr.dll
中多出了一個叫做 ArchiveFolder
的 Class,有別於原本用以支援 ZIP 的 CZipFolder
Class。
第一個漏洞:CVE-2024-26185
在進行逆向分析之前,我們首先對新的「Compressed Archive Folder」進行了簡單的黑箱測試。講到壓縮檔案,那絕對要測試一下 ../
。
第一次測試,我們構造一個名稱為 ..\poc.txt
的檔案,並壓縮成 .rar
,接著上傳至 Windows 主機後點兩下進行瀏覽。結果是,我們僅會在檔案總管中看到一個空的資料夾,並沒有造成任何 Path Traversal:
第二次測試,構造一個名稱為 123\..\poc.txt
的檔案,並壓縮成 rar
,上傳至 Windows 主機後點兩下進行瀏覽。因為 ..
與 123
抵銷的關係,只會在檔案總管中看到 poc.txt
一個檔案:
而 %TEMP%
中對應的暫存檔案也並未逃逸至上層目錄:
除了點兩下互動之外,在檔案總管中對「Compressed (zipped) Folder」或是 「Compressed Archive Folder」類型的檔案按右鍵可以看到 Extract All
的選項,這個選項會嘗試將整個壓縮檔案解壓縮,透過這個選項我們再進行一次測試。
這次,..\poc.txt
被認為會逃逸到上層目錄,所以檔案總管給出一個 Error:
123\..\poc.txt
則解壓縮成功,但一樣只會解壓縮出 poc.txt
由於 Extract All
是直接解壓縮整個檔案,因此我們認為「當檔案包含絕對路徑時」也需要測試一下:
可以發現,透過 Extract All
解壓縮確實有包含資料夾,不過,資料夾 C:
被重新命名成了 C_
資料夾。因此我們可以知道,zipfldr.dll
有對特殊字元進行處理來避免解壓縮時的 Path Traversal 與 Arbitrary File Write。
但當我們「點兩下」包含絕對路徑的 RAR 檔案進行測試時,卻看到一個叫做 Local Disk (C:)
的資料夾!C:
並沒有被替換成 C_
!
如果我們進到資料夾的最內層,並且把 poc.txt
檔案打開,看似一切正常:
但其實,C:
下多出了不應該存在的 poc
資料夾!
表示檔案總管誤認為此處是用來暫放檔案的地方,因此 poc
資料夾中也包含我們剛才打開的 poc.txt
檔案:
也就是說,我們找到了一個 Arbitrary File Write 的漏洞!且由於寫入的目的是放置暫存檔案來進行互動,所以在經過一段時間或是使用者結束瀏覽時就會被刪除,因此我們實際上找到的是一個任意檔案寫/刪除的漏洞。不過稍微稍微可惜的是,任意檔案寫與任意檔案刪除所使用的權限是當前使用者的權限。
這就是 CVE-2024-26185:一個好笑但沒什麼用處的漏洞。因為如果要利用此漏洞創建或刪除特定位置的檔案,那就必須在壓縮檔中建立出相同的路徑,接著還需要誘騙使用者打開所有資料夾後,點擊兩下目標檔案才行,正常人點到一半就會覺得哪裡怪怪的了。
話是這麼說,但根據規則,微軟還是得付我 1,000 美金,好耶。
CVE-2024-26185:成因
剛才發現的 CVE-2024-26185,成因是因為沒有正確的對檔案名稱進行過濾。逆向之後我們發現,在 zipfldr.dll
中,進行解壓縮之前會呼叫 replace_invalid_path_chars
進行過濾。這個 function 將 "*:<>?|
等字元替換為 _
、將 /
替換成 \
。此外,與「Compressed (zipped) Folder」或 「Compressed Archive Folder」互動時,使用者總共有三個方式可以進行解壓縮,這三個行為各自觸發了不同的 function,分別是:
- 點兩下壓縮檔案中的檔案
- 觸發
ExtractFromArchiveByIndex
- 觸發
- 點兩下壓縮檔案中的 cmd、bat、exe 檔案
- 觸發
ExtractEntireArchive
- 觸發
- 對壓縮檔案點擊右鍵,點擊選單中的「Extract All」
- 觸發
ArchiveExtractWizard::ExtractToDestination
- 觸發
逆向這些函數之後我們發現,它們都使用 archive_read_next_header
取得壓縮檔內的檔案名稱,並呼叫 replace_invalid_path_chars
對名稱進行過濾。但卻忘記在「點兩下壓縮檔案中的檔案」對應的 ExtractFromArchiveByIndex
中做這件事情,導致任意檔案寫、任意檔案刪除的發生。
CVE-2024-38165: 繞過 CVE-2024-26185 的修補
在微軟修復 CVE-2024-26185 之後,我們稍微對 patch 進行了檢查,隨機的執行了一些 PoC,結果發現其中一些還是可以繞過 Windows 最新的 patch,讓我們來看看微軟是怎麼修復上一個漏洞的。
在 patch 之後,可以發現微軟在 ExtractFromArchiveByIndex
之前加入了 replace_invalid_path_chars
來對傳入的路徑進行過濾,看起來沒什麼問題。
但實際上,我們可以透過檔案名稱為 \poc\poc.txt
的檔案繞過這個保護。首先 \poc\poc.txt
會先被傳入 replace_invalid_path_chars
進行過濾,但由於名稱中不包含任何非法字元所以沒有產生變化:
接下來,由於目前zipfldr.dll
正在做的事情是「將檔案解壓縮至 %TEMP%
下的資料夾,讓使用者與檔案互動」,因此 zipfldr.dll
需要將 %TEMP%
下的暫存資料夾路徑和我們提供的檔案名稱串接起來:
在 Windows 中,開頭為 C:\
或是 \
開頭的路徑都被視為根路徑,所以實際上,zipfldr.dll
正在串接兩個絕對路徑。而根據 Path 的串接在 Windows 上的實作,若遇到兩個輸入都是絕對路徑時,會直接拿取第二個絕對路徑回傳,因此這個 function 的回傳值便是 C:\poc\poc.txt
,成功繞過保護:
Symlink NTLM Exfiltration
在缺少 replace_invalid_path_chars
的情況下,我們當然可以攻擊成功。那麼,replace_invalid_path_chars
本身真的安全嗎?replace_invalid_path_chars
僅過濾 "*:<>?|
。我們馬上可以發現,「.
」是合法的字元,或許我們可以構造出遠端的路徑,導致 NTLM exfiltration 之類的問題,我們嘗試構造此類路徑:
\\172.23.176.34\Users\nini\Desktop\sharing\test.txt
\Device\Mup\172.23.176.34\Users\nini\Desktop\sharing\test.txt
不過在檔案為 regular file 的情況下,這頂多只會在 C:
之下建立相對應的目錄(在修補 CVE-2024-38165 之前才有用),無法存取遠端的檔案系統。然而,如果我們建立一個指向 \\172.23.176.34\poc\poc.txt
的 symlink,當使用者觸發「Extract All」或是點兩下互動時,檔案總管便會嘗試去與該 IP 的 SMB 進行互動,導致 NTLM leak:
並且,在尚未解壓縮前,Windows 僅使用副檔名判斷壓縮檔案內的檔案類型,欺騙性極高。以此處為例,我們的 symlink 檔案就被當作 Text Document:
不過,在解壓縮時,zipfldr.dll
是透過 CreateSymbolicLinkA
這個 API 來建立 symlink。呼叫的程式必須有高權限才能建立 symlink,而在解壓縮時,檔案總管並不會要求任何權限,而是直接報錯:
雖然檔案總管在呼叫 CreateSymbolicLinkA
時,有啟用 SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE
,但手冊中說到必須要開啟開發人員模式,才能使此 flag 作用:
所以目前這個弱點只能用來攻擊管理員或是開發者,感覺還是一個不完整的功能。因此,這個弱點被判定為不優先修復。
Libarchive
前一部分提到 zipfldr.dll
負責處理與「Compressed (zipped) Folder」、「Compressed Archive Folder」互動的邏輯。但實際上負責解壓縮的是 libarchive,在 Windows 系統上對應的 DLL 是 archiveint.dll
。 libarchive 被廣泛使用在 Linux、BSD、macOS 等作業系統,以及 ClickHouse、Homebrew、Osquery 等等知名大型專案中。自 2016 起,Google 的 OSS-Fuzz 專案便 24 小時不間斷地對其進行模糊測試,是歷經時間考驗的函式庫。在黑箱測試時,我們觀察到下列幾件有趣的事情。
Fun Fact 1: Windows Supports File Formats More Than They Claimed
雖然微軟在 KB5031455 更新中宣稱他們新增了以下 11 種壓縮格式的原生支援,但實際上他們所支援的檔案格式遠遠超過 11 種。
在 zipfldr.dll
中我們看到 Windows 是這樣設定 libarchive 的:
但實際上,archive_read_support_format_all
會啟用 libarchive 中 ar、cpio、lha、mtree、tar、xar、warc、7zip、cab、rar、rar5、iso9660、zip 等 13 種壓縮檔案格式;archive_read_support_filter_all
會啟用 libarchive 中 bzip2、compress、gzip、lzip、lzma、xz、uu、rpm、lrzip、lzop、grzip、lz4、zstd 等 13 種格式。
由於 format 與 filter 可以同時使用,例如,支援 .tar.gz
格式即是同時啟用 format 中的 tar 與 filter 中的 gzip。所以實際上 Windows 11 支援了 13+13+13×13=195 種格式嗎?
大錯特錯,filter 最多可以串連 25 個,例如:archive.rar.gzip.xz.uu.zstd.uu......
也就是說 Windows 11 實際上支援了 13+13+13×13²⁵ 種格式,也就是 91733330193268616658399616035
種格式!佛心公司!佛心公司!
也因此,更新後的攻擊面大幅擴展。只要 libarchive 中的任一檔案格式存在漏洞,在 Windows 上都可以被觸發。此外,同時解析多種 filter 時,也可能存在安全弱點。相較於原本僅 11 種格式的情況,潛在風險已大幅增加。
Fun Fact 2: File Format Confusion
使用 libarchive 時,並不需要提供副檔名或是指定格式,libarchive 會根據檔案內容自動辨識當前檔案的格式,接著以對應的方式進行解壓縮。但在啟用 ZIP 的情況下可能會有 File Format Confusion 的狀況發生。舉個例子,如果我們建立一個 demo3.rar,並且裡面放入一個 poc.zip 的檔案,如下圖所示:
如果我們在 Windows 上直接點兩下 demo3.rar 開啟的話,會發現我們僅能看到 poc2.txt 這個文字檔案,其餘的資料夾、檔案,甚至 poc.zip 都不存在。因為 libarchive 將 demo3.rar 誤認為一個 ZIP 檔案!
為了理解原因,我們首先觀察一下 libarchive 是如何選定檔案格式的。libarchive 會在 choose_format
這個函式中,呼叫已啟用的 format 的 bid
函式,而哪個格式的 bid
回傳的數值最高,libarchive 就會將檔案當作該格式處理。
以 ar
為例,如果檔案的起頭是 "!<arch>\n"
這 8 個字元,archive_read_format_ar_bid
就會回傳 64,推測是 8𝑏𝑖𝑡𝑠 × 8𝑏𝑦𝑡𝑒𝑠 = 64,看起來好像挺有道理的。
接著,看到 RAR 的格式可以發現它最高的分數只會是 30。雖然 30 不知道怎麼算來的,但如果每個檔案格式都是從檔案的起頭開始檢查,應該也很難做出會讓這個機制壞掉的 polyglot,對吧?
但,libarchive 的 ZIP 中有個神奇的模式:seekable。也就是 ZIP 的 signature 不需要在檔案的開頭,libarchive 會自行去尋找,而 seekable zip 最高的數值為 32:
所以當 RAR 壓縮率不足,還保留有 ZIP 的 signature 時,libarchive 會將包有 ZIP 檔案的 RAR 檔案當作 ZIP 來處理。
Fun Fact 3: Sometimes, Libarchive Tries to Spawn an External Executable
在 libarchive 的 source code 中可以發現,如果編譯時缺少一些 library,libarchive 執行時會嘗試直接呼叫外部指令來協助解壓縮:
如果 decompile Windows 中對應的 archiveint.dll
,可以發現某些格式確實會呼叫外部的執行檔案,以 lzop 的 lzop_bidder_init
為例:
因此,根據「libarchive 會自行根據檔案內容決定用什麼格式進行解壓縮」的事實,我們只要將一個 lzop 格式的壓縮檔案的副檔名改為 .rar
,對其點兩下開啟,就能觸發對應的 lzop 解壓縮函式 lzop_bidder_init
:
接著就會看到 archiveint.dll
嘗試使用外部指令 lzop -d
來進行解壓縮。結果就是 explorer.exe
會去 PATH
中尋找有沒有 lzop.exe
存在,並嘗試執行:
RCEs Reported by MORSE: CVE-2024-20696 and CVE-2024-20697
到目前為止,不難發現 libarchive 帶來許多攻擊面。實際上,Windwos 也在 2024 的 1 月修復了微軟研究團隊 Microsoft Offensive Research Security Engineering (MORSE) team 回報的兩個漏洞,分別是與 CVE-2024-20696 與 CVE-2024-20697 兩個 RCE 漏洞。
CVE-2024-20696: OOB Write In copy_from_lzss_window_to_unp
CVE-2024-20696 發生在 libarchive 解壓縮 RAR 格式的檔案時候。在解壓縮的過程中 copy_from_lzss_window_to_unp
的第四個參數 length
是根據 lzss 解壓縮後的狀態計算出的 copy length。由於 copy_from_lzss_window_to_unp
在這裡錯誤地將 length
定義為 int,導致攻擊者可以透過構造 lzss 將 length
變為負數,從而繞過檢查造成越界寫的漏洞。
CVE-2024-20697: OOB Write In execute_filter_e8
CVE-2024-20697 也發生在 libarchive 解壓縮 RAR 格式的檔案時候。如果 RAR 檔案有使用到 filter e8
時(RAR 檔案格式的 filter,與 libarchive 的 filter 無關),解壓縮便會觸發 libarchive 的 execute_filter_e8
。
execute_filter_e8
在檢查 length
時雖然是檢查 length
必須大於等於 4,但是在迴圈計算卻使用了 length - 5
,所以當 legnth
為 4 時,迴圈便會執行 0x100000000 次,導致越界寫。
Fuzzing: Why OSS-Fuzz Never Found hese?
在復現這兩個 CVE 時,由於需要構造 RAR,製作起來較耗費時間,例如在 CVE-2024-20696 中需要構造 lzss 使得 length
的計算結果為負數,而在 CVE-2024-20697 中我們需要放入 filter e8,在構造上較為麻煩。因此我們透過 AFL++ ,將合法的 RAR 以及有使用 filter e8 的 RAR 作為 seed 進行 Fuzzing。意外的是,僅在 56 秒之內我們就找到了一個 CVE-2024-20697 的 crash:
很快就能找到 crash 當然是好事,但這裡有一個大問題:OSS-Fuzz 至少從 2016 年開始就 24 小時全年無休的對 libarchive 進行 Fuzzing,56 秒就能夠找到 crash 的漏洞怎麼可能還沒有被發現呢?
從 OSS-Fuzz 對 libarchive 的總結可以看到,六月時,OSS-Fuzz 對 libarchive 進行的 Fuzzing 的覆蓋率僅有 15.03%:
而且看起來已經持續了很長一段時間:
從檔案來看,可以發現有些 format 基本上沒有被測試過,例如 RAR 的覆蓋率僅有 4.07%
謎底揭曉
在我們準備演講的同時,libarchive 的 GitHub 上有個關於 OSS-Fuzz 的 commit。他是這麼說的: libarchive 在提交給 OSS-Fuzz 執行的設定中,呼叫 CMake 時雖然有啟用 DONT_FAIL_ON_CRC_ERROR
,但卻從來都沒有在 CMake 中好好定義這個 option!
本來,DONT_FAIL_ON_CRC_ERROR
這個 flag 會讓 libarchive 在 CRC 不符的情況下也繼續處理該檔案。而我們都知道 Fuzzer 一直都不擅於構造出正確的 checksum。也就是說 OSS-Fuzz 覆蓋率長期低下的原因是因為 Fuzzer 無法產生出合法的 CRC 來通過檢查,導致 Fuzzer 永遠卡在 libarchive 檢查 CRC 的邏輯之中。
修正之後可以看到 OSS-Fuzz 對 libarchive 的覆蓋率有飛躍性的提升,從 15.03% 提升到 63.10%。
從個別檔案的覆蓋率來看也可以發現大部分的檔案格式覆蓋率大幅提升:
Keep Fuzzing
在進行 code review 的同時我們讓 AFL++ 繼續為我們勞動,除了我們接下來即將提到的 RCE 漏洞之外,我們還透過 Fuzzing 找到了 CVE-2024-48957、CVE-2024-48958 兩個越界讀漏洞。
CVE-2024-26256: Libarchive Remote Code Execution Vulnerability
在分析了微軟自行回報的 CVE-2024-20696 以及 CVE-2024-20697 之後,我們可以發現這兩個漏洞皆出現在解析 RAR 的 archive_read_support_format_rar.c
中,而且都是近三年加入的 feature。
所以我們決定從 RAR 開始進行檢查,看看是否還能發現其他漏洞。而我們第一個想知道的是,CVE-2024-20697 成因中的「filter_e8
」是什麼? Commit message 的「support rar filters」的「filters」是什麼?
RAR 中的 VM
要知道 filter_e8
是什麼,我們首先得先知道:其實 RAR 中有一個 VM!RAR 實際上包含了一個 register-based VM,這個 VM 可以執行自訂的小程式來增加 RAR 的壓縮比。具體方式就是在建立 RAR 檔案時,可以在裡面放入客製的「filter」小程式,所以 filter_e8
實際上就是這麼一個程式,他是專門為了 Intel 的 binary 所產生的一個增加壓縮比的程式,而名稱中的 e8
源自於 Intel 指令集中 near call 的 opcode:
但是 call instruction 與改善壓縮比之間的關係是什麼?以下面的程式為例,假設在程式中,有兩個位置呼叫了 funcA
,程式會產生兩個 call 指令,在這裏分別是位置 0
與位置 0x10
。在 Intel 的機器語言中,我們可以看到 near call 是以 0xe8
開頭,後面跟著四個 bytes,也就是手冊中說的 rel32
。rel32
即是呼叫的目標與下一條指令的相對位置,以第一條 call 指令來看,rel32
是 0x1b
,即是呼叫的目標(0x20),減去下一條指令(0+5)所得出的相對位置 0x1b
:
這兩條指令雖然皆是呼叫 funcA,但由於內容不同,在進行壓縮時表現就較差。那既然 rel32
是可以簡單計算來的,那只要先把 rel32
的位置都先替換成目標的位置,機器語言會就長得一樣,壓縮比就能夠進一步上升!
在解壓縮時,只要將把被替代掉的 rel32
透過計算還原回來就好了,這一步便是使用 e8 filter
將之復原。
而 libarchive 的實作為了簡化,並沒有實作整個 VM,而是單純將 filter 程式的內容進行 crc32 運算,並將結果作為 fingerprint,直接執行對應的 filter,參考 libarchive:
static int
execute_filter(struct archive_read *a, struct rar_filter *filter, struct rar_virtual_machine *vm, size_t pos)
{
if (filter->prog->fingerprint == 0x1D0E06077D)
return execute_filter_delta(filter, vm);
if (filter->prog->fingerprint == 0x35AD576887)
return execute_filter_e8(filter, vm, pos, 0);
if (filter->prog->fingerprint == 0x393CD7E57E)
return execute_filter_e8(filter, vm, pos, 1);
if (filter->prog->fingerprint == 0x951C2C5DC8)
return execute_filter_rgb(filter, vm);
if (filter->prog->fingerprint == 0xD8BC85E701)
return execute_filter_audio(filter, vm);
archive_set_error(&a->archive, ARCHIVE_ERRNO_FILE_FORMAT, "No support for RAR VM program filter");
return 0;
}
在 RAR v5 之後,filter 在 RAR 中變成了 enum 類型,只能選用預先定義好的 filter,可以參考 UnRAR source:
enum FilterType {
// These values must not be changed, because we use them directly
// in RAR5 compression and decompression code.
FILTER_DELTA=0, FILTER_E8, FILTER_E8E9, FILTER_ARM,
FILTER_AUDIO, FILTER_RGB, FILTER_ITANIUM, FILTER_TEXT,
// These values can be changed.
FILTER_LONGRANGE,FILTER_EXHAUSTIVE,FILTER_NONE
};
Code Review
filter 的行為看起來很有趣,而微軟所發現的漏洞也位於 filter 之內,所以我們嘗試對 archive_read_support_format_rar.c
進行 code reveiw。
經過一段時間我們也在copy_from_lzss_window
找到了一個 heap buffer overflow 的漏洞,copy_from_lzss_window
的參數 length
沒有任何檢查就直接使用於 memcpy
,而 buffer 的大小僅為 0x40004 bytes:
從呼叫 copy_from_lzss_window
的地方來看,可以發現這是用來將資料複製進 VM 記憶體的函式:
漏洞成因看起來非常簡單,但較麻煩的是需要構造 libarchive 願意執行的 filter,並提供正確長度的資料。這些資訊並不直接存在於 RAR 中最表層的欄位,而是存在於 data 之中。並且,data 經過 Huffman coding 編碼,因此後面所提供的資訊也必須先進行一次編碼,而且不是 byte by byte 而是 7-bit by 7-bit。由於這部分不是本文的重點,我們不在這裡展開來講,我們鼓勵讀者進行復現。
這個漏洞已經被微軟修復:CVE-2024-26256。而這個漏洞沒有被 Fuzzer 發現的原因也很簡單,雖然 filter->blocklength
只要夠大就能夠觸發越界寫,但需要提供與 filter->blocklength
一樣長的 data 才能通過檢查,而在 coverage 相同的情況下,Fuzzer 通常傾向於將檔案縮小。
Half-day:長得像 0-day 的 1-day
當我們稍早提到「微軟研究團隊 Microsoft Offensive Research Security Engineering (MORSE) team 回報的兩個漏洞」時。我們提到在構造 PoC 上較困難,或許有人馬上就想到了:「為什麼不去 libarchive 的 GitHub repository 找 test 或是說明呢?」對,我們找過了,沒有。
因為當時的 libarchive 甚至還沒有被 patch,或是說,可能甚至沒有人知道漏洞已經存在!微軟在一月時,就已經修復 Windows fork 出去的 libarchive:
在研究結束之後,我們才在 libarchive 的 GitHub repository 上看到對應的兩個 patch ,分別是五月跟四月才被 merge 進去:
那豈不是只要長期有關注 Windows patch 的人馬上就會發現 libarchive upstream 存在兩個尚未修補的漏洞嗎?它們對於 Windows forked 版本的 libarchive 來說是 1-day,因為漏洞已被發現並且修補了;對 libarchive upstream 來說是 0-day,因為尚未有修補存在且維護者甚至可能不知情!我們接下來會將這種情況稱為「0.5-day」或是「Half-day」。
於是我們開始尋找有使用 libarchive 的大型專案,我們想嘗試模擬這個「利用 Half-day 攻擊」的場景,同時也認為,在軟體中採用 libarchive 的廠商會更願意幫我們敦促 libarchive 修補漏洞。
Attacking ClickHouse
經過一番調查後,我們發現 ClickHouse 有使用 libarchive 進行解壓縮,非常有可能包含了存在漏洞的程式碼。在 ClickHouse 中我們可以透過 file table engine 對壓縮檔內的資料進行操作:
不過手冊也提到,ClickHouse 僅支援 zip、tar、7z 三種格式的壓縮檔案:
但真的是這樣嗎?除了 zip 之外,tar 與 7z 在 ClickHouse 中由 TarArchiveReader
與 SevenZipArchiveReader
實作,兩者皆繼承於 LibArchiveReader
。LibArchiveReader
開啟檔案的行為實作於 open
函式,在 source code 中可看到熟悉的 pattern:
static struct archive * open(const String & path_to_archive)
{
auto * archive = archive_read_new();
try
{
archive_read_support_filter_all(archive);
archive_read_support_format_all(archive);
沒錯,ClickHouse 也是使用 archive_read_support_format_all
與 archive_read_support_filter_all
,表示我們一樣可以觸發存在於 RAR 中的弱點!接下來只要讓 ClickHouse 為我們解壓縮檔案即可,雖然現在解壓縮的 feature 可以直接存取 s3 上的檔案,但當時並不能這樣使用:
所以我們必須先上傳檔案,可以透過下列 query 新增一個 table,並且該 table 會以檔案的形式被儲存:
INSERT INTO TABLE FUNCTION
file('poc.7z', 'Native' ,'column1 String') VALUES ('payload')
但這樣產生的 file 會包含 table 的 metadata,在有 metadata 的情況下我們沒有辦法使 libarchive 認為這是一個 RAR 檔案,進而觸發漏洞:
00000000: 0101 0763 6f6c 756d 6e31 0653 7472 696e ...column1.Strin
00000010: 6707 7061 796c 6f61 64 g.payload
我們需要在 ClickHouse 的 Output Data Formats 中尋找哪些格式不會在檔案的開頭存放置 metadata。最後我們決定使用 TabSeparatedRaw
,在 TabSeparatedRaw
中:
- 資料被一列一列儲存
- 一列中的資料被 tab 分隔開
- 資料內不能有 tab 或換行
例如,如果使用下列兩個 qeury:
INSERT INTO TABLE FUNCTION
file('test.7z', 'TabSeparatedRaw', 'column1 String')
VALUES ('row1 string')
INSERT INTO TABLE FUNCTION
file('test.7z', 'TabSeparatedRaw', 'column1 String')
VALUES ('row2 string')
最後產出的檔案內容會是:
row1 string
row2 string
因此,若可以處理好第三點「資料內不能有 tab 或換行」,那我們就可以構造出一個合法的 RAR 檔案!那該怎麼樣避免使用 tab 或換行呢?聽起來很難,但不要忘了,ClickHouse 呼叫了 archive_read_support_filter_all
為我們開啟了所有的 filter!其中,最符合 TabSeperatedRaw
描述的就是 UUencode,經過 UUencode 的資料會長這樣:
begin 600 exploit.rar.uu
M4F%R(1H'`,^0<P``#0````````!287(A&@<`SY!S```-`````````%VI=("0
M,0"VXP0`4,\+``(2'^X4O1EY5QTU#``@````;7-V8W(Q,#`N9&QL`/#<ZFP8
M(AE0S(EB'!(",EM(4I0M-"H*"T6JBHJ!1$1T%IKHFBA0H*:(VV2VP)98R9+*
M"*`T'I;&]1>J]>HZ!>KU4=(ZQTC:4VHB@J4WH2D-%--VSX9S);9IOO.9HF2E
MH47O?/GG[Y^6LSGO/>>Z>>\]YJS!N3_(^_P3SS"O\S`P+6<_V'P(,#`P,#`B
M.EX#-(,!V$Z07A+LVZ?4@K:RB=`NN<+9(IB<?NX``Y[L`3`P,#``,`'/YCL*
因此最後我們只需要將原本的 rar payload 進行 UUEncode 之後上傳:
INSERT INTO TABLE FUNCTION
file('poc.7z', 'TabSeparatedRaw', 'column1 String') VALUES (uu_encoded_rar_payload)
並且觸發解壓縮:
SELECT * FROM file('poc.7z :: **', RawBLOB)
就可以成功觸發越界寫漏洞了:
我們最後透過 ClickHouse 在 Bugcrowd 上的賞金計劃進行回報:
ClickHouse 很快的就進行了修復,並且願意幫我們敦促 libarchive 進行漏洞的修補!雖然不確定微軟是否有告知 libarchive 的維護者們任何關於 CVE-2024-20696 與 CVE-2024-20697 的漏洞資訊(因為沒有任何公開資料,libarchive 在 GitHub repository 上的 Security Advisories 也沒有任何資料可以參考!)。但如同我們先前提到的,這兩個原先由微軟在 forked 版本的 libarchive 中發現的漏洞,最終 libarchive 也分別在五月跟四月獲得修復,結束了「Half-day」的尷尬狀況。
持續追蹤
除了回報上述兩個「Half-day」之外,別忘了我們還有另外回報三個 0-day 漏洞,它們個別是:
- 微軟已經修復的 RCE:CVE-2024-26256
- 4/27 回報
- 8/14 修補
- 9/28 關閉
- filter_audio 中的 oob read
- 3/20 回報
- 4/29 修補
- 9/28 關閉
- filter_delta 中的 oob read
- 3/20 回報
- 4/29 修補
- 9/28 關閉
在回報後的數個月,他們終於獲得修復,這時已經是九月:
Half-day 的迴環複沓
其中較嚴重的是我們已經回報給微軟的漏洞 CVE-2024-26256,微軟在四月時便已經修復:
在三月回報漏洞的同時,我們有詢問 MSRC 是否會將 CVE-2024-26256 的 patch 提交到 libarchive 的 GitHub repository,第一時間沒有獲得回應。在微軟 patch CVE-2024-26256 之後我們又提問了一次,想確定他們是否有將該漏洞的資訊同步給 libarchive 的 maintainers,MSRC 說:「如果你願意的話,我們鼓勵你開一個獨立的 GitHub issue。」為了避免「Half-day」情況發生,所以我們也在收到訊息後馬上到 libarchive 的 Security Advisory 開了一個 issue:
然而,「Half-day」還是發生了,因為太久沒有人回應。所以七月時,我們自己發了一個 PR 將微軟的 patch 移植過來,雖然不確定這個修補方式是不是最好的,但至少比沒有修補還要強。所以,歷史又重演了一次,從四月到修補完成的九月之間,我們又陷入了「Half-day」的情境。
餘下的兩個 0-day
眼尖的讀者或許已經注意到,我們所回報的 Security Advisory 並不是在「Published」的分頁中,而是「Closed」:
除了剛才提到的 CVE-2024-26256 之外,其餘是兩個尚未申請 CVE 的越界讀取漏洞。針對已經有 CVE 編號的修補,人們會意識到這是一個關於安全性的修補。然而,由於其他兩個漏洞並不為人所知,僅修補卻不通知其他人安全風險的存在是一件危險的事情。尤其,libarchive 是一個廣泛使用的函式庫,有許許多多的軟體或服務使用它,導致人們並不知道自己正在使用 libarchive。這類軟體的相依鏈可能十分龐大且複雜,如果開發者或是終端使用者沒有意識到這是一個安全性的更新,那麼,該修補在整個相依鏈中將會極為緩慢地傳播,進而增加潛在風險。
所以,在確認 libarchive 將 issue 關閉之後我們就立即申請了 CVE 編號,這兩個漏洞分別是 CVE-2024-48957 與 CVE-2024-48958,當它們被發佈時,已經是十月,距離修補發布的四月已經過去了六個月。
結語
本文介紹了 Windows 在採用 libarchive 以支援更多壓縮檔案格式時,所引發的一些漏洞與有趣的特性。此外,我們也向 libarchive 回報了數個 0-day 漏洞。
接著,我們在 ClickHouse 上成功利用了我們認為介於 0-day 與 1-day 之間的「Half-day」漏洞。這源於 Windows 將 libarchive fork 出來後,將其編譯為閉源的 archiveint.dll,並在修補該 DLL 後,未能及時通知 libarchive 的維護者,或將修補程式回饋到 libarchive 上游,從而導致了「Half-day」漏洞的產生。
upstream 較晚獲得修補的原因,除了溝通延遲之外,也因缺乏公開可用的修補程式。維護者在 fork 版本已經完成修補後,才在接獲通報時開始著手解決問題。因此,微軟在修補 fork 版本的 libarchive 後,除了應主動通知原始維護者,更應直接提交 Pull Request (PR) 至上游,以協助完成修補。
libarchive 的維護者是一群志願者,而且可能沒有領取任何薪水。而開源精神便是人人皆可「分享、參與、行動」,因此我們認為,研究人員在漏洞回報的過程中,除了提供漏洞分析與 PoC 外,也應該積極的提出修補方案,共同維護開源軟體的安全性與品質。