Files
yml2213 fbcf2d448a feat(反VM): 纯 Python 还原 XTEA 算法 + 从 tdc.js 运行时提取每构建密钥
做了什么
- 确认 collect 的加密算法就是教科书 XTEA (32 轮, delta=0x9E3779B9),
  并证明 crypto.py 原有的"带加盐"写法只是把两个密钥字折叠:
  K' = [k0, k1+salt1, k2+salt2, k3]
- tdc_worker.js 新增 captureKey 模式: 正则改写 VM 的 ADD/XOR opcode 为带日志版本,
  用反向引用自动识别栈变量名 (样本=g/雨云实时=j/1ms=M, 版本无关);
  匹配数不为 1 时显式报错, 不静默失败
- 密钥反推: (sum + K[j]) 是一次 ADD, 两操作数必有一个等于 r*DELTA,
  另一个就是密钥字; 再对 Top-N 候选做 4! 排列 + 明文内容特征验证
- 新增 recover_payload_template / generate_collect_from_template:
  运行时标定一次明文模板, 之后轨迹生成/组包/加密/编码全部在 Python
- 新增 tcaptcha/proxy.py 代理提取, client 支持 --proxy-api 遇 12 自动换 IP

为什么
- 服务端只认 collect 的解密结果, 而密钥是每构建随机的;
  把算法做成纯 Python 后, 只有"取密钥"依赖 Node, 抗发版能力最强

怎么验证的
- 纯 Python XTEA 加密结果与真实 tdc.js 产物前 48 字节逐字节一致
- 密钥按构建固定的证据: 同一脚本 3 次运行密文首块均为 f6a24e3e4fb5dd2b
- 三份脚本 (样本/雨云实时/1ms实时) 均成功复原密钥
- 雨云 aid 端到端: 纯 Python 出包 -> errorCode=0 + 真实 ticket
- 回归测试: tcaptcha/tests/test_xtea_key.py 11 项断言全过,
  文字点选求解 8/8; 新增 tcaptcha/selftest.py 一键复验

已知未解
- 1ms aid=195672344 在答案正确时仍返回 errorCode=12; 已排除密钥、
  遥测引擎、出口 IP (4 个住宅代理)、短时频控、静默窗口
2026-10-11 18:07:09 +08:00

