跳到正文
Hacker News (official RSS)·· 3 小時前精選AI 評分83

六日內完成 Chrome 全鏈爆破,XBOW 內部技術揭秘

Zero to Calc in Six Days: Inside XBOW's Chrome Full Chain

AI 導讀

XBOW 在六天內完成 Chrome 全鏈爆破,成功從渲染器沙箱外執行程式碼並啟動系統計算器。

\n\n- 漏洞組合:V8 編譯器、動畫 WebP 解碼器、Crashpad 記憶體安全漏洞,三階段跨越渲染器與作業系統沙箱。

推薦理由

本文詳述利用 AI 代理在六日內完成 Chrome 全鏈爆破的技術流程,為安全研究者提供可直接借鑑的漏洞利用方法。

正文 · AI 翻譯

最終的連鎖結合了 V8 編譯器漏洞、animated-WebP 解碼器漏洞,以及 Crashpad 的記憶體安全漏洞。這三者合併後,將普通 Chrome 分頁中的 JavaScript 執行轉變為渲染器沙盒外的程式碼執行。

Google 在 2026 年 4 月推出的 VRP 規則下,授予此連鎖漏洞首筆 $250,000 的 Chrome 全連鎖獎金。我們的單獨公告說明瞭該獎金及其對 XBOW 的意義。本文聚焦於漏洞:它跨越的邊界、每個漏洞的根本原因,以及將它們連結的介面。

在已簽署的 Chrome Beta 中,一個網頁透過 XBOW 的全連鎖漏洞啟動計算機。

談論速度和攻擊加速的話題與親身體驗截然不同。早於 LLMs 時,Chrome 全連鎖漏洞通常需要四人團隊約 6-9 個月才能完成,我們的首份提交耗時 2 個月。授獎那份?僅六天。即便是我們也難以理解安全性變化的速度。

我們在六天內完成整個連鎖,從 8 月 2 日(星期日)到 8 月 7 日(星期五)。與其要求單一代理端完整產出漏洞,我們將工作拆分為多個平行任務,目標更為明確:廣泛漏洞搜尋、V8 記憶體腐蝕、V8 沙盒逃逸、渲染器沙盒逃逸以及整合。使用 OpenAI's gpt-5.6-sol model 的 AI 代理搜尋原始碼、測試假設、構建概念驗證並開發漏洞階段;XBOW 研究人員則挑選目標、提供背景、挑戰結果、重新導向搜尋並決定哪些元件要結合。 我們還開發了備用路徑,並為每個階段提供乾淨的介面,這樣就能在不重新構建其餘漏洞的情況下替換單一元件。從已存檔的 Codex 日誌重建使用情況,六天的行動大約耗費 40,000 美元的 API 費用。我們將在十月底的 Unprompted 活動中討論時間線與協調;此處,我們聚焦於漏洞及將它們連結成完整連鎖的攻擊機制。

背景:V8 與渲染器沙盒

V8 首先將 JavaScript 引擎的腐蝕限制在 程序內沙盒 中。大部分 JavaScript 堆物件位於保留的 4 GiB 區域,許多指標以該區域內的偏移量表示。對本機物件與可執行程式碼的參照則採取額外保護。V8 記憶體腐蝕漏洞因此可以讓攻擊者在不立即得到本機程式碼執行的情況下,對沙盒內擁有廣泛控制。V8 在 The V8 sandbox. 描述此設計。

Chrome 接著將渲染器本身置於 作業系統程序沙盒 中。即使攻擊者在渲染器中擁有本機程式碼執行,也無法自由存取機器的其餘部分。Chromium's sandbox design document 描述了此外部邊界。

該連鎖在三個漏洞階段中跨越了兩個邊界:

階段漏洞獲得的能力
JavaScript 引擎Maglev 在去最佳化時重複使用可變封裝數字在 V8 的指標壓縮沙盒內讀寫存取
V8 沙盒Animated WebP 將快取幾何圖形與變更的影像位元組結合一個選定的本機寫入,位於沙盒之外
渲染器沙盒Crashpad 將對本應已凍結的渲染器失敗讀取視為穩定觀測在未沙盒化的崩潰處理程式中執行程式碼

