<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" xml:lang="zh-CN"><generator uri="https://jekyllrb.com/" version="4.3.4">Jekyll</generator><link href="https://haloowhite.com/feed.xml" rel="self" type="application/atom+xml" /><link href="https://haloowhite.com/" rel="alternate" type="text/html" hreflang="zh-CN" /><updated>2026-04-13T15:12:33+08:00</updated><id>https://haloowhite.com/feed.xml</id><title type="html">White’s Blog</title><subtitle>Web Scraping &amp; Reverse Engineering 技术博客 — 专注逆向工程、JSVMP分析、验证码协议解析与自动化工具开发</subtitle><author><name>White</name></author><entry><title type="html">有手就行系列——Cloudflare 5s 盾逆向实战（AST 反混淆 + 加密还原）</title><link href="https://haloowhite.com/2026/04/13/cloudflare-5s-shield-ast-deobfuscation/" rel="alternate" type="text/html" title="有手就行系列——Cloudflare 5s 盾逆向实战（AST 反混淆 + 加密还原）" /><published>2026-04-13T00:00:00+08:00</published><updated>2026-04-13T00:00:00+08:00</updated><id>https://haloowhite.com/2026/04/13/cloudflare-5s-shield-ast-deobfuscation</id><content type="html" xml:base="https://haloowhite.com/2026/04/13/cloudflare-5s-shield-ast-deobfuscation/"><![CDATA[<h2 id="0背景介绍">0、背景介绍</h2>

<p>Cloudflare 的 5s 盾（Challenge Page）大家应该都不陌生，访问被保护的站点时会出现那个经典的 “Just a moment…” 页面。本质上是 Cloudflare 通过下发一段高度混淆的 JavaScript 脚本（ray JS），在浏览器端执行环境检测、指纹采集、加密计算后，将结果 POST 回服务端校验，通过后种上 <code class="language-plaintext highlighter-rouge">cf_clearance</code> Cookie 放行。</p>

<p>本文以实际目标站点为例，从请求链路分析到 AST 反混淆，再到加解密算法还原，完整记录逆向过程。文中涉及的加密算法、字段名均经过实际抓包和代码运行验证。</p>

<blockquote>
  <p><strong>声明</strong>：本文仅供安全研究与技术学习交流，请勿用于任何非法用途。</p>
</blockquote>

<h2 id="1请求链路总览">1、请求链路总览</h2>

<p>整个挑战流程拆分为 4 步：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>┌──────────────────────────────────────────────────────────────────┐
│  Step 1: GET /                                                   │
│  → 拿到 HTML，提取 window._cf_chl_opt 参数                       │
│  → 提取 ray JS 文件路径                                          │
├──────────────────────────────────────────────────────────────────┤
│  Step 2: GET /cdn-cgi/challenge-platform/h/{g|b}/orchestrate/... │
│  → 下载混淆 ray JS（~200KB）                                     │
│  → 提取动态路径、Base64 字符集、追加参数                           │
├──────────────────────────────────────────────────────────────────┤
│  Step 3: POST /cdn-cgi/challenge-platform/h/.../flow/ov1/...     │
│  → 构造挑战数据 → LZW 压缩 → TEA-CTR 加密 → 自定义 Base64 编码   │
│  → 提交第一次 POST（挑战响应）                                    │
├──────────────────────────────────────────────────────────────────┤
│  Step 4: POST（同上 URL）                                        │
│  → 构造 Turnstile 数据，同样加密流程提交第二次 POST               │
│  → 获取 cf_clearance Cookie                                      │
└──────────────────────────────────────────────────────────────────┘
</code></pre></div></div>

<h3 id="11-第一步获取-html-与关键参数">1.1 第一步：获取 HTML 与关键参数</h3>

<p>GET 请求目标站点，403 响应返回一个简洁的 HTML 挑战页面。关键信息在一段 <code class="language-plaintext highlighter-rouge">&lt;script&gt;</code> 中：</p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">window</span><span class="p">.</span><span class="nx">_cf_chl_opt</span> <span class="o">=</span> <span class="p">{</span>
    <span class="na">cvId</span><span class="p">:</span> <span class="dl">'</span><span class="s1">3</span><span class="dl">'</span><span class="p">,</span>
    <span class="na">cZone</span><span class="p">:</span> <span class="dl">'</span><span class="s1">fastlink.so</span><span class="dl">'</span><span class="p">,</span>
    <span class="na">cType</span><span class="p">:</span> <span class="dl">'</span><span class="s1">interactive</span><span class="dl">'</span><span class="p">,</span>       <span class="c1">// 挑战类型：interactive / managed</span>
    <span class="na">cRay</span><span class="p">:</span> <span class="dl">'</span><span class="s1">9eb833680f13fcf8</span><span class="dl">'</span><span class="p">,</span>   <span class="c1">// 请求 Ray ID</span>
    <span class="na">cH</span><span class="p">:</span> <span class="dl">'</span><span class="s1">j9_nAkRY.6cKOoF...</span><span class="dl">'</span><span class="p">,</span>  <span class="c1">// 挑战哈希</span>
    <span class="na">cFPWv</span><span class="p">:</span> <span class="dl">'</span><span class="s1">g</span><span class="dl">'</span><span class="p">,</span>                 <span class="c1">// 路径前缀标识：g 或 b</span>
    <span class="na">cITimeS</span><span class="p">:</span> <span class="dl">'</span><span class="s1">1776059505</span><span class="dl">'</span><span class="p">,</span>      <span class="c1">// 服务器时间戳</span>
    <span class="na">md</span><span class="p">:</span> <span class="dl">'</span><span class="s1">...</span><span class="dl">'</span><span class="p">,</span>                  <span class="c1">// 挑战元数据（超长字符串）</span>
    <span class="na">cTplC</span><span class="p">:</span> <span class="mi">0</span><span class="p">,</span>
    <span class="na">cTplO</span><span class="p">:</span> <span class="mi">0</span><span class="p">,</span>                   <span class="c1">// 新增属性</span>
    <span class="na">cTplV</span><span class="p">:</span> <span class="mi">5</span><span class="p">,</span>
    <span class="na">cTplB</span><span class="p">:</span> <span class="dl">'</span><span class="s1">0</span><span class="dl">'</span><span class="p">,</span>
    <span class="c1">// ...</span>
<span class="p">};</span>
</code></pre></div></div>

<p>提取方式，正则 + demjson3（因为 key 无引号，不是标准 JSON）：</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">match_pattern</span> <span class="o">=</span> <span class="sa">r</span><span class="s">'window\._cf_chl_opt = (.*?);'</span>
<span class="n">_cf_chl_opt</span> <span class="o">=</span> <span class="n">re</span><span class="p">.</span><span class="n">findall</span><span class="p">(</span><span class="n">match_pattern</span><span class="p">,</span> <span class="n">html</span><span class="p">)</span>
<span class="n">_cf_chl_opt_dict</span> <span class="o">=</span> <span class="n">demjson3</span><span class="p">.</span><span class="n">decode</span><span class="p">(</span><span class="n">_cf_chl_opt</span><span class="p">[</span><span class="mi">0</span><span class="p">])</span>
</code></pre></div></div>

<p>同时从 HTML 中提取 ray JS 路径：</p>

<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;script </span><span class="na">src=</span><span class="s">"/cdn-cgi/challenge-platform/h/g/orchestrate/chl_page/v1?ray=9eb833680f13fcf8"</span><span class="nt">&gt;&lt;/script&gt;</span>
</code></pre></div></div>

<p>注意路径中的 <code class="language-plaintext highlighter-rouge">/h/g/</code> —— 这个 <code class="language-plaintext highlighter-rouge">g</code> 对应 <code class="language-plaintext highlighter-rouge">cFPWv</code> 的值，历史版本中有 <code class="language-plaintext highlighter-rouge">b</code> 和 <code class="language-plaintext highlighter-rouge">g</code> 两种。</p>

<h3 id="12-第二步下载并解析-ray-js">1.2 第二步：下载并解析 ray JS</h3>

<p>ray JS 是核心，一段约 <strong>200KB</strong> 的单行混淆 JavaScript。通过实际抓包分析，需要从中提取三个关键信息：</p>

<p><strong>提取动态路径</strong>：POST 接口 URL 的组成部分</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># 新版中直接在引号内匹配
# 格式: /b/ov1/数字:数字:字符串/
</span><span class="n">pattern</span> <span class="o">=</span> <span class="sa">r</span><span class="s">"'/([bg])/ov(\d)/([^']+)'"</span>
<span class="c1"># 示例结果: /b/ov1/711151733:1776056853:IDzOwF4DX3yC2T8JbE817xeJkiLURBMtj2aOzyTbIdg/
</span></code></pre></div></div>

<blockquote>
  <p>⚠️ 注意：旧版（2025年8-9月）中动态路径和 Base64 字符集用 <code class="language-plaintext highlighter-rouge">~</code> 符号包裹，新版（2026年4月实测）已经去掉了 <code class="language-plaintext highlighter-rouge">~</code>，改为直接赋值给变量。如果你用旧版正则匹配不到，大概率是这个原因。</p>
</blockquote>

<p><strong>提取自定义 Base64 字符集</strong>（65 个字符，每次请求都不同）：</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># 新版中赋值给变量 Qs（变量名可能变化）
# Qs='yXBun2Wgm6lrbakMe-VYDQx5Njwf0H478q$IKiGsPcO1UvtJZzpCRLo3dTAh9E+FS'
</span><span class="n">pattern</span> <span class="o">=</span> <span class="sa">r</span><span class="s">"([A-Za-z][A-Za-z0-9])='([A-Za-z0-9+/\$\-]{64,66})'"</span>
</code></pre></div></div>

<p><strong>提取追加的 <code class="language-plaintext highlighter-rouge">_cf_chl_opt</code> 属性</strong>：</p>

<p>ray JS 会向 <code class="language-plaintext highlighter-rouge">window._cf_chl_opt</code> 追加新属性。当前版本追加了 <code class="language-plaintext highlighter-rouge">rbGs2</code>、<code class="language-plaintext highlighter-rouge">kGHpy2</code>、<code class="language-plaintext highlighter-rouge">dNKln0</code>、<code class="language-plaintext highlighter-rouge">OpmT8</code>、<code class="language-plaintext highlighter-rouge">qRrUl6</code> 等属性，其中 <code class="language-plaintext highlighter-rouge">qRrUl6</code> 包含翻译文本和 metadata。</p>

<p>POST 接口完整路径拼装：</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># cdn-cgi/challenge-platform/h/ + 路径前缀处理 + /b/ov1/ + 动态路径 + cRay + / + cH
</span><span class="n">url</span> <span class="o">=</span> <span class="sa">f</span><span class="s">"/cdn-cgi/challenge-platform/h/.../</span><span class="si">{</span><span class="n">dynamic_path</span><span class="si">}{</span><span class="n">cRay</span><span class="si">}</span><span class="s">/</span><span class="si">{</span><span class="n">cH</span><span class="si">}</span><span class="s">"</span>
</code></pre></div></div>

<h3 id="13-第三步构造并加密挑战数据">1.3 第三步：构造并加密挑战数据</h3>

<p>构造一个 JSON 对象作为挑战响应。字段名是混淆后的，但通过逆向可以理解含义：</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">raw_post_data</span> <span class="o">=</span> <span class="p">{</span>
    <span class="s">'fWZgU3'</span><span class="p">:</span> <span class="n">_cf_chl_opt_dict</span><span class="p">[</span><span class="s">'cType'</span><span class="p">],</span>  <span class="c1"># 挑战类型
</span>    <span class="s">'WVeU0'</span><span class="p">:</span>  <span class="n">_cf_chl_opt_dict</span><span class="p">[</span><span class="s">'cvId'</span><span class="p">],</span>   <span class="c1"># 版本 ID
</span>    <span class="s">'Oslmb8'</span><span class="p">:</span> <span class="mf">26.19</span><span class="p">,</span>                       <span class="c1"># JS 执行耗时（秒）
</span>    <span class="s">'uuGY6'</span><span class="p">:</span>  <span class="mf">1.3</span><span class="p">,</span>                         <span class="c1"># 初始化耗时
</span>    <span class="s">'AZJj6'</span><span class="p">:</span>  <span class="n">_cf_chl_opt_dict</span><span class="p">[</span><span class="s">'cITimeS'</span><span class="p">],</span><span class="c1"># 服务器时间戳
</span>    <span class="s">'GkHb5'</span><span class="p">:</span>  <span class="n">_cf_chl_opt_dict</span><span class="p">[</span><span class="s">'md'</span><span class="p">],</span>     <span class="c1"># 挑战元数据
</span>    <span class="s">'yWqY6'</span><span class="p">:</span> <span class="p">{</span>                             <span class="c1"># 事件计数器
</span>        <span class="s">'dwtA3'</span><span class="p">:</span> <span class="mi">0</span><span class="p">,</span>     <span class="c1"># keydown
</span>        <span class="s">'OdVjC8'</span><span class="p">:</span> <span class="mi">1</span><span class="p">,</span>    <span class="c1"># pointermove
</span>        <span class="s">'VAgA8'</span><span class="p">:</span> <span class="mi">2</span><span class="p">,</span>     <span class="c1"># pointerover
</span>        <span class="s">'OwKv1'</span><span class="p">:</span> <span class="mi">0</span><span class="p">,</span>     <span class="c1"># touchstart
</span>        <span class="s">'OQkk2'</span><span class="p">:</span> <span class="mi">1</span><span class="p">,</span>     <span class="c1"># mousemove
</span>        <span class="s">'Qadr2'</span><span class="p">:</span> <span class="mi">0</span><span class="p">,</span>     <span class="c1"># click
</span>        <span class="s">'WWsp6'</span><span class="p">:</span> <span class="mi">0</span><span class="p">,</span>     <span class="c1"># wheel
</span>        <span class="s">'BVVCR5'</span><span class="p">:</span> <span class="mi">4</span>     <span class="c1"># 总计
</span>    <span class="p">},</span>
    <span class="s">'XHHJ5'</span><span class="p">:</span> <span class="bp">False</span><span class="p">,</span>                        <span class="c1"># window.top !== window.self
</span>    <span class="s">'Pemqu1'</span><span class="p">:</span> <span class="p">[</span><span class="s">'window.frameElement'</span><span class="p">],</span>     <span class="c1"># DOM 查询结果
</span>    <span class="s">'TRGB0'</span><span class="p">:</span> <span class="s">'iDOoK3'</span><span class="p">,</span>                    <span class="c1"># 固定值
</span>    <span class="s">'EXKd3'</span><span class="p">:</span> <span class="s">'sTgPn7'</span><span class="p">,</span>                    <span class="c1"># 固定值
</span>    <span class="c1"># ...
</span><span class="p">}</span>
</code></pre></div></div>

<blockquote>
  <p>这些字段名会随版本变化，但含义基本稳定。<strong>事件计数器不需要精确</strong>——设合理非零值就行。</p>
</blockquote>

<h3 id="14-第四步turnstile-二次验证">1.4 第四步：Turnstile 二次验证</h3>

<p>第一次 POST 成功后还需要第二次，载荷中 <code class="language-plaintext highlighter-rouge">fWZgU3</code> 变成 <code class="language-plaintext highlighter-rouge">chl_api_m</code>，附加 Turnstile 特有字段：</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">raw_post_data_2</span> <span class="o">=</span> <span class="p">{</span>
    <span class="s">'fWZgU3'</span><span class="p">:</span> <span class="s">'chl_api_m'</span><span class="p">,</span>
    <span class="s">'wqqTZ2'</span><span class="p">:</span> <span class="s">'managed'</span><span class="p">,</span>                   <span class="c1"># chlApiAction
</span>    <span class="s">'CRawX7'</span><span class="p">:</span> <span class="s">'97a52a911cd8e2e0'</span><span class="p">,</span>          <span class="c1"># chlApicData
</span>    <span class="s">'cyhaB5'</span><span class="p">:</span> <span class="s">'0x4AAAAAAADnPIDROrmt1Wwj'</span><span class="p">,</span>  <span class="c1"># Turnstile sitekey
</span>    <span class="s">'HPjL8'</span><span class="p">:</span> <span class="s">'new'</span><span class="p">,</span>                        <span class="c1"># 固定值
</span>    <span class="s">'jSSVF8'</span><span class="p">:</span> <span class="mf">970.5</span><span class="p">,</span>                       <span class="c1"># 性能计时（ms）
</span>    <span class="c1"># ... 更多时间指标和会话 token
</span><span class="p">}</span>
</code></pre></div></div>

<h2 id="2加密算法还原">2、加密算法还原</h2>

<p>这部分是最有意思的。加密链路经实际代码运行验证：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>JSON 对象 → 自定义 JSON 序列化（字节级） → LZW 压缩 → TEA-CTR 加密 → 自定义 Base64 编码
                                                          ↑
                                                     RSA 密钥交换
