摘要

這份報告提供了 GHSL-2026-140 的全面技術分析,這是在 7-Zip 26.00 版中發現的一個 heap 緩衝區寫入溢位漏洞。該漏洞源於 C++ 位元位移運算中未定義的行為,導致 NTFS 壓縮串流緩衝區配置不足(under-allocation)。此缺陷可能透過 vtable hijacking 導致任意程式碼執行。報告詳細說明了技術根源,分析了相關的程式碼片段,討論了平台相依行為,並檢查了攻擊面。此外,報告還分析了提供的概念驗證 (Proof-of-Concept,PoC) 產生器與驗證方法。並與其他緩衝區溢位漏洞進行了比較分析,以突顯常見的模式和緩解策略。報告最後提出了防禦性程式設計與現代安全實務的建議,以防止類似的記憶體安全問題。

SEGV 只是當機嗎?錯!7-Zip 的 vtable劫持讓 NTFS 檔案變成武器! | 資訊安全新聞

1. 簡介

7-Zip 是一款廣泛使用的開源檔案壓縮軟體,具有高壓縮比。它被大量應用於各種系統,因此其程式碼中的任何安全漏洞都是一個重大問題。這份研究報告重點關注 GHSL-2026-140,這是在 7-Zip 26.00 版中發現的一個嚴重的 heap 緩衝區寫入溢位漏洞。該漏洞凸顯了 C++ 應用程式中記憶體安全持續存在的挑戰,特別是在處理像 NTFS 壓縮串流(compressed stream) [1] 這類複雜檔案格式時。利用此類漏洞可能導致嚴重後果,包括任意程式碼執行,進而破壞系統的完整性與機密性。

此漏洞的核心在於,攻擊者可控的特定輸入值與 C++ 位元位移運算之間複雜的相互作用,導致未定義行為(undefined behavior)並隨後發生記憶體損壞(memory corruption)。這份報告目的在剖析 GHSL-2026-140 背後的技術機制,深入探討其根源、潛在影響以及對軟體安全的更廣泛啟示。

2. GHSL-2026-140 技術分析

2.1. 因 NTFS 壓縮串流緩衝區配置不足導致的 Heap 緩衝區寫入溢位

GHSL-2026-140 漏洞是 7-Zip NTFS 檔案處理常式中的一個 heap 緩衝區溢位。它是由於用來存放 NTFS 壓縮串流的緩衝區配置不足所觸發。根源可追溯到 CInStream::GetCuSize() 函式,該函式計算壓縮單元緩衝區的大小。這個計算涉及一個 32 位元的左移運算: (UInt32)1 << (BlockSizeLog + CompressionUnit) [1]

關鍵條件發生在攻擊者偽造一個 NTFS 映像,使得 ClusterSizeLog >= 28 (解析器接受此值)且壓縮資料屬性中的 CompressionUnit == 4 。在此情況下,位移運算的指數變為 BlockSizeLog + CompressionUnit = 28 + 4 = 32 。將 32 位元的 UInt32 位移 32 個或更多位置會導致 C++ 中的 未定義行為 [1]

2.2. 未定義行為與緩衝區配置不足

在 x86 和 x64 架構上,這種未定義行為通常會因為硬體對位移計數的屏蔽效果,導致 (UInt32)1 << 32 的結果為 1 。因此,原本用來存放原始壓縮資料的 _inBuf 緩衝區,只被配置了 1 個位元組的大小,而不是原本預期的大得多的大小(最大可達 256 MB) [1]

以下來自 NtfsHandler.cpp 的相關程式碼片段說明了緩衝區大小的計算方式:

  1. // NtfsHandler.cpp, line 687
  2. UInt32 GetCuSize() const { return (UInt32)1 << (BlockSizeLog + CompressionUnit); }

在這種配置不足之後,後續的操作會嘗試將大量(最大達 256 MB)且受攻擊者控制的資料寫入這個僅 1 位元組的緩衝區。這發生在呼叫 ReadStream_FALSE 期間,導致嚴重的 heap 緩衝區溢位 [1]

  1. // NtfsHandler.cpp, lines 695-697
  2. UInt32 cuSize = GetCuSize(); // UB → 1 byte on x86/x64
  3. _inBuf.Alloc(cuSize); // allocates 1 byte
  4. _outBuf.Alloc(kNumCacheChunks << _chunkSizeLog); // x86: 2 bytes; x64: 8 GB (succeeds on >= 16 GB RAM)
  5. // NtfsHandler.cpp, lines 940-941
  6. const size_t compressed = (size_t)numChunks << BlockSizeLog; // up to 256 MB
  7. RINOK(ReadStream_FALSE(Stream, _inBuf + offs, compressed)); // writes into 1-byte buffer