動畫 WebP 階段是一個 V8 沙盒逃逸,但我們並未直接攻擊沙盒本身,而是利用 V8 沙盒刻意未強制執行且 Blink 錯誤地認為可以信任的一個屬性。最後,利用 Crashpad 的溢位將跨過最終的外部程序邊界,實現了在主機上無沙盒的程式碼執行。

階段 1:變更不可變物件

第一個漏洞出現在 Maglev,V8 的中階最佳化編譯器。為了理解它,我們需要檢視去最佳化(deoptimization)的過程。

最佳化後的程式碼根據對其處理的值與物件的假設運作。當某個假設在執行時不再成立,V8 必須重建普通的 JavaScript 物件,並以較少專門化的程式碼繼續執行。這些物件在最佳化程式碼執行期間實際上並未存在於記憶體中;編譯器僅以值的形式表示它們。去最佳化必須將它們具體化。

當 V8 具體化物件時,具有雙精度表示的 JavaScript 物件欄位需要一個 HeapNumber。有缺陷的 Maglev 路徑為虛擬值建立了一個舊空間 HeapNumber,並將物件本身存入去最佳化文字陣列:

void BuildHeapNumber(const VirtualObject* vobject) {
  DCHECK_EQ(vobject->object_type(), vobj::ObjectType::kHeapNumber);
  ValueNode* value_node = vobject->get(offsetof(HeapNumber, value_));
  return BuildHeapNumber(value_node->Cast<Float64Constant>()->value());
}

void BuildHeapNumber(Float64 number) {
  DirectHandle<Object> value =
      local_isolate_->factory()->NewHeapNumberFromBits<AllocationType::kOld>(
          number.get_bits());
  translation_array_builder_->StoreLiteral(GetDeoptLiteral(*value));
}

這段程式碼來自 vulnerable maglev-code-generator.cc。漏洞的另一半在於通用具體化器。對於標記為 kStoreHeapObject 的欄位,它會將翻譯槽中持有的物件直接寫入重建的擁有者:

if (marker == kStoreHeapObject) {
  DirectHandle<HeapObject> field_value = slot->storage();
  WRITE_FIELD(*object_storage, offset, *field_value);
  WRITE_BARRIER(*object_storage, offset, *field_value);
  continue;
}

這是來自 translated-state.cc 的對應程式碼。

第一個區塊將可變物件轉換為文字。第二個區塊將該物件的身份(而非僅其數值)複製到每個重建的擁有者。透過巢狀啟動,V8 因此能夠建立不同的 JavaScript 物件,其雙精度欄位共用同一個可變 HeapNumber。

這種行為違反了在 V8 其他地方 所述的編譯器不變式:

HeapNumberRef 只會為不可變的 HeapNumber 建立。

有缺陷的路徑將可變數字偽裝成編譯器允許重複使用的不可變常數。

對於非常大的「2」值

利用對 Array.prototype.at() 的不一致,攻擊者將其作為漏洞。一次編譯器觀測看到安全索引並批准了邊界記憶體取;另一個觀測到同一個常數卻提供了實際的載入偏移。改變一個物件的雙精度欄位因此能改變最佳化程式碼使用的偏移,而不會使先前的邊界判斷失效。

三個最佳化後的載入函式足以揭露附近的 V8 指標,恢復打包雙精度陣列的佈局,並取得攻擊者描述的陣列參考。該偽造陣列隨後被用作啟動,重寫真實 Uint8Array 的 metadata,將普通的索引存取延伸至 V8 整個 4 GiB 的沙盒。

尋找穩定的物件位址還需要一步。攻擊者將參考存入 V8 的大型物件空間,指向其需要定位的任何 JavaScript 物件。V8 在垃圾回收期間自行更新該參考,因此攻擊者能夠在不固定可移動物件的情況下,恢復目前位於沙盒內的位址。