</code></pre></div></div>

<h3 id="21-自定义-json-序列化">2.1 自定义 JSON 序列化</h3>

<p>Cloudflare <strong>没有</strong>用 <code class="language-plaintext highlighter-rouge">JSON.stringify()</code>，而是手写了字节级序列化。直接操作字节数组，逐字符写入 ASCII 码：</p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// null → [110, 117, 108, 108]  即 "null"</span>
<span class="c1">// true → [116, 114, 117, 101]  即 "true"</span>
<span class="c1">// string → 引号包裹 + UTF-8 编码 + JSON 转义处理</span>
<span class="c1">// object → {key:value} 递归，用 hasOwnProperty 遍历</span>
</code></pre></div></div>

<p>转义字符映射也是手写的（<code class="language-plaintext highlighter-rouge">Lq</code> 数组），包含 <code class="language-plaintext highlighter-rouge">\b</code>、<code class="language-plaintext highlighter-rouge">\t</code>、<code class="language-plaintext highlighter-rouge">\n</code>、<code class="language-plaintext highlighter-rouge">\f</code>、<code class="language-plaintext highlighter-rouge">\r</code>、<code class="language-plaintext highlighter-rouge">\"</code>、<code class="language-plaintext highlighter-rouge">\\</code> 的映射。</p>

<h3 id="22-lzw-压缩">2.2 LZW 压缩</h3>

<p>序列化后的字节经过 LZW 压缩。初始字典 0-255 单字符，动态建立新词条。输出是 16-bit 对齐的位流，位宽随字典增长动态增加。</p>

<h3 id="23-rsa-密钥交换">2.3 RSA 密钥交换</h3>

<p>混合加密：RSA 用于交换 TEA 密钥。</p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// 硬编码的 RSA 公钥（实测 2025.08 和 2026.04 完全一致）</span>
<span class="nx">n</span> <span class="o">=</span> <span class="nx">BigInt</span><span class="p">(</span><span class="dl">'</span><span class="s1">0x00e9d3dca1328a49ad3403e4badda37a6a...</span><span class="dl">'</span><span class="p">)</span>  <span class="c1">// 128 字节模数</span>
<span class="nx">e</span> <span class="o">=</span> <span class="nx">BigInt</span><span class="p">(</span><span class="mi">65537</span><span class="p">)</span>

<span class="c1">// 生成 128 字节随机密钥 Lw</span>
<span class="kd">let</span> <span class="nx">Lw</span> <span class="o">=</span> <span class="k">new</span> <span class="nb">Uint8Array</span><span class="p">(</span><span class="mi">128</span><span class="p">);</span>
<span class="nx">crypto</span><span class="p">.</span><span class="nx">getRandomValues</span><span class="p">(</span><span class="nx">Lw</span><span class="p">);</span>
<span class="nx">Lw</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>  <span class="c1">// 确保 &lt; n</span>

<span class="c1">// RSA 加密：LH = Lw^e mod n（快速幂）</span>
</code></pre></div></div>

<p><strong>但是</strong>有一个关键发现：在当前的代码中，动态生成的 <code class="language-plaintext highlighter-rouge">Lw</code> 和 <code class="language-plaintext highlighter-rouge">LH</code> 紧接着就被<strong>固定值覆盖</strong>了：</p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// 动态生成后立即覆盖</span>
<span class="nx">Lw</span> <span class="o">=</span> <span class="k">new</span> <span class="nb">Uint8Array</span><span class="p">([</span><span class="mi">0</span><span class="p">,</span> <span class="mi">171</span><span class="p">,</span> <span class="mi">50</span><span class="p">,</span> <span class="mi">168</span><span class="p">,</span> <span class="mi">12</span><span class="p">,</span> <span class="mi">105</span><span class="p">,</span> <span class="mi">110</span><span class="p">,</span> <span class="mi">252</span><span class="p">,</span> <span class="p">...]);</span>
<span class="nx">LH</span> <span class="o">=</span> <span class="p">[</span><span class="mi">109</span><span class="p">,</span> <span class="mi">128</span><span class="p">,</span> <span class="mi">145</span><span class="p">,</span> <span class="mi">132</span><span class="p">,</span> <span class="mi">21</span><span class="p">,</span> <span class="mi">62</span><span class="p">,</span> <span class="mi">137</span><span class="p">,</span> <span class="mi">240</span><span class="p">,</span> <span class="p">...];</span>
</code></pre></div></div>

<p>这意味着 RSA 密钥交换在这个版本中实际上是”摆设”——每次使用固定的密钥对。这是逆向中的重要发现，意味着<strong>只要这组固定值不变，加解密就是确定性的</strong>。</p>

<blockquote>
  <p>经验证，这组固定值在 2025.08 至 2026.04 的版本中保持不变。</p>
</blockquote>

<h3 id="24-tea-ctr-加密">2.4 TEA-CTR 加密</h3>

<p>数据加密使用 TEA（Tiny Encryption Algorithm），32 轮 Feistel 网络，DELTA 常量 <code class="language-plaintext highlighter-rouge">0x9E3779B9</code>（即 2654435769）。</p>

<p>但不是标准 ECB 模式，而是<strong>自定义的 CTR 模式</strong>：</p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// 对第 i 个 8 字节块：</span>
<span class="c1">// 1. 生成两个 counter</span>
<span class="nx">counter1</span> <span class="o">=</span> <span class="p">[</span><span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="nx">i</span> <span class="o">&amp;</span> <span class="mh">0xFF</span><span class="p">]</span>
<span class="nx">counter2</span> <span class="o">=</span> <span class="p">[</span><span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="p">(</span><span class="nx">i</span><span class="o">+</span><span class="mi">1</span><span class="p">)</span> <span class="o">&amp;</span> <span class="mh">0xFF</span><span class="p">]</span>

<span class="c1">// 2. TEA 加密 counter 生成密钥流</span>
<span class="nx">TEA_Encrypt</span><span class="p">(</span><span class="nx">counter1</span><span class="p">,</span> <span class="nx">keySlice</span><span class="p">)</span>   <span class="c1">// → 8 字节</span>
<span class="nx">TEA_Encrypt</span><span class="p">(</span><span class="nx">counter2</span><span class="p">,</span> <span class="nx">keySlice</span><span class="p">)</span>   <span class="c1">// → 8 字节</span>

<span class="c1">// 3. 拼成 16 字节作为密钥，再次 TEA 加密数据块</span>
<span class="nx">teaKey</span> <span class="o">=</span> <span class="nx">counter1</span><span class="p">.</span><span class="nx">concat</span><span class="p">(</span><span class="nx">counter2</span><span class="p">)</span>
<span class="nx">TEA_Encrypt</span><span class="p">(</span><span class="nx">dataBlock</span><span class="p">,</span> <span class="nx">teaKey</span><span class="p">)</span>
</code></pre></div></div>

<p>密钥选取：从 <code class="language-plaintext highlighter-rouge">Lw</code> 128 字节数组中按 <code class="language-plaintext highlighter-rouge">9 * padding + 40</code> 偏移截取 16 字节。</p>

<p>实际运行验证 TEA 加密（Bun 环境）：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>加密前: [1, 2, 3, 4, 5, 6, 7, 8]
加密后: [24, 76, 70, 253, 111, 118, 182, 253]  ✅
</code></pre></div></div>

<h3 id="25-tea-中的小数点混淆">2.5 TEA 中的小数点混淆</h3>

<p>仔细看 TEA 函数会发现奇怪的小数：</p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">P</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span> <span class="o">&lt;&lt;</span> <span class="mf">16.02</span>   <span class="c1">// 等价于 P[1] &lt;&lt; 16</span>
<span class="nx">F</span> <span class="o">&gt;&gt;&gt;</span> <span class="mf">8.76</span>      <span class="c1">// 等价于 F &gt;&gt;&gt; 8</span>
<span class="mf">255.45</span> <span class="o">&amp;</span> <span class="nx">O</span>      <span class="c1">// 等价于 255 &amp; O</span>
</code></pre></div></div>

<p>JavaScript 位运算会先将操作数转为 32 位整数，所以 <code class="language-plaintext highlighter-rouge">16.02</code> 和 <code class="language-plaintext highlighter-rouge">16</code> 效果完全一样。纯粹增加阅读难度的混淆。</p>

<h3 id="26-自定义-base64">2.6 自定义 Base64</h3>

<p>最后一步，标准 Base64 逻辑（3 字节 → 4 个 6-bit 索引），但查表用每次请求不同的 65 字符集。这使得不同请求产生的密文即使内容相同也完全不同。</p>

<p><strong>加密输出结构</strong>：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>┌──────────────┬──────────┬──────────────────────────┐
│  LH (128B)   │ padding  │  TEA-CTR 加密后的压缩数据  │
│  RSA 密文     │  长度    │                           │
└──────────────┴──────────┴──────────────────────────┘
                ↓ 整体自定义 Base64 编码 ↓
            最终提交的字符串（约 400-2000 字符）
</code></pre></div></div>

<p>实际运行验证完整加密：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>输入: {"fWZgU3":"interactive","WVeU0":"3",...}
加密输出长度: 386
输出: fNB-qXD+$7BugwMUc2Qv0jAuHu9jQE$rBwQ559gg...  ✅
</code></pre></div></div>

<h2 id="3ast-反混淆实战">3、AST 反混淆实战</h2>

<p>这是本文重头戏。Cloudflare 的 ray JS 使用<strong>多层混淆</strong>，直接阅读不可能理解逻辑。反混淆流程基于 Babel AST，分阶段处理。</p>

<h3 id="30-混淆特征识别">3.0 混淆特征识别</h3>

<p>先看混淆后的代码片段：</p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">~</span><span class="kd">function</span> <span class="p">(</span><span class="nx">yG</span><span class="p">,</span> <span class="nx">JU</span><span class="p">,</span> <span class="nx">Jz</span><span class="p">,</span> <span class="p">...)</span> <span class="p">{</span>
  <span class="k">for</span> <span class="p">(</span><span class="nx">yG</span> <span class="o">=</span> <span class="nx">I</span><span class="p">,</span> <span class="kd">function</span> <span class="p">(</span><span class="nx">J</span><span class="p">,</span> <span class="nx">T</span><span class="p">,</span> <span class="nx">DF</span><span class="p">,</span> <span class="nx">ya</span><span class="p">,</span> <span class="nx">l</span><span class="p">,</span> <span class="nx">D</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">for</span> <span class="p">(</span><span class="nx">DF</span> <span class="o">=</span> <span class="p">{</span> <span class="na">J</span><span class="p">:</span> <span class="mi">1107</span><span class="p">,</span> <span class="na">T</span><span class="p">:</span> <span class="mi">1295</span><span class="p">,</span> <span class="p">...</span> <span class="p">},</span> <span class="nx">ya</span> <span class="o">=</span> <span class="nx">I</span><span class="p">,</span> <span class="nx">l</span> <span class="o">=</span> <span class="nx">J</span><span class="p">();</span> <span class="o">!!</span><span class="p">[];)</span> <span class="k">try</span> <span class="p">{</span>
      <span class="k">if</span> <span class="p">(</span><span class="nx">D</span> <span class="o">=</span> <span class="nb">parseInt</span><span class="p">(</span><span class="nx">ya</span><span class="p">(</span><span class="nx">DF</span><span class="p">.</span><span class="nx">J</span><span class="p">))</span> <span class="o">/</span> <span class="mi">1</span> <span class="o">*</span> <span class="p">(</span><span class="nb">parseInt</span><span class="p">(</span><span class="nx">ya</span><span class="p">(</span><span class="nx">DF</span><span class="p">.</span><span class="nx">T</span><span class="p">))</span> <span class="o">/</span> <span class="mi">2</span><span class="p">)</span> <span class="o">+</span> <span class="p">...)</span> <span class="k">break</span><span class="p">;</span>
      <span class="k">else</span> <span class="nx">l</span><span class="p">.</span><span class="nx">push</span><span class="p">(</span><span class="nx">l</span><span class="p">.</span><span class="nx">shift</span><span class="p">())</span>
    <span class="p">}</span> <span class="k">catch</span> <span class="p">(</span><span class="nx">C</span><span class="p">)</span> <span class="p">{</span> <span class="nx">l</span><span class="p">.</span><span class="nx">push</span><span class="p">(</span><span class="nx">l</span><span class="p">.</span><span class="nx">shift</span><span class="p">())</span> <span class="p">}</span>
  <span class="p">}(</span><span class="nx">U</span><span class="p">,</span> <span class="mi">471818</span><span class="p">),</span> <span class="p">...</span>
</code></pre></div></div>

<p>识别出的混淆手法：</p>

<table>
  <thead>
    <tr>
      <th>手法</th>
      <th>特征</th>
      <th>处理策略</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>字符串数组</strong></td>
      <td>大字符串 split 成数组，通过索引访问</td>
      <td>提取 → 计算偏移 → 替换</td>
    </tr>
    <tr>
      <td><strong>数组 shuffle</strong></td>
      <td>push/shift 循环打乱顺序</td>
      <td>本地执行 shuffle 还原</td>
    </tr>
    <tr>
      <td><strong>花指令（运算）</strong></td>
      <td><code class="language-plaintext highlighter-rouge">function(a,b) { return a + b }</code></td>
      <td>内联展开</td>
    </tr>
    <tr>
      <td><strong>花指令（字符串）</strong></td>
      <td><code class="language-plaintext highlighter-rouge">obj['xAxKe'] = 'xooAt'</code> 间接引用</td>
      <td>建映射表 → 替换</td>
    </tr>
    <tr>
      <td><strong>花指令（函数）</strong></td>
      <td><code class="language-plaintext highlighter-rouge">obj.fn(realFn, arg1, arg2)</code> 间接调用</td>
      <td>分析参数 → 内联</td>
    </tr>
    <tr>
      <td><strong>死代码</strong></td>
      <td><code class="language-plaintext highlighter-rouge">if ('abc' === 'def') { ... }</code></td>
      <td>常量折叠 → 删死分支</td>
    </tr>
    <tr>
      <td><strong>控制流平坦化</strong></td>
      <td><code class="language-plaintext highlighter-rouge">switch(order[i++])</code> 分发循环</td>
      <td>提取顺序 → 按序展开</td>
    </tr>
    <tr>
      <td><strong>JSVMP</strong> ⭐新版</td>
      <td><code class="language-plaintext highlighter-rouge">this.h[]</code> + <code class="language-plaintext highlighter-rouge">this.g</code> 寄存器式 VM</td>
      <td>动态分析为主</td>
    </tr>
  </tbody>
</table>

<h3 id="31-字符串数组提取与还原">3.1 字符串数组提取与还原</h3>

<p>混淆的第一道防线。所有有意义的字符串被收集到数组中，通过索引访问。</p>

<p><strong>新旧版差异</strong>：</p>
<ul>
  <li>旧版（2025.08）：用 <code class="language-plaintext highlighter-rouge">|</code> 分隔，如 <code class="language-plaintext highlighter-rouge">"str1|str2|str3".split('|')</code></li>
  <li>新版（2026.04）：用 <code class="language-plaintext highlighter-rouge">!</code> 分隔，1509 段，总长 35488 字符</li>
</ul>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Babel AST 遍历：寻找 "xxx".split("!") 模式</span>
<span class="nx">traverse</span><span class="p">(</span><span class="nx">ast</span><span class="p">,</span> <span class="p">{</span>
  <span class="nx">AssignmentExpression</span><span class="p">(</span><span class="nx">path</span><span class="p">)</span> <span class="p">{</span>
    <span class="kd">const</span> <span class="p">{</span> <span class="nx">left</span><span class="p">,</span> <span class="nx">right</span> <span class="p">}</span> <span class="o">=</span> <span class="nx">path</span><span class="p">.</span><span class="nx">node</span><span class="p">;</span>
    <span class="k">if</span> <span class="p">(</span><span class="nx">t</span><span class="p">.</span><span class="nx">isCallExpression</span><span class="p">(</span><span class="nx">right</span><span class="p">)</span> <span class="o">&amp;&amp;</span>
        <span class="nx">right</span><span class="p">.</span><span class="nx">callee</span><span class="p">.</span><span class="nx">property</span><span class="p">?.</span><span class="nx">name</span> <span class="o">===</span> <span class="dl">"</span><span class="s2">split</span><span class="dl">"</span> <span class="o">&amp;&amp;</span>
        <span class="nx">right</span><span class="p">.</span><span class="nx">callee</span><span class="p">.</span><span class="nx">object</span><span class="p">.</span><span class="nx">value</span><span class="p">?.</span><span class="nx">length</span> <span class="o">&gt;</span> <span class="mi">100</span><span class="p">)</span> <span class="p">{</span>
      <span class="nb">global</span><span class="p">.</span><span class="nx">stringArray</span> <span class="o">=</span> <span class="nx">right</span><span class="p">.</span><span class="nx">callee</span><span class="p">.</span><span class="nx">object</span><span class="p">.</span><span class="nx">value</span><span class="p">.</span><span class="nx">split</span><span class="p">(</span><span class="nx">right</span><span class="p">.</span><span class="nx">arguments</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="nx">value</span><span class="p">);</span>
      <span class="c1">// 找到了，数组长度 1509</span>
    <span class="p">}</span>
  <span class="p">}</span>
<span class="p">});</span>
</code></pre></div></div>

