做了什么 - 确认 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 个住宅代理)、短时频控、静默窗口
25 KiB
逆向工程 Agent 工作指导(通用版)
本文件的用法:动手前先看 §0 一页速查定位场景;动手时按 §3 SOP 执行; 卡住或要下结论时查 §5 经验清单 与 附录 A(自身 bug 排查); 每轮结束按 §6.2 验收清单核对。
三条铁律(先记这三句,其余都是展开):
- 服务端是最高裁判——不校验的东西根本不逆;
- 工具先于结论——把"读代码"变成"查数据库",把"猜公式"变成"捕获值对照";
- 绝不做位置假设——布局会随输入漂移,用「内容特征 + 数学约束」识别结构。
目录
| 章节 | 内容 | 什么时候看 |
|---|---|---|
| §0 | 场景 → 动作速查表 | 每次动手前 |
| §1 | Python / Git / 进度管理 | 项目初始化、每个里程碑 |
| §2 | 四问决策链、四层模型、铁律、分层交付 | 定路线时 |
| §3 | 反混淆 / 反平砍 / 反 VM / 反加密 / 成本闸门 | 拿到源码后 |
| §4 | 为什么要工具、五件套、验收标准 | 动手读代码之前 |
| §5 | 10 条踩坑沉淀 | 下结论前 |
| §6 | 每轮节奏、交付验收、卡住升级 | 每轮开始/结束 |
| 附录 A | 自身 bug 高频陷阱 | 推翻任何结论之前 |
| 附录 B | 信号 → 动作 | 识别不出东西时 |
§0 一页速查
| 你现在的处境 | 立刻做什么 | 去哪节 |
|---|---|---|
| 刚拿到一个接口 / 抓包 | 先做服务端容忍度实验(变异参数看响应),画生死线 | §2.1 |
| 服务端不校验某参数 | 停止逆向它,记入文档 | §2.1 |
| 拿到混淆 JS/源码 | 先建工具(清点 + 提取 + 追踪 + 查询),再读 | §4 |
| 代码读不懂、循环套 switch | 识别为控制流平坦化,按 SOP 还原 | §3.2 |
| 有大 switch + 数字数组 | 判定为字节码 VM,先反平砍解释器再写反汇编器 | §3.3 |
| 输出是乱码/定长串 | 先试解码(剥头 → 标准 base64/hex → 魔改码表) | §3.4 |
| 要猜某个字段的算法 | 别猜:运行时捕获该字段来源值,在输出里搜它的大端编码定位偏移 | §5.2 |
| 公式在单样本上对上了 | 立刻做多维扰动 ≥10 样本 + 真实样本 + 运行时对照三重验证 | §5.5 |
| 结论被推翻 | 在文档显式修正并登记"已排除的假设" | §5.7 |
| 深挖几轮仍卡住 | 回 §0 重新问"服务端到底在不在乎这个细节" | §6.3 |
§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,缺一不可):
- 常量内联:字符串数组 + 解密函数 → 沙箱预执行 → 把所有
dec(a,b)调用点静态替换为字面量 (输入固定输出固定的译码器,离线批量译出存表即可,不必理解译码器内部) - 代理函数消除:
function add(a,b){return a+b}类转发函数折叠为原生表达式 - 死代码剔除:恒假分支、无用逗号表达式
- 产物必须可执行:每轮 AST 变换后做语法校验 + 功能等价验证,否则后续分析建立在坏产物上
⚠️ 第 1 步是后面一切搜索的前提:不内联就搜不到语义(
platform会写成n(l[11],null,17,X))。
3.2 控制流平坦化还原(反平砍)
识别特征:for(;;){switch(状态变量){...}};状态字两级编码(如 case = 31&v,子 = 31&(v>>5))。
SOP:
-
先造工具,再读代码(不要手工抠案例)——目标是把"读机器"变成"查数据库":
① AST 清点全文件状态机 → 起始/结束/状态变量/mask/shift/判别式/子状态变量/各 case 区间 ② 状态提取 → 每个 (case, 子状态) → {约束, 代码, 后继状态} ③ 运行时轨迹追踪 → TypedArray 环形缓冲记录实际状态序列 ④ 执行序渲染 → 按真实顺序排列的代码片段 = 算法正文 ⑤ 查询 CLI → 搜状态代码 / 查某寄存器写入者 / 渲染某机器工具一旦成型,"这个字段谁写的""这段代码在哪台机器"都是秒级查询。
-
状态提取必须用 AST,不要手写括号配平:字符串/正则字面量里的
{}会骗过配平器 (实战中"[\\s\\S]{1,3}"这类正则曾导致 case 边界全错) -
与运行时轨迹联合:光有静态分支图不够,用真实执行序列排除未执行分支
-
交叉验证:工具提取出的转移必须与运行时观测到的转移逐条对齐,能对齐才敢用
-
输出"执行叙事":按真实顺序排列的已解析代码 = 算法正文
-
多层嵌套:外层 case 体里可能是另一个平砍机(状态变量名不同)→ 递归处理; 也可能整段是字节码程序(见 §3.3)
插桩注意:热路径禁函数调用/字符串分配(跨 realm 调用会内存爆炸,易误判为反调试)。
3.3 字节码 VM 还原(devirtualization)
识别特征:巨大的 switch 操作码分发器 + 数字数组字面量(push(18,1,4,0,...))+ Uint8Array 缓冲 + 闭包式子程序调用。
SOP:
- 先判定"是不是 VM":操作码分发器(读数字数组驱动 switch)+ 字节码缓冲 + 虚拟寄存器/栈 → 是
- 解释器通常自己也是平砍代码 → 按 §3.2 反平砍,得到操作码语义表 (每条 handler = 一条指令语义:寄存器读写 / 属性存取 / 算术 / 调用 / 闭包创建 / 异常)
- 提取字节码程序数组(含二进制字面量操作数)
- 写反汇编器:操作码表 × 字节码 → 可读指令流(机械工作,输入已齐)
- 读指令流得到算法;子程序调用(闭包创建)递归展开
解释器 + 字节码 = 完整算法,两者缺一不可;常量池在字节码操作数或独立表中。
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。
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 探针可复现性清单 |