此時,攻擊者已能從 JavaScript 在整個 V8 沙盒中可靠地存取記憶體。

修正方案

V8 移除了文字物件的特殊處理。虛擬 HeapNumber 現在遵循普通捕獲物件具體化路徑,Maglev 亦明確阻止其重複。每一次去最佳化都會具體化一個全新的盒子:

vobj::ObjectType object_type = object->object_type();
DCHECK_NOT_NULL(object->allocation());
// HeapNumbers may be mutable object fields; each materialization must
// create a fresh box, so they are never deduplicated.
// TODO(victorgomes): Constrain which objects may contain mutable
// HeapNumbers. Immutable HeapNumbers can be stored as a literal object
// instead of a captured object.
int dup_id =
    object_type == vobj::ObjectType::kHeapNumber
        ? CreateUnduplicatableId()
        : GetDuplicatedId(reinterpret_cast<intptr_t>(object->allocation()));

程式碼來自 已修正版本。完整補丁是 V8 變更 8255554,公開根因報告是 Chromium 問題 547819997。

階段 2:兩個 WebP 在 V8 隔離區分岔

在尋找 V8 隔離層逃逸時,我們有意避免直接攻擊隔離層本身。相反,我們稍微思考了它的威脅模型,並選擇尋找那些“允許”但可能有問題的事物。

我們發現的漏洞正是此例:WebCodecs 可以透過轉移將一個 ArrayBuffer 交給 ImageDecoder。在普通 JavaScript 語意下,轉移會分離舊緩衝區及其檢視。Blink 隨後可以包裝後端儲存區而不進行複製,並將這些位元組視為其專屬輸入。

但 V8 隔離層的威脅模型不同:一旦攻擊者控制了隔離區內的記憶體,V8 便假設攻擊者能夠破壞那裡的物件與簿記。它並不保證將分離的 ArrayBuffer 在語意上對該攻擊者不可變。強制實現此屬性需移動或複製後端儲存區,或在隔離層設計時要避免的熱路徑中加入檢查。

在完整鏈路中,利用程式使用第一階段提供的隔離區寫入,將一個活躍的 Uint8Array 檢視從其 JSArrayBuffer 的分離通知鏈中移除。當 ImageDecoder 轉移緩衝區時,V8 分離了它仍然知道的檢視。透過被省略檢視的最佳化寫入仍可使用。Blink 認為它擁有這些位元組的專屬許可權;攻擊者仍可更改它們。

讓一個 WebP 觸發另一個的 TOCTOU

這個破碎的所有權假設在 WebP 解碼中表現為檢查時間/使用時間(TOCTOU)漏洞。

概念驗證將兩個相同長度、獨立有效的動畫 WebP 檔案放在同一後端儲存區:

  1. WebP A 描述了一個 64×64 的畫布。它的第一幀寬 48 畫素,起始於 x = 16,高 64 畫素。
  2. ImageDecoder 轉移了緩衝區,其持久化解多工器快取了 A 的幀矩形與負載位置。
  3. 存活的檢視將位元組替換為 WebP B,其當前負載解碼為完整的 64×64 幀。
  4. 解碼器從 B 讀取壓縮資料,但使用來自 A 的幀幾何來描述輸出緩衝區。

WEBPImageDecoder::DecodeSingleFrame() 從快取矩形初始化外部緩衝區的 stride 和大小。它接著獨立將輸出指標移動到該矩形的左上畫素 (程式碼):

const gfx::Rect& frame_rect = buffer.OriginalFrameRect();
if (!decoder_) {
  // Set up decoder_buffer_ with output mode
  if (!WebPInitDecBuffer(&decoder_buffer_)) {
    return SetFailed();
  }
  decoder_buffer_.colorspace = RGBOutputMode();
  decoder_buffer_.u.RGBA.stride =
      Size().width() * sizeof(ImageFrame::PixelData);
  decoder_buffer_.u.RGBA.size =
      decoder_buffer_.u.RGBA.stride * frame_rect.height();
  decoder_buffer_.is_external_memory = 1;
  decoder_ = WebPINewDecoder(&decoder_buffer_);
  if (!decoder_) {
    return SetFailed();
  }
}
decoder_buffer_.u.RGBA.rgba = reinterpret_cast<uint8_t*>(
    buffer.GetAddr(frame_rect.x(), frame_rect.y()));