<p><strong>提取偏移量</strong>：字符串访问函数 <code class="language-plaintext highlighter-rouge">return y = y - 291, T = arr[y], T</code> 中的 <code class="language-plaintext highlighter-rouge">291</code> 即偏移：</p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">traverse</span><span class="p">(</span><span class="nx">ast</span><span class="p">,</span> <span class="p">{</span>
  <span class="nx">ReturnStatement</span><span class="p">(</span><span class="nx">path</span><span class="p">)</span> <span class="p">{</span>
    <span class="kd">const</span> <span class="nx">exprs</span> <span class="o">=</span> <span class="nx">path</span><span class="p">.</span><span class="nx">node</span><span class="p">.</span><span class="nx">argument</span><span class="p">?.</span><span class="nx">expressions</span><span class="p">;</span>
    <span class="k">if</span> <span class="p">(</span><span class="nx">exprs</span><span class="p">?.</span><span class="nx">length</span> <span class="o">===</span> <span class="mi">3</span> <span class="o">&amp;&amp;</span> <span class="nx">exprs</span><span class="p">.</span><span class="nx">slice</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span><span class="mi">2</span><span class="p">).</span><span class="nx">every</span><span class="p">(</span><span class="nx">t</span><span class="p">.</span><span class="nx">isAssignmentExpression</span><span class="p">))</span> <span class="p">{</span>
      <span class="nb">global</span><span class="p">.</span><span class="nx">offsetIndex</span> <span class="o">=</span> <span class="nx">exprs</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="nx">right</span><span class="p">.</span><span class="nx">right</span><span class="p">.</span><span class="nx">value</span><span class="p">;</span>  <span class="c1">// 如 291</span>
    <span class="p">}</span>
  <span class="p">}</span>
<span class="p">});</span>
</code></pre></div></div>

<p><strong>还原数组顺序</strong>：数组被 shuffle 函数打乱了。方法是把 shuffle 代码提取出来本地执行：</p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// 提取 shuffle 函数 + 辅助函数，本地 eval 执行</span>
<span class="kd">const</span> <span class="nx">runCode</span> <span class="o">=</span> <span class="s2">`
let </span><span class="p">${</span><span class="nx">funcName</span><span class="p">}</span><span class="s2"> = function() {
   let arr = </span><span class="p">${</span><span class="nx">JSON</span><span class="p">.</span><span class="nx">stringify</span><span class="p">(</span><span class="nx">stringArray</span><span class="p">)}</span><span class="s2">;
   </span><span class="p">${</span><span class="nx">funcName</span><span class="p">}</span><span class="s2"> = () =&gt; arr;
   return arr;
};
</span><span class="p">${</span><span class="nx">helperFunction</span><span class="p">}</span><span class="s2">
!</span><span class="p">${</span><span class="nx">shuffleCode</span><span class="p">}</span><span class="s2">
</span><span class="p">${</span><span class="nx">funcName</span><span class="p">}</span><span class="s2">()
`</span><span class="p">;</span>
<span class="nx">stringArray</span> <span class="o">=</span> <span class="nb">eval</span><span class="p">(</span><span class="nx">runCode</span><span class="p">);</span>  <span class="c1">// 得到正确顺序</span>
</code></pre></div></div>

<h3 id="32-字符串解密替换">3.2 字符串解密替换</h3>

<p>拿到正确的数组和偏移后，替换所有字符串引用。两种形式：</p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// 形式 A：通过中间变量映射</span>
<span class="nx">NK</span> <span class="o">=</span> <span class="p">{</span> <span class="na">W</span><span class="p">:</span> <span class="mi">1186</span><span class="p">,</span> <span class="na">J</span><span class="p">:</span> <span class="mi">735</span> <span class="p">}</span>
<span class="nx">obj</span><span class="p">[</span><span class="nx">decode</span><span class="p">(</span><span class="nx">NK</span><span class="p">.</span><span class="nx">W</span><span class="p">)]</span>  <span class="err">→</span>  <span class="nx">obj</span><span class="p">[</span><span class="dl">"</span><span class="s2">addEventListener</span><span class="dl">"</span><span class="p">]</span>

<span class="c1">// 形式 B：直接函数调用</span>
<span class="nx">yG</span><span class="p">(</span><span class="mi">533</span><span class="p">)</span>  <span class="err">→</span>  <span class="dl">"</span><span class="s2">document</span><span class="dl">"</span>
</code></pre></div></div>

<p>对应的 AST 处理：</p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">traverse</span><span class="p">(</span><span class="nx">ast</span><span class="p">,</span> <span class="p">{</span>
  <span class="nx">CallExpression</span><span class="p">(</span><span class="nx">path</span><span class="p">)</span> <span class="p">{</span>
    <span class="kd">const</span> <span class="p">{</span> <span class="nx">callee</span><span class="p">,</span> <span class="na">arguments</span><span class="p">:</span> <span class="nx">args</span> <span class="p">}</span> <span class="o">=</span> <span class="nx">path</span><span class="p">.</span><span class="nx">node</span><span class="p">;</span>
    <span class="c1">// yG(533) → 直接替换</span>
    <span class="k">if</span> <span class="p">(</span><span class="nx">t</span><span class="p">.</span><span class="nx">isIdentifier</span><span class="p">(</span><span class="nx">callee</span><span class="p">)</span> <span class="o">&amp;&amp;</span> <span class="nx">args</span><span class="p">.</span><span class="nx">length</span> <span class="o">===</span> <span class="mi">1</span> <span class="o">&amp;&amp;</span> <span class="nx">t</span><span class="p">.</span><span class="nx">isNumericLiteral</span><span class="p">(</span><span class="nx">args</span><span class="p">[</span><span class="mi">0</span><span class="p">]))</span> <span class="p">{</span>
      <span class="nx">path</span><span class="p">.</span><span class="nx">replaceWith</span><span class="p">(</span><span class="nx">t</span><span class="p">.</span><span class="nx">stringLiteral</span><span class="p">(</span><span class="nx">decodeString</span><span class="p">(</span><span class="nx">args</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="nx">value</span><span class="p">)));</span>
    <span class="p">}</span>
  <span class="p">}</span>
<span class="p">});</span>
</code></pre></div></div>

<h3 id="33-花指令消除运算型">3.3 花指令消除（运算型）</h3>

<p>大量简单运算被包装成函数调用：</p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// 混淆前定义</span>
<span class="dl">'</span><span class="s1">pXsvz</span><span class="dl">'</span><span class="p">:</span> <span class="kd">function</span><span class="p">(</span><span class="nx">f</span><span class="p">,</span> <span class="nx">j</span><span class="p">)</span> <span class="p">{</span> <span class="k">return</span> <span class="nx">f</span> <span class="o">&amp;</span> <span class="nx">j</span><span class="p">;</span> <span class="p">}</span>
<span class="c1">// 混淆后调用</span>
<span class="nx">J</span><span class="p">[</span><span class="dl">'</span><span class="s1">pXsvz</span><span class="dl">'</span><span class="p">](</span><span class="nx">C</span><span class="p">,</span> <span class="mi">255</span><span class="p">)</span>  <span class="c1">// 实际就是 C &amp; 255</span>
</code></pre></div></div>

<p>处理：识别只有单个 return 语句的函数定义，建立字典，遍历调用处内联展开。</p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">function</span> <span class="nx">isObfuscatedFunction</span><span class="p">(</span><span class="nx">node</span><span class="p">)</span> <span class="p">{</span>
  <span class="k">return</span> <span class="nx">node</span><span class="p">.</span><span class="nx">type</span> <span class="o">===</span> <span class="dl">"</span><span class="s2">FunctionExpression</span><span class="dl">"</span> <span class="o">&amp;&amp;</span>
         <span class="nx">node</span><span class="p">.</span><span class="nx">body</span><span class="p">.</span><span class="nx">body</span><span class="p">.</span><span class="nx">length</span> <span class="o">===</span> <span class="mi">1</span> <span class="o">&amp;&amp;</span>
         <span class="nx">node</span><span class="p">.</span><span class="nx">body</span><span class="p">.</span><span class="nx">body</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="nx">type</span> <span class="o">===</span> <span class="dl">"</span><span class="s2">ReturnStatement</span><span class="dl">"</span> <span class="o">&amp;&amp;</span>
         <span class="p">[</span><span class="dl">"</span><span class="s2">BinaryExpression</span><span class="dl">"</span><span class="p">,</span> <span class="dl">"</span><span class="s2">LogicalExpression</span><span class="dl">"</span><span class="p">,</span> <span class="dl">"</span><span class="s2">UnaryExpression</span><span class="dl">"</span><span class="p">]</span>
           <span class="p">.</span><span class="nx">includes</span><span class="p">(</span><span class="nx">node</span><span class="p">.</span><span class="nx">body</span><span class="p">.</span><span class="nx">body</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="nx">argument</span><span class="p">.</span><span class="nx">type</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p><strong>关键：需要多轮迭代</strong>。花指令可能嵌套引用：<code class="language-plaintext highlighter-rouge">A['fn1'](B['fn2'](x, y), z)</code>，展开 <code class="language-plaintext highlighter-rouge">fn1</code> 后内部的 <code class="language-plaintext highlighter-rouge">fn2</code> 才暴露出来。代码中循环 3 次确保清除。</p>

<h3 id="34-花指令消除函数调用型">3.4 花指令消除（函数调用型）</h3>

<p>更复杂的一种 —— 包装函数调用而非运算符：</p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// 定义</span>
<span class="dl">'</span><span class="s1">TqoiZ</span><span class="dl">'</span><span class="p">:</span> <span class="kd">function</span><span class="p">(</span><span class="nx">n</span><span class="p">,</span> <span class="nx">P</span><span class="p">)</span> <span class="p">{</span> <span class="k">return</span> <span class="nx">n</span><span class="p">(</span><span class="nx">P</span><span class="p">);</span> <span class="p">}</span>
<span class="c1">// 调用</span>
<span class="nx">K</span><span class="p">[</span><span class="dl">'</span><span class="s1">TqoiZ</span><span class="dl">'</span><span class="p">](</span><span class="nx">realFunc</span><span class="p">,</span> <span class="nx">arg1</span><span class="p">)</span>  <span class="err">→</span>  <span class="nx">realFunc</span><span class="p">(</span><span class="nx">arg1</span><span class="p">)</span>
</code></pre></div></div>

<p>处理时需要分析参数对应关系 —— 第一个参数替换 callee，其余替换函数参数：</p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">if</span> <span class="p">(</span><span class="nx">paramList</span><span class="p">.</span><span class="nx">length</span> <span class="o">-</span> <span class="mi">1</span> <span class="o">===</span> <span class="nx">newExpr</span><span class="p">.</span><span class="nx">arguments</span><span class="p">.</span><span class="nx">length</span><span class="p">)</span> <span class="p">{</span>
  <span class="nx">newExpr</span><span class="p">.</span><span class="nx">callee</span> <span class="o">=</span> <span class="nx">t</span><span class="p">.</span><span class="nx">cloneNode</span><span class="p">(</span><span class="nx">paramList</span><span class="p">[</span><span class="mi">0</span><span class="p">],</span> <span class="kc">true</span><span class="p">);</span>
  <span class="k">for</span> <span class="p">(</span><span class="kd">let</span> <span class="nx">i</span> <span class="o">=</span> <span class="mi">1</span><span class="p">;</span> <span class="nx">i</span> <span class="o">&lt;</span> <span class="nx">paramList</span><span class="p">.</span><span class="nx">length</span><span class="p">;</span> <span class="nx">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
    <span class="nx">newExpr</span><span class="p">.</span><span class="nx">arguments</span><span class="p">[</span><span class="nx">i</span> <span class="o">-</span> <span class="mi">1</span><span class="p">]</span> <span class="o">=</span> <span class="nx">t</span><span class="p">.</span><span class="nx">cloneNode</span><span class="p">(</span><span class="nx">paramList</span><span class="p">[</span><span class="nx">i</span><span class="p">],</span> <span class="kc">true</span><span class="p">);</span>
  <span class="p">}</span>
  <span class="nx">path</span><span class="p">.</span><span class="nx">replaceWith</span><span class="p">(</span><span class="nx">newExpr</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<h3 id="35-死代码消除">3.5 死代码消除</h3>

<p>混淆器插入大量永假条件分支：</p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">if</span> <span class="p">(</span><span class="dl">'</span><span class="s1">abc</span><span class="dl">'</span> <span class="o">===</span> <span class="dl">'</span><span class="s1">def</span><span class="dl">'</span><span class="p">)</span> <span class="p">{</span> <span class="cm">/* 垃圾代码 */</span> <span class="p">}</span> <span class="k">else</span> <span class="p">{</span> <span class="cm">/* 真实逻辑 */</span> <span class="p">}</span>
</code></pre></div></div>

<p>识别两侧都是字面量的比较，直接折叠：</p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">traverse</span><span class="p">(</span><span class="nx">ast</span><span class="p">,</span> <span class="p">{</span>
  <span class="nx">IfStatement</span><span class="p">(</span><span class="nx">path</span><span class="p">)</span> <span class="p">{</span>
    <span class="kd">const</span> <span class="p">{</span> <span class="nx">test</span><span class="p">,</span> <span class="nx">consequent</span><span class="p">,</span> <span class="nx">alternate</span> <span class="p">}</span> <span class="o">=</span> <span class="nx">path</span><span class="p">.</span><span class="nx">node</span><span class="p">;</span>
    <span class="k">if</span> <span class="p">(</span><span class="nx">t</span><span class="p">.</span><span class="nx">isBinaryExpression</span><span class="p">(</span><span class="nx">test</span><span class="p">)</span> <span class="o">&amp;&amp;</span> <span class="nx">bothSidesAreLiterals</span><span class="p">(</span><span class="nx">test</span><span class="p">))</span> <span class="p">{</span>
      <span class="kd">const</span> <span class="nx">result</span> <span class="o">=</span> <span class="nx">evaluateConstant</span><span class="p">(</span><span class="nx">test</span><span class="p">);</span>
      <span class="nx">result</span> <span class="p">?</span> <span class="nx">path</span><span class="p">.</span><span class="nx">replaceWithMultiple</span><span class="p">(</span><span class="nx">consequent</span><span class="p">.</span><span class="nx">body</span><span class="p">)</span>
             <span class="p">:</span> <span class="nx">alternate</span> <span class="p">?</span> <span class="nx">path</span><span class="p">.</span><span class="nx">replaceWithMultiple</span><span class="p">(</span><span class="nx">alternate</span><span class="p">.</span><span class="nx">body</span><span class="p">)</span> <span class="p">:</span> <span class="nx">path</span><span class="p">.</span><span class="nx">remove</span><span class="p">();</span>
    <span class="p">}</span>
  <span class="p">}</span>
<span class="p">});</span>
</code></pre></div></div>

<h3 id="36-控制流平坦化还原">3.6 控制流平坦化还原</h3>

<p>经典 Split-Switch 模式：</p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// 混淆后</span>
<span class="k">for</span> <span class="p">(</span><span class="nx">c</span> <span class="o">=</span> <span class="dl">"</span><span class="s2">4|5|0|2|3|1</span><span class="dl">"</span><span class="p">.</span><span class="nx">split</span><span class="p">(</span><span class="dl">'</span><span class="s1">|</span><span class="dl">'</span><span class="p">),</span> <span class="nx">J</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="o">!!</span><span class="p">[];)</span> <span class="p">{</span>
  <span class="k">switch</span> <span class="p">(</span><span class="nx">c</span><span class="p">[</span><span class="nx">J</span><span class="o">++</span><span class="p">])</span> <span class="p">{</span>
    <span class="k">case</span> <span class="dl">'</span><span class="s1">0</span><span class="dl">'</span><span class="p">:</span> <span class="nx">step0</span><span class="p">();</span> <span class="k">continue</span><span class="p">;</span>
    <span class="k">case</span> <span class="dl">'</span><span class="s1">1</span><span class="dl">'</span><span class="p">:</span> <span class="nx">step1</span><span class="p">();</span> <span class="k">continue</span><span class="p">;</span>
    <span class="c1">// ...</span>
  <span class="p">}</span>
  <span class="k">break</span><span class="p">;</span>
<span class="p">}</span>
<span class="c1">// 还原后，按 4→5→0→2→3→1 顺序展开</span>
<span class="nx">step4</span><span class="p">();</span> <span class="nx">step5</span><span class="p">();</span> <span class="nx">step0</span><span class="p">();</span> <span class="nx">step2</span><span class="p">();</span> <span class="nx">step3</span><span class="p">();</span> <span class="nx">step1</span><span class="p">();</span>
</code></pre></div></div>

