做了什么 - 确认 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 个住宅代理)、短时频控、静默窗口
444 lines
25 KiB
Markdown
444 lines
25 KiB
Markdown
# 逆向工程 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 探针可复现性清单 |
|