下一個語句 將該描述符傳遞給 WebPIUpdate()。

在 ASAN 重現器中,點陣圖跨度為 256 位元組,完整分配為 16,384 位元組。GetAddr(16, 0) 以 64 位元組前進輸出指標,剩餘 16,320 位元組。這正好足以容納 A 的快取矩形:

256 * (64 - 1) + 48 * 4 = 16,320

但 Blink 仍宣稱所有 16,384 位元組。B 的 64 畫素寬負載因此通過 libwebp 的外部緩衝區大小檢查:

256 * (64 - 1) + 64 * 4 = 16,384

WebPIUpdate() 在 Blink 比較解碼尺寸與快取矩形之前寫入畫素。解碼的佔用範圍超出本機 SkBitmap 分配 64 位元組。ASAN 捕捉到從分配第一個超出位元組開始的 256 位元組轉換寫入。

兩個 WebP 本身都是有效的。危險狀態僅因 Blink 將 A 的快取幾何與 B 的當前位元組結合而產生。

從堆疊溢位到一次選擇性的本地寫入

實際攻擊使用相同的漏洞,並以 PartitionAlloc 選擇幾何。兩畫素的框架從 x = 124 開始,畫布寬度為 126 畫素。504 位元組的畫素分配和 496 位元組的 WebPIDecoder 佔據了 512 位元組桶中的相鄰槽位。將輸出指標前進 496 位元組後只剩下 8 位元組,然而解碼器仍認為擁有 504 位元組。

畫素寫入將正在執行的解碼器的本地欄位替換為指向已在本地網路緩衝區中佈置好的兩個 WebPDecBuffer 記錄的指標。解碼完成後,真正的 WebPCopyDecBufferPixels() 程式會跟隨錯誤的來源與目的描述。它將一個選定的 64 位值複製到一個選定的本地地址。

那一次寫入即是影像漏洞與下一階段之間的完整介面。

修正

首個已部署的修正將 rgba、stride 與 size 組合起來,並將宣告的範圍限制為保留的矩形。後續修正移除了一個更微妙的假設:解碼器不再計算分配應包含什麼,而是直接從 ImageFrame 點陣圖本身取得其步幅與範圍:

const gfx::Rect& frame_rect = buffer.OriginalFrameRect();
CHECK(gfx::Rect(Size()).Contains(frame_rect));
CHECK(!frame_rect.IsEmpty());
// The top-left pixel of frame_rect.
ImageFrame::PixelData* frame_rect_front =
    buffer.GetAddr(frame_rect.x(), frame_rect.y());
// The bottom-right pixel of frame_rect.
ImageFrame::PixelData* frame_rect_back =
    buffer.GetAddr(frame_rect.right() - 1, frame_rect.bottom() - 1);
decoder_buffer_.u.RGBA.rgba = reinterpret_cast<uint8_t*>(frame_rect_front);
decoder_buffer_.u.RGBA.stride = buffer.Bitmap().rowBytes();
decoder_buffer_.u.RGBA.size =
    (frame_rect_back - frame_rect_front + 1) * sizeof(ImageFrame::PixelData);

該程式碼出現在後續的 修訂版。根本原因報告與 ASAN 重現程式位於 Chromium 問題 548509153。兩個已部署的修正為 Chromium 變更 8266257 與 Chromium 變更 8352899。

WASM/WCP 交接

第二階段產生了一次選定的本地寫入,位於 V8 隔離區之外。鏈仍需將該寫入轉化為任意本地程式碼執行,而不修改受保護的程式碼指標條目。