<p>实现逻辑：从 init 中提取顺序数组，建立 case→代码块映射，按序展开，替换整个 for 语句。</p>

<h3 id="37-反混淆流水线">3.7 反混淆流水线</h3>

<p>完整的处理流程：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>raw.js                     ← 原始 ray JS
  ↓ handle.js              ← 主 AST（字符串解密 + 花指令 + 死代码 + 控制流）
result.js
  ↓ 二次处理.py             ← 正则清理残留格式
output.js
  ↓ handle-去除花指令.js     ← 二次 AST（深层花指令 + 字符串映射花指令）
final_output.js            ← 可读代码
</code></pre></div></div>

<h2 id="4新版变化jsvmp-的引入">4、新版变化：JSVMP 的引入</h2>

<p><strong>2026 年 4 月实测</strong>，最新的 ray JS 相比 2025 年 8-9 月版本有几个显著变化：</p>

<h3 id="41-不变的部分">4.1 不变的部分</h3>

<ul>
  <li><strong>RSA 模数</strong>：<code class="language-plaintext highlighter-rouge">0x00e9d3dca1328a49ad3403e4badda37a6a...</code>（260 位十六进制，完全未变）</li>
  <li><strong>RSA 公钥指数</strong>：65537</li>
  <li><strong>TEA 算法</strong>：DELTA=2654435769，32 轮，CTR 模式</li>
  <li><strong>加密链路</strong>：JSON 序列化 → LZW → TEA-CTR → 自定义 Base64</li>
</ul>

<h3 id="42-变化的部分">4.2 变化的部分</h3>

<table>
  <thead>
    <tr>
      <th>特征</th>
      <th>旧版 (2025.08)</th>
      <th>新版 (2026.04)</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>路径前缀</td>
      <td><code class="language-plaintext highlighter-rouge">h/b/</code>（cFPWv=b）</td>
      <td><code class="language-plaintext highlighter-rouge">h/g/</code>（cFPWv=g）</td>
    </tr>
    <tr>
      <td>特殊标记</td>
      <td><code class="language-plaintext highlighter-rouge">~</code> 包裹 Base64/路径</td>
      <td>去掉 <code class="language-plaintext highlighter-rouge">~</code>，直接赋值</td>
    </tr>
    <tr>
      <td>字符串分隔符</td>
      <td><code class="language-plaintext highlighter-rouge">\|</code></td>
      <td><code class="language-plaintext highlighter-rouge">!</code></td>
    </tr>
    <tr>
      <td>字符串表规模</td>
      <td>~数百段</td>
      <td>1509 段，35KB</td>
    </tr>
    <tr>
      <td>新增参数</td>
      <td>-</td>
      <td><code class="language-plaintext highlighter-rouge">cTplO</code>、<code class="language-plaintext highlighter-rouge">OpmT8</code> 等</td>
    </tr>
    <tr>
      <td><strong>JSVMP</strong></td>
      <td>无</td>
      <td><strong>205+ 处 <code class="language-plaintext highlighter-rouge">this.h[]</code> 引用</strong></td>
    </tr>
    <tr>
      <td>加密字符串</td>
      <td>无</td>
      <td>26 个 <code class="language-plaintext highlighter-rouge">$</code> 分隔的密文</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">runProgram</code></td>
      <td>无</td>
      <td>3 次调用</td>
    </tr>
  </tbody>
</table>

<h3 id="43-jsvmpjs-虚拟机保护">4.3 JSVMP（JS 虚拟机保护）</h3>

<p>这是最大的变化。新版 ray JS 中出现了大量 JSVMP 特征：</p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// 寄存器访问 (205次)</span>
<span class="k">this</span><span class="p">.</span><span class="nx">h</span><span class="p">[</span><span class="mi">131</span> <span class="o">^</span> <span class="k">this</span><span class="p">.</span><span class="nx">g</span><span class="p">][</span><span class="mi">3</span><span class="p">]</span>
<span class="k">this</span><span class="p">.</span><span class="nx">h</span><span class="p">[</span><span class="k">this</span><span class="p">.</span><span class="nx">g</span> <span class="o">^</span> <span class="mi">234</span><span class="p">]</span>

<span class="c1">// 字节码读取</span>
<span class="k">this</span><span class="p">.</span><span class="nx">h</span><span class="p">[</span><span class="mi">131</span> <span class="o">^</span> <span class="k">this</span><span class="p">.</span><span class="nx">g</span><span class="p">][</span><span class="mi">1</span><span class="p">][</span><span class="dl">"</span><span class="s2">charCodeAt</span><span class="dl">"</span><span class="p">](</span><span class="k">this</span><span class="p">.</span><span class="nx">h</span><span class="p">[</span><span class="mi">131</span> <span class="o">^</span> <span class="k">this</span><span class="p">.</span><span class="nx">g</span><span class="p">][</span><span class="mi">0</span><span class="p">]</span><span class="o">++</span><span class="p">)</span>

<span class="c1">// 操作码分发</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">Q</span> <span class="o">===</span> <span class="mi">175</span><span class="p">)</span> <span class="p">{</span> <span class="cm">/* 操作 A */</span> <span class="p">}</span>
<span class="k">else</span> <span class="k">if</span> <span class="p">(</span><span class="mi">24</span> <span class="o">!==</span> <span class="nx">Q</span><span class="p">)</span> <span class="p">{</span>
  <span class="k">if</span> <span class="p">(</span><span class="mi">101</span> <span class="o">===</span> <span class="nx">Q</span><span class="p">)</span> <span class="p">{</span> <span class="cm">/* 操作 B */</span> <span class="p">}</span>
<span class="p">}</span> <span class="k">else</span> <span class="p">{</span> <span class="cm">/* 操作 C */</span> <span class="p">}</span>
</code></pre></div></div>

<p>这是一个<strong>寄存器式虚拟机</strong>：</p>
<ul>
  <li><code class="language-plaintext highlighter-rouge">this.h[]</code>：寄存器组</li>
  <li><code class="language-plaintext highlighter-rouge">this.g</code>：寄存器偏移/密钥</li>
  <li>字节码从字符串中按字节读取</li>
  <li>操作码通过 XOR 解码后分发</li>
</ul>

<p>JSVMP 使得<strong>纯 AST 静态分析变得困难</strong>——因为核心逻辑被编译成了字节码，AST 只能看到解释器框架，看不到实际业务逻辑。</p>

<p><strong>应对思路</strong>：</p>
<ol>
  <li><strong>动态分析</strong>：hook VM 的寄存器读写和操作码分发，trace 出执行日志</li>
  <li><strong>补环境执行</strong>：构造模拟浏览器环境，直接执行原始 JS</li>
  <li><strong>字节码反编译</strong>：提取字节码字符串和操作码映射表，写反编译器</li>
</ol>

<p>这已经是另一个话题了，后续单独写。</p>

<h2 id="5一些实用经验">5、一些实用经验</h2>

<ol>
  <li><strong>事件计数器是弱校验</strong>：<code class="language-plaintext highlighter-rouge">yWqY6</code> 里的值不需要精确，设合理非零值即可</li>
  <li><strong>DOM 查询结果会变</strong>：当前版本查 <code class="language-plaintext highlighter-rouge">window.frameElement</code>，历史版本查页面元素 ID</li>
  <li><strong>Base64 字符集是一次性密钥</strong>：即使截获密文，不知字符集无法解码</li>
  <li><strong>Bun 比 Node.js 执行更快</strong>：加密脚本跑 Bun 体验好很多</li>
  <li><strong>curl_cffi 是必须的</strong>：需要 TLS 指纹模拟，普通 requests 会被识别</li>
  <li><strong>花指令多轮处理</strong>：一轮不够，3 轮基本能清干净</li>
</ol>

<h2 id="faq">FAQ</h2>

<h3 id="q-cloudflare-5s-盾的加密算法多久更新一次">Q: Cloudflare 5s 盾的加密算法多久更新一次？</h3>

<p>A: 加密算法核心（RSA + TEA + LZW + Base64）经实测从 2025.08 到 2026.04 未变。变化的主要是混淆方式（字符串分隔符、JSVMP 引入）和字段名。</p>

<h3 id="q-为什么不直接-puppeteer-过盾">Q: 为什么不直接 Puppeteer 过盾？</h3>

<p>A: 简单场景可以用，但 Cloudflare 检测无头浏览器。大规模场景下协议方案效率高出几个数量级——一次请求 vs 启动一个完整浏览器实例。</p>

<h3 id="q-ast-反混淆工具推荐">Q: AST 反混淆工具推荐？</h3>

<p>A: Babel 是 JavaScript AST 最佳选择。核心库：<code class="language-plaintext highlighter-rouge">@babel/parser</code>（解析）、<code class="language-plaintext highlighter-rouge">@babel/traverse</code>（遍历）、<code class="language-plaintext highlighter-rouge">@babel/generator</code>（代码生成）、<code class="language-plaintext highlighter-rouge">@babel/types</code>（节点类型判断）。配合 Bun 运行速度很快。</p>

<hr />

<p><strong>免责声明</strong>：本文内容仅供安全研究与技术学习交流，请勿用于非法用途。因使用本文信息导致的后果由使用者自行承担。</p>

<table>
  <tbody>
    <tr>
      <td>© White’s Blog</td>
      <td><a href="https://haloowhite.com">haloowhite.com</a></td>
      <td><a href="https://t.me/haloowhite">Telegram @haloowhite</a></td>
    </tr>
  </tbody>
</table>]]></content><author><name>White</name></author><category term="逆向" /><category term="Cloudflare" /><category term="逆向" /><category term="Cloudflare" /><category term="AST" /><category term="反混淆" /><category term="TEA" /><category term="RSA" /><category term="LZW" /><category term="JSVMP" /><category term="Babel" /><summary type="html"><![CDATA[深入解析 Cloudflare 5s 盾（Challenge Page）的完整请求链路、AST 反混淆流水线（字符串解密、花指令消除、控制流还原）、TEA-CTR + RSA + LZW 加密算法还原，以及 2026 年新版 JSVMP 变化分析。]]></summary></entry><entry><title type="html">ArkoseLabs FunCaptcha 协议逆向与风控要点</title><link href="https://haloowhite.com/2025/11/13/arkose-funcaptcha-reverse-tutorial/" rel="alternate" type="text/html" title="ArkoseLabs FunCaptcha 协议逆向与风控要点" /><published>2025-11-13T00:00:00+08:00</published><updated>2025-11-13T00:00:00+08:00</updated><id>https://haloowhite.com/2025/11/13/arkose-funcaptcha-reverse-tutorial</id><content type="html" xml:base="https://haloowhite.com/2025/11/13/arkose-funcaptcha-reverse-tutorial/"><![CDATA[<p>虽然这一期还是有手就行，但考虑到有点费手，就没加入《有手系列》里，我知道很贴心，不用谢～ 让我们直接进入正题！<br />
相信大家对这个验证码并不陌生，以下是FunCaptcha的一个例子。如果你的环境足够干净，没有被风控的话，做的题目会很少，以及选项中的可切换答案图片（对应难度）也会很少，接下来我会详细说明相关的内容</p>

<p><img src="https://pub-df7ca5ef070b4d47a2a7c8b98941cb71.r2.dev/demo.png" alt="验证码示例图" /></p>

<h2 id="一完整的请求链路">一、完整的请求链路</h2>

<p>这一章节，主要梳理下完整的验证请求链接，整个验证中涉及到的请求链路，下一章节将详细介绍请求里涉及到的参数和返回结果的解析。</p>

<p>整体流程如下：</p>

<h3 id="1获取session-token"><strong>1、获取session token</strong></h3>

<p><code class="language-plaintext highlighter-rouge">POST https://client-api.arkoselabs.com/fc/gt2/public_key/{对应的public-key}</code></p>

<p><img src="https://pub-df7ca5ef070b4d47a2a7c8b98941cb71.r2.dev/session-token.png" alt="Session Token 接口返回结果" /></p>

<p>这一步至关重要，这一步提交的data涉及到你的设备指纹信息，将决定后续你是否需要pow验证，以及相关的验证码难度和数量等。如果构造的设备指纹或请求指纹太劣质，甚至将会被直接拒绝返回 <code class="language-plaintext highlighter-rouge">{"error":"DENIED ACCESS"}</code>，或者在步骤3时直接返回 <code class="language-plaintext highlighter-rouge">{"error":"DENIED ACCESS"}</code>。这一步需要重点关注的是response里的<code class="language-plaintext highlighter-rouge">token</code> 字段分隔符 <code class="language-plaintext highlighter-rouge">|</code> 第一个值即为token，这里是 <code class="language-plaintext highlighter-rouge">26318777fbd628c58.1761804104</code> ，这个token将贯穿整个验证流程。cdn里的js代码是动态变化的，这一点和CloudFlare类似。</p>

<h3 id="2optionalpow挑战如果步骤一中返回结果中需要pow挑战需要先进行pow挑战并提交相关结果给服务器根据返回结果"><strong>2、【Optional】Pow挑战，如果步骤一中返回结果中需要pow挑战，需要先进行pow挑战，并提交相关结果给服务器，根据返回结果</strong></h3>

<h3 id="3获取刷新验证码详情"><strong>3、获取/刷新验证码详情</strong></h3>

<p><code class="language-plaintext highlighter-rouge">POST https://client-api.arkoselabs.com/fc/gfct/</code></p>

<p><img src="https://pub-df7ca5ef070b4d47a2a7c8b98941cb71.r2.dev/captcha-detail.png" alt="验证码详情 接口返回结果" /></p>

<p>如需验证码挑战则这一步请求会返回相应的验证码详情。这一步需要注意的是response里的 <code class="language-plaintext highlighter-rouge">challengeID</code> 和 <code class="language-plaintext highlighter-rouge">_challenge_imgs</code> 里当前验证码的图片。再次请求该接口会刷新当前的验证码</p>

<h3 id="4提交当前验证码结果">4、提交当前验证码结果</h3>

<p><code class="language-plaintext highlighter-rouge">POST https://client-api.arkoselabs.com/fc/ca/</code></p>

<p><img src="https://pub-df7ca5ef070b4d47a2a7c8b98941cb71.r2.dev/submit-result.png" alt="提交验证码接口结果" /></p>

<p>这一步提交后，如果未完成所有的验证的话，则response为<code class="language-plaintext highlighter-rouge">not answered</code> ，<code class="language-plaintext highlighter-rouge">_challenge_imgs</code> 是下一张验证码的图片地址。如果当前是最后一张验证码，通过验证后是 <code class="language-plaintext highlighter-rouge">response</code> 字段为 <code class="language-plaintext highlighter-rouge">answered</code>，<code class="language-plaintext highlighter-rouge">solved</code> 为 <code class="language-plaintext highlighter-rouge">true</code> ，反之则是 <code class="language-plaintext highlighter-rouge">false</code> ，中间有问题回答错了。这一步实则是链式地提交验证码结果，直到所有的验证码都提交答案了，才会得知最终是否通过了验证</p>

<h2 id="二相关请求参数和结果的详细解析">二、相关请求参数和结果的详细解析</h2>

<p>OK，让我们潜入！这一趴才是真正的重点主题，我将详细的剖析具体每个请求里的参数来源及构成，至于 response 里需要关注的重点字段，上一章节里已提及，这一章就不再赘述了！</p>

<h3 id="1获取session-token-1">1、获取Session Token</h3>

<p><code class="language-plaintext highlighter-rouge">POST https://client-api.arkoselabs.com/fc/gt2/public_key/{对应的public-key}</code></p>

<p>需要关注的参数:</p>

<ul>
  <li>
    <p>header里的参数，x-ark-esync-value</p>
  </li>
  <li>url里的public key，每个站点的具体网页（同一个站点，但不同业务场景）都不一样</li>
  <li>请求动态参数，c、userbrowser（请求头UA，与设备指纹一致）、rnd（随机数）</li>
  <li>固定参数，style_theme、capi_mode、public_key、site，直接与网页里的结果固定就好</li>
  <li>data[blob]，这个参数单独拎出来，是因为这个参数是网页后端返回传的，如果网站没传的话，值为 <code class="language-plaintext highlighter-rouge">undefined</code></li>
</ul>

<p>关于请求头里的 <code class="language-plaintext highlighter-rouge">x-ark-esync-value</code> 参数，直接参考下面这个构造函数即可：</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">time</span>
<span class="kn">from</span> <span class="nn">typing</span> <span class="kn">import</span> <span class="n">Optional</span>