_inBuf 的設計目的是存放從磁碟讀取的壓縮資料,而 _outBuf 則存放解壓縮後的輸出。理想情況下,兩者的大小都應該是 GetCuSize() 個位元組。而 _outBuf 大小的獨立計算(同樣受到相同未定義行為位移結果的影響)在 x64 系統上可能導致 8 GB 的配置,但 1 位元組的 _inBuf 仍然會發生溢位 [1]

2.3. 平台相依行為

  • 32 位元建置: 在 32 位元系統上, (size_t)2 << 32 同樣會導致未定義行為,通常結果為 2 。這使得 _inBuf.Alloc(1) _outBuf.Alloc(2) 都能成功配置極小的記憶體,而 heap 溢位也不斷地被觸發 [1]
  • 64 位元建置: 在 64 位元系統上, (size_t)2 << 32 是一個有效的 64 位元位移,結果為 8 GB。在擁有足夠 RAM(例如 >= 16 GB)的系統上, _outBuf.Alloc(8 GB) 的呼叫可能會成功。即使在這種情況下,1 位元組的 _inBuf 仍然會發生溢位,導致相同的漏洞。在記憶體較低的系統上,這個大型配置可能會失敗,導致阻斷服務 (Denial of Service,DoS) 而非程式碼執行 [1]

2.4. 影響:Vtable Hijack 與程式碼執行

heap 緩衝區溢位允許將 256 MB 受攻擊者控制的資料寫入一個 1 位元組的 heap 緩衝區。除錯器分析顯示,在 heap 上,串流物件 ( CInStream ) 僅配置在 _inBuf 之後的 304 個位元組處。第一次 Read() 重覆寫入 64 KB,僅在 304 個位元組後就覆寫了串流物件的 vtable 指標。隨後的下一次 Read() 便會透過這個損壞的 vtable 進行派送,導致典型的 vtable hijack 。由於攻擊者能控制寫入的資料(NTFS cluster 內容),因此可以控制被覆寫的 vtable 指標,進而可能實現任意程式碼執行 [1]

該漏洞影響 x86 和 x64 兩種建置(builds)。攻擊面很廣泛;NTFS 處理常式在 7z.dll 中是啟用的,並使用用簽章的後備檢測機制。這意味著一個精心偽造的 NTFS 映像,無論使用 任何副檔名 (例如 .7z .zip .rar 或無副檔名),只要其他處理常式拒絕它,就可能觸發此漏洞。該漏洞在從偽造映像中解壓縮或測試一個壓縮檔案時被觸發,使用者除了開啟檔案外無需任何互動 [1]

GHSL-2026-140 的 CVSS 分數為 8.8 ,相關的 CWE 為 CWE-787(超出邊界寫入)和 CWE-190(整數溢位或環繞) [1]

3. 概念驗證 (PoC) 分析

GitHub Security Lab 提供了一個 Python PoC 產生器 gen_ntfs_sparse.py ,它會建立一個 poc_ntfs_sparse.ntfs 檔案。這個 PoC 對於理解如何建構惡意的 NTFS 映像來觸發漏洞至關重要。

  1. #!/usr/bin/env python3
  2. """ Generate a sparse NTFS image with ClusterSizeLog=28 and a compressed
  3. $DATA attribute with CompressionUnit=4 to trigger GetCuSize() UB. """
  4. import struct, os, sys
  5. boot = bytearray(512)
  6. boot[0:3] = b'\xEB\x52\x90'
  7. boot[3:11] = b'NTFS '
  8. struct.pack_into('<H', boot, 11, 512)
  9. boot[13] = 0xED # ClusterSizeLog = 28 (negative encoding for 28)
  10. for i in range(14, 21): boot[i] = 0
  11. boot[21] = 0xF8
  12. struct.pack_into('<H', boot, 24, 63)
  13. struct.pack_into('<H', boot, 26, 255)
  14. # ... (truncated for brevity, full PoC available in [1])
  15. # This part of the code snippet is crucial for setting up the specific conditions
  16. # that lead to the heap buffer overflow. Specifically, boot[13] = 0xED sets
  17. # ClusterSizeLog to 28, which is a key component in causing the shift exponent
  18. # to reach 32 when combined with CompressionUnit = 4.
  19. mft_off = 0x10000000 # 256 MB offset for MFT records
  20. phy_size = mft_off + 0x1000 # Physical size of the crafted image
  21. mft = bytearray(4096 * 7) # 7 MFT records
  22. # ... (MFT record construction, truncated for brevity)
  23. out = sys.argv[1] if len(sys.argv) > 1 else "poc_ntfs_sparse.ntfs"
  24. with open(out, 'wb') as f:
  25. f.write(boot)
  26. f.seek(mft_off)
  27. f.write(mft)
  28. f.seek(phy_size - 1)
  29. f.write(b'\x00')
  30. print(f"Generated: {out} ({os.stat(out).st_size} bytes apparent)")