一個熱 WebAssembly 函式透過 V8 全域的 Wasm 程式碼指標表解析其目標。攻擊者改變了定位該表所使用的可寫基底,並提供了一個具有正確型別的替代條目。

我們從先前的 完整鏈路 取用了島嶼跳躍技術。那次漏洞將編碼在 JIT 雙精度常數中的指令片段與短跳轉連結。這一次,我們在 Liftoff 產生的 Wasm 程式碼中使用了 13 個 64 位立即數,並採用 Windows 載入器 ABI。

在 WebPCopyDecBufferPixels() 更改 Wasm 程式碼指標表基底後,下一個 Wasm 呼叫通過簽名檢查並解析了替代條目。該條目指向 chrome.dll 中的 jmp rcx 指令;RCX 指向第一個指令島。

每個島嶼提供最多六個位元組的載入程式,接著是一個短跳轉,跳過編譯器產生的指令,直到下一個島嶼。13 個島嶼合起來形成約 70 位元組的載入程式。它分配可寫記憶體,複製 Crashpad 負載,將對映改為可執行,然後呼叫它。

第三階段:渲染器拒絕冷卻

Chrome 的 Crashpad 處理程式在渲染器沙盒之外執行。收到快照請求時,它會暫停渲染器並從渲染器的位址空間讀取資料結構。處理程式建立 ScopedProcessSuspend,該程式呼叫 NtSuspendProcess(),然後告知快照機制目標已被暫停。

在修正之前的 修訂版 中,Crashpad 的遠端記憶體協助程式之一 ProcessMemoryWin::ReadUpTo() 包含以下恢復路徑:

SIZE_T size_out = 0;
BOOL success = ReadProcessMemory(
    handle_, reinterpret_cast<void*>(address), buffer, size, &size_out);
if (success)
  return base::checked_cast<ssize_t>(size_out);

if (GetLastError() == ERROR_PARTIAL_COPY) {
  // If we can not read the entire section, perform a short read of the first
  // page instead. This is necessary to support ReadCString().
  size_t short_read =
      base::GetPageSize() - (address & (base::GetPageSize() - 1));
  success = ReadProcessMemory(handle_,
                              reinterpret_cast<void*>(address),
                              buffer,
                              short_read,
                              &size_out);
  if (success)
    return base::checked_cast<ssize_t>(size_out);
}

乍看之下,錯誤似乎僅是 short_read 未被限制於 size。然而,計算在 Crashpad 以為已建立的假設下是安全的:即被暫停的程序位址空間不會改變。

回退機制是為了處理從可讀頁面開始並跨入不可讀頁面的讀取。如果一個size位元組的請求跨越該邊界,第一頁剩餘的位元組數必須小於size。因此僅重試這些位元組即可符合呼叫者的目標緩衝區。

在頁面保護穩定時,存在三種常見情況:

  • 位於可讀頁面的請求即時成功。
  • 位於不可讀頁面的請求再次重試時仍失敗。
  • 從可讀頁面跨入不可讀頁面的請求,重試時會產生比原始請求更短的讀取。

因此程式碼將第一次失敗視為關於來源對映幾何的證據。當渲染器能在兩次讀取之間改變對映時,這一推論就不再成立。

擊破凍結

Windows 允許使用 THREAD_CREATE_FLAGS_BYPASS_PROCESS_FREEZE 標誌建立的執行緒在 NtSuspendProcess() 上持續執行。在請求快照之前,原生渲染器有效負載建立了這樣的執行緒。利用者利用了 0x42 標誌:BYPASS_PROCESS_FREEZE 與 SKIP_THREAD_ATTACH。Windows 具備此功能的具體 原因 仍是個謎,但它提供了我們此處所需的關鍵假設違反,允許我們在讀取與重試之間修改頁面保護。