<span class="k">def</span> <span class="nf">arkose_esync_timestamp</span><span class="p">(</span><span class="n">ms</span><span class="p">:</span> <span class="n">Optional</span><span class="p">[</span><span class="nb">int</span><span class="p">]</span> <span class="o">=</span> <span class="bp">None</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">int</span><span class="p">:</span>
    <span class="s">"""
    复现时间戳计算。
    :param ms: 可选的毫秒级时间戳；不传则使用当前时间。
    :return: 向下取整到 21600ms 的时间戳。
    """</span>
    <span class="n">ALIGNMENT_MS</span> <span class="o">=</span> <span class="mi">21600</span>  <span class="c1"># 与脚本中的 b.Jy 相同
</span>    <span class="n">current</span> <span class="o">=</span> <span class="n">ms</span> <span class="k">if</span> <span class="n">ms</span> <span class="ow">is</span> <span class="ow">not</span> <span class="bp">None</span> <span class="k">else</span> <span class="nb">int</span><span class="p">(</span><span class="n">time</span><span class="p">.</span><span class="n">time</span><span class="p">()</span> <span class="o">*</span> <span class="mi">1000</span><span class="p">)</span>
    <span class="k">return</span> <span class="nb">round</span><span class="p">(</span><span class="n">current</span> <span class="o">-</span> <span class="n">current</span> <span class="o">%</span> <span class="n">ALIGNMENT_MS</span><span class="p">)</span>

</code></pre></div></div>

<p>这一步请求其实重中之重是 <code class="language-plaintext highlighter-rouge">c</code> 参数，也就是旧版的 <code class="language-plaintext highlighter-rouge">bda</code> 参数，c的参数的来源其实是将收集到的设备指纹进行混合加密（对称加密 + 非对称加密），具体流程如下</p>

<p><strong>a. 数据预处理</strong></p>

<ul>
  <li>将收集到的设备信息JSON序列化为字节流</li>
</ul>

<p><strong>b. 对称加密（AES-256-GCM）</strong></p>

<ul>
  <li>随机生成32字节AES密钥和12字节IV（初始化向量，实现每次加密都是全新的密钥和IV）</li>
  <li>使用AES-GCM模式加密数据，产生密文和认证标签（tag）</li>
  <li>GCM模式提供加密+完整性校验</li>
</ul>

<p><strong>c. 非对称加密（RSA-OAEP）</strong></p>

<ul>
  <li>用RSA公钥加密AES密钥</li>
  <li>解决密钥传输问题：只有持有RSA私钥的服务端能解密</li>
</ul>

<p><strong>4. 组装最终密文</strong></p>

<ul>
  <li>按顺序Base64编码这几个字段 <code class="language-plaintext highlighter-rouge">IV、Tag、加密后的密钥、密文</code></li>
  <li>将分别编码后的字段直接拼接在一起</li>
</ul>

<p>可参考以下构造函数，Public Key不一样，所对应的 RSA 密钥也都不一样</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">rsa_encrypt_arkose</span><span class="p">(</span><span class="n">fp_data</span><span class="p">:</span> <span class="nb">dict</span><span class="p">):</span>
    <span class="s">"""
    Arkose Labs 加密

    Args:
        data: 字典或字符串
    """</span>
    <span class="c1"># 1. 序列化数据
</span>    <span class="k">if</span> <span class="nb">isinstance</span><span class="p">(</span><span class="n">fp_data</span><span class="p">,</span> <span class="p">(</span><span class="nb">dict</span><span class="p">,</span> <span class="nb">list</span><span class="p">)):</span>
        <span class="n">plaintext</span> <span class="o">=</span> <span class="n">json</span><span class="p">.</span><span class="n">dumps</span><span class="p">(</span><span class="n">data</span><span class="p">,</span> <span class="n">separators</span><span class="o">=</span><span class="p">(</span><span class="s">','</span><span class="p">,</span> <span class="s">':'</span><span class="p">)).</span><span class="n">encode</span><span class="p">()</span>
    <span class="k">else</span><span class="p">:</span>
        <span class="n">plaintext</span> <span class="o">=</span> <span class="n">fp_data</span><span class="p">.</span><span class="n">encode</span><span class="p">()</span> <span class="k">if</span> <span class="nb">isinstance</span><span class="p">(</span><span class="n">fp_data</span><span class="p">,</span> <span class="nb">str</span><span class="p">)</span> <span class="k">else</span> <span class="n">fp_data</span>

    <span class="c1"># 2. 生成随机密钥
</span>    <span class="n">aes_key</span> <span class="o">=</span> <span class="n">get_random_bytes</span><span class="p">(</span><span class="mi">32</span><span class="p">)</span>
    <span class="n">iv</span> <span class="o">=</span> <span class="n">get_random_bytes</span><span class="p">(</span><span class="mi">12</span><span class="p">)</span>

    <span class="c1"># 3. AES-GCM 加密
</span>    <span class="n">cipher</span> <span class="o">=</span> <span class="n">AES</span><span class="p">.</span><span class="n">new</span><span class="p">(</span><span class="n">aes_key</span><span class="p">,</span> <span class="n">AES</span><span class="p">.</span><span class="n">MODE_GCM</span><span class="p">,</span> <span class="n">nonce</span><span class="o">=</span><span class="n">iv</span><span class="p">)</span>
    <span class="n">ciphertext</span><span class="p">,</span> <span class="n">tag</span> <span class="o">=</span> <span class="n">cipher</span><span class="p">.</span><span class="n">encrypt_and_digest</span><span class="p">(</span><span class="n">plaintext</span><span class="p">)</span>

    <span class="c1"># 4. RSA 加密密钥
</span>    <span class="n">rsa_key</span> <span class="o">=</span> <span class="n">RSA</span><span class="p">.</span><span class="n">import_key</span><span class="p">(</span><span class="n">PUBLIC_KEY_PEM</span><span class="p">)</span>
    <span class="n">rsa_cipher</span> <span class="o">=</span> <span class="n">PKCS1_OAEP</span><span class="p">.</span><span class="n">new</span><span class="p">(</span><span class="n">rsa_key</span><span class="p">)</span>
    <span class="n">encrypted_key</span> <span class="o">=</span> <span class="n">rsa_cipher</span><span class="p">.</span><span class="n">encrypt</span><span class="p">(</span><span class="n">aes_key</span><span class="p">)</span>

    <span class="n">iv_b64</span> <span class="o">=</span> <span class="n">base64</span><span class="p">.</span><span class="n">b64encode</span><span class="p">(</span><span class="n">iv</span><span class="p">).</span><span class="n">decode</span><span class="p">()</span>
    <span class="n">tag_b64</span> <span class="o">=</span> <span class="n">base64</span><span class="p">.</span><span class="n">b64encode</span><span class="p">(</span><span class="n">tag</span><span class="p">).</span><span class="n">decode</span><span class="p">()</span>
    <span class="n">key_b64</span> <span class="o">=</span> <span class="n">base64</span><span class="p">.</span><span class="n">b64encode</span><span class="p">(</span><span class="n">encrypted_key</span><span class="p">).</span><span class="n">decode</span><span class="p">()</span>
    <span class="n">cipher_b64</span> <span class="o">=</span> <span class="n">base64</span><span class="p">.</span><span class="n">b64encode</span><span class="p">(</span><span class="n">ciphertext</span><span class="p">).</span><span class="n">decode</span><span class="p">()</span>

    <span class="n">result</span> <span class="o">=</span> <span class="sa">f</span><span class="s">"</span><span class="si">{</span><span class="n">iv_b64</span><span class="si">}{</span><span class="n">tag_b64</span><span class="si">}{</span><span class="n">key_b64</span><span class="si">}{</span><span class="n">cipher_b64</span><span class="si">}</span><span class="s">"</span>

    <span class="k">return</span> <span class="n">result</span>
</code></pre></div></div>

<p>这也就意味着 AES 的密钥是经过 RSA 加密后的，也就无法直接从 <code class="language-plaintext highlighter-rouge">c</code> 字段里拿到的数据直接base64解码拿到密钥明文。如果对设备指纹明文感兴趣的话，可以在  <code class="language-plaintext highlighter-rouge">api.js</code> 文件 (https://client-api.arkoselabs.com/v2/{替换为为对应的public-key}/api.js) 里搜索 <code class="language-plaintext highlighter-rouge">bda</code> 关键词，找到位于<code class="language-plaintext highlighter-rouge">case 21:</code> 的位置，往上找到 <code class="language-plaintext highlighter-rouge">case 9:</code> 位置的 <code class="language-plaintext highlighter-rouge">Cn(r, Fo, Do.publicKey, si);</code>  即该分支的最后一个函数调用，第一个 参数 <code class="language-plaintext highlighter-rouge">r</code> 即为设备指纹原文数组，如下图所示：</p>

<p><img src="https://pub-df7ca5ef070b4d47a2a7c8b98941cb71.r2.dev/fp-content.png" alt="环境指纹明文数组查找" /></p>

<p>至于里面具体的设备指纹都涉及哪些，我就不细细展开了，可以参考这个网站：https://azureflow.github.io/arkose-fp-docs/arkose_re_docs.html ，里面有详细的每个参数的介绍和构造原理。还需要注意的一点是请求时候的请求指纹——JA3、Akamai指纹，这也是风控中很重要的一环！</p>

<p><strong>注意！！！这一步的请求极其重要，携带的设备指纹，以及请求的IP、指纹，将直接决定后续是否需要进行POW挑战，以及验证码的难度（题目数据、题目难度）！</strong></p>

<h3 id="2-获取刷新验证码详情">2、 获取/刷新验证码详情</h3>

<p><code class="language-plaintext highlighter-rouge">POST https://client-api.arkoselabs.com/fc/gfct/</code></p>

<p>这一步没有涉及任何复杂的加密，仅仅是一个时间戳需要简单构造一下，相关请求的参数如下：</p>

<ul>
  <li>
    <p>请求头， x-newrelic-timestamp（实测不携带也不会有影响）</p>
  </li>
  <li>可变参数，token（第一步response有返回）、sid（第一步response有返回）</li>
  <li>固定参数，lang、render_type、isAudioGame、is_compatibility_mode、apiBreakerVersion、analytics_tier，都是固定值，保持和网页端同步即可</li>
</ul>

<p>请求头参数 <code class="language-plaintext highlighter-rouge">x-newrelic-timestamp</code> 直接参考以下 Python 代码即可，拿走不谢~</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="k">def</span> <span class="nf">generate_timestamp_string</span><span class="p">():</span>
      <span class="n">timestamp_ms</span> <span class="o">=</span> <span class="nb">int</span><span class="p">(</span><span class="n">datetime</span><span class="p">.</span><span class="n">now</span><span class="p">().</span><span class="n">timestamp</span><span class="p">()</span> <span class="o">*</span> <span class="mi">1000</span><span class="p">)</span>
      <span class="n">timestamp_str</span> <span class="o">=</span> <span class="nb">str</span><span class="p">(</span><span class="n">timestamp_ms</span><span class="p">)</span>

      <span class="n">x</span> <span class="o">=</span> <span class="n">timestamp_str</span><span class="p">[:</span><span class="mi">7</span><span class="p">]</span>
      <span class="n">l</span> <span class="o">=</span> <span class="n">timestamp_str</span><span class="p">[</span><span class="mi">7</span><span class="p">:</span><span class="mi">13</span><span class="p">]</span>

      <span class="k">return</span> <span class="sa">f</span><span class="s">"</span><span class="si">{</span><span class="n">x</span><span class="si">}</span><span class="s">00</span><span class="si">{</span><span class="n">l</span><span class="si">}</span><span class="s">"</span>
</code></pre></div></div>

<h3 id="3提交验证码详情">3、提交验证码详情</h3>

<p><code class="language-plaintext highlighter-rouge">POST https://client-api.arkoselabs.com/fc/ca/</code></p>

<p>又一个重头戏来咯！需要注意的请求参数：</p>

<ul>
  <li>请求头， x-newrelic-timestamp（相关构造请参考步骤2）、x-requested-id（需构造加密）</li>
  <li>固定参数：render_type、analytics_tier、is_compatibility_mode</li>
  <li>可变参数：session_token（请求1中返回）、game_token（请求2中的）、guess（与回答验证码答案有关，需构造加密）、tguess（同guess，但参数不一样）、bio（Base64解码可见，与用户交互相关）</li>
</ul>

<p>其实这个请求所涉及的加密都使用的标准的 <strong>AES-CBC</strong>，但里面的细节会有点说法，总的加密流程如下，分为三个关键步骤:</p>

<p><strong>a.密钥派生 (EVP_BytesToKey)</strong></p>

<ul>
  <li><strong>输入</strong>： session token(字符串) + 随机盐(8字节)</li>
  <li><strong>过程</strong>：使用 OpenSSL 的 EVP_BytesToKey 算法，通过多轮 MD5 哈希迭代生成足够长度的材料</li>
  <li><strong>输出</strong>: 32字节密钥 + 16字节初始化向量(IV)</li>
</ul>

<p><strong>b. 数据填充与加密</strong></p>

<ul>
  <li><strong>填充</strong>: 使用 PKCS7 标准，将明文补齐到16字节的整数倍</li>
  <li><strong>加密</strong>: AES-256-CBC 模式，使用派生的密钥和IV进行加密</li>
</ul>

<p><strong>c. 结果封装</strong></p>

<p>返回 CryptoJS 标准格式的 JSON:</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"ct"</span><span class="p">:</span><span class="w"> </span><span class="s2">"密文的Base64编码"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"iv"</span><span class="p">:</span><span class="w"> </span><span class="s2">"初始化向量的16进制"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"s"</span><span class="p">:</span><span class="w"> </span><span class="s2">"盐值的16进制"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p><strong>注：也就意味着每次加密都生成新的随机盐，即使相同明文和密码，密文也不同</strong></p>

<p>我知道看的很头大，直接参考下面的 Python 程序即可：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code># pip install pycryptodome

import base64
import json

from Crypto.Cipher import AES
from Crypto.Random import get_random_bytes
from Crypto.Hash import MD5

def evp_bytes_to_key(token, salt, key_len=32, iv_len=16):
    """
    CryptoJS的OpenSSL兼容密钥派生 (EVP_BytesToKey)
    """
    m = []
    i = 0
    while len(b''.join(m)) &lt; (key_len + iv_len):
        md5 = MD5.new()
        data = token.encode() + salt
        if i &gt; 0:
            data = m[i - 1] + data
        md5.update(data)
        m.append(md5.digest())
        i += 1

    ms = b''.join(m)
    return ms[:key_len], ms[key_len:key_len + iv_len]


def encrypt(plaintext: str, token: str):
    """
    模拟CryptoJS的AES.encrypt
    """
    # 生成随机salt (8字节)
    salt = get_random_bytes(8)

    # 派生key和iv
    key, iv = evp_bytes_to_key(token, salt)

    # AES-256-CBC加密
    cipher = AES.new(key, AES.MODE_CBC, iv)

    # Pkcs7填充
    pad_len = 16 - len(plaintext) % 16
    padded = plaintext.encode() + bytes([pad_len] * pad_len)

    # 加密
    ciphertext = cipher.encrypt(padded)

    # 返回CryptoJS格式
    return json.dumps({
        "ct": base64.b64encode(ciphertext).decode(),
        "iv": iv.hex(),
        "s": salt.hex()
    })


def decrypt(encrypted_json: str, token: str):
    """
    模拟CryptoJS的AES.decrypt
    """
    data = json.loads(encrypted_json)

    # 解析参数
    ciphertext = base64.b64decode(data['ct'])
    iv = bytes.fromhex(data['iv'])
    salt = bytes.fromhex(data['s'])

    # 派生key和iv
    key, _ = evp_bytes_to_key(token, salt)

    # AES-256-CBC解密
    cipher = AES.new(key, AES.MODE_CBC, iv)
    plaintext = cipher.decrypt(ciphertext)

    # 去除Pkcs7填充
    pad_len = plaintext[-1]
    return plaintext[:-pad_len].decode()
</code></pre></div></div>

<p>关于 <code class="language-plaintext highlighter-rouge">guess</code> 和 <code class="language-plaintext highlighter-rouge">tguess</code> ，逻辑都是一样的，<code class="language-plaintext highlighter-rouge">token</code> 参数都是对应的session token（请求1中获取的结果），就可以对请求参数进行解密了，guess对应的明文为 <code class="language-plaintext highlighter-rouge">[{"index":0},{"index":2}]</code>  1和2为对应每轮验证码的图片答案下标。至于  <code class="language-plaintext highlighter-rouge">tguess</code> 结构和 <code class="language-plaintext highlighter-rouge">guess</code> 一致，但是里面的参数没有这么标准，每次验证请求的js文件不一样，导致 <code class="language-plaintext highlighter-rouge">tguess</code> 参数构造的明文也不一样，具体可以多解密几个请求中的 <code class="language-plaintext highlighter-rouge">tguess</code> 参数看看。</p>

<p>至于请求头中的 <code class="language-plaintext highlighter-rouge">x-requested-id</code>  ，使用的加密手段和 <code class="language-plaintext highlighter-rouge">guess</code>、<code class="language-plaintext highlighter-rouge">tguess</code> 一致，只不过密钥（Python参考代码中 token 参数）不再是单纯的 session token，而是 <code class="language-plaintext highlighter-rouge">REQUESTED{token}ID</code> 这样的拼接结果，同样，和 <code class="language-plaintext highlighter-rouge">tguess</code> 一样，每次验证的时候明文都会不一样，类似 <code class="language-plaintext highlighter-rouge">{"sc":[376,345]}</code> 这样的明文。</p>

<p>至于整个流程，及相关的所有参数，到这就介绍完了！容我先上个厕所，接下来，我们再讲一讲里面风控需要续哟注意的点。</p>

<h2 id="三关于风控需注意的一些点">三、关于风控需注意的一些点</h2>

<p>这里所提及的风控，其实主要是针对第一步请求中获取session token至关重要，决定了你接下来的每一步是否能够正常走完流程，完成验证，而不是每一步都惨遭  <code class="language-plaintext highlighter-rouge">{"error":"DENIED ACCESS"}</code> 无情拒绝！</p>

<p>这里只是浅浅地提一下，因为能关注到这一步的，也无须我多言了，在这我抛砖引玉一下</p>

<ul>
  <li>
    <p>构造真实的设备指纹，相关的具体每个指纹细节可参考 https://azureflow.github.io/arkose-fp-docs/arkose_re_docs.html （如上文所述）</p>
  </li>
  <li>所有请求的指纹与设备指纹保持一致，所有UA都保持一致</li>
  <li>千万不要忽略 TLS指纹！使用真实的请求指纹，以 Akamai 指纹为标准，具体的指纹生成可参考 https://tls.browserleaks.com/json 在线查看</li>
  <li>尽可能模拟自己是一个真实用户，细心观察网站里面收集的交互信息和日志上传时机及数据，耐心试探风控能够忍耐的下限，或只和其相关的关键接口交互，不浪费任何一个请求资源</li>
</ul>

<p>注：我知道你肯定会有疑惑，那么真实的设备指纹和请求指纹哪来呢，我只能说：<strong>仁者见仁，智者见智</strong>。</p>

<h2 id="四成果展示">四、成果展示</h2>

<p>OK！让我们进入最令人血脉喷张的时刻！噼里啪啦说这么一堆，要是实际检验一下发现没用，那不纯纯浪费我们各自的时间，不仅是你的，还有我的，纯纯就是浪费！</p>

<p>好了，不发表感言废话了，让我们直接连贯开始（我这里就直接手动打码，后续可以接一个自动化打码，这里推荐我自用的 <a href="https://yescaptcha.com/i/HL9j4r">YesCaptcha</a>， 我发誓这不是广子，虽然我真的希望是，如果品牌方看到，希望你能懂这是个人情社会 bushi…对不起我又废话了）！开始潜入！看图即可</p>

<p><img src="https://pub-df7ca5ef070b4d47a2a7c8b98941cb71.r2.dev/verify.png" alt="实战成果截图" /></p>

<p>完结，散会！</p>

<blockquote>
  <p><strong>版权与免责声明</strong></p>
  <ul>
    <li>作者: White · © 2025</li>
    <li>本文仅供学习研究，禁止用于非法用途。转载须注明作者及原文链接。</li>
    <li>代码部分采用 MIT 许可证 (https://opensource.org/licenses/MIT)，文字部分保留所有权利</li>
    <li>作者对任何阅读、转载、使用本文内容所产生的后果不承担责任。</li>
  </ul>
</blockquote>]]></content><author><name>White</name></author><category term="验证码" /><category term="逆向" /><category term="验证码" /><category term="逆向" /><category term="ArkoseLabs" /><category term="FunCaptcha" /><category term="协议分析" /><summary type="html"><![CDATA[深入解析 ArkoseLabs FunCaptcha 验证码的完整请求链路、AES/RSA 加密机制、设备指纹构造与风控策略，附完整协议逆向实战教程。]]></summary></entry><entry><title type="html">有手就行系列——抖音最新bdms_1.0.1.19_fix参数构造a_bogus</title><link href="https://haloowhite.com/2025/08/18/dy-vmp-tutorial/" rel="alternate" type="text/html" title="有手就行系列——抖音最新bdms_1.0.1.19_fix参数构造a_bogus" /><published>2025-08-18T00:00:00+08:00</published><updated>2025-08-18T00:00:00+08:00</updated><id>https://haloowhite.com/2025/08/18/dy-vmp-tutorial</id><content type="html" xml:base="https://haloowhite.com/2025/08/18/dy-vmp-tutorial/"><![CDATA[<h2 id="0背景介绍">0、背景介绍</h2>

<p>本文将简单直接地带你一起通过补环境的方式，实现某音最新的a_bogus参数构造，并实现验证请求返回对应数据。</p>

<p>目标参数使用JSVMP 技术来实现关键的加密逻辑混淆。关于JSVMP，简单来说，就是用js在前端实现了一个栈式虚拟机，通过js实现相关的原子操作（类似汇编里的汇编机器语句）。相关的更详细的资料可参考这篇论文 <a href="https://pub-df7ca5ef070b4d47a2a7c8b98941cb71.r2.dev/Research%20and%20Implementation%20of%20JavaScript%20Code%20Virtualization%20Protection%20Method%20Based%20on%20WebAssembly.pdf">《基于 WebAssembly 的 JavaScript 代码虚拟化保护方法研究与实现》</a> 。</p>

<p><img src="https://pub-df7ca5ef070b4d47a2a7c8b98941cb71.r2.dev/Research%20and%20Implementation%20of%20JavaScript%20Code%20Virtualization%20Protection%20Method%20Based%20on%20WebAssembly%20.png" alt="《基于 WebAssembly 的 JavaScript 代码虚拟化保护方法研究与实现》论文封面" /></p>

<p>这类JSVMP的特征为，在源码中会有一个又臭又长的字符串和一个又臭又长的函数，里面是又臭又长的循环switch结构，其本质是环境初始化的字节码和对应的解释器。</p>

<p>与之类似采用JSVMP手段的，还有知乎的<code class="language-plaintext highlighter-rouge">x-zse-96</code> 参数、腾讯滑块、快手sig3等</p>

<h2 id="1大致流程">1、大致流程</h2>

<p>一般的JSVMP也好，高强度混淆也好，只要补环境补好了，都可以直接无视相关的内部的执行或实现逻辑细节。只需要把js运行中缺失的环境或源码中进行检测的浏览器相关环境给补全即可，然后再添加一个对应的加密函数构造入口，实现能够构造对应参数。</p>

<p>本文面对的抖音JSVMP可能难度会更高一些，等你真正实际上手的时候，发现几乎所有的逻辑都在走 <code class="language-plaintext highlighter-rouge">return X(e, this, arguments, r)</code> ，以及解释器函数 <code class="language-plaintext highlighter-rouge">function d()</code> 中不断循环。一般有几种解决方法，一是在 <code class="language-plaintext highlighter-rouge">d函数</code> 中将关键的函数调用、运算逻辑，以及堆栈变化等插桩打日志，然后根据打印的日志进行分析相关的逻辑和参数构造；还有一个是反编译，将关键的函数执行等分析出来再复现；再有就是，我们本文用的补环境大法，实现补齐代码中用到的浏览器环境，并构造相关的加密入口实现参数生成。</p>

<h2 id="2补环境细节">2、补环境细节</h2>

<p>我这里就直接给出需要补的环境的细节，直接按下面的参数补即可。</p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">/// 安装依赖</span>
<span class="c1">// bun add xhr2</span>

<span class="c1">// 基础全局对象设置</span>
<span class="nb">global</span><span class="p">.</span><span class="nx">XMLHttpRequest</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="dl">'</span><span class="s1">xhr2</span><span class="dl">'</span><span class="p">);</span>

