1. 簡介

已編譯的 V8 JavaScript bytecode 已成為一種少見但有效的 Malware 封裝格式。攻擊者不需要直接提供可讀的 JavaScript,而是可以先將原始碼進行混淆,再使用內含 Node.js runtime 的環境將其編譯成 V8 內部的 bytecode 表示形式,最後 只散布產生的 cache 檔案 — 這是一種既不是一般 JavaScript,也不是傳統 native binary 的 Artifact,同時也落在現有多數 reverse-engineering 工具的能力範圍之外 [1] 。這份報告探討針對 JSCeal stealer 所發展的靜態反混淆方法。 JSCeal 是一種以 加密貨幣 為主要目標的 information stealer,也以 WEEVILPROXY 與 MeadowLocust 等名稱進行追蹤,其活動自 2024 年 3 月起即被觀察到 [1] [5] 。分析重點包括各層混淆機制的技術運作方式、為逆向這些混淆而建立的 recovery pipeline,以及透過該 pipeline 所能取得的能力資訊。

V8 Bytecode 能藏住 Malware 嗎? 破解 JSCeal 的靜態反混淆流程 | 資訊安全新聞

2. Threat Model:兩層疊加的混淆機制

JSCeal 的防護來自兩個依序進行的轉換。首先,JavaScript 原始碼會使用開源工具 javascript-obfuscator 進行處理 [6] ,該工具會套用 identifier 重新命名、使用加密的 string-array extraction、control-flow flattening,以及以 proxy 為基礎的 call indirection。接著,已經過混淆的 script 會被編譯成 V8 bytecode,並以 code-cache 檔案形式散布 ,而不是以原始碼文字形式散布 [1] 。由於 V8 bytecode 格式與版本密切相關,而且從未被設計為 distribution format,因此主流 decompiler 與 JavaScript deobfuscator 幾乎無法處理這類內容。從攻擊者的角度來看,這種組合的成本很低:Node.js 及其 package ecosystem 提供現成的 application building blocks,而 obfuscator 幾乎不需要額外的開發成本,就能增加另一層分析阻礙 [1]

在 JavaScript layer 中,對 JSCeal corpus 的分析反覆辨識出四類 transformation:重新命名的 identifiers;透過 chunk 化並由 decoder 重建 literals 的 string protection;control-flow flattening,也就是讓 functions 變成由 dispatcher 驅動的 state machines;以及 proxy/operation indirection,也就是透過 forwarding helpers 路由 calls,甚至連 addition 或 comparison 這類 primitive operations 都包裝到專用的 helper functions 中 [1]

3. Recovery Pipeline:從 Bytecode 到可讀的 Pseudocode

Pipeline 從 View8 開始。 View8 是一套開源的 V8-bytecode decompiler,最初由 CPR researcher Moshe Marelus 於 2024 年發布,使用經過 patch 的 V8 disassembler,將 cached bytecode 轉換成類似 JavaScript 的 pseudocode 表示形式 [2] 。針對 JSCeal case study,CPR 擴充了 View8,使其 output 能夠重現並序列化,以便進行自動化且可重複的 post-processing,接著在其上建立專用的 deobfuscation pipeline,依序包含 value 與 scope propagation、string reconstruction、control-flow unflattening,以及 proxy/operation-wrapper resolution,最後再進行 cleanup [1] 。執行順序相當重要,因為每個 pass 都會揭露下一個 pass 所依賴的資訊 — 例如 string deobfuscation 不只會還原 literal text,也會取得後續 pass 用來解析 indirection 的 dictionary keys 與 property names [1] 。最後還可以選擇使用 LLM 輔助 function renaming,讓大型 recovered call graphs 更容易瀏覽,而不改變底層邏輯 [1] 。整體流程如下。