當 Crashpad 將渲染器視為暫停時,該執行緒會反覆將攻擊者控制的來源頁在 PAGE_NOACCESS 與 PAGE_READWRITE 之間切換。成功序列為:

  1. Crashpad 發出一個完全包含於單一來源頁面的微小讀取。
  2. 該頁面不可讀,因此 ReadProcessMemory() 回傳 ERROR_PARTIAL_COPY。
  3. Crashpad 將此失敗解讀為請求已跨越頁邊界的證據。
  4. 免凍結的渲染器執行緒將頁面設為可讀。
  5. Crashpad 重試至頁面結尾,複製了超出呼叫者請求量的資料。

對於利用者使用的呼叫點,數值如下:

requested length         = 0x0b8
source offset in page    = 0x0e98
computed retry length    = 0x1000 - 0x0e98 = 0x168
destination heap slot    = 0x0c0
overwrite into next slot = 0x168 - 0x0c0 = 0x0a8

原始的 0xb8 位元組請求在頁邊界前結束。它只能產生所需錯誤,因為此時整頁不可讀。當渲染器將頁面設為可讀後,0x168 位元組的重試成功並超出分配範圍。

為使競爭條件可靠,負載在一次快照請求中提供了 48 個候選讀取。一個免凍結的工作執行緒將它們的頁面在可讀與不可讀之間切換。其他輔助執行緒保持停駐,維持執行緒數量及其相關的 Crashpad 分配穩定。

重新連線一個正在運作的 Crashpad 寫入器

過大的重試僅能破壞其 0xc0 讀取緩衝區槽位後立即的分配。負載安排該分配包含一個活躍的 SnapshotMinidumpMemoryWriter,Crashpad 後續將使用它從渲染器讀取一段記憶體並將位元組透過其輸出路徑傳送。

為建立此相鄰關係,負載公開了一個可讀的程序與載入器圖,包含按選定順序排列的記憶體範圍。Crashpad 為每個範圍分配一個寫入器,然後合併包含的範圍並釋放其寫入器。此排序使得一個已釋放的 0xc0 寫入器槽位緊接著一個仍存活的寫入器;下一個 0xb8 位元組的讀取緩衝區重用該釋放槽。執行緒、TEB 與可讀堆疊的寫入器影響分配順序,因此負載計算這些輸入並調整範圍數量以保持相同的桶位。

在我們測試的具備 CET 的系統上,處理器使用了使用者影子堆疊,而 CFG 將間接呼叫限制在已批准的目標。為了避免違反這些緩解措施,我們使用了正常的呼叫與回傳序列,並將被替代的虛擬呼叫導向現有的 CFG 有效函式。

有缺陷的重試讀取重新寫入了實際寫入器的前 0xa8 個位元組。在偏移 0x00 位置寫回了 SnapshotMinidumpMemoryWriter vtable;在 0x70 位置將寫入器的狀態設為可寫;在 0x78 位置將 MemorySnapshot::Delegate vtable 替換為偏移的 CopyingOutputStreamAdaptor vtable,導致委派回呼進入介面卡的緩衝寫入路徑。覆寫在偏移 0xa8 的 memory_snapshot_ 之前立即結束。

Crashpad 隨後透過其第一個 vtable 進入寫入器的 WriteObject()。WriteObject() 透過保留的快照指標呼叫 Read(),該指標從渲染器讀取受控的 0xd0 位元組範圍。第二個 vtable 的回呼將這些位元組複製到 chrome.exe 中的一個可寫地址,並將同一地址視為介面卡的底層輸出流。

複製的位元組同時構成了一個單方法輸出流和一個 fallback‑launcher 物件。當介面卡重新整理時,該流的虛擬 Write() 進入 Chrome 的 FallbackUnhandledExceptionFilter(),這是一個 CFG 有效目標。這些位元組還將 Chrome 的 fallback‑launcher 全域指向複製的物件,並提供其命令與啟動欄位。過濾器呼叫 FallbackCrashHandlerLauncher::LaunchAndWaitForHandler(),將 cmd /k calc.exe 傳遞給 CreateProcessW。這些呼叫維持了影子堆疊。