<span class="c1">// Window对象（避免循环引用）</span>
<span class="kd">const</span> <span class="nb">window</span> <span class="o">=</span> <span class="p">{</span>
  <span class="na">onwheelx</span><span class="p">:</span> <span class="p">{</span><span class="na">_Ax</span><span class="p">:</span> <span class="dl">'</span><span class="s1">0X21</span><span class="dl">'</span><span class="p">},</span>
  <span class="na">innerHeight</span><span class="p">:</span> <span class="mi">1547</span><span class="p">,</span>
  <span class="na">innerWidth</span><span class="p">:</span> <span class="mi">1917</span><span class="p">,</span>
  <span class="na">outerWidth</span><span class="p">:</span> <span class="mi">3200</span><span class="p">,</span>
  <span class="na">outerHeight</span><span class="p">:</span> <span class="mi">1668</span><span class="p">,</span>
  <span class="na">requestAnimationFrame</span><span class="p">:</span> <span class="kd">function</span><span class="p">()</span> <span class="p">{},</span>
  <span class="na">addEventListener</span><span class="p">:</span> <span class="kd">function</span><span class="p">()</span> <span class="p">{},</span>
  <span class="na">screen</span><span class="p">:</span> <span class="p">{</span>
    <span class="na">availHeight</span><span class="p">:</span> <span class="mi">1668</span><span class="p">,</span>
    <span class="na">availLeft</span><span class="p">:</span> <span class="mi">0</span><span class="p">,</span>
    <span class="na">availTop</span><span class="p">:</span> <span class="mi">25</span><span class="p">,</span>
    <span class="na">availWidth</span><span class="p">:</span> <span class="mi">3200</span><span class="p">,</span>
    <span class="na">colorDepth</span><span class="p">:</span> <span class="mi">24</span><span class="p">,</span>
    <span class="na">height</span><span class="p">:</span> <span class="mi">1800</span><span class="p">,</span>
    <span class="na">isExtended</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
    <span class="na">orientation</span><span class="p">:</span> <span class="p">{</span>
      <span class="na">angle</span><span class="p">:</span> <span class="mi">0</span><span class="p">,</span>
      <span class="na">onchange</span><span class="p">:</span> <span class="kc">null</span><span class="p">,</span>
      <span class="na">type</span><span class="p">:</span> <span class="dl">"</span><span class="s2">landscape-primary</span><span class="dl">"</span><span class="p">,</span>
    <span class="p">},</span>
    <span class="na">pixelDepth</span><span class="p">:</span> <span class="mi">24</span><span class="p">,</span>
    <span class="na">width</span><span class="p">:</span> <span class="mi">3200</span>
  <span class="p">}</span>
<span class="p">};</span>

<span class="c1">// 设置parent引用</span>
<span class="nb">window</span><span class="p">.</span><span class="nx">parent</span> <span class="o">=</span> <span class="nb">window</span><span class="p">;</span>

<span class="c1">// Location对象</span>
<span class="kd">const</span> <span class="nx">location</span> <span class="o">=</span> <span class="p">{</span>
  <span class="na">href</span><span class="p">:</span> <span class="dl">"</span><span class="s2">https://www.douyin.com/jingxuan</span><span class="dl">"</span>
<span class="p">};</span>

<span class="c1">// Document对象</span>
<span class="kd">const</span> <span class="nb">document</span> <span class="o">=</span> <span class="p">{</span>
  <span class="na">all</span><span class="p">:</span> <span class="p">{},</span>
  <span class="na">createElement</span><span class="p">:</span> <span class="kd">function</span><span class="p">(</span><span class="nx">tag_name</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">return</span> <span class="p">{</span>
      <span class="na">classList</span><span class="p">:</span> <span class="p">{}</span>
    <span class="p">};</span>
  <span class="p">},</span>
  <span class="na">documentElement</span><span class="p">:</span> <span class="p">{},</span>
  <span class="na">createEvent</span><span class="p">:</span> <span class="kd">function</span><span class="p">()</span> <span class="p">{</span> <span class="k">return</span> <span class="p">{};</span> <span class="p">},</span>
  <span class="na">addEventListener</span><span class="p">:</span> <span class="kd">function</span><span class="p">()</span> <span class="p">{}</span>
<span class="p">};</span>

<span class="c1">// Navigator对象</span>
<span class="kd">const</span> <span class="nb">navigator</span> <span class="o">=</span> <span class="p">{</span>
  <span class="na">userAgent</span><span class="p">:</span> <span class="dl">'</span><span class="s1">Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36</span><span class="dl">'</span>
<span class="p">};</span>

<span class="c1">// LocalStorage对象</span>
<span class="kd">const</span> <span class="nx">localStorage</span> <span class="o">=</span> <span class="p">{</span>
  <span class="na">getItem</span><span class="p">:</span> <span class="kd">function</span><span class="p">(</span><span class="nx">key</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">if</span> <span class="p">(</span><span class="nx">key</span> <span class="o">===</span> <span class="dl">"</span><span class="s2">xmst</span><span class="dl">"</span><span class="p">)</span> <span class="p">{</span>
      <span class="k">return</span> <span class="dl">'</span><span class="s1">放置你自己的xmst值</span><span class="dl">'</span><span class="p">;</span>
    <span class="p">}</span>
    <span class="k">return</span> <span class="kc">null</span><span class="p">;</span>
  <span class="p">}</span>
<span class="p">};</span>

<span class="c1">// Screen对象</span>
<span class="kd">const</span> <span class="nx">screen</span> <span class="o">=</span> <span class="p">{};</span>

<span class="c1">// 全局引用设置</span>
<span class="nx">globalThis</span><span class="p">.</span><span class="nb">window</span> <span class="o">=</span> <span class="nb">window</span><span class="p">;</span>
<span class="nx">globalThis</span><span class="p">.</span><span class="nx">location</span> <span class="o">=</span> <span class="nx">location</span><span class="p">;</span>
<span class="nx">globalThis</span><span class="p">.</span><span class="nb">document</span> <span class="o">=</span> <span class="nb">document</span><span class="p">;</span>
<span class="nx">globalThis</span><span class="p">.</span><span class="nb">navigator</span> <span class="o">=</span> <span class="nb">navigator</span><span class="p">;</span>
<span class="nx">globalThis</span><span class="p">.</span><span class="nx">localStorage</span> <span class="o">=</span> <span class="nx">localStorage</span><span class="p">;</span>
<span class="nx">globalThis</span><span class="p">.</span><span class="nx">screen</span> <span class="o">=</span> <span class="nx">screen</span><span class="p">;</span>

<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="dl">"</span><span class="s2">环境初始化完成</span><span class="dl">"</span><span class="p">);</span>
</code></pre></div></div>

<p>先把 <code class="language-plaintext highlighter-rouge">/* V 1.0.1.19-fix.01 */</code> 整个源码copy下来放在本地，将上面这段环境初始化放在复制的源码最前面，这样就能够过相关的环境监测（后续有机会可以专门讲下相关的补环境细节和流程）。</p>

<p>接下来，我们现在来好好解释一下参数的构造入口以及需要注意的点。有一个思路的关键点还是在于<code class="language-plaintext highlighter-rouge">X(e, this, arguments, r)</code>，原理就是，我们需要断在真正参数构造的时候，根据相关的几个参数<code class="language-plaintext highlighter-rouge">e</code>、<code class="language-plaintext highlighter-rouge">argument</code>、<code class="language-plaintext highlighter-rouge">this</code>、<code class="language-plaintext highlighter-rouge">r</code> 这几个参数的特征，然后通过判断条件构造加密入口，例如，我这里直接给出关键源码，就能够构造将加密方法暴露在<code class="language-plaintext highlighter-rouge">window.encrypt</code>中，直接调用，传入对应的参数即可得出目标参数<code class="language-plaintext highlighter-rouge">a_bogus</code> 。</p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">var</span> <span class="nx">n</span> <span class="o">=</span> <span class="kd">function</span> <span class="p">()</span> <span class="p">{</span>
  <span class="c1">// 通过特征值判断是否为目标加密函数</span>
  <span class="k">if</span> <span class="p">(</span>
    <span class="nx">JSON</span><span class="p">.</span><span class="nx">stringify</span><span class="p">(</span><span class="nx">e</span><span class="p">[</span><span class="mi">0</span><span class="p">])</span> <span class="o">===</span>
    <span class="dl">"</span><span class="s2">[34,54,0,3,34,30,214,41,212,34,30,214,30,70,54,0,4,74,0,4,30,218,54,0,5,74,0,5,30,72,54,0,6,33,74,2,33,74,0,6,0,1,54,0,7,74,0,7,41,5,74,0,6,53,11,60,161,74,0,6,60,216,30,178,59,2,54,0,8,74,0,8,30,162,18,30,219,73,165,0,1,29,17,5,74,2,3,30,150,41,18,74,0,8,30,162,18,30,163,73,165,74,2,3,30,150,0,2,26,74,0,8,30,162,18,30,219,73,220,0,1,29,41,45,33,74,3,14,0,0,26,33,74,2,37,74,0,8,30,162,18,30,9,0,0,74,0,2,0,2,54,0,9,74,0,8,30,162,18,30,163,73,220,74,0,9,0,2,26,74,0,7,29,41,10,74,0,5,74,0,8,30,178,20,72,34,30,214,18,30,51,63,108,0,1,26,33,74,2,36,74,0,8,30,215,0,1,41,7,33,74,2,5,0,0,26,34,73,214,25,26,74,1,4,18,30,126,34,74,0,2,39,1,0,2,26,33,76]</span><span class="dl">"</span>
  <span class="p">)</span> <span class="p">{</span>
    <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="dl">"</span><span class="s2">找到加密函数入口!</span><span class="dl">"</span><span class="p">);</span>
    <span class="nb">window</span><span class="p">.</span><span class="nx">encrypt</span> <span class="o">=</span> <span class="nx">n</span><span class="p">;</span>
  <span class="p">}</span>
  <span class="k">return</span> <span class="nx">X</span><span class="p">(</span><span class="nx">e</span><span class="p">,</span> <span class="k">this</span><span class="p">,</span> <span class="nx">arguments</span><span class="p">,</span> <span class="nx">r</span><span class="p">);</span>