sequenceDiagram participant A as Analyst participant F as .jsc Payload File participant V as Patched V8 Disassembler participant P as View8 Pseudocode participant D as CPR Deobfuscation Passes participant L as Optional LLM Renaming A->>F: Strip Brotli compression layer F->>V: Feed V8 bytecode cache V->>P: Emit raw View8 pseudocode P->>D: Serialize IR (reproducible pickle) D->>D: Pass 1 - value / scope propagation D->>D: Pass 2 - string reconstruction (RC4 + chunk decode) D->>D: Pass 3 - control-flow unflattening D->>D: Pass 4 - proxy / operation-wrapper resolution D->>L: Send recovered function bodies L-->>D: Candidate function names D-->>A: Readable, annotated pseudocode
圖 1 — 從已編譯 V8 bytecode 到可分析 pseudocode 的靜態 recovery pipeline。

4. 程式分析:View8 Pseudocode 表示法

對於閱讀 pipeline output 的人員而言,其中一項實作細節會直接影響解讀方式:View8 pseudocode 是為了提升可讀性而發明的表示法,而不是實際的 JavaScript grammar。來源文章特別指出一個反覆出現的案例 — 否定的 equality expression 會將否定套用到整個 comparison,而不是套用到單一 operand,因此輸出的形式無法按照一般 JavaScript 進行解析,必須從語意角度理解 [1] 。以下 fragment 取自文章,用來說明這種模式及其註解。

  1. // Source: View8 pseudocode notation, as described in the primary article [1].
  2. // The exclamation mark binds to the ENTIRE comparison, not to r6 alone.
  3. // This is a notational quirk of View8's printer, not literal JS syntax.
  4. !r6 === "0" // printed form emitted by View8
  5. // Semantic reading (what an analyst must mentally substitute):
  6. // NOT (r6 === "0") == r6 !== "0"
  7. //
  8. // Practical impact: any automated pass that pattern-matches on
  9. // comparison operators (e.g. the control-flow unflattening or
  10. // proxy-resolution filters) must special-case this printer
  11. // convention, or it will silently mis-evaluate branch conditions
  12. // during static analysis.

這個單一 expression 的案例之所以具有代表性,正是因為它很小:它顯示 recovered「pseudocode」並不等同於 source,因此每一個後續 pass — string reconstruction、unflattening、wrapper resolution — 都必須依照 View8 自身的 conventions 來撰寫,而不能依照 JavaScript grammar 來處理。Pipeline 作者因此將整體 recovery 目標描述為重建足夠的結構,以便追蹤 program logic、比較 samples,並透過具體的 strings、APIs 與 data flow 驗證行為,而不是追求 byte-perfect source recovery,因為 V8 compilation 從原理上就會造成資訊遺失 [1]

5. String Protection 與 Control-Flow Unflattening

Corpus 中出現兩種 string-obfuscation variants。較舊且較簡單的 variant 會將 fragments 儲存在 plain array 中,再透過 index transformation 取出。現有 samples 中占主導地位的 variant 則加入 encoded chunks、RC4 encryption、一系列 decoder wrapper functions,以及對 chunk index 進行 arithmetic transformations,必須完成這些步驟後才能重建 string [1] 。由於 recovered strings 經常同時作為 dictionary keys 與 control-flow ordering data,因此這個 pass 必須在 flattening 還原之前執行。

Control-flow unflattening 會移除由 dispatcher 驅動的 state machine,並恢復傳統的 branching,但這項 transformation 並非單純的 mechanical process。來源文章指出,原本寫在 flattening loop 內的 if blocks 中的 continue statements,只有在該 loop 的環境中才具有意義 — 在原始 flow 中,它們會跳回 dispatcher 以取得下一個 state — 因此一旦移除 loop,這段 logic 便不再能以原本的形式運作,必須重新撰寫,才能在 unflattened output 中保留等價行為 [1] 。這說明 unflattening 不能只是 blind pattern substitution:必須先追蹤每個 jump 在 state machine 中扮演的語意角色,再決定在 state machine 消失後應如何表達。

6. LLM-Assisted Renaming:實證檢驗