該 PoC 建構了一個手工製作的 NTFS 映像,其中 ClusterSizeLog = 28 (對應 256 MB cluster)和一個壓縮的 $DATA 屬性,其 CompressionUnit = 4 。這些特定值至關重要,因為它們的總和(28 + 4 = 32)會導致 GetCuSize() 函式中發生未定義行為。該 PoC 透過從頭合成整個 MFT 結構,確保了正確的啟動磁區、USN 修復陣列和屬性記錄,從而繞過了標準 NTFS 格式化工具的限制 [1]

4. 驗證

在 Linux x64 的復原模式下,使用 Undefined Behavior Sanitizer (UBSan) 和 clang 編譯器確認了該漏洞。UBSan 明確指出了根源位移的未定義行為:

../../Archive/NtfsHandler.cpp:687:47: runtime error: shift exponent 32 is too large
    for 32-bit type 'UInt32' (aka 'unsigned int')
SUMMARY: UndefinedBehaviorSanitizer: undefined-behavior
    ../../Archive/NtfsHandler.cpp:687:47
        

這段輸出 unequivocally 地確認了,對 32 位元的 UInt32 型別進行指數為 32 的位移運算確實是未定義行為。在此之後,連鎖的記憶體損壞導致了 (segmentation fault,SEGV) ,進一步驗證了該漏洞的可利用性 [1]

../../Common/StreamUtils.cpp:62:27: runtime error: member call on address 0x5d3dd8f776f0
    which does not point to an object of type 'ISequentialInStream'
    note: object has invalid vptr
UndefinedBehaviorSanitizer:DEADLYSIGNAL
==60==ERROR: UndefinedBehaviorSanitizer: SEGV on unknown address 0x000000000018
==60==Hint: address points to the zero page.
        

5. 與相關漏洞的比較分析

7-Zip 漏洞與其他記憶體安全問題(尤其是緩衝區溢位)具有共同特徵。對緩衝區溢位有一般性的理解,對於掌握 GHSL-2026-140 的細微差別至關重要。正如前期文章 [2] 所述,當程式嘗試將資料寫入超過緩衝區已分配記憶體區域時,就會發生緩衝區溢位,這可能損壞資料、導致應用程式 crash 或允許惡意程式碼執行。該文章用一個簡單的 strcpy 例子來說明,其中一個大小不足的目標緩衝區覆寫了相鄰的變數,展示了非預期的資料修改是如何發生的 [2]

另一個相關例子討論了 PHP 中 iptcembed 函式的 heap 緩衝區溢位 [3] 。這個漏洞編號為 CVE-2025-14177,源於檢查時間到使用時間 (Time-of-Check to Time-of-Use,TOCTOU) 的缺陷以及在位元組串流處理過程中缺乏邊界檢查。具體來說, iptcembed 使用 fstat() 來決定所需的緩衝區大小,但如果檔案在檢查和實際使用之間變大(例如,使用 named-pipe),一個小的緩衝區就可能被大量溢位 [3]

PHP iptcembed 漏洞的程式碼片段突顯了在沒有邊界檢查的情況下寫入緩衝區的危險模式:

  1. // Vulnerable write operation in php-src/ext/standard/iptc.c
  2. // No bounds checking before writing to the buffer.
  3. static int php_iptc_get1 (FILE *fp, int spool, unsigned char **spoolbuf)
  4. {
  5. int c;
  6. c = getc (fp);
  7. if (c == EOF) return EOF;
  8. // BUG: Writing to the buffer and incrementing the pointer
  9. // without checking if we have reached the end of the allocated space.
  10. // This leads to a heap buffer overflow if input exceeds sb.st_size.
  11. if (spoolbuf) *(*spoolbuf)++ = c;
  12. return c;
  13. }