<span class="p">};</span>

</code></pre></div></div>

<p>这里，我们还可以用另一种方法，确实是在抖音里这比较取巧的方式（因地适宜），构造XMLHttpRequest对象，并且通过send函数触发相应的参数构造，然后在合适的地方进行截取，将生成的加密参数给存在全局变量里。关键位置在下面，我就直接给出对应的关键结果了，原理还是和上面一样。</p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">var</span> <span class="nx">m</span> <span class="o">=</span> <span class="nx">n</span><span class="p">.</span><span class="nx">apply</span><span class="p">(</span><span class="nx">d</span><span class="p">,</span> <span class="nx">e</span><span class="p">);</span> <span class="c1">// 原始代码</span>
<span class="c1">// 以下是我们添加的截取代码</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">e</span><span class="p">.</span><span class="nx">length</span> <span class="o">==</span> <span class="mi">2</span> <span class="o">&amp;&amp;</span> <span class="nx">e</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="o">===</span> <span class="dl">"</span><span class="s2">a_bogus</span><span class="dl">"</span><span class="p">)</span> <span class="p">{</span>
  <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="dl">"</span><span class="s2">got e !!!</span><span class="dl">"</span><span class="p">);</span>
  <span class="nb">window</span><span class="p">.</span><span class="nx">a_bogus</span> <span class="o">=</span> <span class="nx">e</span><span class="p">[</span><span class="mi">1</span><span class="p">];</span>
  <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="dl">"</span><span class="s2">window.a_bogus</span><span class="dl">"</span><span class="p">,</span> <span class="nb">window</span><span class="p">.</span><span class="nx">a_bogus</span><span class="p">);</span>
  <span class="nx">process</span><span class="p">.</span><span class="nx">exit</span><span class="p">(</span><span class="mi">0</span><span class="p">);</span> <span class="c1">// 退出进程</span>
<span class="p">}</span>
<span class="nx">v</span><span class="p">[</span><span class="o">++</span><span class="nx">p</span><span class="p">]</span> <span class="o">=</span> <span class="nx">m</span><span class="p">;</span> <span class="c1">// 原始代码</span>
</code></pre></div></div>

<p>这里有个细节，在获取到参数后，执行了<code class="language-plaintext highlighter-rouge">process.exit(0);</code> ，不然程序会继续执行然后报错，我只需要将截取到的参数拿出来即可，形式不重要，目的才是关键！</p>

<h2 id="3验证参数参数有效性">3、验证参数参数有效性</h2>

<p>有了上述完整的参数生成逻辑，就能够通过根据相关的请求信息生成目标加密参数了。我这里拿热搜接口作为测试，当然你也可以使用其他的接口进行测试。</p>

<p><img src="https://pub-df7ca5ef070b4d47a2a7c8b98941cb71.r2.dev/blog-dy-%20verify.png" alt="验证成功截图" /></p>

<p>如图所示，不仅生成参数成功，还返回了相关的数据，说明我们构造的参数是合理可用的。有了参数后，可能后续需要注意的就是相关的风控，比如设备、环境信息、还有IP、msToken等。不过只要有足够耐心，并细心，总能初探成果，祝你顺利！</p>

<h2 id="-题外话">:) 题外话</h2>

<p>等之后有时间精力了，我再写一篇详细讲述一下抖音a_bogus生成中用到的魔改rc4、sm3、魔改base64算法，最主要的还是中间的数组校验位。
先挖一个坑，等有空了再更新吧，如果你遇到相关的爬虫、自动化或开发相关的疑难杂症，也欢迎联系我。</p>]]></content><author><name>White</name></author><category term="JSVMP" /><category term="逆向" /><category term="有手就行系列" /><category term="逆向" /><category term="JSVMP" /><category term="抖音" /><category term="a_bogus" /><category term="补环境" /><summary type="html"><![CDATA[通过补环境方式实现抖音最新 bdms_1.0.1.19_fix 版本的 a_bogus 参数构造，详解 JSVMP 虚拟机原理与完整的逆向补环境实战流程。]]></summary></entry><entry><title type="html">一个用于解决JSVMP海量日志导出技巧</title><link href="https://haloowhite.com/2025/08/05/vmp-log-exporter/" rel="alternate" type="text/html" title="一个用于解决JSVMP海量日志导出技巧" /><published>2025-08-05T00:00:00+08:00</published><updated>2025-08-05T00:00:00+08:00</updated><id>https://haloowhite.com/2025/08/05/vmp-log-exporter</id><content type="html" xml:base="https://haloowhite.com/2025/08/05/vmp-log-exporter/"><![CDATA[<p>在运行之前将以下这段代码贴进console中运行，然后执行需要的逻辑，待日志完全打印，手动在console执行 <code class="language-plaintext highlighter-rouge">console.save()</code>，即可立马将所有的日志导出为本地文件</p>