由於完全 recovered 的 JSCeal call graph 可能包含數萬個 functions,CPR 評估 LLM 是否能僅根據 recovered pseudocode 有效提出 function names。兩個 models 都針對相同的 cleaned payload 執行,並各自產生 21,154 個 function names;只有 9.3% 的名稱完全相同,不過作者指出,不同的文字措辭不一定代表對功能的理解有所不同 [1] 。為了評估語意上的 — 而非字面上的 — 一致性,研究人員手動檢視 142 個具有高度 context 的 function subtrees,並判定其中 117 個在兩個 models 的命名下都具有合理性。作者將此視為一項粗略估計,用來衡量自動化階段有多少機會能協助分析人員,而不是誤導分析人員 [1]

7. Recovered Capabilities 與 Local MITM Proxy

將 pipeline 套用到選定的 JSCeal payload 後,研究人員得以逐一檢視並使用具體程式碼驗證多項 capability modules:browser 與 cryptocurrency-wallet credential theft、透過 keydown subscription 而非 system-level hook 實作的 keylogging、screenshot capture、Telegram session-file collection,以及 local HTTPS interception proxy [1] 。Proxy component 會自行建立 trust chain — 產生 key pair、建立具有攻擊者控制 subject 與 issuer fields 的 certificate、對其進行 self-sign,並將其安裝至 operating system 的 trusted root store — 之後,經由 local proxy 的 HTTPS traffic 就可以在轉送前被檢視或修改。這種模式也與 CPR 先前的報告一致,後者已將其與 Malware 的 man-in-the-browser 行為建立關聯 [1] [5]

sequenceDiagram participant M as JSCeal Payload participant K as Certificate Generator participant OS as Trusted Root Store participant B as Victim Browser participant S as Local HTTPS Proxy M->>K: generateKeyPair() K->>K: createCertificate(subject, issuer) K->>K: signCertificate() - self-signed K-->>M: attacker-controlled root certificate M->>OS: certutil -addstore -f root cert.pem OS-->>M: certificate now trusted locally B->>S: HTTPS request (redirected to 127.0.0.1) S-->>B: forged leaf certificate, silently trusted S->>S: inspect / modify traffic in transit S-->>M: hand off intercepted credentials
圖 2 — Local certificate trust chain 如何啟用 JSCeal 的 HTTPS interception proxy。

8. 最新發展與討論

文章最後探討 Malware 持續演進的情況:較新的 samples 開始使用針對更新版本 Node.js/V8 所產生的 V8 code caches,現有 obfuscation 之上又加入額外的 payload-encryption layer,並且開始出現針對 macOS 的攻擊 [1] 。每一項發展都會迫使 pipeline 進行增量式調整 — 新的 V8 version 需要相對應的 patched disassembler,而新的 encryption wrapper 則需要新的 decoding pass,才能讓 View8 output 得以取得 — 這與更廣泛的趨勢一致:對攻擊者而言,擴充 compiled-bytecode obfuscation 的成本很低,但對防禦者而言,移除這些混淆的成本卻相當高 [1] 。這種動態也與其他以 Node.js 為基礎的 Malware 研究相似,攻擊者同樣將 custom encryption 與 Node.js 的 cross-platform reach 結合,以拖慢靜態分析並規避 file-based detection。相關情況也記錄於另一篇針對 YaNB Node.js backdoor 的 技術分析文章 中,該文章同樣描述了在 network transmission 前先套用 XOR encryption 與 compression 的多層處理方式。

9. 結論

JSCeal case study 顯示,只要分析人員願意建立能夠對應 obfuscator 與 compiler 所施加之確切 transformation order 的工具,compiled V8 bytecode 實際上並非無法進行 static analysis。這套 pipeline — decompile、propagate values、reconstruct strings、unflatten control flow、resolve indirection,以及選擇性使用 LLM assistance 進行 rename — 提供了一套可套用到單一 Malware family 之外的 template,適用於任何 compiled、obfuscated 的 V8 payload。同時,過程中所記錄的 notation caveats 與 rewriting decisions 也提醒分析人員:recovered pseudocode 仍然需要謹慎且具備語意感知的閱讀方式,而不能只按照字面解讀 [1]