7-Zip 漏洞是由位移運算中的未定義行為導致配置不足而觸發,而 PHP iptcembed 漏洞則是由於 TOCTOU race condition 和明確缺乏邊界檢查所致,但兩者最終都會導致 heap 緩衝區溢位。共同點在於未能正確管理記憶體邊界,以及未能根據已配置的緩衝區容量驗證輸入大小。7-Zip 的案例尤其棘手,因為它利用了一個微妙的 C++ 語言特性(未定義行為)來達成配置不足,使得僅透過專注於明確邊界檢查的簡單程式碼審查更難發現。

下圖說明了導致 7-Zip heap 溢位的簡化互動流程:

sequenceDiagram participant Attacker participant CraftedNTFSImage participant 7ZipParser participant CInStream participant MemoryAllocator Attacker->>CraftedNTFSImage: Create with ClusterSizeLog=28, CompressionUnit=4 7ZipParser->>CraftedNTFSImage: Parse NTFS image 7ZipParser->>CInStream: Call GetCuSize() CInStream->>CInStream: Calculate (UInt32)1 << (28 + 4) CInStream-->>CInStream: Result: 1 (due to UB on 32-bit shift) CInStream->>MemoryAllocator: Request _inBuf.Alloc(1 byte) MemoryAllocator-->>CInStream: Allocate 1 byte for _inBuf 7ZipParser->>CInStream: Call ReadStream_FALSE(Stream, _inBuf, 256MB) CInStream->>MemoryAllocator: Attempt to write 256MB into 1-byte _inBuf CInStream-->>7ZipParser: Heap Buffer Overflow (vtable hijack) 7ZipParser-->>Attacker: Potential Code Execution

6. 緩解策略

防止像 GHSL-2026-140 這樣的漏洞需要多管齊下的方法,結合安全編碼實務與先進的測試及分析工具。關鍵策略包括:

  • 防禦性程式設計: 明確檢查輸入值和計算出的大小,確保它們不會導致未定義行為或緩衝區溢位。對於 7-Zip 的案例,在位元運算之前驗證位移指數就可以防止配置不足。
  • 記憶體安全語言: 將核心元件轉換為記憶體安全的語言(如 Rust),可以消除包括緩衝區溢位在內的整個記憶體安全錯誤類別。雖然這對於既有的 C/C++ 程式庫來說並非總是可行,但這是新開發工作的長期目標。
  • 靜態分析 在開發過程中採用靜態分析工具,以識別潛在問題,例如整數溢位、超出邊界存取和未定義行為模式。
  • Fuzz 測試: 持續對解析器和處理常式進行 fuzz 測試,輸入格式錯誤或非預期的資料,以揭露可能導致程式 crash 或記憶體損毀的邊緣案例。
  • AddressSanitizer (ASan) 和其他 Sanitizer: 在測試和開發期間使用像 ASan 這樣的執行時期 sanitizer。正如 GHSL-2026-140 的驗證過程所展示的,ASan 在檢測未定義行為和記憶體錯誤方面非常有效。
  • 安全的程式庫使用: 開發人員必須徹底了解他們使用的任何 API,特別是有關記憶體管理和輸入處理的部分,以避免因錯誤配置而導致漏洞。

7. 結論

7-Zip 中的 GHSL-2026-140 heap 緩衝區寫入溢位漏洞,清楚地提醒我們 C++ 開發中存在的微妙但關鍵的記憶體安全挑戰,特別是在處理複雜檔案格式和底層運算時。該漏洞依賴於位元位移中的未定義行為來造成嚴重的配置不足,進而導致 vtable hijack 和潛在的程式碼執行,這凸顯了嚴格的輸入驗證和穩健錯誤處理的重要性。

與其他緩衝區溢位事件(包括 PHP 中的事件)進行的比較分析,強化了記憶體管理失敗的反覆出現的模式,以及開發人員需要敏銳地意識到明確的邊界檢查和隱式的語言特定行為。展望未來,結合防禦性編碼、先進的靜態分析工具動態分析工具,以及在適當時策略性地轉向記憶體安全語言的組合,對於建立能夠抵禦此類複雜攻擊的、更具韌性的軟體系統至關重要。