444 lines
25 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 逆向工程 Agent 工作指导(通用版)
> **本文件的用法**:动手前先看 **§0 一页速查**定位场景;动手时按 **§3 SOP** 执行;
> 卡住或要下结论时查 **§5 经验清单** 与 **附录 A(自身 bug 排查)**;
> 每轮结束按 **§6.2 验收清单**核对。
>
> **三条铁律**(先记这三句,其余都是展开):
> 1. **服务端是最高裁判**——不校验的东西根本不逆;
> 2. **工具先于结论**——把"读代码"变成"查数据库",把"猜公式"变成"捕获值对照";
> 3. **绝不做位置假设**——布局会随输入漂移,用「内容特征 + 数学约束」识别结构。
---
## 目录
| 章节 | 内容 | 什么时候看 |
| --- | --- | --- |
| [§0](#0-一页速查) | 场景 → 动作速查表 | **每次动手前** |
| [§1](#1-工程基础约束) | Python / Git / 进度管理 | 项目初始化、每个里程碑 |
| [§2](#2-核心思路自顶向下) | 四问决策链、四层模型、铁律、分层交付 | 定路线时 |
| [§3](#3-标准作业流程sop) | 反混淆 / 反平砍 / 反 VM / 反加密 / 成本闸门 | 拿到源码后 |
| [§4](#4-工具链建设) | 为什么要工具、五件套、验收标准 | **动手读代码之前** |
| [§5](#5-实战经验检查表) | 10 条踩坑沉淀 | 下结论前 |
| [§6](#6-工作节奏与验收) | 每轮节奏、交付验收、卡住升级 | 每轮开始/结束 |
| [附录 A](#附录-a-自身-bug-排查清单) | 自身 bug 高频陷阱 | **推翻任何结论之前** |
| [附录 B](#附录-b-信号到动作速查) | 信号 → 动作 | 识别不出东西时 |
---
## §0 一页速查
| 你现在的处境 | 立刻做什么 | 去哪节 |
| --- | --- | --- |
| 刚拿到一个接口 / 抓包 | **先做服务端容忍度实验**(变异参数看响应),画生死线 | [§2.1](#21-四问决策链) |
| 服务端不校验某参数 | **停止逆向它**,记入文档 | §2.1 |
| 拿到混淆 JS/源码 | 先建工具(清点 + 提取 + 追踪 + 查询),再读 | [§4](#4-工具链建设) |
| 代码读不懂、循环套 switch | 识别为控制流平坦化,按 SOP 还原 | [§3.2](#32-控制流平坦化还原反平砍) |
| 有大 switch + 数字数组 | 判定为字节码 VM,先反平砍解释器再写反汇编器 | [§3.3](#33-字节码-vm-还原devirtualization) |
| 输出是乱码/定长串 | **先试解码**(剥头 → 标准 base64/hex → 魔改码表) | [§3.4](#34-加密与编码分析反加密) |
| 要猜某个字段的算法 | **别猜**:运行时捕获该字段来源值,在输出里搜它的大端编码定位偏移 | [§5.2](#52-完整性字段校验和长度的通用识别法) |
| 公式在单样本上对上了 | 立刻做**多维扰动 ≥10 样本 + 真实样本 + 运行时对照**三重验证 | [§5.5](#55-逐字节对拍的适用边界认清不可达目标) |
| 结论被推翻 | 在文档**显式修正**并登记"已排除的假设" | [§5.7](#57-记录被推翻的假设负结果也是资产) |
| 深挖几轮仍卡住 | 回 §0 重新问"服务端到底在不在乎这个细节" | [§6.3](#63-卡住时的升级路径) |
---
## §1 工程基础约束
### 1.1 Python 项目
- 依赖用 **uv** 管理,**项目级 venv**:`uv venv` + `uv add <包名>`
- 依赖**按需安装**:用到哪个加哪个,不要预先批量安装
### 1.2 Git
- commit 信息用**中文**:`类型(范围): 一句话总结` + 空行 + 要点列表(做了什么 / 为什么 / 怎么验证的)
- **每个有意义的里程碑单独提交**,不攒大提交
- 提交信息含特殊字符时用 `git commit -F <文件>`,避免 shell 引号踩坑
### 1.3 进度与证据管理(长任务强制)
- **任何进度、证据、灵感都落盘到文档**,不依赖对话记忆
- 按证据分类登记,四栏固定:
| 栏位 | 要求 |
| --- | --- |
| **已知** | 附证据(实验/抓包/源码位置)+ 确信度 |
| **未知** | 附当前假设 + 确信度 |
| **目标** | 本轮要推进到哪 |
| **下一步** | 具体动作,可被下一轮直接执行 |
- 结论必须带来源;没有证据的判断**标注"假设"**
- **推翻旧结论时显式修正文档**,不留过时错误判断(可另设"早期误判记录"备查)
- 不经意的灵感、异常现象也要记录——它们经常是突破口
### 1.4 交付物形态
- 产物是**资产**:可重复生成、可 diff、可复用,不是一次性分析结论
- 深挖成果即使当前用不上也**归档为储备**,不删除
- 每个交付物配一个**一键复验入口**(如 `selftest`)
---
## §2 核心思路:自顶向下
### 2.1 四问决策链
```
第 0 问:服务端到底校验什么? ← 永远的第一步,能砍掉 90% 工作量
第 1 问:目标数据的结构是什么?
第 2 问:输入特征集是什么?
第 3 问:变换算法是什么?
最后 :用目标语言原生重构,跳过一切 VM / 环境层
```
**第 0 问的标准动作**:对参数做变异(截断 / 置空 / 随机体 / 删字段 / 改版本号),用服务端响应画生死线。
- 服务端**不校验**的 → 零工作,记入文档
- **只校验格式/长度** → 结构正确的合成包即可,无需逐字节还原
- **强校验** → 才值得还原算法;即便如此也优先还原"协议结构"而非"采集过程"(指纹值可用固定真实画像)
### 2.2 四层解构模型
| 层 | 内容 | 作用 |
| --- | --- | --- |
| **L1** | 打包与模块拓扑 | 圈定核心加签模块,剔除开源库噪音 |
| **L2** | 混淆层(字符串表 / 平坦化 / 代理函数)| 还原出可读结构 |
| **L3** | 虚拟机与探针层(字节码调度 / 环境检测)| 得到操作码语义 |
| **L4** | **数据协议层**(组包 / 压缩 / 加盐 / 自定义编码)| **最终目标** |
自顶向下逐层剥离,但**注意力始终放在 L4**——L1/L2/L3 都是为读懂 L4 服务的。
### 2.3 三条铁律(展开)
#### 铁律 1:服务端是最高裁判
见 §2.1。**动手逆向任何算法之前先做容忍度实验。**
#### 铁律 2:拒绝盲目补环境(打地鼠陷阱)
错误做法:`window is not defined` 就补 window,报错就挂空函数,无休止缺啥补啥。
| 缺陷 | 说明 |
| --- | --- |
| 知其然不知其所以然 | 不理解风控意图与数据结构 |
| 极度脆弱 | 发版即全盘报废 |
| 性能低下 | 依赖 JSDOM/Headless 无法高并发 |
| 易被探测 | Proxy/堆栈泄露伪造痕迹被拉黑 |
**允许的最低限度**:为让探针跑通而供给"真实语义的值"(**有明确清单、有限次数**),目的是**采集语义**。
一旦开始无清单地连续打补丁,立即停下回到自顶向下。
#### 铁律 3:整体还原优先,拒绝零敲碎打
- 反混淆 / 还原要做就**一次性全量**(常量池整体译出、全源码静态替换),产出可复用资产
- **静态常量优先**:译码器输出与运行时无关时,离线批量译出存表,不在运行时复刻译码器
- 追踪 / 插桩注意开销形态:**热路径上禁止跨边界函数调用与字符串分配**
(用 TypedArray 直写、计数器、采样),否则会误判为"反调试"——**先排除自身工具副作用再下结论**
### 2.4 分层交付(不要追求一步到位的完美纯算)
| 级别 | 形态 | 成本 | 适用 |
| --- | --- | --- | --- |
| **① 纯算** | 目标语言原生实现 | 高(逆向成本)| 最优:高并发、零依赖、抗环境变化 |
| **② 半自动** | 浏览器/沙箱低频维护会话与参数 + 纯算高频发包 | 中 | 算法某部分难还原但输入维度小 |
| **③ 桥接** | Node V8 沙箱托管原 JS 暴露接口 | 低 | 保底,抗小版本更新 |
- **先验证低层级方案是否已被服务端接受**(§2.1),被接受就不要升级成本
- 三级方案在代码里应是**同一接口的三个实现**,上层透明,可随时降级
---
## §3 标准作业流程(SOP)
### 3.0 通用原则
- **每还原一层就验证一次**:与原版做功能等价对比(同输入同输出),多层叠加时不许跳层
- 一切"静态读不懂"的部分,**优先动态采集语义**(探针 / 追踪),而不是硬读
- 每种变换都有固定的三段式:**识别特征 → 标准解法 → 验证方式**
### 3.1 混淆还原(AST 反混淆)
**识别特征**:十六进制变量名、字符串数组+下标取值、代理函数、控制流平坦化、死代码注入、逗号表达式链。
**标准管线**(Babel/swc AST,缺一不可):
1. **常量内联**:字符串数组 + 解密函数 → 沙箱预执行 → 把所有 `dec(a,b)` 调用点静态替换为字面量
(输入固定输出固定的译码器,离线批量译出存表即可,不必理解译码器内部)
2. **代理函数消除**:`function add(a,b){return a+b}` 类转发函数折叠为原生表达式
3. **死代码剔除**:恒假分支、无用逗号表达式
4. **产物必须可执行**:每轮 AST 变换后做语法校验 + 功能等价验证,否则后续分析建立在坏产物上
> ⚠️ 第 1 步是后面一切搜索的前提:**不内联就搜不到语义**(`platform` 会写成 `n(l[11],null,17,X)`)。
### 3.2 控制流平坦化还原(反平砍)
**识别特征**:`for(;;){switch(状态变量){...}}`;状态字两级编码(如 `case = 31&v`,`子 = 31&(v>>5)`)。
**SOP**:
0. **先造工具,再读代码**(不要手工抠案例)——目标是把"读机器"变成"查数据库":
```
① AST 清点全文件状态机 → 起始/结束/状态变量/mask/shift/判别式/子状态变量/各 case 区间
② 状态提取 → 每个 (case, 子状态) → {约束, 代码, 后继状态}
③ 运行时轨迹追踪 → TypedArray 环形缓冲记录实际状态序列
④ 执行序渲染 → 按真实顺序排列的代码片段 = 算法正文
⑤ 查询 CLI → 搜状态代码 / 查某寄存器写入者 / 渲染某机器
```
工具一旦成型,"这个字段谁写的""这段代码在哪台机器"都是秒级查询。
1. **状态提取必须用 AST,不要手写括号配平**:字符串/正则字面量里的 `{` `}` 会骗过配平器
(实战中 `"[\\s\\S]{1,3}"` 这类正则曾导致 case 边界全错)
2. 与**运行时轨迹联合**:光有静态分支图不够,用真实执行序列排除未执行分支
3. **交叉验证**:工具提取出的转移必须与运行时观测到的转移**逐条对齐**,能对齐才敢用
4. 输出"执行叙事":按真实顺序排列的已解析代码 = 算法正文
5. **多层嵌套**:外层 case 体里可能是**另一个平砍机**(状态变量名不同)→ 递归处理;
也可能整段是字节码程序(见 §3.3)
> 插桩注意:热路径禁函数调用/字符串分配(跨 realm 调用会内存爆炸,易误判为反调试)。
### 3.3 字节码 VM 还原(devirtualization)
**识别特征**:巨大的 `switch` 操作码分发器 + 数字数组字面量(`push(18,1,4,0,...)`)+ Uint8Array 缓冲 + 闭包式子程序调用。
**SOP**:
1. **先判定"是不是 VM"**:操作码分发器(读数字数组驱动 switch)+ 字节码缓冲 + 虚拟寄存器/栈 → 是
2. 解释器通常自己也是平砍代码 → 按 §3.2 反平砍,得到**操作码语义表**
(每条 handler = 一条指令语义:寄存器读写 / 属性存取 / 算术 / 调用 / 闭包创建 / 异常)
3. 提取字节码程序数组(含二进制字面量操作数)
4. 写**反汇编器**:操作码表 × 字节码 → 可读指令流(机械工作,输入已齐)
5. 读指令流得到算法;子程序调用(闭包创建)递归展开
> **解释器 + 字节码 = 完整算法,两者缺一不可**;常量池在字节码操作数或独立表中。
### 3.4 加密与编码分析(反加密)
**先分类,再决定策略**——编码可逆,加密需要密钥。
**第 1 步 判定类型**
| 观察 | 判定 |
| --- | --- |
| 输出字符集是标准/魔改 base64、hex | **编码类** → 直接解码看明文结构 |
| 同环境多次采样输出逐字节大范围变化,且与输入无固定对应 | **加密类**(含 nonce / 滚动键)|
| 定长输出 + 雪崩效应 | **哈希/签名类** |
**第 2 步 分类处理**
- **编码类**:剥头(版本前缀)→ 试标准 base64/hex(补 padding)→ 失败则找**魔改码表**
(在常量池里搜 64~66 字符表)或流式变换(逐字符 XOR / 位移,常见 `charCode ^ 常数`)
- **加密类**:
- 差分分析:同环境改单一输入,看密文哪些区域变化 → 定位 nonce 区 / 指纹区 / 密文区
- 找密钥材料:滚动键常见 `key = (key*m + c) % 256`、逐字符 XOR、查表替换
—— 在源码里搜算术特征(`%256`、`^`、异常常数)
- 密钥来自环境/会话(cookie 等)时,纯算只需把该输入**参数化**,无需破解密钥本身
- **哈希/签名类**:先试常见族(MD5 / SHA1 / HMAC),用「签名 = H(公知字段拼接)」假设逐个拼接组合验证
**第 3 步 字节级解剖(对任意黑盒输出)**
- 逐字节差分(多样本对比):恒定字节 = 指纹/版本;变化字节 = nonce/时间戳/密文
- 找时间戳:在变化区搜当前 epoch ms/sec 的大端/小端表示
- 区段边界:半恒定单字节常是长度 / 分隔符
### 3.5 成本闸门(什么该反、什么不该反)
- 每次深入前重问:**服务端校验这个吗?强度多少?**
- 逆向深度与校验强度匹配:不校验 → 零工作;格式校验 → 只做结构;强校验 → 才还原算法
- **边界捕获优于逐指令还原**:在协议关键点(编码入口 / 密钥使用点)一次性插桩捕获输入/码表/输出,
往往一个钩子抵多轮操作码分析 —— 动手前先问"**最少几个边界采样能定契约**"
- **解码先于破解,码表先于差分**:拿到输出先试解码看明文再谈加密;
乱序码表下用标准解码会产生"看似恒定区"的确定性乱码假象
—— **差分结论必须建立在已验证的码表之上**
- 已完成的深挖成果归档为储备资产,不删除
> 本节只给"识别特征 → 标准解法";**工具化、校验和定位、偏移陷阱、验证边界、自身 bug 清单**见 §4、§5 与附录 A。
---
## §4 工具链建设
> **工具先于结论。** 凡涉及"找代码边界 / 找调用点 / 找赋值点",一律上 AST;正则只用于在**已定位的小片段**里粗筛。
### 4.1 五件套(控制流平坦化场景的最小工具集)
| # | 工具 | 输入 → 输出 | 解决什么 |
| --- | --- | --- | --- |
| ① | **状态机清点** | 全文件 → 机器清单(起始/状态变量/掩码/判别式/case 区间)| 先知道"有多少台机器" |
| ② | **状态提取** | 单机器 → `(case, 子状态) → {约束, 代码, 后继}` | 把机器拆成可查的状态表 |
| ③ | **运行时追踪** | 运行一次 → 状态序列(环形缓冲)| 排除未执行分支、验证静态提取 |
| ④ | **执行序渲染** | 状态表 + 轨迹 → 按序排列的代码叙事 | 得到"算法正文" |
| ⑤ | **查询 CLI** | 正则/寄存器名 → 命中的状态与所属机器 | 秒级回答"谁写了它" |
### 4.2 工具验收标准
- 能回答三个问题:**「这个字段谁写的」「这段代码在哪」「这台机器有多少状态」**
- 答案必须与**运行时观测一致**(否则工具在骗你)
- **可复用**:一次投入,后续所有分析都变成查询
### 4.3 配套的运行时捕获工具
| 捕获对象 | 用途 |
| --- | --- |
| 累加器最终值 | 在输出里搜其大端/小端编码 → **反推字段偏移与整块布局**(§5.2)|
| 中间缓冲区内容 | 判断校验和的**真实求和对象**(常不是最终载荷)|
| 子程序返回值 | 定位"这块数据由哪个子程序产出" |
| 环境属性读取点 | 得到**有限的有效输入特征集** |
**探针可复现性清单**(不满足则实验不可复现,会浪费时间追不存在的"算法差异"):
- [ ] 固定时钟(`Date` / `performance` / `timeOrigin`)
- [ ] 固定 PRNG(`Math.random` / `crypto.getRandomValues` / `randomUUID`)
- [ ] 明确"每进程只调用一次"或显式复位累积状态
- [ ] 明确区分**初始化时刻**与**调用时刻**(见 §5.4)
---
## §5 实战经验(检查表)
> 每条都来自"踩过坑再总结",附**识别信号 → 标准动作**。
### 5.1 工具先于结论
- 正则处理代码 = 技术债;代码结构一律 AST
- **先内联再搜索**:混淆常量必须先批量内联成字面量,搜出的代码才有语义
- 工具要可复用、可查询;一次投入,后续全是查询
### 5.2 完整性字段(校验和/长度)的通用识别法
- **布局模式**:二进制载荷常是 `[2字节校验和][块内容]` 顺序拼接(多块)
- **求和形式**:`校验和 = Σ(块内容 & MASK)`,且**块内嵌套的其它校验和字段在计算时按 0 计**
(信号:中间缓冲区对应位置是 0,最终载荷那里已被后续写入覆盖)
- **MASK 是常量池里的数**:同一源码常有**多个同类累加器、掩码各不相同** → 要**列全**,别找到一个就以为找全
- **杀手锏**:运行时捕获累加器最终值 → 在输出里**搜索该值的大端/小端编码**
→ 命中位置即字段偏移;多个字段一起做即可反推**整块布局**(比盲试公式高效一个数量级)
- **不要只试通用哈希族**(CRC/Adler/FNV/Murmur/区间和):
自研校验和多以"逐字节掩码求和"出现 —— 先按上一条定位字段,再回源码读公式
### 5.3 绝不做位置假设(最大的坑)
- **块长会随环境数据变化 → 后续所有偏移漂移**。
单画像验证通过的"固定偏移"(如"校验和一定在 [78:80]")换画像立刻失效
- **正确做法:用「内容特征 + 数学约束」识别结构,而不是用偏移**
- 内容特征:块头魔数(如块起始固定是 `ff 7f 75 ...`)
- 数学约束:`Σ(块 & MASK) == 该块前面的 2 字节`
- 两者同时满足 → 唯一确定块的位置与长度
- **任何偏移结论都要跨样本 / 跨画像复验**,否则不要写进代码
### 5.4 把"时间"和"随机"当作独立实验维度
- **时间层次解耦实验**:区分「模块初始化时刻」与「请求/调用时刻」——两种变化各跑一次
- 若"固定 init、变化 call"导致**载荷零差异** ⇒ 所有时间字段都来自 init
- 这一次实验就定下整套回填策略(把所有时间字段按**同一目标 init 时刻**统一回填)
- **累积状态**:同进程重复调用常使输出变长/变化(内部记账)。
确定性目标应定义为"**模块初始化后的首次调用**"
- 随机源不固定时,不要下任何"算法差异"结论(先按 §4.3 清单排查)
### 5.5 逐字节对拍的适用边界(认清不可达目标)
- 逐字节对拍**只在受控随机源下才有意义**。
真实环境每次调用含随机区,同会话相隔数秒的两个真实样本可有**上千字节不同**
—— 这不是"还没做到",而是**不可达目标**
- **验收标准要分层**:
| 层级 | 内容 |
| --- | --- |
| ① | 受控环境**逐字节一致**(证明算法还原正确)|
| ② | 真实样本上验证**不变量与公式**(版本、校验和、时间字段关系、结构可自动定位)|
| ③ | **运行时插桩值 == 输出字段**(三方对照)|
- **三重验证法**:多样本(≥10,多维扰动)+ 真实样本 + 运行时捕获对照。
只靠单一样本"公式对上"极易是巧合(**弱校验和尤其容易假命中**)
### 5.6 自身工具 bug 的高频陷阱(先怀疑自己)
动手改结论前逐条排查,详见 [附录 A](#附录-a-自身-bug-排查清单)。
### 5.7 记录被推翻的假设(负结果也是资产)
- 文档里除"已知/未知"外,还要有**"已排除的假设"**:试过什么、怎么否定的、证据是什么
- **负结论要写清适用条件**,否则未来会被误用
- 示例(本项目实战中被推翻的判断):早期"31% 常量指纹"、"某字段 = 某累加器"、"真机块结构与沙箱不同"
—— 全部靠负结果登记避免了重复投入
### 5.8 标定式交付(算法难还原但输入维度小)
- 当某部分算法难完整还原,但其输入维度很小(时间、设备画像)时,交付
**「模板 + 一次探针标定 + 定点回填」**:
| 环节 | 做法 |
| --- | --- |
| 模板 | 一次真实采样(含全部画像数据)|
| 标定 | 再用一次**不同取值**的采样,反解变换类型(异或/加法)与常数 |
| 回填 | 按目标取值重算受影响字段(含重新计算受影响的校验和,注意 §5.6 的依赖顺序)|
- 该方案**自动适配画像差异**(块长 / 偏移 / 常数),比硬编码偏移健壮得多
- 标定结果**可持久化**(JSON),生成时无需再次采样
### 5.9 用回归测试固化每一条已确证的性质
- 一旦确证某个公式/不变量,**立刻写成断言测试**并把样本打包成 fixtures
- 覆盖范围:编解码往返、捕获样本复现、公式(多样本 + 真实样本)、
结构自动发现、跨画像生成、标定流程
- 交付一个 `selftest` 入口,让别人一条命令复验全部结论
### 5.10 分层交付的落地形态
- 纯算 / 半自动 / 桥接 = **同一接口的三个实现**,调用方无感
- 服务端一旦收紧、或某画像标定失败,可**立刻降级**而不改调用方
---
## §6 工作节奏与验收
### 6.1 每轮节奏
```
① 读进度文档的「下一步」 → ② 选区分度最大的实验 → ③ 执行并记录证据
→ ④ 更新「已知/未知/下一步」 → ⑤ 提交(中文 commit)→ ⑥ 下一轮
```
- 实验设计优先做**区分度最大的那个**("不变性实验""容忍度实验"),它能直接决定路线分叉
- 每轮结束必须有**可验证的产物**(工具/文档/测试/结论),不要只留"分析过程"
### 6.2 交付前验收清单
- [ ] 服务端容忍度实验做了吗?不校验的部分是否已剔除出工作范围?
- [ ] 结论是否**带证据来源**?(实验 / 抓包 / 源码位置)
- [ ] 是否做了**三重验证**(多样本 + 真实样本 + 运行时对照)?
- [ ] 是否做了**跨样本/跨画像复验**(尤其任何偏移结论)?
- [ ] 被推翻的旧结论是否已在文档中**显式修正**?
- [ ] 是否**先怀疑过自己的工具**(附录 A 全过一遍)?
- [ ] 产物是否可复现、可 diff、有一键复验入口?
- [ ] 分层交付是否已就位(最低成本方案先验证)?
- [ ] 是不是把"不可达目标"(如真实环境逐字节对拍)误当成了"待办"?
### 6.3 卡住时的升级路径
| 症状 | 升级动作 |
| --- | --- |
| 同一细节卡住 > 几轮 | 回 §2.1 第 0 问:**服务端到底在不在乎这个细节** |
| 静态读不懂 | 改为**动态采集语义**(探针/追踪),不要硬读 |
| 公式猜不出来 | 改为**运行时捕获 + 定位偏移**(§5.2),或改走**标定式交付**(§5.8)|
| 插桩就崩/卡死 | 先做 **no-op 对照实验**排除自身工具副作用,再判断是否反调试 |
| 样本之间结论矛盾 | 检查是否**编码层/码表轮换**或**块长漂移**(§5.3、附录 A 第 5/6 条)|
---
## 附录 A 自身 bug 排查清单
> **推翻任何结论之前**,先逐条排查以下陷阱——它们在本项目实战中全部真实发生过。
| # | 陷阱 | 典型症状 | 正确做法 |
| --- | --- | --- | --- |
| 1 | **运算符优先级** | Python `X & M == v` 被解析成 `X & (M == v)` | 一律加括号:`(X & M) == v` |
| 2 | **多样本扫描用统一上界** | 各样本长度不同(如 1074~1171)导致长样本被截断 | 用**各自长度** |
| 3 | **依赖顺序** | 校验和 A 的覆盖区间包含校验和 B | **必须先算被包含者**,再算包含者 |
| 4 | **括号配平不考虑字面量** | 字符串/正则里的 `{ }` 破坏配平 | 用 **AST** 取精确区间 |
| 5 | **编码层陷阱** | 参数在 **body** 而非 header/cookie/query;urlencoded 把 `+` 变空格(而自定义码表恰含 `+`)| 先确认数据位置;解码前**还原 `+`** |
| 6 | **码表/版本轮换** | 用一套码表解另一版本,得到"看似合理的乱码" | 先确认版本,**每版本单独取码表** |
| 7 | **工具解析失败被忽略** | 提取结果与运行时不一致却继续用 | 提取结果必须与**运行时轨迹逐条对齐** |
| 8 | **静默异常** | `except: pass` 掩盖了真实错误 | 断言 + 显式失败 |
---
## 附录 B 信号到动作速查
| 观察到的信号 | 最可能的性质 | 下一步动作 |
| --- | --- | --- |
| `for(;;){switch(x)}` + 状态字 | 控制流平坦化 | §3.2 |
| 巨大 switch + 数字数组 + Uint8Array | 字节码 VM | §3.3 |
| 字符串数组 + 下标取值 | 字符串混淆 | §3.1 常量内联 |
| 输出 = 版本前缀 + 定长乱码 | 编码类 | §3.4 第 2 步(试魔改码表)|
| 同环境多次输出大范围变化 | 加密 / 含 nonce | §3.4 第 2 步(差分定位)|
| 输出定长 + 输入微改即全变 | 哈希 / 签名 | §3.4(试常见族 + 拼接组合)|
| 载荷中 `[小值][定长块]` 交替 | `[校验和][块内容]` 布局 | §5.2 |
| 常量字节 + 变化字节混合 | 指纹 + nonce/时间戳 | §3.4 第 3 步(逐字节差分)|
| 单画像能对上、换画像就错 | 偏移漂移 | §5.3 |
| 同一实验两次结果不同 | 随机源未固定 | §4.3 探针可复现性清单 |