由於 Windows ASLR 只在載入模組時隨機化一次,而非每個程序都隨機化(這是使用重定位而非 PIC/PIE 程式碼的決策結果),它在渲染器和處理器中都以相同位址對映 chrome.exe。這意味著渲染器的 payload 可以從自身程序的映像基址推算出處理器的位址,同時透過映像時間戳與大小驗證精確版本,以套用任何版本特定偏移(考慮到 Chrome 的快速發布週期,我們必須能夠在新版本發布時迅速重新定位鏈路)。

修正

修正後的 實作 移除了 Crashpad 對目標在兩次讀取之間保持不變的依賴。重試現在受源頁邊界與呼叫者原始請求的雙重限制:

if (GetLastError() == ERROR_PARTIAL_COPY) {
  // If we can not read the entire section, perform a short read of the first
  // page instead. This is necessary to support ReadCString().
  const size_t page_remainder =
      base::GetPageSize() - (address & (base::GetPageSize() - 1));
  const size_t short_read = std::min(size, page_remainder);
  success = ReadProcessMemory(handle_,
                              reinterpret_cast<void*>(address),
                              buffer,
                              short_read,
                              &size_out);
  if (success)
    return base::checked_cast<ssize_t>(size_out);
}

此限制是必要的防禦式程式設計,但最好的理解方式是移除一個時間假設:即使源在失敗讀取與重試之間改變,Crashpad 也無法寫入目標之外。

相同的修補程式通過在前進目標指標前拒絕任何大於剩餘請求計數的 ReadUpTo() 結果來加固通用呼叫者。此檢查出現在 process_memory.cc。

完整修正是 Chromium 變更 8267322,公開報告為 Chromium 問題 545820931。

替換中間

現在包括 Google 在內的所有人都將 LLM 指向像 Chrome 這樣的程式碼庫,漏洞衝突成為主要風險。為了防範此情況,我們將利用設計成模組化階段,並為每個部份準備備用漏洞。這一策略在我們的第一個鏈路版本——使用 Gin 漏洞逃逸 V8 沙盒時——證明有效,該版本被發現與問題 542670566 重複。經過幾小時的高壓工作,我們將 Gin‑based V8 逃逸替換為本文描述的動畫 WebP 階段;此替換階段挽救了鏈路,並且 Google 確認鏈路成功彈出 calc。

結論

我們應該從這條鏈路得出什麼結論?根據我們所知,它是 LLM 時代第一個獲得獎勵的完整 Chrome 利用鏈路。以下是我們的一些想法:

  1. 對於漏洞本身,我們注意到一個事實(巧合的是)所有三個漏洞最終都是關於改變本以為不可變的東西:在第一階段,一個 HeapNumber;在第二階段,一個轉移的 WebP 緩衝區;在第三階段,對一個應該已凍結程序的頁面保護。這類不變性違規可能是 LLM 擅長的領域:它們能夠運用對作業系統 API 和開源程式碼的百科全書式知識,提出不變性失效的情境。
  2. 作為 VRP 報告者,除非你提供超越基礎 LLM 的新內容(而且往往即使如此也不足以拯救你),否則避免衝突是 非常困難的。我們的第一條完整鏈路(Andrés Luksenberg 將在 OffensiveCon Tokyo 發表)在最後階段與 Google 內部幾個月前發現的錯誤相衝突而失敗,我們也見過 其他鏈路自此以後遭遇同樣命運。把精力放在深入瞭解目標上,並準備好面對失望!
  3. 即使大部分工作由 LLM 完成,若給你的代理提供高品質的範例,你會更成功。在我們的案例中,代理能夠借鑑 XBOW’s Native Team 成員最初編寫的利用技術,例如 “island hopping” loader,來構建模組化且可靠的利用程式。
  4. 六天和 40,000 美元在獲得 Chrome 鏈路的回報面前,並不算太多時間或金錢!然而,這並不意味著任何人只要投入那麼多時間和金錢就能得到 Chrome 鏈路(如前所述,我們確實投入了相當多的人力專業與判斷),但確實意味著門檻已經 顯著 降低。請相應防禦。

來源:Hacker News (official RSS) · xbow.com