<p>接下来就可以安安心心在本地分析日志内容了</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="nx">VmpLogExporter</span> <span class="p">{</span>
    <span class="kd">constructor</span><span class="p">(</span><span class="nx">config</span> <span class="o">=</span> <span class="p">{})</span> <span class="p">{</span>
        <span class="k">this</span><span class="p">.</span><span class="nx">config</span> <span class="o">=</span> <span class="p">{</span>
            <span class="na">maxConsoleLines</span><span class="p">:</span> <span class="nx">config</span><span class="p">.</span><span class="nx">maxConsoleLines</span> <span class="o">||</span> <span class="mi">800</span><span class="p">,</span>
            <span class="na">exportBatchSize</span><span class="p">:</span> <span class="nx">config</span><span class="p">.</span><span class="nx">exportBatchSize</span> <span class="o">||</span> <span class="mi">500</span><span class="p">,</span>
            <span class="na">autoExportInterval</span><span class="p">:</span> <span class="nx">config</span><span class="p">.</span><span class="nx">autoExportInterval</span> <span class="o">||</span> <span class="mi">60000</span><span class="p">,</span> <span class="c1">// 1分钟</span>
            <span class="na">enableAutoExport</span><span class="p">:</span> <span class="nx">config</span><span class="p">.</span><span class="nx">enableAutoExport</span> <span class="o">!==</span> <span class="kc">false</span><span class="p">,</span>
            <span class="na">logPrefix</span><span class="p">:</span> <span class="nx">config</span><span class="p">.</span><span class="nx">logPrefix</span> <span class="o">||</span> <span class="dl">'</span><span class="s1">VMP_LOG</span><span class="dl">'</span><span class="p">,</span>
            <span class="na">keepInConsole</span><span class="p">:</span> <span class="nx">config</span><span class="p">.</span><span class="nx">keepInConsole</span> <span class="o">||</span> <span class="mi">100</span> <span class="c1">// 控制台保留行数</span>
        <span class="p">};</span>
        
        <span class="k">this</span><span class="p">.</span><span class="nx">logBuffer</span> <span class="o">=</span> <span class="p">[];</span>
        <span class="k">this</span><span class="p">.</span><span class="nx">fileCounter</span> <span class="o">=</span> <span class="mi">1</span><span class="p">;</span>
        <span class="k">this</span><span class="p">.</span><span class="nx">consoleLineCount</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
        <span class="k">this</span><span class="p">.</span><span class="nx">isExporting</span> <span class="o">=</span> <span class="kc">false</span><span class="p">;</span>
        
        <span class="k">this</span><span class="p">.</span><span class="nx">init</span><span class="p">();</span>
    <span class="p">}</span>
    
    <span class="nx">init</span><span class="p">()</span> <span class="p">{</span>
        <span class="k">this</span><span class="p">.</span><span class="nx">hijackConsole</span><span class="p">();</span>
        <span class="k">if</span> <span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">config</span><span class="p">.</span><span class="nx">enableAutoExport</span><span class="p">)</span> <span class="p">{</span>
            <span class="k">this</span><span class="p">.</span><span class="nx">startAutoExport</span><span class="p">();</span>
        <span class="p">}</span>
        <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s2">`🚀 VMP日志导出系统已启动 - 将自动保存日志避免浏览器崩溃`</span><span class="p">);</span>
    <span class="p">}</span>
    
    <span class="nx">hijackConsole</span><span class="p">()</span> <span class="p">{</span>
        <span class="kd">const</span> <span class="nx">originalMethods</span> <span class="o">=</span> <span class="p">{};</span>
        <span class="p">[</span><span class="dl">'</span><span class="s1">log</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">warn</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">error</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">info</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">debug</span><span class="dl">'</span><span class="p">].</span><span class="nx">forEach</span><span class="p">(</span><span class="nx">method</span> <span class="o">=&gt;</span> <span class="p">{</span>
            <span class="nx">originalMethods</span><span class="p">[</span><span class="nx">method</span><span class="p">]</span> <span class="o">=</span> <span class="nx">console</span><span class="p">[</span><span class="nx">method</span><span class="p">];</span>
            
            <span class="nx">console</span><span class="p">[</span><span class="nx">method</span><span class="p">]</span> <span class="o">=</span> <span class="p">(...</span><span class="nx">args</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
                <span class="c1">// 记录到缓冲区</span>
                <span class="k">this</span><span class="p">.</span><span class="nx">addToBuffer</span><span class="p">(</span><span class="nx">method</span><span class="p">,</span> <span class="nx">args</span><span class="p">);</span>
                
                <span class="c1">// 检查是否需要清理console</span>
                <span class="k">this</span><span class="p">.</span><span class="nx">consoleLineCount</span><span class="o">++</span><span class="p">;</span>
                <span class="k">if</span> <span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">consoleLineCount</span> <span class="o">&gt;=</span> <span class="k">this</span><span class="p">.</span><span class="nx">config</span><span class="p">.</span><span class="nx">maxConsoleLines</span><span class="p">)</span> <span class="p">{</span>
                    <span class="k">this</span><span class="p">.</span><span class="nx">manageConsole</span><span class="p">();</span>
                <span class="p">}</span>
                
                <span class="c1">// 正常输出到console</span>
                <span class="k">return</span> <span class="nx">originalMethods</span><span class="p">[</span><span class="nx">method</span><span class="p">].</span><span class="nx">apply</span><span class="p">(</span><span class="nx">console</span><span class="p">,</span> <span class="nx">args</span><span class="p">);</span>
            <span class="p">};</span>
        <span class="p">});</span>
        
        <span class="k">this</span><span class="p">.</span><span class="nx">originalMethods</span> <span class="o">=</span> <span class="nx">originalMethods</span><span class="p">;</span>
    <span class="p">}</span>
    
    <span class="nx">addToBuffer</span><span class="p">(</span><span class="nx">level</span><span class="p">,</span> <span class="nx">args</span><span class="p">)</span> <span class="p">{</span>
        <span class="kd">const</span> <span class="nx">timestamp</span> <span class="o">=</span> <span class="k">new</span> <span class="nb">Date</span><span class="p">().</span><span class="nx">toISOString</span><span class="p">();</span>
        <span class="kd">const</span> <span class="nx">logEntry</span> <span class="o">=</span> <span class="p">{</span>
            <span class="nx">timestamp</span><span class="p">,</span>
            <span class="nx">level</span><span class="p">,</span>
            <span class="na">args</span><span class="p">:</span> <span class="k">this</span><span class="p">.</span><span class="nx">serializeArgs</span><span class="p">(</span><span class="nx">args</span><span class="p">),</span>
            <span class="na">raw</span><span class="p">:</span> <span class="nx">args</span>
        <span class="p">};</span>
        
        <span class="k">this</span><span class="p">.</span><span class="nx">logBuffer</span><span class="p">.</span><span class="nx">push</span><span class="p">(</span><span class="nx">logEntry</span><span class="p">);</span>
        
        <span class="c1">// 如果缓冲区过大，自动导出</span>
        <span class="k">if</span> <span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">logBuffer</span><span class="p">.</span><span class="nx">length</span> <span class="o">&gt;=</span> <span class="k">this</span><span class="p">.</span><span class="nx">config</span><span class="p">.</span><span class="nx">exportBatchSize</span><span class="p">)</span> <span class="p">{</span>
            <span class="k">this</span><span class="p">.</span><span class="nx">exportLogs</span><span class="p">();</span>
        <span class="p">}</span>
    <span class="p">}</span>
    
    <span class="nx">serializeArgs</span><span class="p">(</span><span class="nx">args</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">return</span> <span class="nx">args</span><span class="p">.</span><span class="nx">map</span><span class="p">(</span><span class="nx">arg</span> <span class="o">=&gt;</span> <span class="p">{</span>
            <span class="k">if</span> <span class="p">(</span><span class="k">typeof</span> <span class="nx">arg</span> <span class="o">===</span> <span class="dl">'</span><span class="s1">object</span><span class="dl">'</span> <span class="o">&amp;&amp;</span> <span class="nx">arg</span> <span class="o">!==</span> <span class="kc">null</span><span class="p">)</span> <span class="p">{</span>
                <span class="k">try</span> <span class="p">{</span>
                    <span class="k">return</span> <span class="nx">JSON</span><span class="p">.</span><span class="nx">stringify</span><span class="p">(</span><span class="nx">arg</span><span class="p">,</span> <span class="kc">null</span><span class="p">,</span> <span class="mi">2</span><span class="p">);</span>
                <span class="p">}</span> <span class="k">catch</span> <span class="p">(</span><span class="nx">e</span><span class="p">)</span> <span class="p">{</span>
                    <span class="k">return</span> <span class="dl">'</span><span class="s1">[Circular Reference Object]</span><span class="dl">'</span><span class="p">;</span>
                <span class="p">}</span>
            <span class="p">}</span>
            <span class="k">return</span> <span class="nb">String</span><span class="p">(</span><span class="nx">arg</span><span class="p">);</span>
        <span class="p">});</span>
    <span class="p">}</span>
    
    <span class="nx">manageConsole</span><span class="p">()</span> <span class="p">{</span>
        <span class="k">if</span> <span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">isExporting</span><span class="p">)</span> <span class="k">return</span><span class="p">;</span>
        
        <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s2">`📤 正在导出日志避免浏览器卡死...`</span><span class="p">);</span>
        
        <span class="c1">// 导出当前日志</span>
        <span class="k">this</span><span class="p">.</span><span class="nx">exportLogs</span><span class="p">();</span>
        
        <span class="c1">// 清理console但保留最新的一些日志</span>
        <span class="nx">console</span><span class="p">.</span><span class="nx">clear</span><span class="p">();</span>
        
        <span class="c1">// 重新显示最近的重要日志</span>
        <span class="kd">const</span> <span class="nx">recentLogs</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">logBuffer</span><span class="p">.</span><span class="nx">slice</span><span class="p">(</span><span class="o">-</span><span class="k">this</span><span class="p">.</span><span class="nx">config</span><span class="p">.</span><span class="nx">keepInConsole</span><span class="p">);</span>
        <span class="nx">recentLogs</span><span class="p">.</span><span class="nx">forEach</span><span class="p">(</span><span class="nx">log</span> <span class="o">=&gt;</span> <span class="p">{</span>
            <span class="k">this</span><span class="p">.</span><span class="nx">originalMethods</span><span class="p">[</span><span class="nx">log</span><span class="p">.</span><span class="nx">level</span><span class="p">].</span><span class="nx">apply</span><span class="p">(</span><span class="nx">console</span><span class="p">,</span> <span class="p">[</span>
                <span class="s2">`[</span><span class="p">${</span><span class="nx">log</span><span class="p">.</span><span class="nx">timestamp</span><span class="p">}</span><span class="s2">]`</span><span class="p">,</span> 
                <span class="p">...</span><span class="nx">log</span><span class="p">.</span><span class="nx">raw</span>
            <span class="p">]);</span>
        <span class="p">});</span>
        
        <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s2">`🔄 Console已清理，保留了最近</span><span class="p">${</span><span class="nx">recentLogs</span><span class="p">.</span><span class="nx">length</span><span class="p">}</span><span class="s2">条日志`</span><span class="p">);</span>
        <span class="k">this</span><span class="p">.</span><span class="nx">consoleLineCount</span> <span class="o">=</span> <span class="nx">recentLogs</span><span class="p">.</span><span class="nx">length</span><span class="p">;</span>
    <span class="p">}</span>
    
    <span class="k">async</span> <span class="nx">exportLogs</span><span class="p">()</span> <span class="p">{</span>
        <span class="k">if</span> <span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">isExporting</span> <span class="o">||</span> <span class="k">this</span><span class="p">.</span><span class="nx">logBuffer</span><span class="p">.</span><span class="nx">length</span> <span class="o">===</span> <span class="mi">0</span><span class="p">)</span> <span class="k">return</span><span class="p">;</span>
        
        <span class="k">this</span><span class="p">.</span><span class="nx">isExporting</span> <span class="o">=</span> <span class="kc">true</span><span class="p">;</span>
        
        <span class="k">try</span> <span class="p">{</span>
            <span class="kd">const</span> <span class="nx">timestamp</span> <span class="o">=</span> <span class="k">new</span> <span class="nb">Date</span><span class="p">().</span><span class="nx">toISOString</span><span class="p">().</span><span class="nx">replace</span><span class="p">(</span><span class="sr">/</span><span class="se">[</span><span class="sr">:.</span><span class="se">]</span><span class="sr">/g</span><span class="p">,</span> <span class="dl">'</span><span class="s1">-</span><span class="dl">'</span><span class="p">);</span>
            <span class="kd">const</span> <span class="nx">filename</span> <span class="o">=</span> <span class="s2">`</span><span class="p">${</span><span class="k">this</span><span class="p">.</span><span class="nx">config</span><span class="p">.</span><span class="nx">logPrefix</span><span class="p">}</span><span class="s2">_</span><span class="p">${</span><span class="nx">timestamp</span><span class="p">}</span><span class="s2">_part</span><span class="p">${</span><span class="k">this</span><span class="p">.</span><span class="nx">fileCounter</span><span class="p">}</span><span class="s2">.json`</span><span class="p">;</span>
            
            <span class="kd">const</span> <span class="nx">exportData</span> <span class="o">=</span> <span class="p">{</span>
                <span class="na">exportInfo</span><span class="p">:</span> <span class="p">{</span>
                    <span class="na">timestamp</span><span class="p">:</span> <span class="k">new</span> <span class="nb">Date</span><span class="p">().</span><span class="nx">toISOString</span><span class="p">(),</span>
                    <span class="na">partNumber</span><span class="p">:</span> <span class="k">this</span><span class="p">.</span><span class="nx">fileCounter</span><span class="p">,</span>
                    <span class="na">totalLogs</span><span class="p">:</span> <span class="k">this</span><span class="p">.</span><span class="nx">logBuffer</span><span class="p">.</span><span class="nx">length</span><span class="p">,</span>
                    <span class="na">source</span><span class="p">:</span> <span class="dl">'</span><span class="s1">VMP_Logger</span><span class="dl">'</span>
                <span class="p">},</span>
                <span class="na">logs</span><span class="p">:</span> <span class="k">this</span><span class="p">.</span><span class="nx">logBuffer</span>
            <span class="p">};</span>
            
            <span class="c1">// 创建并下载文件</span>
            <span class="kd">const</span> <span class="nx">blob</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Blob</span><span class="p">([</span><span class="nx">JSON</span><span class="p">.</span><span class="nx">stringify</span><span class="p">(</span><span class="nx">exportData</span><span class="p">,</span> <span class="kc">null</span><span class="p">,</span> <span class="mi">2</span><span class="p">)],</span> <span class="p">{</span> 
                <span class="na">type</span><span class="p">:</span> <span class="dl">'</span><span class="s1">application/json</span><span class="dl">'</span> 
            <span class="p">});</span>
            <span class="kd">const</span> <span class="nx">url</span> <span class="o">=</span> <span class="nx">URL</span><span class="p">.</span><span class="nx">createObjectURL</span><span class="p">(</span><span class="nx">blob</span><span class="p">);</span>
            
            <span class="kd">const</span> <span class="nx">a</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">createElement</span><span class="p">(</span><span class="dl">'</span><span class="s1">a</span><span class="dl">'</span><span class="p">);</span>
            <span class="nx">a</span><span class="p">.</span><span class="nx">href</span> <span class="o">=</span> <span class="nx">url</span><span class="p">;</span>
            <span class="nx">a</span><span class="p">.</span><span class="nx">download</span> <span class="o">=</span> <span class="nx">filename</span><span class="p">;</span>
            <span class="nx">a</span><span class="p">.</span><span class="nx">style</span><span class="p">.</span><span class="nx">display</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">none</span><span class="dl">'</span><span class="p">;</span>
            <span class="nb">document</span><span class="p">.</span><span class="nx">body</span><span class="p">.</span><span class="nx">appendChild</span><span class="p">(</span><span class="nx">a</span><span class="p">);</span>
            <span class="nx">a</span><span class="p">.</span><span class="nx">click</span><span class="p">();</span>
            <span class="nb">document</span><span class="p">.</span><span class="nx">body</span><span class="p">.</span><span class="nx">removeChild</span><span class="p">(</span><span class="nx">a</span><span class="p">);</span>
            <span class="nx">URL</span><span class="p">.</span><span class="nx">revokeObjectURL</span><span class="p">(</span><span class="nx">url</span><span class="p">);</span>
            
            <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s2">`✅ 已导出 </span><span class="p">${</span><span class="k">this</span><span class="p">.</span><span class="nx">logBuffer</span><span class="p">.</span><span class="nx">length</span><span class="p">}</span><span class="s2"> 条日志到文件: </span><span class="p">${</span><span class="nx">filename</span><span class="p">}</span><span class="s2">`</span><span class="p">);</span>
            
            <span class="c1">// 清空缓冲区</span>
            <span class="k">this</span><span class="p">.</span><span class="nx">logBuffer</span> <span class="o">=</span> <span class="p">[];</span>
            <span class="k">this</span><span class="p">.</span><span class="nx">fileCounter</span><span class="o">++</span><span class="p">;</span>
            
        <span class="p">}</span> <span class="k">catch</span> <span class="p">(</span><span class="nx">error</span><span class="p">)</span> <span class="p">{</span>
            <span class="nx">console</span><span class="p">.</span><span class="nx">error</span><span class="p">(</span><span class="dl">'</span><span class="s1">❌ 导出日志失败:</span><span class="dl">'</span><span class="p">,</span> <span class="nx">error</span><span class="p">);</span>
        <span class="p">}</span> <span class="k">finally</span> <span class="p">{</span>
            <span class="k">this</span><span class="p">.</span><span class="nx">isExporting</span> <span class="o">=</span> <span class="kc">false</span><span class="p">;</span>
        <span class="p">}</span>
    <span class="p">}</span>
    
    <span class="nx">startAutoExport</span><span class="p">()</span> <span class="p">{</span>
        <span class="nx">setInterval</span><span class="p">(()</span> <span class="o">=&gt;</span> <span class="p">{</span>
            <span class="k">if</span> <span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">logBuffer</span><span class="p">.</span><span class="nx">length</span> <span class="o">&gt;</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
                <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s2">`⏰ 定时导出日志: </span><span class="p">${</span><span class="k">this</span><span class="p">.</span><span class="nx">logBuffer</span><span class="p">.</span><span class="nx">length</span><span class="p">}</span><span class="s2"> 条`</span><span class="p">);</span>
                <span class="k">this</span><span class="p">.</span><span class="nx">exportLogs</span><span class="p">();</span>
            <span class="p">}</span>
        <span class="p">},</span> <span class="k">this</span><span class="p">.</span><span class="nx">config</span><span class="p">.</span><span class="nx">autoExportInterval</span><span class="p">);</span>
    <span class="p">}</span>
    
    <span class="c1">// 手动导出当前缓冲区</span>
    <span class="nx">manualExport</span><span class="p">()</span> <span class="p">{</span>
        <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="dl">'</span><span class="s1">🖱️ 手动导出日志...</span><span class="dl">'</span><span class="p">);</span>
        <span class="k">this</span><span class="p">.</span><span class="nx">exportLogs</span><span class="p">();</span>
    <span class="p">}</span>
    
    <span class="c1">// 导出所有日志的合并版本</span>
    <span class="nx">exportAllLogs</span><span class="p">()</span> <span class="p">{</span>
        <span class="kd">const</span> <span class="nx">timestamp</span> <span class="o">=</span> <span class="k">new</span> <span class="nb">Date</span><span class="p">().</span><span class="nx">toISOString</span><span class="p">().</span><span class="nx">replace</span><span class="p">(</span><span class="sr">/</span><span class="se">[</span><span class="sr">:.</span><span class="se">]</span><span class="sr">/g</span><span class="p">,</span> <span class="dl">'</span><span class="s1">-</span><span class="dl">'</span><span class="p">);</span>
        <span class="kd">const</span> <span class="nx">filename</span> <span class="o">=</span> <span class="s2">`</span><span class="p">${</span><span class="k">this</span><span class="p">.</span><span class="nx">config</span><span class="p">.</span><span class="nx">logPrefix</span><span class="p">}</span><span class="s2">_COMPLETE_</span><span class="p">${</span><span class="nx">timestamp</span><span class="p">}</span><span class="s2">.txt`</span><span class="p">;</span>
        
        <span class="kd">const</span> <span class="nx">allLogs</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">logBuffer</span><span class="p">.</span><span class="nx">map</span><span class="p">(</span><span class="nx">log</span> <span class="o">=&gt;</span> <span class="p">{</span>
            <span class="k">return</span> <span class="s2">`[</span><span class="p">${</span><span class="nx">log</span><span class="p">.</span><span class="nx">timestamp</span><span class="p">}</span><span class="s2">] </span><span class="p">${</span><span class="nx">log</span><span class="p">.</span><span class="nx">level</span><span class="p">.</span><span class="nx">toUpperCase</span><span class="p">()}</span><span class="s2">: </span><span class="p">${</span><span class="nx">log</span><span class="p">.</span><span class="nx">args</span><span class="p">.</span><span class="nx">join</span><span class="p">(</span><span class="dl">'</span><span class="s1"> </span><span class="dl">'</span><span class="p">)}</span><span class="s2">`</span><span class="p">;</span>
        <span class="p">}).</span><span class="nx">join</span><span class="p">(</span><span class="dl">'</span><span class="se">\n</span><span class="dl">'</span><span class="p">);</span>
        
        <span class="kd">const</span> <span class="nx">blob</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Blob</span><span class="p">([</span><span class="nx">allLogs</span><span class="p">],</span> <span class="p">{</span> <span class="na">type</span><span class="p">:</span> <span class="dl">'</span><span class="s1">text/plain</span><span class="dl">'</span> <span class="p">});</span>
        <span class="kd">const</span> <span class="nx">url</span> <span class="o">=</span> <span class="nx">URL</span><span class="p">.</span><span class="nx">createObjectURL</span><span class="p">(</span><span class="nx">blob</span><span class="p">);</span>
        
        <span class="kd">const</span> <span class="nx">a</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">createElement</span><span class="p">(</span><span class="dl">'</span><span class="s1">a</span><span class="dl">'</span><span class="p">);</span>
        <span class="nx">a</span><span class="p">.</span><span class="nx">href</span> <span class="o">=</span> <span class="nx">url</span><span class="p">;</span>
        <span class="nx">a</span><span class="p">.</span><span class="nx">download</span> <span class="o">=</span> <span class="nx">filename</span><span class="p">;</span>
        <span class="nx">a</span><span class="p">.</span><span class="nx">click</span><span class="p">();</span>
        <span class="nx">URL</span><span class="p">.</span><span class="nx">revokeObjectURL</span><span class="p">(</span><span class="nx">url</span><span class="p">);</span>
        
        <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s2">`📄 已导出完整文本日志: </span><span class="p">${</span><span class="nx">filename</span><span class="p">}</span><span class="s2">`</span><span class="p">);</span>
    <span class="p">}</span>
    
    <span class="c1">// 获取状态信息</span>
    <span class="nx">getStatus</span><span class="p">()</span> <span class="p">{</span>
        <span class="k">return</span> <span class="p">{</span>
            <span class="na">bufferSize</span><span class="p">:</span> <span class="k">this</span><span class="p">.</span><span class="nx">logBuffer</span><span class="p">.</span><span class="nx">length</span><span class="p">,</span>
            <span class="na">consoleLines</span><span class="p">:</span> <span class="k">this</span><span class="p">.</span><span class="nx">consoleLineCount</span><span class="p">,</span>
            <span class="na">fileCounter</span><span class="p">:</span> <span class="k">this</span><span class="p">.</span><span class="nx">fileCounter</span><span class="p">,</span>
            <span class="na">isExporting</span><span class="p">:</span> <span class="k">this</span><span class="p">.</span><span class="nx">isExporting</span>
        <span class="p">};</span>
    <span class="p">}</span>
    
    <span class="c1">// 恢复原始console</span>
    <span class="nx">restore</span><span class="p">()</span> <span class="p">{</span>
        <span class="p">[</span><span class="dl">'</span><span class="s1">log</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">warn</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">error</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">info</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">debug</span><span class="dl">'</span><span class="p">].</span><span class="nx">forEach</span><span class="p">(</span><span class="nx">method</span> <span class="o">=&gt;</span> <span class="p">{</span>
            <span class="nx">console</span><span class="p">[</span><span class="nx">method</span><span class="p">]</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">originalMethods</span><span class="p">[</span><span class="nx">method</span><span class="p">];</span>
        <span class="p">});</span>
        <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="dl">'</span><span class="s1">🔄 已恢复原始console行为</span><span class="dl">'</span><span class="p">);</span>
    <span class="p">}</span>
<span class="p">}</span>

<span class="c1">// 使用示例和配置</span>
<span class="kd">const</span> <span class="nx">vmpLogger</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">VmpLogExporter</span><span class="p">({</span>
    <span class="na">maxConsoleLines</span><span class="p">:</span> <span class="mi">1000000</span><span class="p">,</span>      <span class="c1">// console最大行数</span>
    <span class="na">exportBatchSize</span><span class="p">:</span> <span class="mi">1000000</span><span class="p">,</span>      <span class="c1">// 批量导出大小</span>
    <span class="na">autoExportInterval</span><span class="p">:</span> <span class="mi">60000</span><span class="p">,</span> <span class="c1">// 60秒自动导出</span>
    <span class="na">enableAutoExport</span><span class="p">:</span> <span class="kc">false</span><span class="p">,</span>    <span class="c1">// 启用自动导出</span>
    <span class="na">logPrefix</span><span class="p">:</span> <span class="dl">'</span><span class="s1">VMP_REVERSE</span><span class="dl">'</span><span class="p">,</span>  <span class="c1">// 文件前缀</span>
    <span class="na">keepInConsole</span><span class="p">:</span> <span class="mi">1000000</span>      <span class="c1">// console保留行数</span>
<span class="p">});</span>

<span class="c1">// 全局访问接口</span>
<span class="nb">window</span><span class="p">.</span><span class="nx">vmpLogger</span> <span class="o">=</span> <span class="nx">vmpLogger</span><span class="p">;</span>

<span class="c1">// 添加快捷命令</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">save</span> <span class="o">=</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="nx">vmpLogger</span><span class="p">.</span><span class="nx">manualExport</span><span class="p">();</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">saveAll</span> <span class="o">=</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="nx">vmpLogger</span><span class="p">.</span><span class="nx">exportAllLogs</span><span class="p">();</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">status</span> <span class="o">=</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="nx">console</span><span class="p">.</span><span class="nx">table</span><span class="p">(</span><span class="nx">vmpLogger</span><span class="p">.</span><span class="nx">getStatus</span><span class="p">());</span>

<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s2">`
🎯 VMP逆向日志系统使用说明:
• 系统会自动管理console日志，避免浏览器卡死
• 所有日志都会保存，不会丢失任何VMP算法信息
• 快捷命令:
  - console.save() : 手动导出当前日志
  - console.saveAll() : 导出完整文本日志
  - console.status() : 查看系统状态
• 日志文件会自动下载到Downloads文件夹
• 每个JSON文件包含完整的时间戳和元数据
`</span><span class="p">);</span>
</code></pre></div></div>]]></content><author><name>White</name></author><category term="JSVMP" /><category term="逆向" /><category term="逆向" /><category term="JSVMP" /><category term="日志导出" /><category term="JavaScript" /><category term="调试工具" /><summary type="html"><![CDATA[解决 JSVMP 逆向分析中海量日志导出难题：一段 JS 代码实现 console 日志自动收集、批量导出到本地文件，支持自动分批和定时导出。]]></summary></entry></feed>