feat: 初始化通用JS逆向工程Skill项目
包含完整的JS逆向工程技能体系: - SKILL.md: 5阶段工作流,深度集成 js-reverse + chrome-devtools MCP - references/: 7份参考文档(加密算法、混淆还原、Hook技巧、反调试、环境补全、协议分析、MCP手册) - scripts/: 4个CLI工具(环境检查、沙箱执行器、Hook生成器、加密识别器) - templates/: 4套项目模板(纯算法还原、VM沙箱、WASM加载、浏览器自动化) - examples/: 完整分析流程示例 Made-with: Cursor
This commit is contained in:
@@ -0,0 +1,3 @@
|
|||||||
|
.DS_Store
|
||||||
|
node_modules/
|
||||||
|
*.log
|
||||||
@@ -0,0 +1,167 @@
|
|||||||
|
# JS 逆向工程 Skill
|
||||||
|
|
||||||
|
通用 JavaScript 逆向工程技能,通过 Node.js 实现算法还原和模拟请求。深度集成 `js-reverse` 和 `chrome-devtools` 两个 MCP 工具,覆盖主流 JS 逆向场景。
|
||||||
|
|
||||||
|
## 核心能力
|
||||||
|
|
||||||
|
- **加密算法还原**: MD5/SHA/AES/DES/RSA/HMAC/Base64 等常见加密算法的 Node.js 纯算法复现
|
||||||
|
- **JS 混淆还原**: OB混淆、控制流平坦化、eval打包、自定义VM/JSVMP 等混淆类型的分析策略
|
||||||
|
- **动态 Cookie 逆向**: eval首包、api预热、浏览器指纹等 Cookie 生成机制的破解
|
||||||
|
- **WASM 逆向**: WebAssembly 加密模块的加载、环境补全和调用
|
||||||
|
- **协议层对抗**: TLS 指纹检测、HTTP/2 强制、频率限制等协议层防护的应对
|
||||||
|
- **MCP 工具深度集成**: 源码搜索、断点调试、Hook注入、网络监听全自动化
|
||||||
|
|
||||||
|
## 项目结构
|
||||||
|
|
||||||
|
|
||||||
|
```
|
||||||
|
hello_js_reverse_skill/
|
||||||
|
├── SKILL.md # 技能定义(AI Agent 读取的核心文件)
|
||||||
|
├── README.md # 项目文档
|
||||||
|
│
|
||||||
|
├── references/ # 参考文档(知识库)
|
||||||
|
│ ├── workflow-overview.md # 工作流总览与解法模式决策树
|
||||||
|
│ ├── crypto-patterns.md # 加密算法识别与 Node.js 还原代码
|
||||||
|
│ ├── obfuscation-guide.md # JS 混淆类型识别与还原策略
|
||||||
|
│ ├── hook-techniques.md # Hook 技术大全(13种 Hook 模板)
|
||||||
|
│ ├── anti-debug.md # 反调试对抗手册(7种绕过方案)
|
||||||
|
│ ├── environment-patch.md # 环境补全指南(VM沙箱/WASM/jQuery/XHR)
|
||||||
|
│ ├── protocol-analysis.md # 协议层分析(TLS/HTTP2/UA/频率限制)
|
||||||
|
│ └── mcp-cookbook.md # MCP 工具使用手册(5大场景操作步骤)
|
||||||
|
│
|
||||||
|
├── scripts/ # 工具脚本
|
||||||
|
│ ├── check-deps.sh # 环境依赖检查
|
||||||
|
│ ├── sandbox-runner.js # VM 沙箱执行器(CLI工具)
|
||||||
|
│ ├── hook-generator.js # Hook 代码生成器(10种Hook模板)
|
||||||
|
│ └── crypto-identifier.js # 加密算法识别器(密文特征分析)
|
||||||
|
│
|
||||||
|
├── templates/ # 项目模板(按解法模式分类)
|
||||||
|
│ ├── node-request/ # 模式A:纯 Node.js 算法还原
|
||||||
|
│ │ ├── main.js
|
||||||
|
│ │ ├── utils/encrypt.js
|
||||||
|
│ │ ├── utils/request.js
|
||||||
|
│ │ └── package.json
|
||||||
|
│ ├── vm-sandbox/ # 模式B:VM 沙箱执行
|
||||||
|
│ │ ├── main.js
|
||||||
|
│ │ ├── utils/sandbox.js
|
||||||
|
│ │ ├── utils/request.js
|
||||||
|
│ │ └── package.json
|
||||||
|
│ ├── wasm-loader/ # 模式C:WASM 加载还原
|
||||||
|
│ │ ├── main.js
|
||||||
|
│ │ ├── utils/wasm-loader.js
|
||||||
|
│ │ ├── utils/env-patch.js
|
||||||
|
│ │ └── package.json
|
||||||
|
│ └── browser-auto/ # 模式D:浏览器自动化
|
||||||
|
│ ├── main.js
|
||||||
|
│ └── package.json
|
||||||
|
│
|
||||||
|
└── examples/ # 示例
|
||||||
|
└── demo-analysis.md # 完整分析流程示例
|
||||||
|
```
|
||||||
|
|
||||||
|
## 快速开始
|
||||||
|
|
||||||
|
### 1. 环境检查
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash scripts/check-deps.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 作为 Cursor Skill 使用
|
||||||
|
|
||||||
|
将本目录放置到 `.cursor/skills/` 下,AI Agent 会自动读取 `SKILL.md` 获取逆向分析能力。
|
||||||
|
|
||||||
|
### 3. 工具脚本
|
||||||
|
|
||||||
|
**加密算法识别**:
|
||||||
|
```bash
|
||||||
|
node scripts/crypto-identifier.js "e10adc3949ba59abbe56e057f20f883e"
|
||||||
|
```
|
||||||
|
|
||||||
|
**生成 Hook 代码**:
|
||||||
|
```bash
|
||||||
|
node scripts/hook-generator.js --type=cookie
|
||||||
|
node scripts/hook-generator.js --type=xhr --target="/api/data"
|
||||||
|
node scripts/hook-generator.js --type=all
|
||||||
|
```
|
||||||
|
|
||||||
|
**VM 沙箱执行**:
|
||||||
|
```bash
|
||||||
|
node scripts/sandbox-runner.js obfuscated.js --extract-cookie --verbose
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. 使用项目模板
|
||||||
|
|
||||||
|
根据分析结果选择合适的模板:
|
||||||
|
|
||||||
|
| 场景 | 模板 | 何时使用 |
|
||||||
|
|------|------|---------|
|
||||||
|
| 标准加密算法 | `templates/node-request/` | 加密可用 crypto 库还原 |
|
||||||
|
| 动态 Cookie | `templates/vm-sandbox/` | 服务端返回 JS 生成 Cookie |
|
||||||
|
| WASM 加密 | `templates/wasm-loader/` | 加密在 .wasm 中实现 |
|
||||||
|
| TLS/环境依赖 | `templates/browser-auto/` | 无法脱离浏览器环境 |
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp -r templates/node-request/ my_project/
|
||||||
|
cd my_project && npm install
|
||||||
|
# 修改 main.js 中的配置和加密逻辑
|
||||||
|
node main.js
|
||||||
|
```
|
||||||
|
|
||||||
|
## MCP 工具集成
|
||||||
|
|
||||||
|
本 Skill 深度集成两个 MCP 服务器:
|
||||||
|
|
||||||
|
### js-reverse MCP
|
||||||
|
|
||||||
|
| 核心工具 | 用途 |
|
||||||
|
|---------|------|
|
||||||
|
| `search_in_sources` | 在所有 JS 源码中搜索加密关键词 |
|
||||||
|
| `set_breakpoint_on_text` | 按代码文本设置断点 |
|
||||||
|
| `trace_function` | 追踪函数调用,自动记录参数和返回值 |
|
||||||
|
| `inject_before_load` | 页面加载前注入 Hook 脚本 |
|
||||||
|
| `break_on_xhr` | XHR 请求断点 |
|
||||||
|
| `get_paused_info` | 获取断点处的调用栈和变量 |
|
||||||
|
| `save_script_source` | 保存脚本到本地分析 |
|
||||||
|
|
||||||
|
### chrome-devtools MCP
|
||||||
|
|
||||||
|
| 核心工具 | 用途 |
|
||||||
|
|---------|------|
|
||||||
|
| `new_page` / `navigate_page` | 页面导航 |
|
||||||
|
| `click` / `fill` / `type_text` | UI 交互 |
|
||||||
|
| `evaluate_script` | 在页面执行 JS |
|
||||||
|
| `take_screenshot` / `take_snapshot` | 截图/DOM快照 |
|
||||||
|
| `emulate` | 模拟 UA/设备/网络 |
|
||||||
|
| `get_network_request` | 获取请求详情(支持保存响应体) |
|
||||||
|
|
||||||
|
详见 `references/mcp-cookbook.md`。
|
||||||
|
|
||||||
|
## 逆向场景覆盖
|
||||||
|
|
||||||
|
| 场景 | 技术点 | 参考文档 |
|
||||||
|
|------|--------|---------|
|
||||||
|
| 请求参数签名 | MD5/HMAC/自定义哈希 | `crypto-patterns.md` |
|
||||||
|
| 动态 Cookie | eval首包/api预热/指纹Cookie | `hook-techniques.md` |
|
||||||
|
| 响应数据加密 | AES/DES/RC4解密 | `crypto-patterns.md` |
|
||||||
|
| OB混淆 | 字符串还原/CFF/变量重命名 | `obfuscation-guide.md` |
|
||||||
|
| 自定义VM/JSVMP | I/O追踪/Hook解释器 | `obfuscation-guide.md` |
|
||||||
|
| WASM加密 | 加载/环境补全/导出函数调用 | `environment-patch.md` |
|
||||||
|
| TLS指纹 | 浏览器自动化/curl-impersonate | `protocol-analysis.md` |
|
||||||
|
| HTTP/2 | Node.js http2模块 | `protocol-analysis.md` |
|
||||||
|
| 字体反爬 | 字体文件解析/CMAP映射 | SKILL.md |
|
||||||
|
| WebSocket | WS消息分析/协议解析 | `mcp-cookbook.md` |
|
||||||
|
| 反调试 | debugger绕过/检测对抗 | `anti-debug.md` |
|
||||||
|
| 环境检测 | webdriver/蜜罐/指纹 | `environment-patch.md` |
|
||||||
|
|
||||||
|
## 技术栈
|
||||||
|
|
||||||
|
- **Node.js**: 运行时环境
|
||||||
|
- **crypto** (内置): MD5/SHA/AES/DES/HMAC
|
||||||
|
- **crypto-js**: CryptoJS 兼容实现
|
||||||
|
- **node-forge**: RSA/PKI
|
||||||
|
- **axios**: HTTP 请求
|
||||||
|
- **vm** (内置): 沙箱执行
|
||||||
|
- **WebAssembly** (内置): WASM 加载
|
||||||
|
- **http2** (内置): HTTP/2 请求
|
||||||
|
- **playwright-core**: 浏览器自动化
|
||||||
@@ -0,0 +1,426 @@
|
|||||||
|
---
|
||||||
|
name: hello_js_reverse_skill
|
||||||
|
description: >
|
||||||
|
通用 JavaScript 逆向工程技能:通过浏览器动态调试与静态源码分析,定位并还原网站加密/签名逻辑,
|
||||||
|
使用 Node.js 实现算法复现与模拟请求,自动化完成数据采集任务。
|
||||||
|
深度集成 js-reverse MCP(源码分析、断点调试、反混淆)与 chrome-devtools MCP(页面交互、网络监听、脚本注入)。
|
||||||
|
argument-hint: "<目标URL> [需要分析的加密参数名, 如 sign, m, token]"
|
||||||
|
---
|
||||||
|
|
||||||
|
# JS 逆向工程技能
|
||||||
|
|
||||||
|
你是一名专精 JavaScript 逆向工程的 Node.js 开发工程师。你的核心能力是:针对任意目标网站,
|
||||||
|
通过逆向分析还原前端加密/签名逻辑,编写 Node.js 脚本实现算法复现与自动化数据采集。
|
||||||
|
|
||||||
|
## 核心武器
|
||||||
|
|
||||||
|
你拥有两个 MCP 工具集:
|
||||||
|
|
||||||
|
1. **js-reverse MCP**:JS 源码的静态分析利器
|
||||||
|
- `search_in_sources`:在所有已加载 JS 中搜索关键词(参数名、加密函数特征)
|
||||||
|
- `get_script_source` / `save_script_source`:获取/保存脚本源码
|
||||||
|
- `set_breakpoint_on_text`:按代码文本设置断点
|
||||||
|
- `trace_function`:追踪函数调用,记录入参和返回值
|
||||||
|
- `break_on_xhr`:在 XHR/Fetch 请求匹配时暂停
|
||||||
|
- `inject_before_load`:页面加载前注入 Hook 脚本
|
||||||
|
- `evaluate_script`:在页面上下文执行 JS
|
||||||
|
- `get_paused_info`:获取断点处的调用栈和变量
|
||||||
|
- `step_into` / `step_over` / `step_out`:单步调试
|
||||||
|
- `list_network_requests` / `get_network_request`:网络请求监控
|
||||||
|
- `list_websocket_connections` / `analyze_websocket_messages`:WebSocket 分析
|
||||||
|
|
||||||
|
2. **chrome-devtools MCP**:浏览器自动化与交互
|
||||||
|
- `new_page` / `navigate_page`:页面导航
|
||||||
|
- `click` / `fill` / `type_text`:UI 交互
|
||||||
|
- `evaluate_script`:执行 JS 脚本
|
||||||
|
- `list_network_requests` / `get_network_request`:网络请求(含响应体保存)
|
||||||
|
- `take_screenshot` / `take_snapshot`:截图与 DOM 快照
|
||||||
|
- `emulate`:模拟 User-Agent、视口、网络条件
|
||||||
|
- `wait_for`:等待页面元素/文本
|
||||||
|
|
||||||
|
**原则:能用 MCP 就不手动,能自动化就不要求用户操作。两个 MCP 交替配合形成分析闭环。**
|
||||||
|
|
||||||
|
## 工作流程
|
||||||
|
|
||||||
|
### Phase 0:任务理解与项目初始化
|
||||||
|
|
||||||
|
收到用户的目标 URL 和分析需求后:
|
||||||
|
|
||||||
|
1. 明确分析目标:需要还原哪些加密参数、目标数据是什么
|
||||||
|
2. 创建项目目录(以目标网站/功能命名),结构参考 `templates/` 下的模板
|
||||||
|
3. 确认 MCP 工具可用:调用 `list_pages` 验证浏览器连接
|
||||||
|
|
||||||
|
### Phase 1:目标侦察(自动执行)
|
||||||
|
|
||||||
|
使用 MCP 工具完成以下侦察,**不需要用户手动操作**:
|
||||||
|
|
||||||
|
#### 1.1 页面加载与观察
|
||||||
|
|
||||||
|
```
|
||||||
|
Actions:
|
||||||
|
- [chrome-devtools] new_page → 打开目标 URL
|
||||||
|
- [chrome-devtools] take_snapshot → 获取页面结构和内容
|
||||||
|
- [chrome-devtools] take_screenshot → 截取页面视觉状态
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 1.2 网络请求捕获
|
||||||
|
|
||||||
|
```
|
||||||
|
Actions:
|
||||||
|
- [js-reverse] list_network_requests → 获取所有网络请求列表
|
||||||
|
- [js-reverse] get_network_request → 获取关键数据接口的详细信息
|
||||||
|
重点关注:
|
||||||
|
- Request URL、Method
|
||||||
|
- Request Headers(Cookie、自定义签名头)
|
||||||
|
- Query Params / Request Body(识别加密参数)
|
||||||
|
- Response 数据结构
|
||||||
|
- [chrome-devtools] evaluate_script → 触发翻页/交互,产生更多请求
|
||||||
|
- 重复上述步骤,收集多次请求进行对比
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 1.3 加密参数识别
|
||||||
|
|
||||||
|
对比多次请求,分析每个参数:
|
||||||
|
- **固定值**:直接硬编码或从页面提取
|
||||||
|
- **动态值**:判断变化因子(时间戳、页码、随机数、自增计数器)
|
||||||
|
- **加密值**:根据长度、字符集、格式初步判断算法类型
|
||||||
|
|
||||||
|
#### 1.4 输出侦察报告
|
||||||
|
|
||||||
|
```
|
||||||
|
📋 目标信息
|
||||||
|
━━━━━━━━━━━━━━━━━━━━━━━━
|
||||||
|
目标网站:[URL]
|
||||||
|
分析目标:[需要还原的加密逻辑]
|
||||||
|
数据接口:[API endpoint]
|
||||||
|
|
||||||
|
🔗 接口参数分析
|
||||||
|
━━━━━━━━━━━━━━━━━━━━━━━━
|
||||||
|
URL:[完整请求URL]
|
||||||
|
Method:GET/POST
|
||||||
|
Headers:
|
||||||
|
- Cookie: [关键字段及示例]
|
||||||
|
- [自定义头]: [示例值]
|
||||||
|
加密参数:
|
||||||
|
- 参数名: [名称] | 示例值: [值] | 长度: [N] | 字符集: [hex/base64/...] | 初步猜测: [算法]
|
||||||
|
|
||||||
|
📊 响应数据样本
|
||||||
|
━━━━━━━━━━━━━━━━━━━━━━━━
|
||||||
|
[前2-3条数据]
|
||||||
|
|
||||||
|
🧠 逆向技能点分析
|
||||||
|
━━━━━━━━━━━━━━━━━━━━━━━━
|
||||||
|
本目标涉及的逆向技能点:
|
||||||
|
1. [如:OB混淆还原]
|
||||||
|
2. [如:动态Cookie生成]
|
||||||
|
3. [如:AES-CBC加密]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Phase 2:静态分析(使用 js-reverse MCP)
|
||||||
|
|
||||||
|
根据 Phase 1 识别到的加密参数,深入 JS 源码:
|
||||||
|
|
||||||
|
#### 2.1 关键词搜索定位
|
||||||
|
|
||||||
|
```
|
||||||
|
Actions:
|
||||||
|
- [js-reverse] search_in_sources(query="加密参数名")
|
||||||
|
- [js-reverse] search_in_sources(query="encrypt|sign|token|md5|sha|aes|des|rsa|hmac|btoa|atob|CryptoJS")
|
||||||
|
- [js-reverse] search_in_sources(query="XMLHttpRequest|$.ajax|fetch|beforeSend")
|
||||||
|
- [js-reverse] search_in_sources(query="document.cookie")
|
||||||
|
|
||||||
|
根据搜索结果:
|
||||||
|
- [js-reverse] get_script_source → 读取包含加密逻辑的源码片段
|
||||||
|
- [js-reverse] save_script_source → 保存关键脚本到本地分析
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2.2 混淆识别与还原
|
||||||
|
|
||||||
|
参考 `references/obfuscation-guide.md` 识别混淆类型:
|
||||||
|
|
||||||
|
| 混淆类型 | 特征 | 还原策略 |
|
||||||
|
|---------|------|---------|
|
||||||
|
| OB 混淆 (obfuscator.io) | `_0x` 前缀变量、十六进制字符串数组 | 字符串解密 + 变量重命名 |
|
||||||
|
| 控制流平坦化 (CFF) | `switch-case` 状态机、`while(true)` 循环 | 追踪状态转移还原执行顺序 |
|
||||||
|
| eval/Function 打包 | `eval(...)` 或 `new Function(...)` 包裹 | Hook eval/Function 拦截源码 |
|
||||||
|
| AAEncode/JJEncode | 颜文字、符号编码 | 直接 eval 获取原始代码 |
|
||||||
|
| JSFuck | `[]!+` 字符组合 | 浏览器执行还原 |
|
||||||
|
| 字符串编码 | `\x48\x65\x6c\x6c\x6f`、Unicode 转义 | 解码还原可读字符串 |
|
||||||
|
| 自定义 VM/字节码 | 超大数组 + 解释器循环 | 追踪解释器执行流,提取有效操作 |
|
||||||
|
| JSVMP | 200KB+ 文件、自定义解释器 | 不反编译,通过 I/O 定位关键函数 |
|
||||||
|
|
||||||
|
```
|
||||||
|
Actions:
|
||||||
|
- [js-reverse] get_script_source → 获取混淆代码片段
|
||||||
|
- 使用 scripts/deobfuscate.js 进行基础反混淆
|
||||||
|
- [js-reverse] evaluate_script → 在浏览器中执行解密/还原操作
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2.3 调用链追踪
|
||||||
|
|
||||||
|
```
|
||||||
|
Actions:
|
||||||
|
- [js-reverse] break_on_xhr(url="数据接口路径")
|
||||||
|
- [js-reverse] get_paused_info → 获取完整调用栈
|
||||||
|
- 从调用栈中逐层定位:请求发送 → 参数构造 → 加密函数 → 密钥/明文来源
|
||||||
|
- [js-reverse] set_breakpoint_on_text(text="加密函数名") → 设置断点
|
||||||
|
- [js-reverse] get_paused_info(includeScopes=true) → 获取局部变量和闭包变量
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2.4 提取核心逻辑
|
||||||
|
|
||||||
|
将加密相关函数及其完整依赖链提取保存:
|
||||||
|
|
||||||
|
```
|
||||||
|
Actions:
|
||||||
|
- [js-reverse] save_script_source → 保存完整脚本
|
||||||
|
- 手动提取关键函数到 config/encrypt.js
|
||||||
|
- 用中文注释标注每个函数的作用、输入输出
|
||||||
|
```
|
||||||
|
|
||||||
|
### Phase 3:动态验证(js-reverse + chrome-devtools 配合)
|
||||||
|
|
||||||
|
对静态分析的结论进行运行时验证:
|
||||||
|
|
||||||
|
#### 3.1 Hook 注入验证
|
||||||
|
|
||||||
|
```
|
||||||
|
Actions:
|
||||||
|
- [js-reverse] inject_before_load(script=HookScript)
|
||||||
|
注入以下 Hook(参考 references/hook-techniques.md):
|
||||||
|
|
||||||
|
① Cookie setter Hook — 捕获动态 Cookie 生成
|
||||||
|
② XHR/Fetch Hook — 拦截请求参数
|
||||||
|
③ 加密函数 Hook — 记录入参和返回值
|
||||||
|
④ eval/Function Hook — 捕获动态代码
|
||||||
|
|
||||||
|
- [js-reverse] navigate_page(type="reload") → 重新加载触发 Hook
|
||||||
|
- [js-reverse] list_console_messages → 读取 Hook 输出
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3.2 断点调试确认
|
||||||
|
|
||||||
|
```
|
||||||
|
Actions:
|
||||||
|
- [js-reverse] set_breakpoint_on_text(text="关键代码片段")
|
||||||
|
- 触发目标操作(翻页、提交等)
|
||||||
|
- [js-reverse] get_paused_info → 确认运行时变量值
|
||||||
|
- [js-reverse] step_into / step_over → 单步跟踪执行流程
|
||||||
|
|
||||||
|
重点确认:
|
||||||
|
- 加密算法的具体模式(AES 的 ECB/CBC、填充方式、密钥长度)
|
||||||
|
- 参数拼接顺序和格式
|
||||||
|
- 时间戳精度(秒 vs 毫秒)
|
||||||
|
- 密钥/IV 的来源(硬编码 vs 服务端返回 vs 动态计算)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3.3 多次请求对比
|
||||||
|
|
||||||
|
```
|
||||||
|
Actions:
|
||||||
|
- [chrome-devtools] evaluate_script → 触发多次数据请求
|
||||||
|
- [js-reverse] list_network_requests → 收集多次请求
|
||||||
|
- 对比加密参数变化规律,确认变化因子
|
||||||
|
```
|
||||||
|
|
||||||
|
### Phase 4:Node.js 算法还原
|
||||||
|
|
||||||
|
根据分析结论选择适当的解法模式(参考 `references/workflow-overview.md`),生成 Node.js 代码:
|
||||||
|
|
||||||
|
#### 解法模式选择
|
||||||
|
|
||||||
|
| 模式 | 适用场景 | 模板 |
|
||||||
|
|------|---------|------|
|
||||||
|
| A: 纯 Node.js 算法还原 | 加密逻辑可完整提取,无浏览器环境依赖 | `templates/node-request/` |
|
||||||
|
| B: Node vm 沙箱执行 | 服务端返回混淆 JS 用于生成 Cookie/Token | `templates/vm-sandbox/` |
|
||||||
|
| C: WASM 加载还原 | 加密逻辑在 WebAssembly 中实现 | `templates/wasm-loader/` |
|
||||||
|
| D: 浏览器自动化 | TLS 指纹检测、复杂环境依赖、无法脱离浏览器 | `templates/browser-auto/` |
|
||||||
|
|
||||||
|
#### 编码原则
|
||||||
|
|
||||||
|
1. **先通后全**:先成功请求到第 1 页/第 1 条数据,验证加密正确后再扩展
|
||||||
|
2. **优先纯 Node.js**:标准算法(MD5/SHA/AES/DES/RSA/HMAC/Base64)使用 `crypto` 或 `crypto-js` 还原
|
||||||
|
3. **中间值对比**:打印关键中间值(明文、密文、签名),与浏览器抓包值逐一比对
|
||||||
|
4. **配置外置**:密钥、Headers 模板等写入独立配置文件
|
||||||
|
5. **错误处理**:包含重试机制、频率控制、异常告警
|
||||||
|
|
||||||
|
#### 项目结构
|
||||||
|
|
||||||
|
```
|
||||||
|
project_name/
|
||||||
|
├── config/
|
||||||
|
│ ├── encrypt.js # 提取的原始 JS 加密代码(当需要 vm 执行时)
|
||||||
|
│ └── keys.json # 密钥、IV 等配置
|
||||||
|
├── utils/
|
||||||
|
│ ├── encrypt.js # 加密参数生成函数(Node.js 还原)
|
||||||
|
│ └── request.js # 请求封装(Headers、Cookie、重试)
|
||||||
|
├── main.js # 主脚本:采集数据 + 计算/输出结果
|
||||||
|
├── package.json # 依赖管理
|
||||||
|
└── README.md # 解题/分析思路记录
|
||||||
|
```
|
||||||
|
|
||||||
|
### Phase 5:验证与交付
|
||||||
|
|
||||||
|
```
|
||||||
|
Actions:
|
||||||
|
1. 运行 main.js,确认输出正确数据
|
||||||
|
2. 与浏览器实际数据交叉验证
|
||||||
|
3. 生成 README.md,记录:
|
||||||
|
- 目标信息与接口分析
|
||||||
|
- 加密逻辑还原过程
|
||||||
|
- 涉及的逆向技能点
|
||||||
|
- 运行方式与依赖说明
|
||||||
|
```
|
||||||
|
|
||||||
|
## 常见逆向场景速查
|
||||||
|
|
||||||
|
### 场景 1:请求参数签名(sign/m/token)
|
||||||
|
|
||||||
|
```
|
||||||
|
特征:请求 URL 或 Body 中包含看似随机的签名参数
|
||||||
|
定位:搜索参数名 → 追踪赋值来源 → 定位签名函数
|
||||||
|
常见算法:MD5(拼接字符串)、HMAC-SHA256、自定义哈希
|
||||||
|
MCP 操作:
|
||||||
|
- search_in_sources(query="sign=|m=|token=")
|
||||||
|
- break_on_xhr(url="接口路径") → get_paused_info
|
||||||
|
- trace_function(functionName="签名函数名")
|
||||||
|
```
|
||||||
|
|
||||||
|
### 场景 2:动态 Cookie 生成
|
||||||
|
|
||||||
|
```
|
||||||
|
特征:Cookie 中有频繁变化的字段,页面 JS 动态写入
|
||||||
|
定位:Hook document.cookie setter → 追踪写入来源
|
||||||
|
类型:
|
||||||
|
a. eval 首包:请求返回混淆 JS → eval 执行 → 写入 Cookie
|
||||||
|
b. 预热请求:/api2 等接口返回 JS → 注入 window 变量 → 计算 Cookie
|
||||||
|
c. 指纹 Cookie:收集浏览器信息 → base64 编码 → 写入
|
||||||
|
MCP 操作:
|
||||||
|
- inject_before_load(script="Cookie setter Hook")
|
||||||
|
- evaluate_script(function="document.cookie")
|
||||||
|
- list_network_requests → 识别预热请求
|
||||||
|
```
|
||||||
|
|
||||||
|
### 场景 3:响应数据加密
|
||||||
|
|
||||||
|
```
|
||||||
|
特征:接口返回的不是明文 JSON,而是加密字符串
|
||||||
|
定位:Hook JSON.parse 或定位解密函数入口
|
||||||
|
常见算法:AES-CBC/ECB、DES、RC4、自定义异或
|
||||||
|
MCP 操作:
|
||||||
|
- search_in_sources(query="decrypt|JSON.parse|atob")
|
||||||
|
- set_breakpoint_on_text(text="解密函数") → step_into
|
||||||
|
```
|
||||||
|
|
||||||
|
### 场景 4:JS 混淆/OB 混淆
|
||||||
|
|
||||||
|
```
|
||||||
|
特征:大量 _0x 前缀变量、十六进制字符串数组、控制流平坦化
|
||||||
|
还原:字符串数组还原 → 变量重命名 → 控制流平坦化还原
|
||||||
|
MCP 操作:
|
||||||
|
- save_script_source → 保存完整混淆代码
|
||||||
|
- search_in_sources → 搜索关键逻辑
|
||||||
|
- evaluate_script → 在浏览器执行解密函数还原字符串
|
||||||
|
```
|
||||||
|
|
||||||
|
### 场景 5:WASM 加密
|
||||||
|
|
||||||
|
```
|
||||||
|
特征:加密函数调用 WebAssembly 导出函数
|
||||||
|
还原:Node.js 直接加载 .wasm 文件,调用导出函数
|
||||||
|
注意:检查 wasm imports,可能需要补环境
|
||||||
|
MCP 操作:
|
||||||
|
- search_in_sources(query="WebAssembly|.wasm|instantiate")
|
||||||
|
- list_network_requests(resourceTypes=["Other"]) → 找 .wasm 文件
|
||||||
|
- evaluate_script → 测试 wasm 函数的 I/O
|
||||||
|
```
|
||||||
|
|
||||||
|
### 场景 6:TLS 指纹/协议检测
|
||||||
|
|
||||||
|
```
|
||||||
|
特征:算法全对但请求仍失败(token failed / 403)
|
||||||
|
原因:服务器通过 TLS Client Hello 或 HTTP 协议版本识别客户端
|
||||||
|
解法:
|
||||||
|
a. 使用浏览器自动化(Playwright/Puppeteer)
|
||||||
|
b. 使用支持自定义 TLS 指纹的库(如 curl-impersonate)
|
||||||
|
c. 使用 HTTP/2 协议(Node.js http2 模块)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 场景 7:WebSocket 通信
|
||||||
|
|
||||||
|
```
|
||||||
|
特征:数据通过 WebSocket 传输,非 HTTP 接口
|
||||||
|
MCP 操作:
|
||||||
|
- list_websocket_connections → 识别 WS 连接
|
||||||
|
- analyze_websocket_messages → 分析消息格式和模式
|
||||||
|
- get_websocket_messages → 获取具体消息内容
|
||||||
|
```
|
||||||
|
|
||||||
|
### 场景 8:字体反爬
|
||||||
|
|
||||||
|
```
|
||||||
|
特征:页面数字/文字使用自定义字体,复制出来是乱码
|
||||||
|
还原:下载字体文件 → 解析 CMAP 映射表 → 建立字符映射关系
|
||||||
|
MCP 操作:
|
||||||
|
- list_network_requests(resourceTypes=["Font"]) → 找字体文件
|
||||||
|
- evaluate_script → 读取页面实际渲染的文字
|
||||||
|
```
|
||||||
|
|
||||||
|
## 反调试对抗策略
|
||||||
|
|
||||||
|
参考 `references/anti-debug.md` 处理以下常见反调试手段:
|
||||||
|
|
||||||
|
| 手段 | 绕过方法 |
|
||||||
|
|------|---------|
|
||||||
|
| `debugger` 死循环 | `inject_before_load` 覆写 `debugger` 语句,或在 DevTools 中 Never pause here |
|
||||||
|
| `setInterval` 检测开发者工具 | Hook `setInterval`,过滤检测函数 |
|
||||||
|
| `console.log` 检测 | 不覆写原生函数,使用 `inject_before_load` 在检测之前收集数据 |
|
||||||
|
| `Function.toString()` 检测 | 使用 Proxy 代理而非直接覆写 |
|
||||||
|
| 格式化检测(函数 toString 含换行) | 保持函数单行格式 |
|
||||||
|
| `Object.defineProperty` 篡改 | 保存原始引用,在注入脚本中使用 |
|
||||||
|
| 内存/时间差检测 | 使用非侵入式 Hook(logpoint 而非断点) |
|
||||||
|
|
||||||
|
## 工具使用最佳实践
|
||||||
|
|
||||||
|
### MCP 工具配合模式
|
||||||
|
|
||||||
|
```
|
||||||
|
静态定位 → 动态验证 → 补充分析 → 再次验证
|
||||||
|
↓ ↓ ↓ ↓
|
||||||
|
js-reverse js-reverse js-reverse js-reverse
|
||||||
|
搜索源码 设置断点 扩展搜索 trace函数
|
||||||
|
获取变量 读取更多源码
|
||||||
|
```
|
||||||
|
|
||||||
|
### 效率原则
|
||||||
|
|
||||||
|
1. **先 Network 后 Sources**:先确定数据接口和参数格式,再去源码中搜索
|
||||||
|
2. **先验证 I/O 后反编译**:确认函数输入输出正确,不要通读混淆代码
|
||||||
|
3. **先找骨架函数后读细节**:搜索关键词锁定入口,不要通读全部代码
|
||||||
|
4. **先纯请求后浏览器**:先尝试最轻量的方案,失败再升级
|
||||||
|
5. **先判断真假分支后还原算法**:混淆代码可能有大量死代码
|
||||||
|
|
||||||
|
### 调试方法论
|
||||||
|
|
||||||
|
1. **五步定位法**:
|
||||||
|
- Network 面板 → 找数据接口和参数格式
|
||||||
|
- `search_in_sources` → 搜索关键词定位源码
|
||||||
|
- 定位入口函数 → `beforeSend`、`$.ajax`、`fetch`、`XMLHttpRequest.open`
|
||||||
|
- 追踪调用链 → 从入口向上找参数生成逻辑
|
||||||
|
- 验证算法 → 用已知输入输出验证还原结果
|
||||||
|
|
||||||
|
2. **大文件分析策略**(50 万字符以上的混淆文件):
|
||||||
|
- 不要通读全文
|
||||||
|
- 搜索关键词锁定骨架:`search_in_sources(query="function sp(|document.cookie|/api")`
|
||||||
|
- 按调用栈逐层展开
|
||||||
|
|
||||||
|
## 经验法则
|
||||||
|
|
||||||
|
1. **Cookie setter 是最高性价比 Hook 点**:比追完整 AST 更快
|
||||||
|
2. **预热请求不是装饰**:`/api2` 类请求在运行时注入关键变量
|
||||||
|
3. **Node vm 沙箱 ≠ 浏览器**:有些反调试只在非浏览器环境触发
|
||||||
|
4. **WASM panic 常常是环境缺失**:`unreachable` 优先怀疑 DOM 缺失
|
||||||
|
5. **先做最小补环境,不要上来就开浏览器**
|
||||||
|
6. **TLS 指纹是终极壁垒**:算法全对但仍失败时,考虑 TLS 指纹
|
||||||
|
7. **真假请求链是常见干扰**:页面表面的 API 调用可能是烟雾弹
|
||||||
|
8. **HTTP/2 可能是唯一的"密码"**:不是所有题都需要 JS 逆向
|
||||||
@@ -0,0 +1,152 @@
|
|||||||
|
# 示例:某网站请求签名逆向分析
|
||||||
|
|
||||||
|
> 这是一个虚拟的分析示例,展示完整的逆向分析流程。
|
||||||
|
|
||||||
|
## Phase 1:目标侦察
|
||||||
|
|
||||||
|
### 目标信息
|
||||||
|
|
||||||
|
- **网站**: https://example.com/data
|
||||||
|
- **目标**: 采集商品列表数据
|
||||||
|
- **接口**: `GET /api/v1/products?page=1&sign=xxx&t=1710000000`
|
||||||
|
|
||||||
|
### 网络请求分析
|
||||||
|
|
||||||
|
使用 MCP 工具捕获请求:
|
||||||
|
|
||||||
|
```
|
||||||
|
[js-reverse] list_network_requests(resourceTypes=["XHR"])
|
||||||
|
→ 发现数据接口: /api/v1/products
|
||||||
|
|
||||||
|
[js-reverse] get_network_request(reqid=42)
|
||||||
|
→ 获取完整请求信息
|
||||||
|
```
|
||||||
|
|
||||||
|
**请求详情**:
|
||||||
|
```
|
||||||
|
URL: https://example.com/api/v1/products?page=1&sign=a1b2c3d4e5f6...&t=1710000000
|
||||||
|
Method: GET
|
||||||
|
Headers:
|
||||||
|
Cookie: session=abc123; token=xyz789
|
||||||
|
X-Request-Id: 9f8e7d6c
|
||||||
|
```
|
||||||
|
|
||||||
|
**加密参数分析**:
|
||||||
|
| 参数 | 示例值 | 长度 | 字符集 | 初步判断 |
|
||||||
|
|------|--------|------|--------|---------|
|
||||||
|
| sign | a1b2c3d4e5f67890... | 32 | hex | MD5 |
|
||||||
|
| t | 1710000000 | 10 | 数字 | 时间戳(秒) |
|
||||||
|
|
||||||
|
## Phase 2:静态分析
|
||||||
|
|
||||||
|
### 搜索加密入口
|
||||||
|
|
||||||
|
```
|
||||||
|
[js-reverse] search_in_sources(query="sign=")
|
||||||
|
→ 在 /static/js/app.js 第 1234 行找到: params.sign = generateSign(page, timestamp)
|
||||||
|
|
||||||
|
[js-reverse] search_in_sources(query="generateSign")
|
||||||
|
→ 在 /static/js/utils.js 第 567 行找到函数定义
|
||||||
|
```
|
||||||
|
|
||||||
|
### 读取关键代码
|
||||||
|
|
||||||
|
```
|
||||||
|
[js-reverse] get_script_source(url="https://example.com/static/js/utils.js", startLine=560, endLine=590)
|
||||||
|
```
|
||||||
|
|
||||||
|
还原后的关键代码:
|
||||||
|
```javascript
|
||||||
|
function generateSign(page, timestamp) {
|
||||||
|
var SECRET = 'my_secret_key_2024';
|
||||||
|
var raw = page + '|' + timestamp + '|' + SECRET;
|
||||||
|
return md5(raw);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Phase 3:动态验证
|
||||||
|
|
||||||
|
### 注入 Hook 验证
|
||||||
|
|
||||||
|
```
|
||||||
|
[js-reverse] inject_before_load(script="XHR Hook + generateSign Hook")
|
||||||
|
[js-reverse] navigate_page(type="reload")
|
||||||
|
[js-reverse] list_console_messages()
|
||||||
|
```
|
||||||
|
|
||||||
|
Hook 输出:
|
||||||
|
```
|
||||||
|
[Hook:Custom] generateSign 调用
|
||||||
|
[Hook:Custom] 参数: [1, 1710000000]
|
||||||
|
[Hook:Custom] 返回: "e10adc3949ba59abbe56e057f20f883e"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 验证 MD5
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const crypto = require('crypto');
|
||||||
|
const raw = '1|1710000000|my_secret_key_2024';
|
||||||
|
const sign = crypto.createHash('md5').update(raw).digest('hex');
|
||||||
|
console.log(sign); // e10adc3949ba59abbe56e057f20f883e ✓ 匹配!
|
||||||
|
```
|
||||||
|
|
||||||
|
## Phase 4:Node.js 还原
|
||||||
|
|
||||||
|
选择 **模式A:纯 Node.js 算法还原**。
|
||||||
|
|
||||||
|
### 加密函数
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const crypto = require('crypto');
|
||||||
|
|
||||||
|
const SECRET = 'my_secret_key_2024';
|
||||||
|
|
||||||
|
function generateSign(page, timestamp) {
|
||||||
|
const raw = `${page}|${timestamp}|${SECRET}`;
|
||||||
|
return crypto.createHash('md5').update(raw).digest('hex');
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 请求脚本
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
async function fetchPage(page) {
|
||||||
|
const t = Math.floor(Date.now() / 1000);
|
||||||
|
const sign = generateSign(page, t);
|
||||||
|
|
||||||
|
const response = await axios.get('https://example.com/api/v1/products', {
|
||||||
|
params: { page, sign, t },
|
||||||
|
headers: {
|
||||||
|
Cookie: 'session=abc123',
|
||||||
|
'User-Agent': 'Mozilla/5.0 ...',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Phase 5:验证
|
||||||
|
|
||||||
|
运行结果:
|
||||||
|
```
|
||||||
|
[*] 项目:商品数据采集
|
||||||
|
[+] 正在采集第 1/5 页... ✓ 获取 20 条数据
|
||||||
|
[+] 正在采集第 2/5 页... ✓ 获取 20 条数据
|
||||||
|
[+] 正在采集第 3/5 页... ✓ 获取 20 条数据
|
||||||
|
[+] 正在采集第 4/5 页... ✓ 获取 20 条数据
|
||||||
|
[+] 正在采集第 5/5 页... ✓ 获取 20 条数据
|
||||||
|
[+] 采集完成,共 100 条数据
|
||||||
|
|
||||||
|
============================== 计算结果 ==============================
|
||||||
|
答案:125890
|
||||||
|
======================================================================
|
||||||
|
```
|
||||||
|
|
||||||
|
## 涉及的逆向技能点
|
||||||
|
|
||||||
|
1. Network 抓包分析
|
||||||
|
2. JS 源码搜索定位
|
||||||
|
3. MD5 签名还原
|
||||||
|
4. XHR Hook 验证
|
||||||
|
5. Node.js 纯算法模拟请求
|
||||||
@@ -0,0 +1,337 @@
|
|||||||
|
# 反调试对抗手册
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
|
||||||
|
现代网站常采用多种反调试技术阻止逆向分析。本手册涵盖常见反调试手段及其绕过策略。
|
||||||
|
|
||||||
|
## 1. debugger 语句
|
||||||
|
|
||||||
|
### 表现
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 无限 debugger 循环
|
||||||
|
setInterval(function() { debugger; }, 100);
|
||||||
|
|
||||||
|
// 条件 debugger
|
||||||
|
(function check() {
|
||||||
|
debugger;
|
||||||
|
check();
|
||||||
|
})();
|
||||||
|
|
||||||
|
// 隐藏 debugger(通过 constructor)
|
||||||
|
(function() {
|
||||||
|
var a = new Function("debugger");
|
||||||
|
setInterval(a, 1000);
|
||||||
|
})();
|
||||||
|
```
|
||||||
|
|
||||||
|
### 绕过方案
|
||||||
|
|
||||||
|
**方案 A:MCP 注入绕过**
|
||||||
|
```
|
||||||
|
[js-reverse] inject_before_load(script=反debugger脚本)
|
||||||
|
```
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 覆写 Function 构造器,过滤 debugger
|
||||||
|
const _Function = Function;
|
||||||
|
Function = function() {
|
||||||
|
const body = arguments[arguments.length - 1];
|
||||||
|
if (typeof body === 'string' && body.indexOf('debugger') !== -1) {
|
||||||
|
arguments[arguments.length - 1] = body.replace(/debugger/g, '');
|
||||||
|
}
|
||||||
|
return _Function.apply(this, arguments);
|
||||||
|
};
|
||||||
|
Function.prototype = _Function.prototype;
|
||||||
|
|
||||||
|
// 覆写定时器,过滤 debugger
|
||||||
|
const _setInterval = setInterval;
|
||||||
|
setInterval = function(fn, ms) {
|
||||||
|
if (typeof fn === 'function' && fn.toString().indexOf('debugger') !== -1) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (typeof fn === 'string' && fn.indexOf('debugger') !== -1) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
return _setInterval.apply(this, arguments);
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
**方案 B:断点条件设置 false**
|
||||||
|
```
|
||||||
|
[js-reverse] set_breakpoint_on_text(text="debugger", condition="false")
|
||||||
|
```
|
||||||
|
|
||||||
|
## 2. 开发者工具检测
|
||||||
|
|
||||||
|
### 2.1 窗口尺寸检测
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
setInterval(function() {
|
||||||
|
if (window.outerHeight - window.innerHeight > 200 ||
|
||||||
|
window.outerWidth - window.innerWidth > 200) {
|
||||||
|
// DevTools 已打开
|
||||||
|
document.body.innerHTML = '';
|
||||||
|
}
|
||||||
|
}, 500);
|
||||||
|
```
|
||||||
|
|
||||||
|
**绕过**:
|
||||||
|
```javascript
|
||||||
|
// 注入后锁定尺寸
|
||||||
|
Object.defineProperty(window, 'outerHeight', { get: () => window.innerHeight });
|
||||||
|
Object.defineProperty(window, 'outerWidth', { get: () => window.innerWidth });
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.2 console.log 计时检测
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
setInterval(function() {
|
||||||
|
var start = Date.now();
|
||||||
|
console.log('check');
|
||||||
|
console.clear();
|
||||||
|
if (Date.now() - start > 10) {
|
||||||
|
// DevTools 已打开(console.log 在 DevTools 打开时较慢)
|
||||||
|
window.location = 'about:blank';
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
```
|
||||||
|
|
||||||
|
**绕过**:
|
||||||
|
```javascript
|
||||||
|
// 覆写 console 方法为空操作
|
||||||
|
const noop = function() {};
|
||||||
|
console.log = noop;
|
||||||
|
console.clear = noop;
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.3 toString 检测
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
var devtools = /./;
|
||||||
|
devtools.toString = function() {
|
||||||
|
// 只有在 DevTools 打开时,console.log 才会调用对象的 toString
|
||||||
|
isDevToolsOpen = true;
|
||||||
|
return '';
|
||||||
|
};
|
||||||
|
console.log(devtools);
|
||||||
|
```
|
||||||
|
|
||||||
|
**绕过**:
|
||||||
|
```javascript
|
||||||
|
// 覆写 console.log,阻止 toString 调用检测
|
||||||
|
const _log = console.log;
|
||||||
|
console.log = function() {
|
||||||
|
// 不调用 toString
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. 代码完整性检测
|
||||||
|
|
||||||
|
### 3.1 函数 toString 检测
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
function critical() {
|
||||||
|
// 重要逻辑
|
||||||
|
}
|
||||||
|
if (critical.toString().indexOf('\n') !== -1) {
|
||||||
|
// 代码被格式化,触发反调试
|
||||||
|
while(true) {}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**绕过**:
|
||||||
|
```javascript
|
||||||
|
// 保存原始 toString 结果
|
||||||
|
const origToString = critical.toString();
|
||||||
|
Object.defineProperty(critical, 'toString', {
|
||||||
|
value: function() { return origToString; }
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2 源码长度检测
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
if (someFunction.toString().length !== 1234) {
|
||||||
|
// 代码被修改
|
||||||
|
throw new Error('Integrity check failed');
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**绕过**:不修改原始函数,使用 `inject_before_load` 在执行之前收集所需数据。
|
||||||
|
|
||||||
|
## 4. 时间差检测
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
var t1 = Date.now();
|
||||||
|
// ... 执行代码 ...
|
||||||
|
var t2 = Date.now();
|
||||||
|
if (t2 - t1 > 100) {
|
||||||
|
// 可能在断点处暂停了
|
||||||
|
window.location = 'about:blank';
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**绕过**:
|
||||||
|
```javascript
|
||||||
|
// Hook Date.now 返回连续值
|
||||||
|
const _now = Date.now;
|
||||||
|
let fakeTime = _now();
|
||||||
|
Date.now = function() {
|
||||||
|
fakeTime += 1;
|
||||||
|
return fakeTime;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 或使用 performance.now
|
||||||
|
const _perfNow = performance.now;
|
||||||
|
performance.now = function() {
|
||||||
|
return _perfNow.call(performance);
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5. 环境检测
|
||||||
|
|
||||||
|
### 5.1 Node.js 环境检测
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
if (typeof process !== 'undefined' && process.versions && process.versions.node) {
|
||||||
|
throw new Error('Node.js detected');
|
||||||
|
}
|
||||||
|
if (typeof module !== 'undefined' && module.exports) {
|
||||||
|
throw new Error('CommonJS detected');
|
||||||
|
}
|
||||||
|
if (typeof global !== 'undefined') {
|
||||||
|
throw new Error('Non-browser detected');
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**绕过**:在 vm 沙箱中删除这些全局变量。
|
||||||
|
|
||||||
|
### 5.2 浏览器指纹检测
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
if (!window.chrome || !window.chrome.runtime) {
|
||||||
|
throw new Error('Not Chrome');
|
||||||
|
}
|
||||||
|
if (navigator.webdriver) {
|
||||||
|
throw new Error('WebDriver detected');
|
||||||
|
}
|
||||||
|
if (navigator.plugins.length === 0) {
|
||||||
|
throw new Error('Headless browser detected');
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**绕过**:补全环境变量(参考 `environment-patch.md`)。
|
||||||
|
|
||||||
|
### 5.3 Selenium / Puppeteer 检测
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const checks = [
|
||||||
|
'webdriver' in navigator,
|
||||||
|
'_Selenium_IDE_Recorder' in window,
|
||||||
|
'callSelenium' in document,
|
||||||
|
'__webdriver_script_fn' in document,
|
||||||
|
'$cdc_asdjflasutopfhvcZLmcfl_' in document,
|
||||||
|
'_phantom' in window,
|
||||||
|
'callPhantom' in window
|
||||||
|
];
|
||||||
|
if (checks.some(Boolean)) {
|
||||||
|
throw new Error('Automation detected');
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**绕过**:
|
||||||
|
```javascript
|
||||||
|
// 清除自动化指纹
|
||||||
|
delete navigator.webdriver;
|
||||||
|
Object.defineProperty(navigator, 'webdriver', { get: () => undefined });
|
||||||
|
delete window._Selenium_IDE_Recorder;
|
||||||
|
// ... 逐个清除
|
||||||
|
```
|
||||||
|
|
||||||
|
## 6. Object.defineProperty 篡改
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 改变属性定义行为
|
||||||
|
const _defineProperty = Object.defineProperty;
|
||||||
|
Object.defineProperty = function(obj, prop, descriptor) {
|
||||||
|
if (prop === 'cookie') {
|
||||||
|
return ""; // 阻止 Cookie Hook
|
||||||
|
}
|
||||||
|
return _defineProperty.apply(this, arguments);
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
**绕过**:在注入脚本中保存原始引用。
|
||||||
|
```javascript
|
||||||
|
// 在所有代码之前保存原始方法
|
||||||
|
const originalDefineProperty = Object.defineProperty;
|
||||||
|
// 后续使用 originalDefineProperty 而非 Object.defineProperty
|
||||||
|
```
|
||||||
|
|
||||||
|
## 7. Proxy 检测
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 某些代码会检测对象是否被 Proxy 包装
|
||||||
|
try {
|
||||||
|
new Proxy({}, {});
|
||||||
|
} catch(e) {
|
||||||
|
// 环境异常
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**注意**:使用 Proxy 做 Hook 时,确保 Proxy 行为与原始对象一致。
|
||||||
|
|
||||||
|
## 通用反反调试注入脚本
|
||||||
|
|
||||||
|
以下脚本适合通过 `inject_before_load` 在页面加载前注入:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
(function() {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
// 保存原始方法引用
|
||||||
|
const originals = {
|
||||||
|
defineProperty: Object.defineProperty,
|
||||||
|
getOwnPropertyDescriptor: Object.getOwnPropertyDescriptor,
|
||||||
|
setInterval: window.setInterval,
|
||||||
|
setTimeout: window.setTimeout,
|
||||||
|
Function: window.Function,
|
||||||
|
eval: window.eval,
|
||||||
|
dateNow: Date.now,
|
||||||
|
};
|
||||||
|
|
||||||
|
// 1. 拦截 debugger
|
||||||
|
const _Function = originals.Function;
|
||||||
|
window.Function = function() {
|
||||||
|
let body = arguments[arguments.length - 1];
|
||||||
|
if (typeof body === 'string' && body.includes('debugger')) {
|
||||||
|
arguments[arguments.length - 1] = body.replace(/debugger\s*;?/g, '');
|
||||||
|
}
|
||||||
|
return _Function.apply(this, arguments);
|
||||||
|
};
|
||||||
|
window.Function.prototype = _Function.prototype;
|
||||||
|
|
||||||
|
// 2. 过滤 debugger 定时器
|
||||||
|
const _setInterval = originals.setInterval;
|
||||||
|
window.setInterval = function(fn, ms) {
|
||||||
|
if (typeof fn === 'function' && fn.toString().includes('debugger')) return -1;
|
||||||
|
if (typeof fn === 'string' && fn.includes('debugger')) return -1;
|
||||||
|
return _setInterval.apply(this, arguments);
|
||||||
|
};
|
||||||
|
|
||||||
|
const _setTimeout = originals.setTimeout;
|
||||||
|
window.setTimeout = function(fn, ms) {
|
||||||
|
if (typeof fn === 'function' && fn.toString().includes('debugger')) return -1;
|
||||||
|
if (typeof fn === 'string' && fn.includes('debugger')) return -1;
|
||||||
|
return _setTimeout.apply(this, arguments);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 3. 隐藏 webdriver 属性
|
||||||
|
originals.defineProperty.call(Object, navigator, 'webdriver', {
|
||||||
|
get: () => undefined, configurable: true
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('[AntiDebug] 反反调试脚本已注入');
|
||||||
|
})();
|
||||||
|
```
|
||||||
@@ -0,0 +1,255 @@
|
|||||||
|
# 常见加密模式识别与还原
|
||||||
|
|
||||||
|
## 快速识别表
|
||||||
|
|
||||||
|
| 特征 | 可能的算法 | 验证方法 |
|
||||||
|
|------|-----------|---------|
|
||||||
|
| 32位十六进制 | MD5 | 用已知输入验证 |
|
||||||
|
| 40位十六进制 | SHA-1 | 同上 |
|
||||||
|
| 64位十六进制 | SHA-256 | 同上 |
|
||||||
|
| 128位十六进制 | SHA-512 | 同上 |
|
||||||
|
| `=` 结尾,含 `A-Za-z0-9+/` | Base64 | 直接 atob 解码 |
|
||||||
|
| `=` 结尾,含 `A-Za-z0-9-_` | Base64url | 替换 `-_` 为 `+/` 后解码 |
|
||||||
|
| 固定长度密文,16字节倍数 | AES (128-bit key) | 寻找 key/iv |
|
||||||
|
| 固定长度密文,8字节倍数 | DES / 3DES | 寻找 8字节 key |
|
||||||
|
| 超长数字字符串 | RSA | 寻找公钥 |
|
||||||
|
| `CryptoJS` 关键词 | CryptoJS 库 | 直接搜索源码 |
|
||||||
|
|
||||||
|
## 1. MD5
|
||||||
|
|
||||||
|
### 标准 MD5
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const crypto = require('crypto');
|
||||||
|
function md5(str) {
|
||||||
|
return crypto.createHash('md5').update(str).digest('hex');
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 自定义 MD5(非标准实现)
|
||||||
|
|
||||||
|
某些网站修改了 MD5 的内部参数(如 chrsz=16),导致输出与标准 MD5 不同。
|
||||||
|
|
||||||
|
**识别方法**:用相同输入对比标准 MD5 输出,如果不一致则为自定义实现。
|
||||||
|
|
||||||
|
**还原策略**:必须提取原始 JS 实现,在 Node.js 中直接执行。
|
||||||
|
|
||||||
|
### 常见 MD5 签名模式
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 模式1:简单拼接
|
||||||
|
sign = md5(param1 + param2 + timestamp + secret)
|
||||||
|
|
||||||
|
// 模式2:排序拼接
|
||||||
|
params = Object.keys(data).sort().map(k => k + '=' + data[k]).join('&')
|
||||||
|
sign = md5(params + secret)
|
||||||
|
|
||||||
|
// 模式3:嵌套哈希
|
||||||
|
sign = md5(md5(password) + timestamp)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 2. HMAC
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const crypto = require('crypto');
|
||||||
|
function hmacSha256(message, secret) {
|
||||||
|
return crypto.createHmac('sha256', secret).update(message).digest('hex');
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**识别特征**:搜索 `HMAC`、`createHmac`、`CryptoJS.HmacSHA256`
|
||||||
|
|
||||||
|
## 3. AES
|
||||||
|
|
||||||
|
### AES-CBC
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const crypto = require('crypto');
|
||||||
|
|
||||||
|
function aesEncrypt(plaintext, key, iv) {
|
||||||
|
const cipher = crypto.createCipheriv('aes-128-cbc', key, iv);
|
||||||
|
cipher.setAutoPadding(true); // PKCS7
|
||||||
|
let encrypted = cipher.update(plaintext, 'utf8', 'base64');
|
||||||
|
encrypted += cipher.final('base64');
|
||||||
|
return encrypted;
|
||||||
|
}
|
||||||
|
|
||||||
|
function aesDecrypt(ciphertext, key, iv) {
|
||||||
|
const decipher = crypto.createDecipheriv('aes-128-cbc', key, iv);
|
||||||
|
decipher.setAutoPadding(true);
|
||||||
|
let decrypted = decipher.update(ciphertext, 'base64', 'utf8');
|
||||||
|
decrypted += decipher.final('utf8');
|
||||||
|
return decrypted;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### AES-ECB
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
function aesEcbEncrypt(plaintext, key) {
|
||||||
|
const cipher = crypto.createCipheriv('aes-128-ecb', key, null);
|
||||||
|
cipher.setAutoPadding(true);
|
||||||
|
let encrypted = cipher.update(plaintext, 'utf8', 'base64');
|
||||||
|
encrypted += cipher.final('base64');
|
||||||
|
return encrypted;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### CryptoJS 兼容
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const CryptoJS = require('crypto-js');
|
||||||
|
|
||||||
|
// CryptoJS 默认使用 PKCS7 填充
|
||||||
|
const encrypted = CryptoJS.AES.encrypt(plaintext, CryptoJS.enc.Utf8.parse(key), {
|
||||||
|
iv: CryptoJS.enc.Utf8.parse(iv),
|
||||||
|
mode: CryptoJS.mode.CBC,
|
||||||
|
padding: CryptoJS.pad.Pkcs7
|
||||||
|
}).toString();
|
||||||
|
```
|
||||||
|
|
||||||
|
**关键参数识别**:
|
||||||
|
- key 长度:16字节=AES-128,24字节=AES-192,32字节=AES-256
|
||||||
|
- iv 长度:固定16字节
|
||||||
|
- 模式:CBC(需要iv)、ECB(不需要iv)、CTR、GCM
|
||||||
|
- 填充:PKCS7(最常见)、ZeroPadding、NoPadding
|
||||||
|
- 输出格式:Base64(最常见)、Hex
|
||||||
|
|
||||||
|
## 4. DES / 3DES
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
function desEncrypt(plaintext, key) {
|
||||||
|
const cipher = crypto.createCipheriv('des-ecb', key, null); // 8字节key
|
||||||
|
cipher.setAutoPadding(true);
|
||||||
|
let encrypted = cipher.update(plaintext, 'utf8', 'base64');
|
||||||
|
encrypted += cipher.final('base64');
|
||||||
|
return encrypted;
|
||||||
|
}
|
||||||
|
|
||||||
|
function tripleDesEncrypt(plaintext, key, iv) {
|
||||||
|
const cipher = crypto.createCipheriv('des-ede3-cbc', key, iv); // 24字节key
|
||||||
|
cipher.setAutoPadding(true);
|
||||||
|
let encrypted = cipher.update(plaintext, 'utf8', 'base64');
|
||||||
|
encrypted += cipher.final('base64');
|
||||||
|
return encrypted;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5. RSA
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const crypto = require('crypto');
|
||||||
|
|
||||||
|
function rsaEncrypt(plaintext, publicKey) {
|
||||||
|
const buffer = Buffer.from(plaintext, 'utf8');
|
||||||
|
const encrypted = crypto.publicEncrypt({
|
||||||
|
key: publicKey,
|
||||||
|
padding: crypto.constants.RSA_PKCS1_PADDING
|
||||||
|
}, buffer);
|
||||||
|
return encrypted.toString('base64');
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**使用 node-forge(更灵活)**:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const forge = require('node-forge');
|
||||||
|
|
||||||
|
function rsaEncrypt(plaintext, publicKeyPem) {
|
||||||
|
const publicKey = forge.pki.publicKeyFromPem(publicKeyPem);
|
||||||
|
const encrypted = publicKey.encrypt(plaintext, 'RSAES-PKCS1-V1_5');
|
||||||
|
return forge.util.encode64(encrypted);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**常见 RSA 公钥格式**:
|
||||||
|
- PEM 格式(`-----BEGIN PUBLIC KEY-----`)
|
||||||
|
- 模数(n) + 指数(e) 格式(需要手动构造 PEM)
|
||||||
|
|
||||||
|
## 6. Base64 变体
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 标准 Base64
|
||||||
|
const encoded = Buffer.from(str).toString('base64');
|
||||||
|
const decoded = Buffer.from(encoded, 'base64').toString();
|
||||||
|
|
||||||
|
// Base64url(URL安全变体)
|
||||||
|
function base64url(str) {
|
||||||
|
return Buffer.from(str).toString('base64')
|
||||||
|
.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 自定义字符表 Base64
|
||||||
|
function customBase64(str, table) {
|
||||||
|
const std = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
|
||||||
|
const b64 = Buffer.from(str).toString('base64');
|
||||||
|
return b64.split('').map(c => {
|
||||||
|
const idx = std.indexOf(c);
|
||||||
|
return idx >= 0 ? table[idx] : c;
|
||||||
|
}).join('');
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 7. 异或加密(XOR)
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
function xorEncrypt(plaintext, key) {
|
||||||
|
const result = [];
|
||||||
|
for (let i = 0; i < plaintext.length; i++) {
|
||||||
|
result.push(plaintext.charCodeAt(i) ^ key.charCodeAt(i % key.length));
|
||||||
|
}
|
||||||
|
return Buffer.from(result).toString('hex');
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 8. RC4
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
function rc4(data, key) {
|
||||||
|
const s = Array.from({ length: 256 }, (_, i) => i);
|
||||||
|
let j = 0;
|
||||||
|
for (let i = 0; i < 256; i++) {
|
||||||
|
j = (j + s[i] + key.charCodeAt(i % key.length)) % 256;
|
||||||
|
[s[i], s[j]] = [s[j], s[i]];
|
||||||
|
}
|
||||||
|
let i = 0; j = 0;
|
||||||
|
const result = [];
|
||||||
|
for (let k = 0; k < data.length; k++) {
|
||||||
|
i = (i + 1) % 256;
|
||||||
|
j = (j + s[i]) % 256;
|
||||||
|
[s[i], s[j]] = [s[j], s[i]];
|
||||||
|
result.push(data.charCodeAt(k) ^ s[(s[i] + s[j]) % 256]);
|
||||||
|
}
|
||||||
|
return Buffer.from(result);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 9. 时间戳处理
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 毫秒级(13位)
|
||||||
|
const tsMs = Date.now(); // 1710000000000
|
||||||
|
|
||||||
|
// 秒级(10位)
|
||||||
|
const tsSec = Math.floor(Date.now() / 1000); // 1710000000
|
||||||
|
|
||||||
|
// 注意:某些网站使用服务端时间戳,需要从响应头或接口获取
|
||||||
|
```
|
||||||
|
|
||||||
|
## 10. 参数签名常见拼接模式
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 模式1:固定格式
|
||||||
|
sign = md5(`page=${page}&t=${timestamp}&key=${secret}`)
|
||||||
|
|
||||||
|
// 模式2:所有参数排序拼接
|
||||||
|
const params = { page: 1, size: 10, t: Date.now() };
|
||||||
|
const str = Object.keys(params).sort().map(k => `${k}=${params[k]}`).join('&');
|
||||||
|
sign = md5(str + secret)
|
||||||
|
|
||||||
|
// 模式3:JSON 字符串
|
||||||
|
sign = md5(JSON.stringify(data) + secret)
|
||||||
|
|
||||||
|
// 模式4:管道分隔
|
||||||
|
sign = md5(`${page}|${timestamp}|${secret}`)
|
||||||
|
```
|
||||||
@@ -0,0 +1,398 @@
|
|||||||
|
# 环境补全指南
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
|
||||||
|
将浏览器端 JS 代码移植到 Node.js 执行时,需要模拟浏览器环境。本指南提供各种场景下的
|
||||||
|
环境补全方案,从最小补全到完整模拟。
|
||||||
|
|
||||||
|
## 原则
|
||||||
|
|
||||||
|
1. **最小补全优先**:只补目标代码实际访问的 API,不要一次性补全所有浏览器 API
|
||||||
|
2. **按需扩展**:运行报错时再补对应的属性/方法
|
||||||
|
3. **真实值优先**:尽量使用真实浏览器采集的值,而非随意伪造
|
||||||
|
|
||||||
|
## 1. Node.js vm 沙箱最小环境
|
||||||
|
|
||||||
|
### 基础模板
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const vm = require('vm');
|
||||||
|
|
||||||
|
function createMinimalSandbox(options = {}) {
|
||||||
|
const sandbox = {
|
||||||
|
// 全局对象
|
||||||
|
window: null,
|
||||||
|
self: null,
|
||||||
|
globalThis: null,
|
||||||
|
|
||||||
|
// DOM 最小模拟
|
||||||
|
document: {
|
||||||
|
cookie: options.cookie || '',
|
||||||
|
createElement: (tag) => ({
|
||||||
|
tagName: tag.toUpperCase(),
|
||||||
|
style: {},
|
||||||
|
setAttribute: () => {},
|
||||||
|
getAttribute: () => null,
|
||||||
|
appendChild: () => {},
|
||||||
|
innerHTML: '',
|
||||||
|
src: '',
|
||||||
|
}),
|
||||||
|
getElementById: () => null,
|
||||||
|
getElementsByTagName: () => [],
|
||||||
|
getElementsByClassName: () => [],
|
||||||
|
querySelector: () => null,
|
||||||
|
querySelectorAll: () => [],
|
||||||
|
head: { appendChild: () => {} },
|
||||||
|
body: { appendChild: () => {} },
|
||||||
|
location: { href: options.url || 'https://example.com', hostname: 'example.com' },
|
||||||
|
referrer: options.referrer || '',
|
||||||
|
title: '',
|
||||||
|
readyState: 'complete',
|
||||||
|
},
|
||||||
|
|
||||||
|
// Navigator
|
||||||
|
navigator: {
|
||||||
|
userAgent: options.userAgent || 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
||||||
|
appCodeName: 'Mozilla',
|
||||||
|
appName: 'Netscape',
|
||||||
|
appVersion: '5.0',
|
||||||
|
platform: 'MacIntel',
|
||||||
|
language: 'zh-CN',
|
||||||
|
languages: ['zh-CN', 'zh', 'en'],
|
||||||
|
cookieEnabled: true,
|
||||||
|
onLine: true,
|
||||||
|
plugins: { length: 3 },
|
||||||
|
mimeTypes: { length: 4 },
|
||||||
|
webdriver: false,
|
||||||
|
hardwareConcurrency: 8,
|
||||||
|
maxTouchPoints: 0,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Location
|
||||||
|
location: {
|
||||||
|
href: options.url || 'https://example.com',
|
||||||
|
protocol: 'https:',
|
||||||
|
host: 'example.com',
|
||||||
|
hostname: 'example.com',
|
||||||
|
port: '',
|
||||||
|
pathname: '/',
|
||||||
|
search: '',
|
||||||
|
hash: '',
|
||||||
|
origin: 'https://example.com',
|
||||||
|
},
|
||||||
|
|
||||||
|
// Screen
|
||||||
|
screen: {
|
||||||
|
width: 1920,
|
||||||
|
height: 1080,
|
||||||
|
availWidth: 1920,
|
||||||
|
availHeight: 1055,
|
||||||
|
colorDepth: 24,
|
||||||
|
pixelDepth: 24,
|
||||||
|
},
|
||||||
|
|
||||||
|
// 定时器
|
||||||
|
setTimeout: setTimeout,
|
||||||
|
setInterval: setInterval,
|
||||||
|
clearTimeout: clearTimeout,
|
||||||
|
clearInterval: clearInterval,
|
||||||
|
|
||||||
|
// 内置对象
|
||||||
|
String, Array, Object, Math, Date, RegExp, JSON, Map, Set, WeakMap, WeakSet,
|
||||||
|
parseInt, parseFloat, isNaN, isFinite, NaN, Infinity, undefined,
|
||||||
|
encodeURIComponent, decodeURIComponent,
|
||||||
|
encodeURI, decodeURI,
|
||||||
|
escape, unescape,
|
||||||
|
Error, TypeError, RangeError, SyntaxError, ReferenceError,
|
||||||
|
ArrayBuffer, Uint8Array, Int32Array, Float64Array, DataView,
|
||||||
|
Promise,
|
||||||
|
Proxy, Reflect,
|
||||||
|
Symbol,
|
||||||
|
|
||||||
|
// Base64
|
||||||
|
btoa: (str) => Buffer.from(str, 'binary').toString('base64'),
|
||||||
|
atob: (b64) => Buffer.from(b64, 'base64').toString('binary'),
|
||||||
|
|
||||||
|
// Console(用于调试)
|
||||||
|
console: console,
|
||||||
|
};
|
||||||
|
|
||||||
|
// 循环引用
|
||||||
|
sandbox.window = sandbox;
|
||||||
|
sandbox.self = sandbox;
|
||||||
|
sandbox.globalThis = sandbox;
|
||||||
|
sandbox.top = sandbox;
|
||||||
|
sandbox.parent = sandbox;
|
||||||
|
sandbox.frames = sandbox;
|
||||||
|
|
||||||
|
return sandbox;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用示例
|
||||||
|
function runInSandbox(code, options = {}) {
|
||||||
|
const sandbox = createMinimalSandbox(options);
|
||||||
|
vm.createContext(sandbox);
|
||||||
|
|
||||||
|
try {
|
||||||
|
vm.runInContext(code, sandbox, {
|
||||||
|
timeout: options.timeout || 5000,
|
||||||
|
filename: options.filename || 'sandbox.js',
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
console.error('沙箱执行错误:', e.message);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
|
||||||
|
return sandbox;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Cookie 拦截模式
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
function createCookieTrapSandbox(options = {}) {
|
||||||
|
const sandbox = createMinimalSandbox(options);
|
||||||
|
const cookies = {};
|
||||||
|
|
||||||
|
Object.defineProperty(sandbox.document, 'cookie', {
|
||||||
|
get() {
|
||||||
|
return Object.entries(cookies).map(([k, v]) => `${k}=${v}`).join('; ');
|
||||||
|
},
|
||||||
|
set(val) {
|
||||||
|
const parts = val.split(';')[0].split('=');
|
||||||
|
const name = parts[0].trim();
|
||||||
|
const value = parts.slice(1).join('=').trim();
|
||||||
|
cookies[name] = value;
|
||||||
|
console.log(`[Sandbox] Cookie Set: ${name}=${value}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
sandbox._cookies = cookies;
|
||||||
|
return sandbox;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 2. WASM 环境补全
|
||||||
|
|
||||||
|
### 基础 WASM 加载
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const fs = require('fs');
|
||||||
|
|
||||||
|
async function loadWasm(wasmPath, importObject = {}) {
|
||||||
|
const wasmBuffer = fs.readFileSync(wasmPath);
|
||||||
|
|
||||||
|
const defaultImports = {
|
||||||
|
env: {
|
||||||
|
memory: new WebAssembly.Memory({ initial: 256 }),
|
||||||
|
table: new WebAssembly.Table({ initial: 0, element: 'anyfunc' }),
|
||||||
|
abort: () => { throw new Error('WASM abort'); },
|
||||||
|
...importObject.env,
|
||||||
|
},
|
||||||
|
wasi_snapshot_preview1: {
|
||||||
|
fd_write: () => 0,
|
||||||
|
fd_close: () => 0,
|
||||||
|
fd_seek: () => 0,
|
||||||
|
proc_exit: () => {},
|
||||||
|
...importObject.wasi_snapshot_preview1,
|
||||||
|
},
|
||||||
|
...importObject,
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await WebAssembly.instantiate(wasmBuffer, defaultImports);
|
||||||
|
return result.instance;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### wasm-bindgen (Rust) 环境补全
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
class Window {
|
||||||
|
constructor() {
|
||||||
|
this.document = {
|
||||||
|
body: {},
|
||||||
|
createElement: () => ({}),
|
||||||
|
getElementById: () => null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function patchWasmBindgenEnv() {
|
||||||
|
const win = new Window();
|
||||||
|
win.window = win;
|
||||||
|
win.self = win;
|
||||||
|
|
||||||
|
globalThis.Window = Window;
|
||||||
|
globalThis.window = win;
|
||||||
|
globalThis.self = win;
|
||||||
|
globalThis.document = win.document;
|
||||||
|
|
||||||
|
// wasm-bindgen 可能检查 instanceof
|
||||||
|
// 确保 win instanceof Window === true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Emscripten 环境补全
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const Module = {
|
||||||
|
print: console.log,
|
||||||
|
printErr: console.error,
|
||||||
|
TOTAL_MEMORY: 16777216,
|
||||||
|
noInitialRun: true,
|
||||||
|
onRuntimeInitialized: function() {
|
||||||
|
console.log('WASM Runtime Ready');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. jQuery 模拟
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
function createjQueryStub() {
|
||||||
|
const $ = function(selector) {
|
||||||
|
return {
|
||||||
|
length: 1,
|
||||||
|
val: () => '',
|
||||||
|
text: () => '',
|
||||||
|
html: () => '',
|
||||||
|
attr: () => '',
|
||||||
|
css: () => ({}),
|
||||||
|
find: () => $(''),
|
||||||
|
each: (fn) => { fn(0, {}); },
|
||||||
|
click: () => {},
|
||||||
|
on: () => {},
|
||||||
|
ajax: $.ajax,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
$.ajax = function(options) {
|
||||||
|
console.log('[jQuery] $.ajax:', options.url, options.data);
|
||||||
|
return { done: (fn) => ({ fail: () => ({}) }) };
|
||||||
|
};
|
||||||
|
|
||||||
|
$.get = $.post = $.ajax;
|
||||||
|
$.fn = $.prototype = {};
|
||||||
|
$.extend = Object.assign;
|
||||||
|
|
||||||
|
return $;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4. XMLHttpRequest 模拟
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
function createXHRStub(interceptor) {
|
||||||
|
return class XMLHttpRequest {
|
||||||
|
constructor() {
|
||||||
|
this.readyState = 0;
|
||||||
|
this.status = 0;
|
||||||
|
this.responseText = '';
|
||||||
|
this.response = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
open(method, url) {
|
||||||
|
this._method = method;
|
||||||
|
this._url = url;
|
||||||
|
this.readyState = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
setRequestHeader(name, value) {
|
||||||
|
this._headers = this._headers || {};
|
||||||
|
this._headers[name] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
send(body) {
|
||||||
|
if (interceptor) {
|
||||||
|
interceptor({
|
||||||
|
method: this._method,
|
||||||
|
url: this._url,
|
||||||
|
headers: this._headers,
|
||||||
|
body: body,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
this.readyState = 4;
|
||||||
|
this.status = 200;
|
||||||
|
if (this.onreadystatechange) this.onreadystatechange();
|
||||||
|
if (this.onload) this.onload();
|
||||||
|
}
|
||||||
|
|
||||||
|
addEventListener(event, handler) {
|
||||||
|
this['on' + event] = handler;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5. 环境检测绕过清单
|
||||||
|
|
||||||
|
| 检测项 | Node.js 默认 | 需要补全的值 |
|
||||||
|
|--------|-------------|-------------|
|
||||||
|
| `typeof window` | `undefined` | `object` |
|
||||||
|
| `typeof document` | `undefined` | `object` |
|
||||||
|
| `typeof navigator` | `undefined` | `object` |
|
||||||
|
| `typeof process` | `object` (暴露) | 需要删除 |
|
||||||
|
| `typeof module` | `object` (暴露) | 需要删除 |
|
||||||
|
| `typeof global` | `object` (暴露) | 视情况删除 |
|
||||||
|
| `navigator.webdriver` | N/A | `false` / `undefined` |
|
||||||
|
| `window.chrome` | N/A | `{ runtime: {} }` |
|
||||||
|
| `navigator.plugins.length` | N/A | `> 0` |
|
||||||
|
|
||||||
|
### 清除 Node.js 特征
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
function hideNodeFeatures(sandbox) {
|
||||||
|
delete sandbox.process;
|
||||||
|
delete sandbox.module;
|
||||||
|
delete sandbox.exports;
|
||||||
|
delete sandbox.require;
|
||||||
|
delete sandbox.global;
|
||||||
|
delete sandbox.__filename;
|
||||||
|
delete sandbox.__dirname;
|
||||||
|
delete sandbox.Buffer;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 6. 完整环境补全模板(jsdom)
|
||||||
|
|
||||||
|
当最小补全不够时,使用 jsdom 提供完整 DOM 环境:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const { JSDOM } = require('jsdom');
|
||||||
|
|
||||||
|
function createFullBrowserEnv(options = {}) {
|
||||||
|
const dom = new JSDOM('<!DOCTYPE html><html><body></body></html>', {
|
||||||
|
url: options.url || 'https://example.com',
|
||||||
|
referrer: options.referrer || '',
|
||||||
|
contentType: 'text/html',
|
||||||
|
pretendToBeVisual: true,
|
||||||
|
runScripts: 'dangerously',
|
||||||
|
resources: 'usable',
|
||||||
|
});
|
||||||
|
|
||||||
|
const { window } = dom;
|
||||||
|
|
||||||
|
// 补充 jsdom 缺少的 API
|
||||||
|
if (!window.btoa) {
|
||||||
|
window.btoa = (str) => Buffer.from(str, 'binary').toString('base64');
|
||||||
|
}
|
||||||
|
if (!window.atob) {
|
||||||
|
window.atob = (b64) => Buffer.from(b64, 'base64').toString('binary');
|
||||||
|
}
|
||||||
|
|
||||||
|
return { dom, window, document: window.document };
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## MCP 辅助确定环境需求
|
||||||
|
|
||||||
|
```
|
||||||
|
1. [js-reverse] search_in_sources(query="window.|document.|navigator.")
|
||||||
|
→ 识别代码访问了哪些浏览器 API
|
||||||
|
|
||||||
|
2. [js-reverse] evaluate_script(function="列出所有被访问的全局变量")
|
||||||
|
→ 确定需要补全的范围
|
||||||
|
|
||||||
|
3. [js-reverse] set_breakpoint_on_text(text="typeof window")
|
||||||
|
→ 在环境检测处断点,确认检测逻辑
|
||||||
|
```
|
||||||
@@ -0,0 +1,397 @@
|
|||||||
|
# JS Hook 技术大全
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
|
||||||
|
Hook 是 JS 逆向中最核心的技术之一。通过劫持/拦截 JS 原生函数或对象属性,可以捕获加密函数的
|
||||||
|
输入输出、Cookie 的生成过程、网络请求的参数构造等关键信息。
|
||||||
|
|
||||||
|
## 注入方式
|
||||||
|
|
||||||
|
### 通过 MCP 注入(推荐)
|
||||||
|
|
||||||
|
```
|
||||||
|
[js-reverse] inject_before_load(script=HookScript)
|
||||||
|
→ 在页面脚本加载前注入,确保 Hook 在目标代码之前生效
|
||||||
|
|
||||||
|
[js-reverse] evaluate_script(function=HookScript)
|
||||||
|
→ 在当前页面上下文执行,适合页面已加载后的动态注入
|
||||||
|
```
|
||||||
|
|
||||||
|
### 通过浏览器控制台注入
|
||||||
|
|
||||||
|
在 chrome-devtools MCP 中使用:
|
||||||
|
```
|
||||||
|
[chrome-devtools] evaluate_script(function=HookScript)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Hook 模板库
|
||||||
|
|
||||||
|
### 1. Cookie Setter Hook
|
||||||
|
|
||||||
|
**用途**:监控所有 Cookie 写入操作,定位动态 Cookie 生成逻辑
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
(function() {
|
||||||
|
let cookieTemp = '';
|
||||||
|
Object.defineProperty(document, 'cookie', {
|
||||||
|
set: function(val) {
|
||||||
|
console.log('[Hook] Cookie Set:', val);
|
||||||
|
if (val.indexOf('目标cookie名') !== -1) {
|
||||||
|
console.log('[Hook] ★ 目标Cookie捕获:', val);
|
||||||
|
console.trace('[Hook] Cookie 调用栈');
|
||||||
|
}
|
||||||
|
cookieTemp = val;
|
||||||
|
return val;
|
||||||
|
},
|
||||||
|
get: function() {
|
||||||
|
return cookieTemp;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. XHR Hook
|
||||||
|
|
||||||
|
**用途**:拦截所有 XMLHttpRequest 请求,捕获完整请求参数
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
(function() {
|
||||||
|
const _open = XMLHttpRequest.prototype.open;
|
||||||
|
const _send = XMLHttpRequest.prototype.send;
|
||||||
|
const _setHeader = XMLHttpRequest.prototype.setRequestHeader;
|
||||||
|
|
||||||
|
XMLHttpRequest.prototype.open = function(method, url) {
|
||||||
|
this._hookMethod = method;
|
||||||
|
this._hookUrl = url;
|
||||||
|
this._hookHeaders = {};
|
||||||
|
console.log('[Hook] XHR Open:', method, url);
|
||||||
|
return _open.apply(this, arguments);
|
||||||
|
};
|
||||||
|
|
||||||
|
XMLHttpRequest.prototype.setRequestHeader = function(name, value) {
|
||||||
|
this._hookHeaders[name] = value;
|
||||||
|
return _setHeader.apply(this, arguments);
|
||||||
|
};
|
||||||
|
|
||||||
|
XMLHttpRequest.prototype.send = function(body) {
|
||||||
|
console.log('[Hook] XHR Send:', {
|
||||||
|
method: this._hookMethod,
|
||||||
|
url: this._hookUrl,
|
||||||
|
headers: this._hookHeaders,
|
||||||
|
body: body
|
||||||
|
});
|
||||||
|
if (this._hookUrl.indexOf('目标接口') !== -1) {
|
||||||
|
console.log('[Hook] ★ 目标请求捕获');
|
||||||
|
console.trace('[Hook] 请求调用栈');
|
||||||
|
}
|
||||||
|
return _send.apply(this, arguments);
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Fetch Hook
|
||||||
|
|
||||||
|
**用途**:拦截 fetch API 请求
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
(function() {
|
||||||
|
const _fetch = window.fetch;
|
||||||
|
window.fetch = function(url, options) {
|
||||||
|
console.log('[Hook] Fetch:', url, options);
|
||||||
|
if (typeof url === 'string' && url.indexOf('目标接口') !== -1) {
|
||||||
|
console.log('[Hook] ★ 目标Fetch捕获');
|
||||||
|
console.trace('[Hook] Fetch 调用栈');
|
||||||
|
}
|
||||||
|
return _fetch.apply(this, arguments);
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. $.ajax Hook(jQuery 场景)
|
||||||
|
|
||||||
|
**用途**:拦截 jQuery AJAX 请求,捕获加密参数
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
(function() {
|
||||||
|
if (typeof $ === 'undefined' || typeof $.ajax === 'undefined') return;
|
||||||
|
|
||||||
|
const _ajax = $.ajax;
|
||||||
|
$.ajax = function(options) {
|
||||||
|
if (typeof options === 'object') {
|
||||||
|
console.log('[Hook] $.ajax:', {
|
||||||
|
url: options.url,
|
||||||
|
method: options.type || options.method,
|
||||||
|
data: options.data,
|
||||||
|
headers: options.headers
|
||||||
|
});
|
||||||
|
|
||||||
|
if (options.url && options.url.indexOf('目标接口') !== -1) {
|
||||||
|
console.log('[Hook] ★ 目标Ajax捕获');
|
||||||
|
if (options.data && options.data.m) {
|
||||||
|
console.log('[Hook] 加密参数 m:', options.data.m);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return _ajax.apply(this, arguments);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 同时 Hook ajaxSetup
|
||||||
|
if ($.ajaxSetup) {
|
||||||
|
const _setup = $.ajaxSetup;
|
||||||
|
$.ajaxSetup = function(options) {
|
||||||
|
console.log('[Hook] $.ajaxSetup:', options);
|
||||||
|
return _setup.apply(this, arguments);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. eval / Function Hook
|
||||||
|
|
||||||
|
**用途**:捕获动态生成和执行的代码
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
(function() {
|
||||||
|
// eval Hook
|
||||||
|
const _eval = window.eval;
|
||||||
|
window.eval = function(code) {
|
||||||
|
console.log('[Hook] eval 调用, 代码长度:', (typeof code === 'string') ? code.length : 'N/A');
|
||||||
|
if (typeof code === 'string' && code.length < 5000) {
|
||||||
|
console.log('[Hook] eval 代码:', code.substring(0, 500));
|
||||||
|
}
|
||||||
|
return _eval.apply(this, arguments);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Function 构造器 Hook
|
||||||
|
const _Function = Function;
|
||||||
|
const handler = {
|
||||||
|
construct(target, args) {
|
||||||
|
const body = args[args.length - 1];
|
||||||
|
console.log('[Hook] new Function, body 长度:', body ? body.length : 0);
|
||||||
|
if (body && body.indexOf('目标关键词') !== -1) {
|
||||||
|
console.log('[Hook] ★ 目标 Function 捕获:', body.substring(0, 500));
|
||||||
|
}
|
||||||
|
return new target(...args);
|
||||||
|
},
|
||||||
|
apply(target, thisArg, args) {
|
||||||
|
const body = args[args.length - 1];
|
||||||
|
console.log('[Hook] Function(), body 长度:', body ? body.length : 0);
|
||||||
|
return target.apply(thisArg, args);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
window.Function = new Proxy(_Function, handler);
|
||||||
|
})();
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6. JSON.parse Hook
|
||||||
|
|
||||||
|
**用途**:捕获 JSON 解析操作,常用于响应数据解密场景
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
(function() {
|
||||||
|
const _parse = JSON.parse;
|
||||||
|
JSON.parse = function(text) {
|
||||||
|
const result = _parse.apply(this, arguments);
|
||||||
|
console.log('[Hook] JSON.parse:', typeof text === 'string' ? text.substring(0, 200) : text);
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7. JSON.stringify Hook
|
||||||
|
|
||||||
|
**用途**:捕获 JSON 序列化操作,常在签名前执行
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
(function() {
|
||||||
|
const _stringify = JSON.stringify;
|
||||||
|
JSON.stringify = function(obj) {
|
||||||
|
const result = _stringify.apply(this, arguments);
|
||||||
|
console.log('[Hook] JSON.stringify:', result ? result.substring(0, 200) : result);
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
```
|
||||||
|
|
||||||
|
### 8. atob / btoa Hook
|
||||||
|
|
||||||
|
**用途**:捕获 Base64 编解码操作
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
(function() {
|
||||||
|
const _atob = window.atob;
|
||||||
|
const _btoa = window.btoa;
|
||||||
|
|
||||||
|
window.atob = function(str) {
|
||||||
|
const result = _atob(str);
|
||||||
|
console.log('[Hook] atob:', str.substring(0, 100), '→', result.substring(0, 100));
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
window.btoa = function(str) {
|
||||||
|
const result = _btoa(str);
|
||||||
|
console.log('[Hook] btoa:', str.substring(0, 100), '→', result.substring(0, 100));
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
```
|
||||||
|
|
||||||
|
### 9. setTimeout / setInterval Hook
|
||||||
|
|
||||||
|
**用途**:监控定时器调用,识别反调试和周期性操作
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
(function() {
|
||||||
|
const _setTimeout = window.setTimeout;
|
||||||
|
const _setInterval = window.setInterval;
|
||||||
|
|
||||||
|
window.setTimeout = function(fn, delay) {
|
||||||
|
const fnStr = typeof fn === 'function' ? fn.toString().substring(0, 200) : String(fn).substring(0, 200);
|
||||||
|
// 过滤 debugger 反调试
|
||||||
|
if (fnStr.indexOf('debugger') !== -1) {
|
||||||
|
console.log('[Hook] setTimeout 拦截 debugger,已跳过');
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
return _setTimeout.apply(this, arguments);
|
||||||
|
};
|
||||||
|
|
||||||
|
window.setInterval = function(fn, delay) {
|
||||||
|
const fnStr = typeof fn === 'function' ? fn.toString().substring(0, 200) : String(fn).substring(0, 200);
|
||||||
|
if (fnStr.indexOf('debugger') !== -1) {
|
||||||
|
console.log('[Hook] setInterval 拦截 debugger,已跳过');
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
return _setInterval.apply(this, arguments);
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
```
|
||||||
|
|
||||||
|
### 10. WebSocket Hook
|
||||||
|
|
||||||
|
**用途**:拦截 WebSocket 消息
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
(function() {
|
||||||
|
const _WebSocket = window.WebSocket;
|
||||||
|
window.WebSocket = function(url, protocols) {
|
||||||
|
console.log('[Hook] WebSocket 连接:', url);
|
||||||
|
const ws = new _WebSocket(url, protocols);
|
||||||
|
|
||||||
|
const _send = ws.send.bind(ws);
|
||||||
|
ws.send = function(data) {
|
||||||
|
console.log('[Hook] WS 发送:', data);
|
||||||
|
return _send(data);
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.addEventListener('message', function(event) {
|
||||||
|
console.log('[Hook] WS 接收:', event.data);
|
||||||
|
});
|
||||||
|
|
||||||
|
return ws;
|
||||||
|
};
|
||||||
|
window.WebSocket.prototype = _WebSocket.prototype;
|
||||||
|
})();
|
||||||
|
```
|
||||||
|
|
||||||
|
### 11. window 属性访问 Hook(蜜罐检测)
|
||||||
|
|
||||||
|
**用途**:监控对 window 属性的访问,识别蜜罐检测
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
(function() {
|
||||||
|
const handler = {
|
||||||
|
get(target, prop, receiver) {
|
||||||
|
if (['__selenium', '__webdriver_script_fn', '__driver_evaluate',
|
||||||
|
'$cdc_asdjflasutopfhvcZLmcfl_', 'callPhantom', '_phantom'].includes(prop)) {
|
||||||
|
console.log('[Hook] ★ 蜜罐属性访问:', prop);
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return Reflect.get(target, prop, receiver);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
// 注意:直接代理 window 风险较大,仅在需要时使用
|
||||||
|
})();
|
||||||
|
```
|
||||||
|
|
||||||
|
### 12. Canvas 指纹 Hook
|
||||||
|
|
||||||
|
**用途**:拦截 Canvas 指纹采集
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
(function() {
|
||||||
|
const _toDataURL = HTMLCanvasElement.prototype.toDataURL;
|
||||||
|
HTMLCanvasElement.prototype.toDataURL = function() {
|
||||||
|
console.log('[Hook] Canvas toDataURL 调用');
|
||||||
|
console.trace('[Hook] Canvas 调用栈');
|
||||||
|
return _toDataURL.apply(this, arguments);
|
||||||
|
};
|
||||||
|
|
||||||
|
const _toBlob = HTMLCanvasElement.prototype.toBlob;
|
||||||
|
HTMLCanvasElement.prototype.toBlob = function() {
|
||||||
|
console.log('[Hook] Canvas toBlob 调用');
|
||||||
|
return _toBlob.apply(this, arguments);
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
```
|
||||||
|
|
||||||
|
### 13. Navigator 属性 Hook
|
||||||
|
|
||||||
|
**用途**:伪装浏览器指纹
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
(function() {
|
||||||
|
Object.defineProperty(navigator, 'webdriver', {
|
||||||
|
get: () => false
|
||||||
|
});
|
||||||
|
Object.defineProperty(navigator, 'plugins', {
|
||||||
|
get: () => [1, 2, 3, 4, 5]
|
||||||
|
});
|
||||||
|
Object.defineProperty(navigator, 'languages', {
|
||||||
|
get: () => ['zh-CN', 'zh', 'en']
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
```
|
||||||
|
|
||||||
|
## 组合 Hook 模板
|
||||||
|
|
||||||
|
### 通用逆向分析 Hook(一键注入)
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
(function() {
|
||||||
|
console.log('[Hook] ========== 通用逆向Hook已注入 ==========');
|
||||||
|
|
||||||
|
// 1. Cookie 监控
|
||||||
|
let _cookie = document.cookie;
|
||||||
|
Object.defineProperty(document, 'cookie', {
|
||||||
|
set(v) { console.log('[Cookie] Set:', v); _cookie += '; ' + v; },
|
||||||
|
get() { return _cookie; }
|
||||||
|
});
|
||||||
|
|
||||||
|
// 2. XHR 监控
|
||||||
|
const _xhrOpen = XMLHttpRequest.prototype.open;
|
||||||
|
const _xhrSend = XMLHttpRequest.prototype.send;
|
||||||
|
XMLHttpRequest.prototype.open = function(m, u) { this._m = m; this._u = u; return _xhrOpen.apply(this, arguments); };
|
||||||
|
XMLHttpRequest.prototype.send = function(b) { console.log('[XHR]', this._m, this._u, b); return _xhrSend.apply(this, arguments); };
|
||||||
|
|
||||||
|
// 3. Fetch 监控
|
||||||
|
const _fetch = window.fetch;
|
||||||
|
window.fetch = function() { console.log('[Fetch]', ...arguments); return _fetch.apply(this, arguments); };
|
||||||
|
|
||||||
|
// 4. debugger 拦截
|
||||||
|
const _si = window.setInterval;
|
||||||
|
window.setInterval = function(fn, d) {
|
||||||
|
if (typeof fn === 'function' && fn.toString().indexOf('debugger') > -1) return -1;
|
||||||
|
return _si.apply(this, arguments);
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log('[Hook] ========== Hook注入完成 ==========');
|
||||||
|
})();
|
||||||
|
```
|
||||||
|
|
||||||
|
## MCP 注入最佳实践
|
||||||
|
|
||||||
|
1. **使用 `inject_before_load`**:确保 Hook 在目标代码之前生效
|
||||||
|
2. **使用 `console.log` 输出**:通过 `list_console_messages` 收集结果
|
||||||
|
3. **使用 `console.trace`**:在关键点输出调用栈
|
||||||
|
4. **避免覆写原型链的 toString**:防止被反检测代码发现
|
||||||
|
5. **使用 Proxy 代替直接覆写**:更隐蔽,不改变 `typeof` 结果
|
||||||
@@ -0,0 +1,264 @@
|
|||||||
|
# MCP 工具使用手册
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
|
||||||
|
本手册详细说明 `js-reverse` 和 `chrome-devtools` 两个 MCP 服务器的协作使用方式,
|
||||||
|
提供常见逆向场景下的具体操作步骤。
|
||||||
|
|
||||||
|
## 工具速查表
|
||||||
|
|
||||||
|
### js-reverse MCP(逆向分析核心)
|
||||||
|
|
||||||
|
| 类别 | 工具 | 核心用途 |
|
||||||
|
|------|------|---------|
|
||||||
|
| **源码搜索** | `search_in_sources` | 在所有 JS 中搜索关键词 |
|
||||||
|
| **源码获取** | `get_script_source` | 获取指定脚本的代码片段 |
|
||||||
|
| **源码保存** | `save_script_source` | 将脚本保存到本地文件 |
|
||||||
|
| **脚本列表** | `list_scripts` | 列出页面加载的所有脚本 |
|
||||||
|
| **断点设置** | `set_breakpoint_on_text` | 按代码文本设置断点 |
|
||||||
|
| **XHR 断点** | `break_on_xhr` | 在 XHR 请求时暂停 |
|
||||||
|
| **函数追踪** | `trace_function` | 追踪函数调用,记录参数 |
|
||||||
|
| **暂停信息** | `get_paused_info` | 获取断点处的栈和变量 |
|
||||||
|
| **单步调试** | `step_into/over/out` | 单步执行 |
|
||||||
|
| **脚本注入** | `inject_before_load` | 页面加载前注入 JS |
|
||||||
|
| **脚本执行** | `evaluate_script` | 在页面执行 JS |
|
||||||
|
| **网络请求** | `list_network_requests` | 列出网络请求 |
|
||||||
|
| **请求详情** | `get_network_request` | 获取请求完整信息 |
|
||||||
|
| **请求来源** | `get_request_initiator` | 获取请求的 JS 调用栈 |
|
||||||
|
| **WebSocket** | `list_websocket_connections` | 列出 WS 连接 |
|
||||||
|
| **WS 分析** | `analyze_websocket_messages` | 分析 WS 消息模式 |
|
||||||
|
| **控制台** | `list_console_messages` | 读取控制台输出 |
|
||||||
|
|
||||||
|
### chrome-devtools MCP(浏览器自动化)
|
||||||
|
|
||||||
|
| 类别 | 工具 | 核心用途 |
|
||||||
|
|------|------|---------|
|
||||||
|
| **页面管理** | `new_page` / `navigate_page` / `close_page` | 页面操作 |
|
||||||
|
| **UI 交互** | `click` / `fill` / `type_text` / `press_key` | 元素交互 |
|
||||||
|
| **DOM 快照** | `take_snapshot` | 获取页面文本结构 |
|
||||||
|
| **截图** | `take_screenshot` | 页面截图 |
|
||||||
|
| **脚本执行** | `evaluate_script` | 执行 JS |
|
||||||
|
| **设备模拟** | `emulate` | 模拟 UA/视口/网络 |
|
||||||
|
| **网络请求** | `list_network_requests` / `get_network_request` | 请求监控 |
|
||||||
|
| **等待** | `wait_for` | 等待文本出现 |
|
||||||
|
| **对话框** | `handle_dialog` | 处理弹窗 |
|
||||||
|
|
||||||
|
## 场景化操作手册
|
||||||
|
|
||||||
|
### 场景 1:分析数据接口的加密参数
|
||||||
|
|
||||||
|
**目标**:找到接口中加密参数的生成逻辑
|
||||||
|
|
||||||
|
```
|
||||||
|
步骤 1:打开目标页面
|
||||||
|
[chrome-devtools] new_page(url="https://target.com")
|
||||||
|
|
||||||
|
步骤 2:获取页面结构
|
||||||
|
[chrome-devtools] take_snapshot() → 确认页面正常加载
|
||||||
|
|
||||||
|
步骤 3:触发数据请求(如翻页)
|
||||||
|
[chrome-devtools] click(uid="翻页按钮的uid")
|
||||||
|
或
|
||||||
|
[chrome-devtools] evaluate_script(function="document.querySelector('.next').click()")
|
||||||
|
|
||||||
|
步骤 4:查看网络请求
|
||||||
|
[js-reverse] list_network_requests(resourceTypes=["XHR","Fetch"])
|
||||||
|
|
||||||
|
步骤 5:获取目标请求详情
|
||||||
|
[js-reverse] get_network_request(reqid=目标请求ID)
|
||||||
|
→ 记录 URL、Headers、Params、Response
|
||||||
|
|
||||||
|
步骤 6:获取请求调用栈
|
||||||
|
[js-reverse] get_request_initiator(requestId=目标请求ID)
|
||||||
|
→ 找到发起请求的 JS 函数
|
||||||
|
|
||||||
|
步骤 7:搜索加密参数
|
||||||
|
[js-reverse] search_in_sources(query="参数名=")
|
||||||
|
[js-reverse] search_in_sources(query="参数名")
|
||||||
|
|
||||||
|
步骤 8:读取相关源码
|
||||||
|
[js-reverse] get_script_source(url="包含加密逻辑的脚本URL", startLine=X, endLine=Y)
|
||||||
|
|
||||||
|
步骤 9:设置断点验证
|
||||||
|
[js-reverse] set_breakpoint_on_text(text="加密函数调用")
|
||||||
|
触发请求 →
|
||||||
|
[js-reverse] get_paused_info(includeScopes=true)
|
||||||
|
→ 查看变量值确认加密逻辑
|
||||||
|
```
|
||||||
|
|
||||||
|
### 场景 2:动态 Cookie 逆向
|
||||||
|
|
||||||
|
**目标**:找到 Cookie 的生成逻辑
|
||||||
|
|
||||||
|
```
|
||||||
|
步骤 1:注入 Cookie Hook
|
||||||
|
[js-reverse] inject_before_load(script="Cookie setter Hook 代码")
|
||||||
|
|
||||||
|
步骤 2:刷新页面触发 Cookie 生成
|
||||||
|
[js-reverse] navigate_page(type="reload")
|
||||||
|
|
||||||
|
步骤 3:读取 Hook 输出
|
||||||
|
[js-reverse] list_console_messages(types=["log"])
|
||||||
|
→ 找到 Cookie 设置记录和调用栈
|
||||||
|
|
||||||
|
步骤 4:根据调用栈定位生成函数
|
||||||
|
[js-reverse] search_in_sources(query="目标函数名")
|
||||||
|
[js-reverse] get_script_source(url="...", startLine=X, endLine=Y)
|
||||||
|
|
||||||
|
步骤 5:分析生成逻辑
|
||||||
|
[js-reverse] set_breakpoint_on_text(text="cookie生成代码片段")
|
||||||
|
刷新页面 →
|
||||||
|
[js-reverse] get_paused_info → 查看入参
|
||||||
|
[js-reverse] step_into → 进入加密函数
|
||||||
|
[js-reverse] get_paused_info → 查看中间值
|
||||||
|
```
|
||||||
|
|
||||||
|
### 场景 3:混淆代码分析
|
||||||
|
|
||||||
|
**目标**:还原混淆的 JS 代码
|
||||||
|
|
||||||
|
```
|
||||||
|
步骤 1:列出所有脚本
|
||||||
|
[js-reverse] list_scripts()
|
||||||
|
→ 找到可疑的混淆脚本(通常文件名无意义或体积很大)
|
||||||
|
|
||||||
|
步骤 2:保存混淆脚本到本地
|
||||||
|
[js-reverse] save_script_source(url="混淆脚本URL", filePath="./project/obfuscated.js")
|
||||||
|
|
||||||
|
步骤 3:搜索关键特征
|
||||||
|
[js-reverse] search_in_sources(query="_0x", urlFilter="混淆脚本URL")
|
||||||
|
→ 确认混淆类型(OB混淆)
|
||||||
|
|
||||||
|
[js-reverse] search_in_sources(query="switch|while.*true", urlFilter="混淆脚本URL")
|
||||||
|
→ 检查是否有控制流平坦化
|
||||||
|
|
||||||
|
步骤 4:在浏览器中还原字符串
|
||||||
|
[js-reverse] evaluate_script(function="返回字符串数组的代码")
|
||||||
|
→ 获取解密后的字符串映射
|
||||||
|
|
||||||
|
步骤 5:定位关键函数
|
||||||
|
[js-reverse] search_in_sources(query="encrypt|sign|md5|aes", urlFilter="混淆脚本URL")
|
||||||
|
→ 找到加密相关代码的大致位置
|
||||||
|
[js-reverse] get_script_source → 获取上下文
|
||||||
|
```
|
||||||
|
|
||||||
|
### 场景 4:WASM 逆向
|
||||||
|
|
||||||
|
**目标**:分析 WASM 加密函数的输入输出
|
||||||
|
|
||||||
|
```
|
||||||
|
步骤 1:搜索 WASM 加载代码
|
||||||
|
[js-reverse] search_in_sources(query="WebAssembly|.wasm|instantiate")
|
||||||
|
|
||||||
|
步骤 2:找到 WASM 文件
|
||||||
|
[js-reverse] list_network_requests(resourceTypes=["Other"])
|
||||||
|
→ 找到 .wasm 文件的请求
|
||||||
|
|
||||||
|
步骤 3:分析 WASM 调用
|
||||||
|
[js-reverse] search_in_sources(query="exports.|instance.exports")
|
||||||
|
→ 找到导出函数的调用代码
|
||||||
|
|
||||||
|
步骤 4:追踪输入输出
|
||||||
|
[js-reverse] set_breakpoint_on_text(text="wasm导出函数调用")
|
||||||
|
触发请求 →
|
||||||
|
[js-reverse] get_paused_info → 查看入参
|
||||||
|
[js-reverse] step_over → 执行函数
|
||||||
|
[js-reverse] get_paused_info → 查看返回值
|
||||||
|
|
||||||
|
步骤 5:在浏览器中测试
|
||||||
|
[js-reverse] evaluate_script(function="调用wasm函数并返回结果")
|
||||||
|
→ 验证 I/O 关系
|
||||||
|
|
||||||
|
步骤 6:下载 WASM 文件
|
||||||
|
[chrome-devtools] get_network_request(reqid=WASM请求ID, responseFilePath="./project/main.wasm")
|
||||||
|
```
|
||||||
|
|
||||||
|
### 场景 5:WebSocket 通信分析
|
||||||
|
|
||||||
|
**目标**:分析 WebSocket 消息格式和加密
|
||||||
|
|
||||||
|
```
|
||||||
|
步骤 1:列出 WS 连接
|
||||||
|
[js-reverse] list_websocket_connections()
|
||||||
|
|
||||||
|
步骤 2:分析消息模式
|
||||||
|
[js-reverse] analyze_websocket_messages(wsid=连接ID, direction="sent")
|
||||||
|
[js-reverse] analyze_websocket_messages(wsid=连接ID, direction="received")
|
||||||
|
|
||||||
|
步骤 3:获取具体消息
|
||||||
|
[js-reverse] get_websocket_messages(wsid=连接ID, show_content=true)
|
||||||
|
|
||||||
|
步骤 4:搜索 WS 相关代码
|
||||||
|
[js-reverse] search_in_sources(query="WebSocket|ws.send|onmessage")
|
||||||
|
```
|
||||||
|
|
||||||
|
## 高级技巧
|
||||||
|
|
||||||
|
### 使用 trace_function 自动记录函数调用
|
||||||
|
|
||||||
|
```
|
||||||
|
[js-reverse] trace_function(
|
||||||
|
functionName="encrypt",
|
||||||
|
logArgs=true,
|
||||||
|
logThis=false,
|
||||||
|
pause=false
|
||||||
|
)
|
||||||
|
```
|
||||||
|
→ 每次 `encrypt` 被调用时自动记录参数,不暂停执行
|
||||||
|
|
||||||
|
### 使用条件断点
|
||||||
|
|
||||||
|
```
|
||||||
|
[js-reverse] set_breakpoint_on_text(
|
||||||
|
text="fetch(",
|
||||||
|
condition="arguments[0].indexOf('/api/data') !== -1"
|
||||||
|
)
|
||||||
|
```
|
||||||
|
→ 只在请求特定接口时暂停
|
||||||
|
|
||||||
|
### 使用 inject_before_load 进行预分析
|
||||||
|
|
||||||
|
```
|
||||||
|
[js-reverse] inject_before_load(script=`
|
||||||
|
// 记录所有全局变量的创建
|
||||||
|
const knownGlobals = new Set(Object.keys(window));
|
||||||
|
setInterval(() => {
|
||||||
|
const newGlobals = Object.keys(window).filter(k => !knownGlobals.has(k));
|
||||||
|
if (newGlobals.length > 0) {
|
||||||
|
console.log('[Monitor] 新全局变量:', newGlobals);
|
||||||
|
newGlobals.forEach(k => knownGlobals.add(k));
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
`)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 使用 evaluate_script 提取运行时数据
|
||||||
|
|
||||||
|
```
|
||||||
|
[js-reverse] evaluate_script(function=`
|
||||||
|
// 提取当前页面的所有 Cookie
|
||||||
|
JSON.stringify(document.cookie.split('; ').reduce((obj, pair) => {
|
||||||
|
const [k, v] = pair.split('=');
|
||||||
|
obj[k] = v;
|
||||||
|
return obj;
|
||||||
|
}, {}))
|
||||||
|
`)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 两个 MCP 的分工原则
|
||||||
|
|
||||||
|
| 需求 | 使用 js-reverse | 使用 chrome-devtools |
|
||||||
|
|------|----------------|---------------------|
|
||||||
|
| 搜索 JS 源码 | ✅ `search_in_sources` | ❌ |
|
||||||
|
| 设置断点 | ✅ `set_breakpoint_on_text` | ❌ |
|
||||||
|
| 单步调试 | ✅ `step_into/over/out` | ❌ |
|
||||||
|
| 函数追踪 | ✅ `trace_function` | ❌ |
|
||||||
|
| 注入 Hook | ✅ `inject_before_load` | ❌ |
|
||||||
|
| 页面截图 | ✅ 可以 | ✅ 更灵活(指定元素) |
|
||||||
|
| UI 交互(点击/输入) | ❌ | ✅ `click/fill/type_text` |
|
||||||
|
| 模拟 UA/设备 | ❌ | ✅ `emulate` |
|
||||||
|
| DOM 快照 | ❌ | ✅ `take_snapshot` |
|
||||||
|
| 保存请求响应到文件 | ❌ | ✅ `get_network_request(responseFilePath)` |
|
||||||
|
| WebSocket 分析 | ✅ 完整支持 | ❌ |
|
||||||
|
| 请求调用栈 | ✅ `get_request_initiator` | ❌ |
|
||||||
|
| 执行 JS | ✅ 支持 `mainWorld` 选项 | ✅ 支持 `args` 传 uid |
|
||||||
@@ -0,0 +1,198 @@
|
|||||||
|
# JS 混淆识别与还原指南
|
||||||
|
|
||||||
|
## 混淆类型速查
|
||||||
|
|
||||||
|
### 1. OB 混淆 (obfuscator.io)
|
||||||
|
|
||||||
|
**识别特征**:
|
||||||
|
- 大量 `_0x` 前缀变量名(如 `_0x4a3b2c`)
|
||||||
|
- 顶部有十六进制字符串数组(`var _0xabc = ['...', '...', ...]`)
|
||||||
|
- 字符串数组旋转函数
|
||||||
|
- 十六进制属性访问(`obj['_0x1234']` 替代 `obj.method`)
|
||||||
|
|
||||||
|
**还原步骤**:
|
||||||
|
1. 定位字符串数组和旋转函数
|
||||||
|
2. 执行旋转函数得到最终字符串数组
|
||||||
|
3. 全局替换十六进制索引为实际字符串
|
||||||
|
4. 简化数学表达式和逻辑运算
|
||||||
|
5. 变量重命名提高可读性
|
||||||
|
|
||||||
|
**MCP 辅助**:
|
||||||
|
```
|
||||||
|
[js-reverse] search_in_sources(query="_0x") → 定位混淆入口
|
||||||
|
[js-reverse] evaluate_script → 在浏览器中执行字符串数组还原
|
||||||
|
[js-reverse] save_script_source → 保存到本地进一步分析
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 控制流平坦化 (CFF)
|
||||||
|
|
||||||
|
**识别特征**:
|
||||||
|
```javascript
|
||||||
|
var state = initialState;
|
||||||
|
while (true) {
|
||||||
|
switch (state) {
|
||||||
|
case 'A': /* ... */ state = 'C'; break;
|
||||||
|
case 'B': /* ... */ state = 'D'; break;
|
||||||
|
case 'C': /* ... */ state = 'B'; break;
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**还原步骤**:
|
||||||
|
1. 找到初始状态值
|
||||||
|
2. 按状态转移顺序排列代码块
|
||||||
|
3. 去掉 switch-case 包装,还原为顺序代码
|
||||||
|
4. 简化多余的变量赋值
|
||||||
|
|
||||||
|
**MCP 辅助**:
|
||||||
|
```
|
||||||
|
[js-reverse] set_breakpoint_on_text(text="switch") → 在状态机入口断点
|
||||||
|
[js-reverse] step_over → 逐步追踪状态转移
|
||||||
|
[js-reverse] get_paused_info → 查看当前状态值
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. eval / Function 打包
|
||||||
|
|
||||||
|
**识别特征**:
|
||||||
|
```javascript
|
||||||
|
eval(function(p,a,c,k,e,d){...}('encoded_string',...))
|
||||||
|
// 或
|
||||||
|
new Function('return ' + decryptedCode)()
|
||||||
|
```
|
||||||
|
|
||||||
|
**还原步骤**:
|
||||||
|
1. Hook `eval` 和 `Function` 构造器,拦截实际执行的代码
|
||||||
|
2. 或者将 `eval()` 替换为 `console.log()` 查看解密后的代码
|
||||||
|
3. 可能有多层嵌套,需要逐层解包
|
||||||
|
|
||||||
|
**MCP 辅助**:
|
||||||
|
```
|
||||||
|
[js-reverse] inject_before_load(script="eval/Function Hook")
|
||||||
|
[js-reverse] list_console_messages → 读取解包后的代码
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. AAEncode
|
||||||
|
|
||||||
|
**识别特征**:
|
||||||
|
```javascript
|
||||||
|
゚ω゚ノ= /`m´)ノ ~┻━┻ //*´∇`*/ ['_'];
|
||||||
|
// 全是日文颜文字字符
|
||||||
|
```
|
||||||
|
|
||||||
|
**还原**:直接在浏览器 console 执行,或去掉最外层执行函数改为输出。
|
||||||
|
|
||||||
|
### 5. JJEncode
|
||||||
|
|
||||||
|
**识别特征**:
|
||||||
|
```javascript
|
||||||
|
$=~[];$={___:++$,$$$$:(![]+"")[$],...
|
||||||
|
// 全是 $ 和特殊字符
|
||||||
|
```
|
||||||
|
|
||||||
|
**还原**:同 AAEncode,直接执行或替换执行为输出。
|
||||||
|
|
||||||
|
### 6. JSFuck
|
||||||
|
|
||||||
|
**识别特征**:
|
||||||
|
```javascript
|
||||||
|
[][(![]+[])[+[]]+(![]+[])[!+[]+!+[]]+(![]+[])[+!+[]]+(!![]+[])[+[]]]...
|
||||||
|
// 仅使用 []()!+ 六种字符
|
||||||
|
```
|
||||||
|
|
||||||
|
**还原**:直接在浏览器执行。
|
||||||
|
|
||||||
|
### 7. 自定义 VM / 字节码解释器
|
||||||
|
|
||||||
|
**识别特征**:
|
||||||
|
- 超大数组作为"字节码"
|
||||||
|
- 解释器循环,包含 `switch` 或函数查找表
|
||||||
|
- 通常在 IIFE 中
|
||||||
|
- 无法通过简单字符串替换还原
|
||||||
|
|
||||||
|
**还原策略**:
|
||||||
|
1. **不要尝试反编译字节码**
|
||||||
|
2. 找到解释器的输入和输出接口
|
||||||
|
3. 通过 Hook 解释器的关键操作(函数调用、赋值、返回)来理解行为
|
||||||
|
4. 直接在 Node.js 中运行字节码解释器
|
||||||
|
|
||||||
|
**MCP 辅助**:
|
||||||
|
```
|
||||||
|
[js-reverse] trace_function(functionName="解释器函数")
|
||||||
|
[js-reverse] set_breakpoint_on_text(text="switch") → 在解释器循环断点
|
||||||
|
[js-reverse] step_over → 观察每步操作
|
||||||
|
```
|
||||||
|
|
||||||
|
### 8. JSVMP(JS 虚拟机保护)
|
||||||
|
|
||||||
|
**识别特征**:
|
||||||
|
- 超大 JS 文件(200KB+)
|
||||||
|
- 包含自定义解释器和操作码表
|
||||||
|
- 函数名和变量名完全无意义
|
||||||
|
- 改写浏览器原生 API
|
||||||
|
|
||||||
|
**还原策略**:
|
||||||
|
1. **不要反编译,通过 I/O 定位**
|
||||||
|
2. Hook 所有出口(XHR、Cookie、fetch)
|
||||||
|
3. 追踪加密函数的输入和输出
|
||||||
|
4. 用已知 I/O 反推算法
|
||||||
|
|
||||||
|
**MCP 辅助**:
|
||||||
|
```
|
||||||
|
[js-reverse] break_on_xhr(url="目标接口") → 在请求发出时暂停
|
||||||
|
[js-reverse] get_paused_info → 获取调用栈
|
||||||
|
[js-reverse] inject_before_load → 注入全局 Hook
|
||||||
|
```
|
||||||
|
|
||||||
|
## 通用反混淆 Node.js 工具
|
||||||
|
|
||||||
|
### 使用 AST 进行基础还原
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const parser = require('@babel/parser');
|
||||||
|
const traverse = require('@babel/traverse').default;
|
||||||
|
const generate = require('@babel/generator').default;
|
||||||
|
const t = require('@babel/types');
|
||||||
|
|
||||||
|
function deobfuscate(code) {
|
||||||
|
const ast = parser.parse(code);
|
||||||
|
|
||||||
|
traverse(ast, {
|
||||||
|
// 还原十六进制字符串
|
||||||
|
StringLiteral(path) {
|
||||||
|
if (/^\\x/.test(path.node.extra?.raw || '')) {
|
||||||
|
path.node.extra = undefined;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// 还原计算属性为点号访问
|
||||||
|
MemberExpression(path) {
|
||||||
|
if (t.isStringLiteral(path.node.property) && /^[a-zA-Z_$]/.test(path.node.property.value)) {
|
||||||
|
path.node.computed = false;
|
||||||
|
path.node.property = t.identifier(path.node.property.value);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// 折叠常量表达式
|
||||||
|
BinaryExpression(path) {
|
||||||
|
if (t.isNumericLiteral(path.node.left) && t.isNumericLiteral(path.node.right)) {
|
||||||
|
const result = eval(`${path.node.left.value} ${path.node.operator} ${path.node.right.value}`);
|
||||||
|
if (typeof result === 'number' && isFinite(result)) {
|
||||||
|
path.replaceWith(t.numericLiteral(result));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return generate(ast, { comments: false }).code;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 混淆代码分析的 MCP 工作流
|
||||||
|
|
||||||
|
```
|
||||||
|
1. save_script_source → 保存混淆代码到本地
|
||||||
|
2. search_in_sources → 搜索可能的入口函数
|
||||||
|
3. set_breakpoint_on_text → 在入口设断点
|
||||||
|
4. get_paused_info → 查看运行时变量,理解数据流
|
||||||
|
5. evaluate_script → 在浏览器执行还原操作
|
||||||
|
6. step_into → 跟进关键函数调用
|
||||||
|
```
|
||||||
@@ -0,0 +1,269 @@
|
|||||||
|
# 协议层分析与对抗
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
|
||||||
|
有些网站不仅在 JS 层面做加密保护,还在协议层面进行检测(TLS 指纹、HTTP/2 强制等)。
|
||||||
|
当算法全部还原正确但请求仍然失败时,需要考虑协议层问题。
|
||||||
|
|
||||||
|
## 1. TLS 指纹检测
|
||||||
|
|
||||||
|
### 原理
|
||||||
|
|
||||||
|
服务器通过分析 TLS Client Hello 报文中的特征来识别客户端类型:
|
||||||
|
- Cipher Suites 列表和顺序
|
||||||
|
- TLS 扩展列表和顺序
|
||||||
|
- 支持的曲线(Elliptic Curves)
|
||||||
|
- 签名算法
|
||||||
|
- ALPN 协议列表
|
||||||
|
|
||||||
|
不同客户端(Chrome、Firefox、curl、Node.js)的 TLS 指纹不同。
|
||||||
|
|
||||||
|
### 检测表现
|
||||||
|
|
||||||
|
- 请求返回 `403 Forbidden`
|
||||||
|
- 返回空响应或错误页面
|
||||||
|
- 返回 `token failed`、`access denied` 等自定义错误
|
||||||
|
- 同样的参数在浏览器中正常,在 Node.js 中失败
|
||||||
|
|
||||||
|
### 解决方案
|
||||||
|
|
||||||
|
#### 方案 A:使用浏览器自动化(最可靠)
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 使用 chrome-devtools MCP 在真实浏览器中执行请求
|
||||||
|
// [chrome-devtools] evaluate_script
|
||||||
|
const response = await fetch('https://target.com/api/data', {
|
||||||
|
method: 'GET',
|
||||||
|
headers: { 'Cookie': cookieValue },
|
||||||
|
credentials: 'include'
|
||||||
|
});
|
||||||
|
const data = await response.json();
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
MCP 操作:
|
||||||
|
[chrome-devtools] evaluate_script(function="fetch(url).then(r=>r.json()).then(d=>JSON.stringify(d))")
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 方案 B:curl-impersonate
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 使用 curl-impersonate 模拟 Chrome 的 TLS 指纹
|
||||||
|
curl_chrome116 https://target.com/api/data \
|
||||||
|
-H "Cookie: session=xxx" \
|
||||||
|
-H "User-Agent: Mozilla/5.0 ..."
|
||||||
|
```
|
||||||
|
|
||||||
|
Node.js 中通过子进程调用:
|
||||||
|
```javascript
|
||||||
|
const { execSync } = require('child_process');
|
||||||
|
|
||||||
|
function fetchWithChromeTLS(url, headers = {}) {
|
||||||
|
const headerArgs = Object.entries(headers)
|
||||||
|
.map(([k, v]) => `-H "${k}: ${v}"`)
|
||||||
|
.join(' ');
|
||||||
|
const result = execSync(`curl_chrome116 "${url}" ${headerArgs} -s`, {
|
||||||
|
encoding: 'utf-8',
|
||||||
|
timeout: 30000,
|
||||||
|
});
|
||||||
|
return JSON.parse(result);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 方案 C:自定义 TLS(高级)
|
||||||
|
|
||||||
|
使用支持自定义 TLS 指纹的库(如 `tls-client` 的 Node.js 绑定)。
|
||||||
|
|
||||||
|
## 2. HTTP/2 协议
|
||||||
|
|
||||||
|
### 检测表现
|
||||||
|
|
||||||
|
- HTTP/1.1 请求返回错误
|
||||||
|
- HTTP/2 请求正常返回数据
|
||||||
|
- 抓包可以看到服务器仅接受 h2 连接
|
||||||
|
|
||||||
|
### Node.js HTTP/2 请求
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const http2 = require('http2');
|
||||||
|
|
||||||
|
function http2Request(url, options = {}) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const urlObj = new URL(url);
|
||||||
|
const client = http2.connect(`${urlObj.protocol}//${urlObj.host}`);
|
||||||
|
|
||||||
|
client.on('error', reject);
|
||||||
|
|
||||||
|
const headers = {
|
||||||
|
':method': options.method || 'GET',
|
||||||
|
':path': urlObj.pathname + urlObj.search,
|
||||||
|
':scheme': urlObj.protocol.replace(':', ''),
|
||||||
|
':authority': urlObj.host,
|
||||||
|
'user-agent': options.userAgent || 'Mozilla/5.0 ...',
|
||||||
|
'accept': 'application/json',
|
||||||
|
...options.headers,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (options.cookie) {
|
||||||
|
headers['cookie'] = options.cookie;
|
||||||
|
}
|
||||||
|
|
||||||
|
const req = client.request(headers);
|
||||||
|
|
||||||
|
let data = '';
|
||||||
|
req.on('data', (chunk) => { data += chunk; });
|
||||||
|
req.on('end', () => {
|
||||||
|
client.close();
|
||||||
|
resolve(data);
|
||||||
|
});
|
||||||
|
req.on('error', reject);
|
||||||
|
|
||||||
|
if (options.body) {
|
||||||
|
req.write(options.body);
|
||||||
|
}
|
||||||
|
req.end();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用示例
|
||||||
|
async function main() {
|
||||||
|
const data = await http2Request('https://target.com/api/data?page=1', {
|
||||||
|
headers: {
|
||||||
|
'cookie': 'm=xxx; sessionid=yyy',
|
||||||
|
'referer': 'https://target.com/',
|
||||||
|
}
|
||||||
|
});
|
||||||
|
console.log(JSON.parse(data));
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. User-Agent 校验
|
||||||
|
|
||||||
|
### 常见场景
|
||||||
|
|
||||||
|
- 某些页面要求特定 UA
|
||||||
|
- 不同 UA 返回不同内容
|
||||||
|
- 空 UA 或非浏览器 UA 被拒绝
|
||||||
|
|
||||||
|
### Node.js 设置
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const axios = require('axios');
|
||||||
|
|
||||||
|
const client = axios.create({
|
||||||
|
headers: {
|
||||||
|
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### MCP 模拟
|
||||||
|
|
||||||
|
```
|
||||||
|
[chrome-devtools] emulate(userAgent="自定义UA")
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4. Referer 校验
|
||||||
|
|
||||||
|
### 常见场景
|
||||||
|
|
||||||
|
- API 接口检查 Referer 头
|
||||||
|
- 必须来自特定页面的请求
|
||||||
|
- 缺少 Referer 返回 403
|
||||||
|
|
||||||
|
### 解决方案
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
headers: {
|
||||||
|
'Referer': 'https://target.com/page',
|
||||||
|
'Origin': 'https://target.com',
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5. CORS 与跨域
|
||||||
|
|
||||||
|
### 预检请求 (Preflight)
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 某些请求会先发 OPTIONS 预检
|
||||||
|
// 确保模拟请求时也处理 CORS 相关头
|
||||||
|
headers: {
|
||||||
|
'Origin': 'https://target.com',
|
||||||
|
'Access-Control-Request-Method': 'POST',
|
||||||
|
'Access-Control-Request-Headers': 'content-type',
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 6. 请求频率限制
|
||||||
|
|
||||||
|
### 常见策略
|
||||||
|
|
||||||
|
- IP 维度限流
|
||||||
|
- Cookie/Session 维度限流
|
||||||
|
- 请求间隔检测
|
||||||
|
- 滑动窗口计数
|
||||||
|
|
||||||
|
### 对抗方式
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 请求间隔控制
|
||||||
|
function sleep(ms) {
|
||||||
|
return new Promise(resolve => setTimeout(resolve, ms));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchWithRateLimit(urls, delayMs = 1000) {
|
||||||
|
const results = [];
|
||||||
|
for (const url of urls) {
|
||||||
|
const data = await fetch(url);
|
||||||
|
results.push(data);
|
||||||
|
await sleep(delayMs + Math.random() * 500); // 随机化延迟
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 7. IP 封禁对抗
|
||||||
|
|
||||||
|
### 代理轮换
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const HttpsProxyAgent = require('https-proxy-agent');
|
||||||
|
|
||||||
|
const proxies = [
|
||||||
|
'http://proxy1:port',
|
||||||
|
'http://proxy2:port',
|
||||||
|
];
|
||||||
|
|
||||||
|
function getRandomProxy() {
|
||||||
|
return proxies[Math.floor(Math.random() * proxies.length)];
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchWithProxy(url, options = {}) {
|
||||||
|
const agent = new HttpsProxyAgent(getRandomProxy());
|
||||||
|
return fetch(url, { ...options, agent });
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 诊断流程
|
||||||
|
|
||||||
|
```
|
||||||
|
请求失败?
|
||||||
|
│
|
||||||
|
├─ 检查 HTTP 状态码
|
||||||
|
│ ├─ 403 → TLS 指纹 / Referer / UA / IP 封禁
|
||||||
|
│ ├─ 412 → 前置条件失败(Cookie / Token 过期)
|
||||||
|
│ ├─ 429 → 频率限制
|
||||||
|
│ └─ 200 但数据异常 → 参数错误 / 加密不对
|
||||||
|
│
|
||||||
|
├─ 同样的请求在浏览器中成功?
|
||||||
|
│ ├─ 是 → TLS 指纹问题,切换到浏览器方案
|
||||||
|
│ └─ 否 → 参数/Cookie 确实有问题,重新分析
|
||||||
|
│
|
||||||
|
├─ curl 命令测试
|
||||||
|
│ ├─ curl 成功 → Node.js 请求头/参数构造问题
|
||||||
|
│ └─ curl 也失败 → TLS / 协议 / IP 问题
|
||||||
|
│
|
||||||
|
└─ 使用 HTTP/2?
|
||||||
|
├─ HTTP/2 成功 → 切换到 http2 模块
|
||||||
|
└─ HTTP/2 也失败 → TLS 指纹问题
|
||||||
|
```
|
||||||
@@ -0,0 +1,128 @@
|
|||||||
|
# JS 逆向工作流总览
|
||||||
|
|
||||||
|
## 解法模式决策树
|
||||||
|
|
||||||
|
```
|
||||||
|
目标网站数据接口
|
||||||
|
│
|
||||||
|
├─ 能否抓到明确的数据接口?
|
||||||
|
│ ├─ 否 → WebSocket?→ 场景7
|
||||||
|
│ │ 页面渲染数据?→ 场景8(字体反爬/CSS干扰)
|
||||||
|
│ └─ 是 ↓
|
||||||
|
│
|
||||||
|
├─ 请求中有加密参数?
|
||||||
|
│ ├─ 否 → 直接请求即可,检查是否有 Cookie/Header 校验
|
||||||
|
│ └─ 是 ↓
|
||||||
|
│
|
||||||
|
├─ 加密逻辑在哪里?
|
||||||
|
│ ├─ 明文 JS(可直接定位)→ 模式A:纯 Node.js 还原
|
||||||
|
│ ├─ 混淆 JS(OB/CFF/eval)→ 反混淆后 → 模式A 或 模式B
|
||||||
|
│ ├─ 服务端返回 JS 动态执行 → 模式B:VM 沙箱执行
|
||||||
|
│ ├─ WASM 二进制 → 模式C:WASM 加载
|
||||||
|
│ ├─ JSVMP 虚拟机保护 → 先尝试 I/O 定位,失败则 → 模式D
|
||||||
|
│ └─ 无法脱离浏览器环境 → 模式D:浏览器自动化
|
||||||
|
│
|
||||||
|
├─ 纯请求方案是否可行?
|
||||||
|
│ ├─ 请求成功 → 完成
|
||||||
|
│ └─ 请求失败(token failed / 403)↓
|
||||||
|
│
|
||||||
|
└─ TLS 指纹 / HTTP/2 检测?
|
||||||
|
├─ 是 → 切换到模式D 或使用 HTTP/2
|
||||||
|
└─ 否 → 检查 Cookie、时间戳、参数拼接错误
|
||||||
|
```
|
||||||
|
|
||||||
|
## 模式详解
|
||||||
|
|
||||||
|
### 模式 A:纯 Node.js 算法还原
|
||||||
|
|
||||||
|
**适用条件**:
|
||||||
|
- 加密算法可用标准库实现(MD5/SHA/AES/DES/RSA/HMAC)
|
||||||
|
- 无浏览器环境检测
|
||||||
|
- 无 TLS 指纹校验
|
||||||
|
|
||||||
|
**技术栈**:
|
||||||
|
- `crypto`(Node.js 内置)— MD5/SHA/AES/DES/HMAC
|
||||||
|
- `crypto-js` — CryptoJS 兼容实现
|
||||||
|
- `node-rsa` / `node-forge` — RSA 加解密
|
||||||
|
- `axios` / `node-fetch` — HTTP 请求
|
||||||
|
- `http2`(Node.js 内置)— HTTP/2 请求
|
||||||
|
|
||||||
|
**模板**:`templates/node-request/`
|
||||||
|
|
||||||
|
### 模式 B:Node vm 沙箱执行
|
||||||
|
|
||||||
|
**适用条件**:
|
||||||
|
- 服务端返回混淆 JS,需要 eval 执行生成 Cookie/Token
|
||||||
|
- 加密逻辑依赖大量上下文,手动还原成本过高
|
||||||
|
|
||||||
|
**技术栈**:
|
||||||
|
- `vm`(Node.js 内置)— 沙箱执行
|
||||||
|
- `vm2` — 更安全的沙箱(可选)
|
||||||
|
- `jsdom` — 模拟 DOM 环境(可选)
|
||||||
|
|
||||||
|
**注意事项**:
|
||||||
|
- 必须构造最小沙箱环境(document、navigator、location 等)
|
||||||
|
- Cookie setter 需要拦截
|
||||||
|
- setTimeout/setInterval 需要提供但可能需要限制
|
||||||
|
|
||||||
|
**模板**:`templates/vm-sandbox/`
|
||||||
|
|
||||||
|
### 模式 C:WASM 加载还原
|
||||||
|
|
||||||
|
**适用条件**:
|
||||||
|
- 加密函数通过 WebAssembly 实现
|
||||||
|
- WASM 有明确的导出函数接口
|
||||||
|
|
||||||
|
**技术栈**:
|
||||||
|
- `fs`(Node.js 内置)— 读取 .wasm 文件
|
||||||
|
- `WebAssembly`(Node.js 内置)— 加载和实例化
|
||||||
|
|
||||||
|
**注意事项**:
|
||||||
|
- 先验证 I/O,不要先反编译 WASM
|
||||||
|
- 检查 `WebAssembly.Module.imports()` 了解环境依赖
|
||||||
|
- wasm-bindgen 生成的 WASM 需要补 `Window` 类和 DOM
|
||||||
|
- `unreachable` panic 通常是环境缺失
|
||||||
|
|
||||||
|
**模板**:`templates/wasm-loader/`
|
||||||
|
|
||||||
|
### 模式 D:浏览器自动化
|
||||||
|
|
||||||
|
**适用条件**:
|
||||||
|
- TLS 指纹检测,纯请求无法通过
|
||||||
|
- 复杂环境依赖(window 蜜罐、Canvas 指纹等)
|
||||||
|
- 加密逻辑过于复杂,还原成本远大于自动化
|
||||||
|
|
||||||
|
**技术栈**:
|
||||||
|
- `playwright` / `puppeteer` — 浏览器自动化
|
||||||
|
- chrome-devtools MCP — 直接操作已有 Chrome
|
||||||
|
|
||||||
|
**注意事项**:
|
||||||
|
- 复用本机 Chrome 实例避免指纹差异
|
||||||
|
- 注入脚本截取中间数据
|
||||||
|
- 控制请求频率避免触发风控
|
||||||
|
|
||||||
|
**模板**:`templates/browser-auto/`
|
||||||
|
|
||||||
|
## 通用调试流程
|
||||||
|
|
||||||
|
```
|
||||||
|
1. 打开 Network 面板
|
||||||
|
↓
|
||||||
|
2. 触发目标操作(搜索/翻页/登录)
|
||||||
|
↓
|
||||||
|
3. 找到数据接口,分析参数
|
||||||
|
↓
|
||||||
|
4. 搜索参数名定位 JS 源码
|
||||||
|
↓
|
||||||
|
5. 设置 XHR 断点追踪调用栈
|
||||||
|
↓
|
||||||
|
6. 在关键函数设置断点
|
||||||
|
↓
|
||||||
|
7. 单步调试确认加密逻辑
|
||||||
|
↓
|
||||||
|
8. 还原算法并对比中间值
|
||||||
|
↓
|
||||||
|
9. 编写 Node.js 脚本
|
||||||
|
↓
|
||||||
|
10. 运行验证
|
||||||
|
```
|
||||||
Executable
+87
@@ -0,0 +1,87 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# 检查 JS 逆向项目所需的依赖环境
|
||||||
|
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
NC='\033[0m'
|
||||||
|
|
||||||
|
echo "=========================================="
|
||||||
|
echo " JS 逆向工程环境检查"
|
||||||
|
echo "=========================================="
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
check_command() {
|
||||||
|
local cmd=$1
|
||||||
|
local name=$2
|
||||||
|
local install_hint=$3
|
||||||
|
|
||||||
|
if command -v "$cmd" &> /dev/null; then
|
||||||
|
local version=$($cmd --version 2>&1 | head -1)
|
||||||
|
echo -e " ${GREEN}✓${NC} $name: $version"
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
echo -e " ${RED}✗${NC} $name: 未安装"
|
||||||
|
if [ -n "$install_hint" ]; then
|
||||||
|
echo -e " ${YELLOW}→ 安装: $install_hint${NC}"
|
||||||
|
fi
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
check_node_module() {
|
||||||
|
local module=$1
|
||||||
|
local name=$2
|
||||||
|
|
||||||
|
if node -e "require('$module')" 2>/dev/null; then
|
||||||
|
local version=$(node -e "console.log(require('$module/package.json').version)" 2>/dev/null || echo "unknown")
|
||||||
|
echo -e " ${GREEN}✓${NC} $name: v$version"
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
echo -e " ${YELLOW}○${NC} $name: 未安装(可选)"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
errors=0
|
||||||
|
|
||||||
|
echo "▸ 基础环境"
|
||||||
|
check_command "node" "Node.js" "https://nodejs.org/ 或 brew install node" || ((errors++))
|
||||||
|
check_command "npm" "npm" "随 Node.js 一起安装" || ((errors++))
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo "▸ 浏览器"
|
||||||
|
if [ -f "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" ]; then
|
||||||
|
chrome_version=$("/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" --version 2>/dev/null)
|
||||||
|
echo -e " ${GREEN}✓${NC} Google Chrome: $chrome_version"
|
||||||
|
elif command -v google-chrome &> /dev/null; then
|
||||||
|
chrome_version=$(google-chrome --version 2>/dev/null)
|
||||||
|
echo -e " ${GREEN}✓${NC} Google Chrome: $chrome_version"
|
||||||
|
else
|
||||||
|
echo -e " ${YELLOW}○${NC} Google Chrome: 未检测到(浏览器自动化模式需要)"
|
||||||
|
fi
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo "▸ 全局 npm 包"
|
||||||
|
check_node_module "crypto-js" "crypto-js"
|
||||||
|
check_node_module "axios" "axios"
|
||||||
|
check_node_module "node-forge" "node-forge"
|
||||||
|
check_node_module "jsdom" "jsdom"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo "▸ 可选工具"
|
||||||
|
check_command "curl" "curl" "系统自带"
|
||||||
|
check_command "jq" "jq" "brew install jq"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo "=========================================="
|
||||||
|
if [ $errors -gt 0 ]; then
|
||||||
|
echo -e " ${RED}检查完成: $errors 个必需依赖缺失${NC}"
|
||||||
|
echo ""
|
||||||
|
echo " 快速安装缺失依赖:"
|
||||||
|
echo " npm install -g crypto-js axios node-forge"
|
||||||
|
exit 1
|
||||||
|
else
|
||||||
|
echo -e " ${GREEN}检查完成: 环境就绪${NC}"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
@@ -0,0 +1,291 @@
|
|||||||
|
/**
|
||||||
|
* 加密算法识别器
|
||||||
|
* 根据密文特征自动识别可能的加密算法
|
||||||
|
*
|
||||||
|
* 使用方式:
|
||||||
|
* node crypto-identifier.js "e10adc3949ba59abbe56e057f20f883e"
|
||||||
|
* node crypto-identifier.js --file=captured_params.json
|
||||||
|
*/
|
||||||
|
|
||||||
|
const PATTERNS = [
|
||||||
|
{
|
||||||
|
name: 'MD5',
|
||||||
|
regex: /^[0-9a-f]{32}$/i,
|
||||||
|
description: '32位十六进制字符串',
|
||||||
|
length: 32,
|
||||||
|
charset: 'hex',
|
||||||
|
verify: '对比标准 MD5 输出。如果不一致,可能是自定义 MD5(如 chrsz=16)',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'MD5 (大写)',
|
||||||
|
regex: /^[0-9A-F]{32}$/,
|
||||||
|
description: '32位大写十六进制',
|
||||||
|
length: 32,
|
||||||
|
charset: 'HEX',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'SHA-1',
|
||||||
|
regex: /^[0-9a-f]{40}$/i,
|
||||||
|
description: '40位十六进制字符串',
|
||||||
|
length: 40,
|
||||||
|
charset: 'hex',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'SHA-256',
|
||||||
|
regex: /^[0-9a-f]{64}$/i,
|
||||||
|
description: '64位十六进制字符串',
|
||||||
|
length: 64,
|
||||||
|
charset: 'hex',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'SHA-512',
|
||||||
|
regex: /^[0-9a-f]{128}$/i,
|
||||||
|
description: '128位十六进制字符串',
|
||||||
|
length: 128,
|
||||||
|
charset: 'hex',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Base64',
|
||||||
|
regex: /^[A-Za-z0-9+/]+=*$/,
|
||||||
|
description: '标准 Base64 编码',
|
||||||
|
charset: 'base64',
|
||||||
|
verify: '尝试 atob() 解码,检查结果是否有意义',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Base64url',
|
||||||
|
regex: /^[A-Za-z0-9_-]+$/,
|
||||||
|
description: 'URL安全 Base64 编码(无填充)',
|
||||||
|
charset: 'base64url',
|
||||||
|
verify: '替换 -_ 为 +/ 后尝试解码',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'AES-CBC/ECB (Base64)',
|
||||||
|
regex: /^[A-Za-z0-9+/]+=*$/,
|
||||||
|
test: (s) => {
|
||||||
|
const raw = Buffer.from(s, 'base64');
|
||||||
|
return raw.length > 0 && raw.length % 16 === 0;
|
||||||
|
},
|
||||||
|
description: 'Base64 编码,解码后长度为 16 的倍数',
|
||||||
|
verify: '需要找到 key (16/24/32字节) 和 iv (16字节,CBC模式)',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'AES-CBC/ECB (Hex)',
|
||||||
|
regex: /^[0-9a-f]+$/i,
|
||||||
|
test: (s) => s.length >= 32 && s.length % 32 === 0,
|
||||||
|
description: '十六进制编码,长度为 32 的倍数',
|
||||||
|
verify: '需要找到 key 和 iv',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'DES-ECB/CBC (Base64)',
|
||||||
|
regex: /^[A-Za-z0-9+/]+=*$/,
|
||||||
|
test: (s) => {
|
||||||
|
const raw = Buffer.from(s, 'base64');
|
||||||
|
return raw.length > 0 && raw.length % 8 === 0 && raw.length % 16 !== 0;
|
||||||
|
},
|
||||||
|
description: 'Base64 编码,解码后长度为 8 的倍数但不是 16 的倍数',
|
||||||
|
verify: '需要找到 8 字节 key',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'RSA (Base64)',
|
||||||
|
regex: /^[A-Za-z0-9+/]+=*$/,
|
||||||
|
test: (s) => {
|
||||||
|
const raw = Buffer.from(s, 'base64');
|
||||||
|
return raw.length >= 64;
|
||||||
|
},
|
||||||
|
description: '较长的 Base64 字符串(64字节以上)',
|
||||||
|
verify: '需要找到公钥/私钥',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'RSA (Hex)',
|
||||||
|
regex: /^[0-9a-f]+$/i,
|
||||||
|
test: (s) => s.length >= 128,
|
||||||
|
description: '较长的十六进制字符串',
|
||||||
|
verify: '搜索 RSA、publicKey、modulus 关键词',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'HMAC-SHA256',
|
||||||
|
regex: /^[0-9a-f]{64}$/i,
|
||||||
|
description: '64位十六进制(与 SHA-256 相同格式)',
|
||||||
|
verify: '搜索 HMAC、createHmac、HmacSHA256 关键词',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'UUID',
|
||||||
|
regex: /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i,
|
||||||
|
description: 'UUID 格式',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '时间戳 (秒)',
|
||||||
|
regex: /^1[0-9]{9}$/,
|
||||||
|
description: '10位数字,Unix 时间戳(秒)',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '时间戳 (毫秒)',
|
||||||
|
regex: /^1[0-9]{12}$/,
|
||||||
|
description: '13位数字,Unix 时间戳(毫秒)',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'JWT',
|
||||||
|
regex: /^eyJ[A-Za-z0-9_-]+\.eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/,
|
||||||
|
description: 'JSON Web Token(三段 Base64url)',
|
||||||
|
verify: '分别解码三段:Header.Payload.Signature',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '十六进制编码字符串',
|
||||||
|
regex: /^([0-9a-f]{2})+$/i,
|
||||||
|
test: (s) => s.length > 10 && s.length % 2 === 0,
|
||||||
|
description: '连续十六进制对',
|
||||||
|
verify: '尝试 Buffer.from(str, "hex").toString()',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
function identify(ciphertext) {
|
||||||
|
const results = [];
|
||||||
|
const trimmed = ciphertext.trim();
|
||||||
|
|
||||||
|
for (const pattern of PATTERNS) {
|
||||||
|
if (pattern.regex.test(trimmed)) {
|
||||||
|
if (pattern.test && !pattern.test(trimmed)) continue;
|
||||||
|
if (pattern.length && trimmed.length !== pattern.length) continue;
|
||||||
|
|
||||||
|
results.push({
|
||||||
|
algorithm: pattern.name,
|
||||||
|
confidence: calculateConfidence(trimmed, pattern),
|
||||||
|
description: pattern.description,
|
||||||
|
verify: pattern.verify || '',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
results.sort((a, b) => b.confidence - a.confidence);
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
function calculateConfidence(text, pattern) {
|
||||||
|
let score = 50;
|
||||||
|
|
||||||
|
if (pattern.length && text.length === pattern.length) score += 30;
|
||||||
|
if (pattern.charset === 'hex' && /^[0-9a-f]+$/i.test(text)) score += 10;
|
||||||
|
if (pattern.charset === 'base64' && text.endsWith('=')) score += 10;
|
||||||
|
if (pattern.test && pattern.test(text)) score += 20;
|
||||||
|
|
||||||
|
return Math.min(score, 95);
|
||||||
|
}
|
||||||
|
|
||||||
|
function analyzeMultiple(samples) {
|
||||||
|
const allResults = samples.map(s => ({ sample: s, matches: identify(s) }));
|
||||||
|
|
||||||
|
const lengthSet = new Set(samples.map(s => s.length));
|
||||||
|
const charsetConsistent = samples.every(s => /^[0-9a-f]+$/i.test(s)) ||
|
||||||
|
samples.every(s => /^[A-Za-z0-9+/]+=*$/.test(s));
|
||||||
|
|
||||||
|
const analysis = {
|
||||||
|
sampleCount: samples.length,
|
||||||
|
lengths: [...lengthSet],
|
||||||
|
fixedLength: lengthSet.size === 1,
|
||||||
|
charsetConsistent,
|
||||||
|
results: allResults,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (analysis.fixedLength && analysis.charsetConsistent) {
|
||||||
|
analysis.recommendation = '所有样本长度相同且字符集一致,很可能是同一算法的不同输出';
|
||||||
|
} else if (!analysis.fixedLength) {
|
||||||
|
analysis.recommendation = '样本长度不一致,可能涉及变长输入或不同算法';
|
||||||
|
}
|
||||||
|
|
||||||
|
return analysis;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatReport(ciphertext) {
|
||||||
|
const results = identify(ciphertext);
|
||||||
|
|
||||||
|
let report = '';
|
||||||
|
report += `\n🔍 密文分析报告\n`;
|
||||||
|
report += `${'━'.repeat(50)}\n`;
|
||||||
|
report += `输入: ${ciphertext.substring(0, 80)}${ciphertext.length > 80 ? '...' : ''}\n`;
|
||||||
|
report += `长度: ${ciphertext.length}\n`;
|
||||||
|
report += `字符集: ${detectCharset(ciphertext)}\n`;
|
||||||
|
report += `${'━'.repeat(50)}\n`;
|
||||||
|
|
||||||
|
if (results.length === 0) {
|
||||||
|
report += `\n❌ 未识别到已知的加密模式\n`;
|
||||||
|
report += `建议:搜索 JS 源码中的加密函数名\n`;
|
||||||
|
} else {
|
||||||
|
report += `\n可能的算法(按可信度排序):\n\n`;
|
||||||
|
results.forEach((r, i) => {
|
||||||
|
report += ` ${i + 1}. ${r.algorithm} (${r.confidence}%)\n`;
|
||||||
|
report += ` 描述: ${r.description}\n`;
|
||||||
|
if (r.verify) report += ` 验证: ${r.verify}\n`;
|
||||||
|
report += `\n`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Base64 解码尝试
|
||||||
|
if (/^[A-Za-z0-9+/]+=*$/.test(ciphertext)) {
|
||||||
|
try {
|
||||||
|
const decoded = Buffer.from(ciphertext, 'base64');
|
||||||
|
report += `📦 Base64 解码结果:\n`;
|
||||||
|
report += ` 二进制长度: ${decoded.length} 字节\n`;
|
||||||
|
if (decoded.length % 16 === 0) report += ` ⚠ 长度是 16 的倍数(可能是 AES)\n`;
|
||||||
|
if (decoded.length % 8 === 0) report += ` ⚠ 长度是 8 的倍数(可能是 DES)\n`;
|
||||||
|
const textAttempt = decoded.toString('utf-8');
|
||||||
|
if (/^[\x20-\x7e]+$/.test(textAttempt)) {
|
||||||
|
report += ` 文本: ${textAttempt.substring(0, 100)}\n`;
|
||||||
|
} else {
|
||||||
|
report += ` 十六进制: ${decoded.toString('hex').substring(0, 100)}\n`;
|
||||||
|
}
|
||||||
|
} catch (e) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
return report;
|
||||||
|
}
|
||||||
|
|
||||||
|
function detectCharset(str) {
|
||||||
|
if (/^[0-9]+$/.test(str)) return '纯数字';
|
||||||
|
if (/^[0-9a-f]+$/i.test(str)) return '十六进制';
|
||||||
|
if (/^[A-Za-z0-9+/]+=*$/.test(str)) return 'Base64';
|
||||||
|
if (/^[A-Za-z0-9_-]+$/.test(str)) return 'Base64url / 字母数字';
|
||||||
|
if (/^[\x20-\x7e]+$/.test(str)) return '可打印ASCII';
|
||||||
|
return '混合/二进制';
|
||||||
|
}
|
||||||
|
|
||||||
|
// CLI
|
||||||
|
if (require.main === module) {
|
||||||
|
const args = process.argv.slice(2);
|
||||||
|
|
||||||
|
if (args.length === 0) {
|
||||||
|
console.log('用法:');
|
||||||
|
console.log(' node crypto-identifier.js "<密文>"');
|
||||||
|
console.log(' node crypto-identifier.js --file=params.json');
|
||||||
|
console.log(' node crypto-identifier.js --compare "<样本1>" "<样本2>" ...');
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (args[0] === '--compare') {
|
||||||
|
const samples = args.slice(1);
|
||||||
|
const analysis = analyzeMultiple(samples);
|
||||||
|
console.log('\n📊 多样本对比分析');
|
||||||
|
console.log('━'.repeat(50));
|
||||||
|
console.log(`样本数量: ${analysis.sampleCount}`);
|
||||||
|
console.log(`长度分布: ${analysis.lengths.join(', ')}`);
|
||||||
|
console.log(`固定长度: ${analysis.fixedLength ? '是' : '否'}`);
|
||||||
|
console.log(`字符集一致: ${analysis.charsetConsistent ? '是' : '否'}`);
|
||||||
|
if (analysis.recommendation) console.log(`\n💡 ${analysis.recommendation}`);
|
||||||
|
analysis.results.forEach((r, i) => {
|
||||||
|
console.log(`\n--- 样本 ${i + 1}: ${r.sample.substring(0, 40)}... ---`);
|
||||||
|
r.matches.slice(0, 3).forEach(m => {
|
||||||
|
console.log(` ${m.algorithm} (${m.confidence}%)`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else if (args[0].startsWith('--file=')) {
|
||||||
|
const fs = require('fs');
|
||||||
|
const data = JSON.parse(fs.readFileSync(args[0].substring(7), 'utf-8'));
|
||||||
|
const samples = Array.isArray(data) ? data : Object.values(data);
|
||||||
|
samples.forEach((s, i) => {
|
||||||
|
console.log(formatReport(String(s)));
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.log(formatReport(args[0]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { identify, analyzeMultiple, formatReport };
|
||||||
@@ -0,0 +1,346 @@
|
|||||||
|
/**
|
||||||
|
* Hook 代码生成器
|
||||||
|
* 根据逆向目标快速生成 Hook 注入代码
|
||||||
|
*
|
||||||
|
* 使用方式:
|
||||||
|
* node hook-generator.js --type=cookie
|
||||||
|
* node hook-generator.js --type=xhr --target="/api/data"
|
||||||
|
* node hook-generator.js --type=all
|
||||||
|
* node hook-generator.js --type=custom --function="encrypt" --module="CryptoJS"
|
||||||
|
*/
|
||||||
|
|
||||||
|
const HOOK_TEMPLATES = {
|
||||||
|
|
||||||
|
cookie: (options = {}) => {
|
||||||
|
const target = options.target || '';
|
||||||
|
return `(function() {
|
||||||
|
var _cookie = document.cookie || '';
|
||||||
|
Object.defineProperty(document, 'cookie', {
|
||||||
|
set: function(val) {
|
||||||
|
${target ? `if (val.indexOf('${target}') !== -1) {` : '{'}
|
||||||
|
console.log('[Hook:Cookie] Set:', val);
|
||||||
|
console.trace('[Hook:Cookie] 调用栈');
|
||||||
|
}
|
||||||
|
var parts = val.split(';');
|
||||||
|
var mainPart = parts[0];
|
||||||
|
if (_cookie) _cookie += '; ';
|
||||||
|
_cookie += mainPart;
|
||||||
|
},
|
||||||
|
get: function() { return _cookie; },
|
||||||
|
configurable: true
|
||||||
|
});
|
||||||
|
console.log('[Hook:Cookie] Cookie Hook 已注入');
|
||||||
|
})();`;
|
||||||
|
},
|
||||||
|
|
||||||
|
xhr: (options = {}) => {
|
||||||
|
const target = options.target || '';
|
||||||
|
return `(function() {
|
||||||
|
var _open = XMLHttpRequest.prototype.open;
|
||||||
|
var _send = XMLHttpRequest.prototype.send;
|
||||||
|
var _setHeader = XMLHttpRequest.prototype.setRequestHeader;
|
||||||
|
|
||||||
|
XMLHttpRequest.prototype.open = function(method, url) {
|
||||||
|
this.__hookMethod = method;
|
||||||
|
this.__hookUrl = url;
|
||||||
|
this.__hookHeaders = {};
|
||||||
|
return _open.apply(this, arguments);
|
||||||
|
};
|
||||||
|
|
||||||
|
XMLHttpRequest.prototype.setRequestHeader = function(name, value) {
|
||||||
|
this.__hookHeaders[name] = value;
|
||||||
|
return _setHeader.apply(this, arguments);
|
||||||
|
};
|
||||||
|
|
||||||
|
XMLHttpRequest.prototype.send = function(body) {
|
||||||
|
${target ? `if (this.__hookUrl && this.__hookUrl.indexOf('${target}') !== -1) {` : '{'}
|
||||||
|
console.log('[Hook:XHR] ★', this.__hookMethod, this.__hookUrl);
|
||||||
|
console.log('[Hook:XHR] Headers:', JSON.stringify(this.__hookHeaders));
|
||||||
|
console.log('[Hook:XHR] Body:', body);
|
||||||
|
console.trace('[Hook:XHR] 调用栈');
|
||||||
|
}
|
||||||
|
return _send.apply(this, arguments);
|
||||||
|
};
|
||||||
|
console.log('[Hook:XHR] XHR Hook 已注入');
|
||||||
|
})();`;
|
||||||
|
},
|
||||||
|
|
||||||
|
fetch: (options = {}) => {
|
||||||
|
const target = options.target || '';
|
||||||
|
return `(function() {
|
||||||
|
var _fetch = window.fetch;
|
||||||
|
window.fetch = function(input, init) {
|
||||||
|
var url = typeof input === 'string' ? input : (input.url || '');
|
||||||
|
${target ? `if (url.indexOf('${target}') !== -1) {` : '{'}
|
||||||
|
console.log('[Hook:Fetch] ★', url);
|
||||||
|
console.log('[Hook:Fetch] Options:', JSON.stringify(init || {}));
|
||||||
|
console.trace('[Hook:Fetch] 调用栈');
|
||||||
|
}
|
||||||
|
return _fetch.apply(this, arguments);
|
||||||
|
};
|
||||||
|
console.log('[Hook:Fetch] Fetch Hook 已注入');
|
||||||
|
})();`;
|
||||||
|
},
|
||||||
|
|
||||||
|
eval: () => {
|
||||||
|
return `(function() {
|
||||||
|
var _eval = window.eval;
|
||||||
|
window.eval = function(code) {
|
||||||
|
console.log('[Hook:eval] 代码长度:', typeof code === 'string' ? code.length : 'N/A');
|
||||||
|
if (typeof code === 'string' && code.length < 10000) {
|
||||||
|
console.log('[Hook:eval] 内容:', code.substring(0, 500));
|
||||||
|
}
|
||||||
|
return _eval.apply(this, arguments);
|
||||||
|
};
|
||||||
|
|
||||||
|
var _Function = Function;
|
||||||
|
window.Function = function() {
|
||||||
|
var body = arguments[arguments.length - 1];
|
||||||
|
console.log('[Hook:Function] body长度:', body ? body.length : 0);
|
||||||
|
if (body && body.length < 5000) {
|
||||||
|
console.log('[Hook:Function] 内容:', body.substring(0, 500));
|
||||||
|
}
|
||||||
|
return _Function.apply(this, arguments);
|
||||||
|
};
|
||||||
|
window.Function.prototype = _Function.prototype;
|
||||||
|
console.log('[Hook:eval] eval/Function Hook 已注入');
|
||||||
|
})();`;
|
||||||
|
},
|
||||||
|
|
||||||
|
json: () => {
|
||||||
|
return `(function() {
|
||||||
|
var _parse = JSON.parse;
|
||||||
|
var _stringify = JSON.stringify;
|
||||||
|
|
||||||
|
JSON.parse = function(text) {
|
||||||
|
var result = _parse.apply(this, arguments);
|
||||||
|
console.log('[Hook:JSON] parse:', typeof text === 'string' ? text.substring(0, 200) : typeof text);
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
JSON.stringify = function(obj) {
|
||||||
|
var result = _stringify.apply(this, arguments);
|
||||||
|
console.log('[Hook:JSON] stringify:', result ? result.substring(0, 200) : result);
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
console.log('[Hook:JSON] JSON Hook 已注入');
|
||||||
|
})();`;
|
||||||
|
},
|
||||||
|
|
||||||
|
base64: () => {
|
||||||
|
return `(function() {
|
||||||
|
var _atob = window.atob;
|
||||||
|
var _btoa = window.btoa;
|
||||||
|
|
||||||
|
window.atob = function(str) {
|
||||||
|
var result = _atob(str);
|
||||||
|
console.log('[Hook:Base64] atob:', str.substring(0, 80), '→', result.substring(0, 80));
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
window.btoa = function(str) {
|
||||||
|
var result = _btoa(str);
|
||||||
|
console.log('[Hook:Base64] btoa:', str.substring(0, 80), '→', result.substring(0, 80));
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
console.log('[Hook:Base64] Base64 Hook 已注入');
|
||||||
|
})();`;
|
||||||
|
},
|
||||||
|
|
||||||
|
websocket: () => {
|
||||||
|
return `(function() {
|
||||||
|
var _WS = window.WebSocket;
|
||||||
|
window.WebSocket = function(url, protocols) {
|
||||||
|
console.log('[Hook:WS] 连接:', url);
|
||||||
|
var ws = new _WS(url, protocols);
|
||||||
|
var _send = ws.send.bind(ws);
|
||||||
|
ws.send = function(data) {
|
||||||
|
console.log('[Hook:WS] 发送:', typeof data === 'string' ? data.substring(0, 200) : '[binary]');
|
||||||
|
return _send(data);
|
||||||
|
};
|
||||||
|
ws.addEventListener('message', function(e) {
|
||||||
|
console.log('[Hook:WS] 接收:', typeof e.data === 'string' ? e.data.substring(0, 200) : '[binary]');
|
||||||
|
});
|
||||||
|
return ws;
|
||||||
|
};
|
||||||
|
window.WebSocket.prototype = _WS.prototype;
|
||||||
|
console.log('[Hook:WS] WebSocket Hook 已注入');
|
||||||
|
})();`;
|
||||||
|
},
|
||||||
|
|
||||||
|
debugger_bypass: () => {
|
||||||
|
return `(function() {
|
||||||
|
var _Function = Function;
|
||||||
|
Function = function() {
|
||||||
|
var body = arguments[arguments.length - 1];
|
||||||
|
if (typeof body === 'string' && body.indexOf('debugger') !== -1) {
|
||||||
|
arguments[arguments.length - 1] = body.replace(/debugger\\s*;?/g, '');
|
||||||
|
}
|
||||||
|
return _Function.apply(this, arguments);
|
||||||
|
};
|
||||||
|
Function.prototype = _Function.prototype;
|
||||||
|
|
||||||
|
var _si = window.setInterval;
|
||||||
|
window.setInterval = function(fn, ms) {
|
||||||
|
if (typeof fn === 'function' && fn.toString().indexOf('debugger') > -1) return -1;
|
||||||
|
if (typeof fn === 'string' && fn.indexOf('debugger') > -1) return -1;
|
||||||
|
return _si.apply(this, arguments);
|
||||||
|
};
|
||||||
|
|
||||||
|
var _st = window.setTimeout;
|
||||||
|
window.setTimeout = function(fn, ms) {
|
||||||
|
if (typeof fn === 'function' && fn.toString().indexOf('debugger') > -1) return -1;
|
||||||
|
if (typeof fn === 'string' && fn.indexOf('debugger') > -1) return -1;
|
||||||
|
return _st.apply(this, arguments);
|
||||||
|
};
|
||||||
|
console.log('[Hook:AntiDebug] debugger 绕过已注入');
|
||||||
|
})();`;
|
||||||
|
},
|
||||||
|
|
||||||
|
stealth: () => {
|
||||||
|
return `(function() {
|
||||||
|
Object.defineProperty(navigator, 'webdriver', { get: function() { return undefined; }, configurable: true });
|
||||||
|
|
||||||
|
window.chrome = window.chrome || {};
|
||||||
|
window.chrome.runtime = window.chrome.runtime || {};
|
||||||
|
|
||||||
|
Object.defineProperty(navigator, 'plugins', {
|
||||||
|
get: function() {
|
||||||
|
return [
|
||||||
|
{ name: 'Chrome PDF Plugin', filename: 'internal-pdf-viewer' },
|
||||||
|
{ name: 'Chrome PDF Viewer', filename: 'mhjfbmdgcfjbbpaeojofohoefgiehjai' },
|
||||||
|
{ name: 'Native Client', filename: 'internal-nacl-plugin' }
|
||||||
|
];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Object.defineProperty(navigator, 'languages', {
|
||||||
|
get: function() { return ['zh-CN', 'zh', 'en']; }
|
||||||
|
});
|
||||||
|
|
||||||
|
var _getParameter = WebGLRenderingContext.prototype.getParameter;
|
||||||
|
WebGLRenderingContext.prototype.getParameter = function(param) {
|
||||||
|
if (param === 37445) return 'Intel Inc.';
|
||||||
|
if (param === 37446) return 'Intel Iris OpenGL Engine';
|
||||||
|
return _getParameter.apply(this, arguments);
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log('[Hook:Stealth] 隐身模式已注入');
|
||||||
|
})();`;
|
||||||
|
},
|
||||||
|
|
||||||
|
custom: (options = {}) => {
|
||||||
|
const funcName = options.function || 'targetFunction';
|
||||||
|
return `(function() {
|
||||||
|
// 追踪 ${funcName} 函数
|
||||||
|
if (typeof ${funcName} !== 'undefined') {
|
||||||
|
var _orig = ${funcName};
|
||||||
|
${funcName} = function() {
|
||||||
|
console.log('[Hook:Custom] ${funcName} 调用');
|
||||||
|
console.log('[Hook:Custom] 参数:', JSON.stringify(Array.from(arguments)));
|
||||||
|
var result = _orig.apply(this, arguments);
|
||||||
|
console.log('[Hook:Custom] 返回:', JSON.stringify(result));
|
||||||
|
console.trace('[Hook:Custom] 调用栈');
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
console.log('[Hook:Custom] ${funcName} Hook 已注入');
|
||||||
|
} else {
|
||||||
|
console.warn('[Hook:Custom] ${funcName} 未定义,等待加载...');
|
||||||
|
// 使用 getter 延迟 Hook
|
||||||
|
var _origVal;
|
||||||
|
Object.defineProperty(window, '${funcName}', {
|
||||||
|
get: function() { return _origVal; },
|
||||||
|
set: function(val) {
|
||||||
|
if (typeof val === 'function') {
|
||||||
|
var _fn = val;
|
||||||
|
_origVal = function() {
|
||||||
|
console.log('[Hook:Custom] ${funcName} 调用');
|
||||||
|
console.log('[Hook:Custom] 参数:', JSON.stringify(Array.from(arguments)));
|
||||||
|
var result = _fn.apply(this, arguments);
|
||||||
|
console.log('[Hook:Custom] 返回:', JSON.stringify(result));
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
_origVal = val;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
configurable: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})();`;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
function generateHook(type, options = {}) {
|
||||||
|
const generator = HOOK_TEMPLATES[type];
|
||||||
|
if (!generator) {
|
||||||
|
throw new Error(`未知的 Hook 类型: ${type}。可用类型: ${Object.keys(HOOK_TEMPLATES).join(', ')}`);
|
||||||
|
}
|
||||||
|
return generator(options);
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateAllHooks(options = {}) {
|
||||||
|
const hooks = [
|
||||||
|
HOOK_TEMPLATES.debugger_bypass(),
|
||||||
|
HOOK_TEMPLATES.stealth(),
|
||||||
|
HOOK_TEMPLATES.cookie(options),
|
||||||
|
HOOK_TEMPLATES.xhr(options),
|
||||||
|
HOOK_TEMPLATES.fetch(options),
|
||||||
|
HOOK_TEMPLATES.eval(),
|
||||||
|
HOOK_TEMPLATES.base64(),
|
||||||
|
];
|
||||||
|
return hooks.join('\n\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
// CLI
|
||||||
|
if (require.main === module) {
|
||||||
|
const args = process.argv.slice(2);
|
||||||
|
const options = {};
|
||||||
|
|
||||||
|
args.forEach(arg => {
|
||||||
|
if (arg.startsWith('--type=')) options.type = arg.substring(7);
|
||||||
|
else if (arg.startsWith('--target=')) options.target = arg.substring(9);
|
||||||
|
else if (arg.startsWith('--function=')) options.function = arg.substring(11);
|
||||||
|
else if (arg.startsWith('--output=')) options.output = arg.substring(9);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!options.type) {
|
||||||
|
console.log('用法: node hook-generator.js --type=<type> [options]');
|
||||||
|
console.log('');
|
||||||
|
console.log('类型:');
|
||||||
|
console.log(' cookie Cookie setter 拦截');
|
||||||
|
console.log(' xhr XMLHttpRequest 拦截');
|
||||||
|
console.log(' fetch Fetch API 拦截');
|
||||||
|
console.log(' eval eval/Function 拦截');
|
||||||
|
console.log(' json JSON.parse/stringify 拦截');
|
||||||
|
console.log(' base64 atob/btoa 拦截');
|
||||||
|
console.log(' websocket WebSocket 拦截');
|
||||||
|
console.log(' debugger_bypass debugger 反调试绕过');
|
||||||
|
console.log(' stealth 浏览器指纹隐身');
|
||||||
|
console.log(' custom 自定义函数拦截');
|
||||||
|
console.log(' all 生成所有 Hook(组合包)');
|
||||||
|
console.log('');
|
||||||
|
console.log('选项:');
|
||||||
|
console.log(' --target=<url> 目标接口路径过滤');
|
||||||
|
console.log(' --function=<name> 自定义函数名(type=custom 时)');
|
||||||
|
console.log(' --output=<file> 输出到文件');
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
let code;
|
||||||
|
if (options.type === 'all') {
|
||||||
|
code = generateAllHooks(options);
|
||||||
|
} else {
|
||||||
|
code = generateHook(options.type, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.output) {
|
||||||
|
const fs = require('fs');
|
||||||
|
fs.writeFileSync(options.output, code);
|
||||||
|
console.log(`Hook 代码已保存到: ${options.output}`);
|
||||||
|
} else {
|
||||||
|
console.log(code);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { generateHook, generateAllHooks, HOOK_TEMPLATES };
|
||||||
@@ -0,0 +1,359 @@
|
|||||||
|
/**
|
||||||
|
* VM 沙箱执行器
|
||||||
|
* 用于在隔离的 Node.js 沙箱中执行从浏览器提取的 JS 代码
|
||||||
|
*
|
||||||
|
* 使用方式:
|
||||||
|
* node sandbox-runner.js <js-file> [options]
|
||||||
|
* node sandbox-runner.js code.js --url=https://target.com --extract-cookie
|
||||||
|
*/
|
||||||
|
|
||||||
|
const vm = require('vm');
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
class SandboxRunner {
|
||||||
|
constructor(options = {}) {
|
||||||
|
this.options = {
|
||||||
|
url: options.url || 'https://example.com',
|
||||||
|
userAgent: options.userAgent || 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
||||||
|
timeout: options.timeout || 10000,
|
||||||
|
extractCookie: options.extractCookie || false,
|
||||||
|
verbose: options.verbose || false,
|
||||||
|
...options,
|
||||||
|
};
|
||||||
|
|
||||||
|
this.cookies = {};
|
||||||
|
this.logs = [];
|
||||||
|
this.errors = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
createSandbox() {
|
||||||
|
const self = this;
|
||||||
|
const urlObj = new URL(this.options.url);
|
||||||
|
|
||||||
|
const sandbox = {
|
||||||
|
window: null,
|
||||||
|
self: null,
|
||||||
|
globalThis: null,
|
||||||
|
top: null,
|
||||||
|
parent: null,
|
||||||
|
frames: null,
|
||||||
|
|
||||||
|
document: this._createDocument(),
|
||||||
|
navigator: this._createNavigator(),
|
||||||
|
location: this._createLocation(urlObj),
|
||||||
|
screen: { width: 1920, height: 1080, availWidth: 1920, availHeight: 1055, colorDepth: 24, pixelDepth: 24 },
|
||||||
|
history: { length: 1, pushState: () => {}, replaceState: () => {}, back: () => {}, forward: () => {} },
|
||||||
|
|
||||||
|
setTimeout: (fn, ms) => setTimeout(fn, Math.min(ms || 0, 5000)),
|
||||||
|
setInterval: (fn, ms) => setInterval(fn, Math.max(ms || 100, 100)),
|
||||||
|
clearTimeout, clearInterval,
|
||||||
|
requestAnimationFrame: (fn) => setTimeout(fn, 16),
|
||||||
|
cancelAnimationFrame: clearTimeout,
|
||||||
|
|
||||||
|
String, Array, Object, Math, Date, RegExp, JSON, Map, Set, WeakMap, WeakSet,
|
||||||
|
Number, Boolean, Symbol, BigInt,
|
||||||
|
parseInt, parseFloat, isNaN, isFinite, NaN, Infinity, undefined,
|
||||||
|
encodeURIComponent, decodeURIComponent, encodeURI, decodeURI,
|
||||||
|
escape, unescape,
|
||||||
|
Error, TypeError, RangeError, SyntaxError, ReferenceError, URIError, EvalError,
|
||||||
|
ArrayBuffer, Uint8Array, Uint16Array, Uint32Array, Int8Array, Int16Array, Int32Array,
|
||||||
|
Float32Array, Float64Array, DataView,
|
||||||
|
Promise, Proxy, Reflect,
|
||||||
|
TextEncoder: typeof TextEncoder !== 'undefined' ? TextEncoder : class {},
|
||||||
|
TextDecoder: typeof TextDecoder !== 'undefined' ? TextDecoder : class {},
|
||||||
|
URL: typeof URL !== 'undefined' ? URL : class {},
|
||||||
|
URLSearchParams: typeof URLSearchParams !== 'undefined' ? URLSearchParams : class {},
|
||||||
|
|
||||||
|
btoa: (str) => Buffer.from(str, 'binary').toString('base64'),
|
||||||
|
atob: (b64) => Buffer.from(b64, 'base64').toString('binary'),
|
||||||
|
|
||||||
|
console: {
|
||||||
|
log: (...args) => { self.logs.push(args.join(' ')); if (self.options.verbose) console.log('[Sandbox]', ...args); },
|
||||||
|
warn: (...args) => { self.logs.push('[WARN] ' + args.join(' ')); },
|
||||||
|
error: (...args) => { self.errors.push(args.join(' ')); if (self.options.verbose) console.error('[Sandbox Error]', ...args); },
|
||||||
|
info: (...args) => { self.logs.push('[INFO] ' + args.join(' ')); },
|
||||||
|
debug: () => {},
|
||||||
|
trace: () => {},
|
||||||
|
clear: () => {},
|
||||||
|
},
|
||||||
|
|
||||||
|
XMLHttpRequest: this._createXHR(),
|
||||||
|
fetch: async () => new Response('{}'),
|
||||||
|
|
||||||
|
Event: class Event { constructor(type) { this.type = type; } },
|
||||||
|
CustomEvent: class CustomEvent { constructor(type, opts) { this.type = type; this.detail = opts?.detail; } },
|
||||||
|
MutationObserver: class MutationObserver { observe() {} disconnect() {} },
|
||||||
|
IntersectionObserver: class IntersectionObserver { observe() {} disconnect() {} },
|
||||||
|
ResizeObserver: class ResizeObserver { observe() {} disconnect() {} },
|
||||||
|
};
|
||||||
|
|
||||||
|
sandbox.window = sandbox;
|
||||||
|
sandbox.self = sandbox;
|
||||||
|
sandbox.globalThis = sandbox;
|
||||||
|
sandbox.top = sandbox;
|
||||||
|
sandbox.parent = sandbox;
|
||||||
|
sandbox.frames = sandbox;
|
||||||
|
|
||||||
|
// Cookie 拦截
|
||||||
|
if (this.options.extractCookie) {
|
||||||
|
this._setupCookieTrap(sandbox);
|
||||||
|
}
|
||||||
|
|
||||||
|
return sandbox;
|
||||||
|
}
|
||||||
|
|
||||||
|
_createDocument() {
|
||||||
|
const createElement = (tag) => ({
|
||||||
|
tagName: tag.toUpperCase(),
|
||||||
|
style: new Proxy({}, { get: () => '', set: () => true }),
|
||||||
|
classList: { add: () => {}, remove: () => {}, contains: () => false },
|
||||||
|
setAttribute: () => {},
|
||||||
|
getAttribute: () => null,
|
||||||
|
removeAttribute: () => {},
|
||||||
|
appendChild: (child) => child,
|
||||||
|
removeChild: () => {},
|
||||||
|
insertBefore: () => {},
|
||||||
|
addEventListener: () => {},
|
||||||
|
removeEventListener: () => {},
|
||||||
|
dispatchEvent: () => true,
|
||||||
|
innerHTML: '', innerText: '', textContent: '',
|
||||||
|
src: '', href: '', id: '', className: '',
|
||||||
|
parentNode: null, parentElement: null,
|
||||||
|
childNodes: [], children: [], firstChild: null, lastChild: null,
|
||||||
|
nextSibling: null, previousSibling: null,
|
||||||
|
offsetWidth: 100, offsetHeight: 100,
|
||||||
|
getBoundingClientRect: () => ({ top: 0, left: 0, bottom: 100, right: 100, width: 100, height: 100 }),
|
||||||
|
querySelector: () => null,
|
||||||
|
querySelectorAll: () => [],
|
||||||
|
getElementsByTagName: () => [],
|
||||||
|
getElementsByClassName: () => [],
|
||||||
|
cloneNode: () => createElement(tag),
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
cookie: '',
|
||||||
|
createElement,
|
||||||
|
createElementNS: (ns, tag) => createElement(tag),
|
||||||
|
createTextNode: (text) => ({ nodeType: 3, textContent: text }),
|
||||||
|
createDocumentFragment: () => ({ appendChild: () => {}, childNodes: [] }),
|
||||||
|
getElementById: () => null,
|
||||||
|
getElementsByTagName: (tag) => tag === 'script' ? [1, 2, 3] : [],
|
||||||
|
getElementsByClassName: () => [],
|
||||||
|
getElementsByName: () => [],
|
||||||
|
querySelector: () => null,
|
||||||
|
querySelectorAll: () => [],
|
||||||
|
addEventListener: () => {},
|
||||||
|
removeEventListener: () => {},
|
||||||
|
head: { appendChild: () => {} },
|
||||||
|
body: { appendChild: () => {}, innerHTML: '' },
|
||||||
|
documentElement: { style: {} },
|
||||||
|
readyState: 'complete',
|
||||||
|
title: '',
|
||||||
|
referrer: '',
|
||||||
|
domain: '',
|
||||||
|
URL: this.options.url,
|
||||||
|
characterSet: 'UTF-8',
|
||||||
|
contentType: 'text/html',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
_createNavigator() {
|
||||||
|
return {
|
||||||
|
userAgent: this.options.userAgent,
|
||||||
|
appCodeName: 'Mozilla',
|
||||||
|
appName: 'Netscape',
|
||||||
|
appVersion: '5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36',
|
||||||
|
platform: 'MacIntel',
|
||||||
|
product: 'Gecko',
|
||||||
|
productSub: '20030107',
|
||||||
|
vendor: 'Google Inc.',
|
||||||
|
vendorSub: '',
|
||||||
|
language: 'zh-CN',
|
||||||
|
languages: ['zh-CN', 'zh', 'en'],
|
||||||
|
cookieEnabled: true,
|
||||||
|
onLine: true,
|
||||||
|
doNotTrack: null,
|
||||||
|
hardwareConcurrency: 8,
|
||||||
|
maxTouchPoints: 0,
|
||||||
|
webdriver: false,
|
||||||
|
plugins: { length: 3, item: () => null, namedItem: () => null, refresh: () => {} },
|
||||||
|
mimeTypes: { length: 4, item: () => null, namedItem: () => null },
|
||||||
|
permissions: { query: async () => ({ state: 'granted' }) },
|
||||||
|
mediaDevices: { enumerateDevices: async () => [] },
|
||||||
|
geolocation: { getCurrentPosition: () => {} },
|
||||||
|
connection: { effectiveType: '4g', downlink: 10 },
|
||||||
|
getBattery: async () => ({ charging: true, chargingTime: 0, dischargingTime: Infinity, level: 1 }),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
_createLocation(urlObj) {
|
||||||
|
return {
|
||||||
|
href: urlObj.href,
|
||||||
|
protocol: urlObj.protocol,
|
||||||
|
host: urlObj.host,
|
||||||
|
hostname: urlObj.hostname,
|
||||||
|
port: urlObj.port,
|
||||||
|
pathname: urlObj.pathname,
|
||||||
|
search: urlObj.search,
|
||||||
|
hash: urlObj.hash,
|
||||||
|
origin: urlObj.origin,
|
||||||
|
assign: () => {},
|
||||||
|
replace: () => {},
|
||||||
|
reload: () => {},
|
||||||
|
toString: () => urlObj.href,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
_createXHR() {
|
||||||
|
return class XMLHttpRequest {
|
||||||
|
constructor() {
|
||||||
|
this.readyState = 0;
|
||||||
|
this.status = 0;
|
||||||
|
this.statusText = '';
|
||||||
|
this.responseText = '';
|
||||||
|
this.response = '';
|
||||||
|
this.responseType = '';
|
||||||
|
this.withCredentials = false;
|
||||||
|
}
|
||||||
|
open() { this.readyState = 1; }
|
||||||
|
setRequestHeader() {}
|
||||||
|
send() {
|
||||||
|
this.readyState = 4;
|
||||||
|
this.status = 200;
|
||||||
|
this.responseText = '{}';
|
||||||
|
this.response = '{}';
|
||||||
|
if (this.onreadystatechange) this.onreadystatechange();
|
||||||
|
if (this.onload) this.onload();
|
||||||
|
}
|
||||||
|
addEventListener(e, fn) { this['on' + e] = fn; }
|
||||||
|
abort() {}
|
||||||
|
getResponseHeader() { return null; }
|
||||||
|
getAllResponseHeaders() { return ''; }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
_setupCookieTrap(sandbox) {
|
||||||
|
const self = this;
|
||||||
|
const originalCookie = sandbox.document.cookie || '';
|
||||||
|
|
||||||
|
Object.defineProperty(sandbox.document, 'cookie', {
|
||||||
|
get() {
|
||||||
|
return Object.entries(self.cookies)
|
||||||
|
.map(([k, v]) => `${k}=${v}`)
|
||||||
|
.join('; ');
|
||||||
|
},
|
||||||
|
set(val) {
|
||||||
|
const mainPart = val.split(';')[0];
|
||||||
|
const eqIdx = mainPart.indexOf('=');
|
||||||
|
if (eqIdx > 0) {
|
||||||
|
const name = mainPart.substring(0, eqIdx).trim();
|
||||||
|
const value = mainPart.substring(eqIdx + 1).trim();
|
||||||
|
self.cookies[name] = value;
|
||||||
|
if (self.options.verbose) {
|
||||||
|
console.log(`[Cookie Trap] ${name}=${value}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
configurable: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (originalCookie) {
|
||||||
|
originalCookie.split(';').forEach(pair => {
|
||||||
|
const [k, ...rest] = pair.trim().split('=');
|
||||||
|
if (k) self.cookies[k.trim()] = rest.join('=');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
run(code, filename = 'sandbox.js') {
|
||||||
|
const sandbox = this.createSandbox();
|
||||||
|
vm.createContext(sandbox);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = vm.runInContext(code, sandbox, {
|
||||||
|
timeout: this.options.timeout,
|
||||||
|
filename,
|
||||||
|
displayErrors: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
result,
|
||||||
|
cookies: { ...this.cookies },
|
||||||
|
logs: [...this.logs],
|
||||||
|
errors: [...this.errors],
|
||||||
|
sandbox,
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: e.message,
|
||||||
|
stack: e.stack,
|
||||||
|
cookies: { ...this.cookies },
|
||||||
|
logs: [...this.logs],
|
||||||
|
errors: [...this.errors],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
runFile(filePath) {
|
||||||
|
const code = fs.readFileSync(filePath, 'utf-8');
|
||||||
|
return this.run(code, path.basename(filePath));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CLI 入口
|
||||||
|
if (require.main === module) {
|
||||||
|
const args = process.argv.slice(2);
|
||||||
|
|
||||||
|
if (args.length === 0) {
|
||||||
|
console.log('用法: node sandbox-runner.js <js-file> [options]');
|
||||||
|
console.log('');
|
||||||
|
console.log('选项:');
|
||||||
|
console.log(' --url=<url> 设置 location.href');
|
||||||
|
console.log(' --extract-cookie 启用 Cookie 拦截');
|
||||||
|
console.log(' --verbose 输出详细日志');
|
||||||
|
console.log(' --timeout=<ms> 执行超时(默认 10000)');
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
const filePath = args[0];
|
||||||
|
const options = {};
|
||||||
|
|
||||||
|
args.slice(1).forEach(arg => {
|
||||||
|
if (arg === '--extract-cookie') options.extractCookie = true;
|
||||||
|
else if (arg === '--verbose') options.verbose = true;
|
||||||
|
else if (arg.startsWith('--url=')) options.url = arg.substring(6);
|
||||||
|
else if (arg.startsWith('--timeout=')) options.timeout = parseInt(arg.substring(10));
|
||||||
|
});
|
||||||
|
|
||||||
|
const runner = new SandboxRunner(options);
|
||||||
|
const result = runner.runFile(filePath);
|
||||||
|
|
||||||
|
console.log('\n========== 执行结果 ==========');
|
||||||
|
console.log('状态:', result.success ? '成功' : '失败');
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
console.log('错误:', result.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Object.keys(result.cookies).length > 0) {
|
||||||
|
console.log('\nCookies:');
|
||||||
|
Object.entries(result.cookies).forEach(([k, v]) => {
|
||||||
|
console.log(` ${k} = ${v}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.logs.length > 0) {
|
||||||
|
console.log('\n日志:');
|
||||||
|
result.logs.forEach(log => console.log(` ${log}`));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.errors.length > 0) {
|
||||||
|
console.log('\n错误日志:');
|
||||||
|
result.errors.forEach(err => console.log(` ${err}`));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { SandboxRunner };
|
||||||
@@ -0,0 +1,170 @@
|
|||||||
|
/**
|
||||||
|
* 主脚本模板 - 浏览器自动化
|
||||||
|
*
|
||||||
|
* 适用场景:TLS 指纹检测、复杂环境依赖、window 蜜罐
|
||||||
|
* 使用 Playwright 控制真实 Chrome 浏览器执行请求
|
||||||
|
*
|
||||||
|
* TODO: 根据逆向分析结果修改配置和逻辑
|
||||||
|
*/
|
||||||
|
|
||||||
|
const { chromium } = require('playwright-core');
|
||||||
|
|
||||||
|
// ============ 配置区域 ============
|
||||||
|
|
||||||
|
const CONFIG = {
|
||||||
|
name: '示例项目 - 浏览器自动化',
|
||||||
|
description: '采集全部5页数据',
|
||||||
|
|
||||||
|
// 目标URL
|
||||||
|
pageURL: 'https://target.com/page',
|
||||||
|
|
||||||
|
// Chrome 路径(macOS 默认路径)
|
||||||
|
chromePath: '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
|
||||||
|
// Linux: '/usr/bin/google-chrome'
|
||||||
|
// Windows: 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe'
|
||||||
|
|
||||||
|
// 是否显示浏览器
|
||||||
|
headless: false,
|
||||||
|
|
||||||
|
totalPages: 5,
|
||||||
|
delay: 2000,
|
||||||
|
|
||||||
|
// 登录 Cookie
|
||||||
|
cookies: [
|
||||||
|
// { name: 'sessionid', value: 'xxx', domain: 'target.com', path: '/' },
|
||||||
|
],
|
||||||
|
|
||||||
|
// 注入脚本(可选)
|
||||||
|
injectScript: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
// ============ 主逻辑 ============
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
console.log(`[*] 项目:${CONFIG.name}`);
|
||||||
|
console.log(`[*] 目标:${CONFIG.description}`);
|
||||||
|
console.log('');
|
||||||
|
|
||||||
|
// 启动浏览器
|
||||||
|
console.log('[*] 启动浏览器...');
|
||||||
|
const browser = await chromium.launch({
|
||||||
|
executablePath: CONFIG.chromePath,
|
||||||
|
headless: CONFIG.headless,
|
||||||
|
args: [
|
||||||
|
'--disable-blink-features=AutomationControlled',
|
||||||
|
'--no-sandbox',
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const context = await browser.newContext({
|
||||||
|
userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
||||||
|
viewport: { width: 1920, height: 1080 },
|
||||||
|
locale: 'zh-CN',
|
||||||
|
});
|
||||||
|
|
||||||
|
// 设置 Cookie
|
||||||
|
if (CONFIG.cookies.length > 0) {
|
||||||
|
await context.addCookies(CONFIG.cookies);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 隐藏自动化特征
|
||||||
|
await context.addInitScript(() => {
|
||||||
|
Object.defineProperty(navigator, 'webdriver', { get: () => undefined });
|
||||||
|
window.chrome = { runtime: {} };
|
||||||
|
});
|
||||||
|
|
||||||
|
// 注入自定义脚本
|
||||||
|
if (CONFIG.injectScript) {
|
||||||
|
await context.addInitScript(CONFIG.injectScript);
|
||||||
|
}
|
||||||
|
|
||||||
|
const page = await context.newPage();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 导航到目标页面
|
||||||
|
console.log('[*] 打开目标页面...');
|
||||||
|
await page.goto(CONFIG.pageURL, { waitUntil: 'networkidle', timeout: 30000 });
|
||||||
|
console.log('[+] 页面加载完成');
|
||||||
|
|
||||||
|
// 采集数据
|
||||||
|
const allData = [];
|
||||||
|
|
||||||
|
for (let pageNum = 1; pageNum <= CONFIG.totalPages; pageNum++) {
|
||||||
|
// TODO: 根据实际页面结构修改数据提取逻辑
|
||||||
|
const pageData = await extractPageData(page, pageNum);
|
||||||
|
allData.push(...pageData);
|
||||||
|
console.log(`[+] 正在采集第 ${pageNum}/${CONFIG.totalPages} 页... ✓ 获取 ${pageData.length} 条数据`);
|
||||||
|
|
||||||
|
if (pageNum < CONFIG.totalPages) {
|
||||||
|
// 翻页
|
||||||
|
await goToNextPage(page, pageNum + 1);
|
||||||
|
await sleep(CONFIG.delay);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`\n[+] 采集完成,共 ${allData.length} 条数据`);
|
||||||
|
|
||||||
|
// 计算结果
|
||||||
|
const result = allData.reduce((sum, val) => sum + val, 0);
|
||||||
|
|
||||||
|
console.log('\n' + '='.repeat(30) + ' 计算结果 ' + '='.repeat(30));
|
||||||
|
console.log(`答案:${result}`);
|
||||||
|
console.log('='.repeat(70));
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[!] 错误:', error.message);
|
||||||
|
await page.screenshot({ path: 'error_screenshot.png' });
|
||||||
|
console.log('[*] 已保存错误截图: error_screenshot.png');
|
||||||
|
} finally {
|
||||||
|
await browser.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从页面提取数据
|
||||||
|
* TODO: 根据实际页面结构修改
|
||||||
|
*/
|
||||||
|
async function extractPageData(page, pageNum) {
|
||||||
|
// 方式1:从页面DOM提取
|
||||||
|
const data = await page.evaluate(() => {
|
||||||
|
const rows = document.querySelectorAll('.data-row');
|
||||||
|
return Array.from(rows).map(row => {
|
||||||
|
const value = row.querySelector('.value')?.textContent?.trim();
|
||||||
|
return parseFloat(value) || 0;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return data;
|
||||||
|
|
||||||
|
// 方式2:拦截网络响应
|
||||||
|
// const responsePromise = page.waitForResponse(resp => resp.url().includes('/api/data'));
|
||||||
|
// // 触发数据加载...
|
||||||
|
// const response = await responsePromise;
|
||||||
|
// const json = await response.json();
|
||||||
|
// return json.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 翻到下一页
|
||||||
|
* TODO: 根据实际页面结构修改翻页逻辑
|
||||||
|
*/
|
||||||
|
async function goToNextPage(page, targetPage) {
|
||||||
|
// 方式1:点击翻页按钮
|
||||||
|
await page.click(`.pagination a:has-text("${targetPage}")`);
|
||||||
|
await page.waitForLoadState('networkidle');
|
||||||
|
|
||||||
|
// 方式2:等待特定元素出现
|
||||||
|
// await page.waitForSelector('.data-loaded');
|
||||||
|
|
||||||
|
// 方式3:直接导航到带页码的URL
|
||||||
|
// await page.goto(`${CONFIG.pageURL}?page=${targetPage}`, { waitUntil: 'networkidle' });
|
||||||
|
}
|
||||||
|
|
||||||
|
function sleep(ms) {
|
||||||
|
return new Promise(resolve => setTimeout(resolve, ms));
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch(error => {
|
||||||
|
console.error('\n[!] 运行错误:', error.message);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"name": "js-reverse-browser-auto",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "JS逆向 - 浏览器自动化模板",
|
||||||
|
"main": "main.js",
|
||||||
|
"scripts": {
|
||||||
|
"start": "node main.js",
|
||||||
|
"test": "node main.js --test"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"playwright-core": "^1.45.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
/**
|
||||||
|
* 主脚本模板 - 纯 Node.js 算法还原
|
||||||
|
*
|
||||||
|
* 适用场景:加密逻辑可完整提取,无浏览器环境依赖
|
||||||
|
*
|
||||||
|
* TODO: 根据逆向分析结果修改以下配置和逻辑
|
||||||
|
*/
|
||||||
|
|
||||||
|
const { generateM, generateSign } = require('./utils/encrypt');
|
||||||
|
const { RequestClient } = require('./utils/request');
|
||||||
|
|
||||||
|
// ============ 配置区域 ============
|
||||||
|
|
||||||
|
const CONFIG = {
|
||||||
|
// 目标信息
|
||||||
|
name: '示例项目',
|
||||||
|
description: '采集全部5页数据,计算数值总和',
|
||||||
|
|
||||||
|
// 接口配置
|
||||||
|
baseURL: 'https://target.com',
|
||||||
|
apiPath: '/api/data',
|
||||||
|
referer: 'https://target.com/page',
|
||||||
|
|
||||||
|
// 采集配置
|
||||||
|
totalPages: 5,
|
||||||
|
delay: 1500,
|
||||||
|
|
||||||
|
// 认证信息
|
||||||
|
cookies: {
|
||||||
|
// sessionid: 'your_session_id',
|
||||||
|
},
|
||||||
|
|
||||||
|
// 自定义请求头
|
||||||
|
headers: {
|
||||||
|
// 'X-Custom-Header': 'value',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// ============ 主逻辑 ============
|
||||||
|
|
||||||
|
async function fetchPage(client, page) {
|
||||||
|
// TODO: 根据实际接口修改参数构造逻辑
|
||||||
|
const m = generateM(page);
|
||||||
|
const timestamp = Math.floor(Date.now() / 1000);
|
||||||
|
|
||||||
|
const params = {
|
||||||
|
page,
|
||||||
|
m,
|
||||||
|
t: timestamp,
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await client.requestWithDelay({
|
||||||
|
method: 'GET',
|
||||||
|
url: CONFIG.apiPath,
|
||||||
|
params,
|
||||||
|
});
|
||||||
|
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractData(responseData) {
|
||||||
|
// TODO: 根据实际响应格式修改数据提取逻辑
|
||||||
|
if (responseData && responseData.data) {
|
||||||
|
return responseData.data;
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function calculateResult(allData) {
|
||||||
|
// TODO: 根据题目要求修改计算逻辑
|
||||||
|
// 示例:求所有数值的总和
|
||||||
|
const sum = allData.reduce((acc, item) => {
|
||||||
|
const value = typeof item === 'number' ? item : (item.value || 0);
|
||||||
|
return acc + value;
|
||||||
|
}, 0);
|
||||||
|
return sum;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
console.log(`[*] 项目:${CONFIG.name}`);
|
||||||
|
console.log(`[*] 目标:${CONFIG.description}`);
|
||||||
|
console.log('');
|
||||||
|
|
||||||
|
const client = new RequestClient({
|
||||||
|
baseURL: CONFIG.baseURL,
|
||||||
|
cookies: CONFIG.cookies,
|
||||||
|
headers: CONFIG.headers,
|
||||||
|
referer: CONFIG.referer,
|
||||||
|
delay: CONFIG.delay,
|
||||||
|
});
|
||||||
|
|
||||||
|
const allData = [];
|
||||||
|
|
||||||
|
for (let page = 1; page <= CONFIG.totalPages; page++) {
|
||||||
|
try {
|
||||||
|
const responseData = await fetchPage(client, page);
|
||||||
|
const pageData = extractData(responseData);
|
||||||
|
allData.push(...pageData);
|
||||||
|
console.log(`[+] 正在采集第 ${page}/${CONFIG.totalPages} 页... ✓ 获取 ${pageData.length} 条数据`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`[-] 第 ${page} 页采集失败: ${error.message}`);
|
||||||
|
if (error.response) {
|
||||||
|
console.error(` 状态码: ${error.response.status}`);
|
||||||
|
console.error(` 响应: ${JSON.stringify(error.response.data).substring(0, 200)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`\n[+] 采集完成,共 ${allData.length} 条数据`);
|
||||||
|
|
||||||
|
const result = calculateResult(allData);
|
||||||
|
|
||||||
|
console.log('\n' + '='.repeat(30) + ' 计算结果 ' + '='.repeat(30));
|
||||||
|
console.log(`答案:${result}`);
|
||||||
|
console.log('='.repeat(70));
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch(error => {
|
||||||
|
console.error('\n[!] 运行错误:', error.message);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"name": "js-reverse-project",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "JS逆向 - 纯Node.js算法还原模板",
|
||||||
|
"main": "main.js",
|
||||||
|
"scripts": {
|
||||||
|
"start": "node main.js",
|
||||||
|
"test": "node main.js --test"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"axios": "^1.7.0",
|
||||||
|
"crypto-js": "^4.2.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
/**
|
||||||
|
* 加密参数生成模块
|
||||||
|
* 根据逆向分析还原的加密逻辑,生成请求所需的加密参数
|
||||||
|
*/
|
||||||
|
|
||||||
|
const crypto = require('crypto');
|
||||||
|
|
||||||
|
function md5(str) {
|
||||||
|
return crypto.createHash('md5').update(str).digest('hex');
|
||||||
|
}
|
||||||
|
|
||||||
|
function sha256(str) {
|
||||||
|
return crypto.createHash('sha256').update(str).digest('hex');
|
||||||
|
}
|
||||||
|
|
||||||
|
function hmacSha256(message, secret) {
|
||||||
|
return crypto.createHmac('sha256', secret).update(message).digest('hex');
|
||||||
|
}
|
||||||
|
|
||||||
|
function aesEncrypt(plaintext, key, iv, mode = 'cbc') {
|
||||||
|
const algorithm = `aes-${key.length * 8}-${mode}`;
|
||||||
|
const cipher = crypto.createCipheriv(algorithm, key, mode === 'ecb' ? null : iv);
|
||||||
|
cipher.setAutoPadding(true);
|
||||||
|
let encrypted = cipher.update(plaintext, 'utf8', 'base64');
|
||||||
|
encrypted += cipher.final('base64');
|
||||||
|
return encrypted;
|
||||||
|
}
|
||||||
|
|
||||||
|
function aesDecrypt(ciphertext, key, iv, mode = 'cbc') {
|
||||||
|
const algorithm = `aes-${key.length * 8}-${mode}`;
|
||||||
|
const decipher = crypto.createDecipheriv(algorithm, key, mode === 'ecb' ? null : iv);
|
||||||
|
decipher.setAutoPadding(true);
|
||||||
|
let decrypted = decipher.update(ciphertext, 'base64', 'utf8');
|
||||||
|
decrypted += decipher.final('utf8');
|
||||||
|
return decrypted;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成请求签名
|
||||||
|
* TODO: 根据实际逆向分析修改此函数
|
||||||
|
* @param {Object} params - 请求参数
|
||||||
|
* @returns {string} 签名值
|
||||||
|
*/
|
||||||
|
function generateSign(params) {
|
||||||
|
const { page, timestamp } = params;
|
||||||
|
// 示例:sign = md5(page + "|" + timestamp + "|" + SECRET)
|
||||||
|
const SECRET = 'your_secret_here';
|
||||||
|
return md5(`${page}|${timestamp}|${SECRET}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成加密参数 m
|
||||||
|
* TODO: 根据实际逆向分析修改此函数
|
||||||
|
* @param {number} page - 页码
|
||||||
|
* @returns {string} 加密后的参数
|
||||||
|
*/
|
||||||
|
function generateM(page) {
|
||||||
|
const timestamp = Math.floor(Date.now() / 1000);
|
||||||
|
const sign = generateSign({ page, timestamp });
|
||||||
|
return `${sign}|${timestamp}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
md5,
|
||||||
|
sha256,
|
||||||
|
hmacSha256,
|
||||||
|
aesEncrypt,
|
||||||
|
aesDecrypt,
|
||||||
|
generateSign,
|
||||||
|
generateM,
|
||||||
|
};
|
||||||
@@ -0,0 +1,128 @@
|
|||||||
|
/**
|
||||||
|
* 请求封装模块
|
||||||
|
* 封装 HTTP 请求,处理 Headers、Cookie、重试、频率控制
|
||||||
|
*/
|
||||||
|
|
||||||
|
const axios = require('axios');
|
||||||
|
|
||||||
|
const DEFAULT_HEADERS = {
|
||||||
|
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
||||||
|
'Accept': 'application/json, text/javascript, */*; q=0.01',
|
||||||
|
'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
|
||||||
|
'Accept-Encoding': 'gzip, deflate, br',
|
||||||
|
'Connection': 'keep-alive',
|
||||||
|
'X-Requested-With': 'XMLHttpRequest',
|
||||||
|
};
|
||||||
|
|
||||||
|
class RequestClient {
|
||||||
|
constructor(options = {}) {
|
||||||
|
this.baseURL = options.baseURL || '';
|
||||||
|
this.cookies = options.cookies || {};
|
||||||
|
this.headers = { ...DEFAULT_HEADERS, ...options.headers };
|
||||||
|
this.delay = options.delay || 1000;
|
||||||
|
this.maxRetries = options.maxRetries || 3;
|
||||||
|
this.referer = options.referer || '';
|
||||||
|
|
||||||
|
this.client = axios.create({
|
||||||
|
baseURL: this.baseURL,
|
||||||
|
timeout: options.timeout || 30000,
|
||||||
|
headers: this.headers,
|
||||||
|
maxRedirects: 5,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getCookieString() {
|
||||||
|
return Object.entries(this.cookies)
|
||||||
|
.map(([k, v]) => `${k}=${v}`)
|
||||||
|
.join('; ');
|
||||||
|
}
|
||||||
|
|
||||||
|
setCookie(name, value) {
|
||||||
|
this.cookies[name] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
parseCookies(setCookieHeader) {
|
||||||
|
if (!setCookieHeader) return;
|
||||||
|
const headers = Array.isArray(setCookieHeader) ? setCookieHeader : [setCookieHeader];
|
||||||
|
headers.forEach(h => {
|
||||||
|
const mainPart = h.split(';')[0];
|
||||||
|
const eqIdx = mainPart.indexOf('=');
|
||||||
|
if (eqIdx > 0) {
|
||||||
|
this.cookies[mainPart.substring(0, eqIdx).trim()] = mainPart.substring(eqIdx + 1).trim();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async request(config) {
|
||||||
|
const finalConfig = {
|
||||||
|
...config,
|
||||||
|
headers: {
|
||||||
|
...this.headers,
|
||||||
|
...config.headers,
|
||||||
|
'Cookie': this.getCookieString(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
if (this.referer) {
|
||||||
|
finalConfig.headers['Referer'] = this.referer;
|
||||||
|
}
|
||||||
|
|
||||||
|
let lastError;
|
||||||
|
for (let attempt = 1; attempt <= this.maxRetries; attempt++) {
|
||||||
|
try {
|
||||||
|
const response = await this.client.request(finalConfig);
|
||||||
|
this.parseCookies(response.headers['set-cookie']);
|
||||||
|
return response;
|
||||||
|
} catch (error) {
|
||||||
|
lastError = error;
|
||||||
|
const status = error.response?.status;
|
||||||
|
console.error(`[Request] 第 ${attempt}/${this.maxRetries} 次请求失败: ${status || error.message}`);
|
||||||
|
|
||||||
|
if (status === 429) {
|
||||||
|
const waitTime = this.delay * attempt * 2;
|
||||||
|
console.log(`[Request] 频率限制,等待 ${waitTime}ms...`);
|
||||||
|
await this.sleep(waitTime);
|
||||||
|
} else if (status >= 500) {
|
||||||
|
await this.sleep(this.delay * attempt);
|
||||||
|
} else if (status === 403 || status === 412) {
|
||||||
|
console.error('[Request] 访问被拒绝,可能需要更新 Cookie 或检查加密参数');
|
||||||
|
throw error;
|
||||||
|
} else {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw lastError;
|
||||||
|
}
|
||||||
|
|
||||||
|
async get(url, params = {}, extraHeaders = {}) {
|
||||||
|
return this.request({
|
||||||
|
method: 'GET',
|
||||||
|
url,
|
||||||
|
params,
|
||||||
|
headers: extraHeaders,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async post(url, data = {}, extraHeaders = {}) {
|
||||||
|
return this.request({
|
||||||
|
method: 'POST',
|
||||||
|
url,
|
||||||
|
data,
|
||||||
|
headers: extraHeaders,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
sleep(ms) {
|
||||||
|
return new Promise(resolve => setTimeout(resolve, ms));
|
||||||
|
}
|
||||||
|
|
||||||
|
async requestWithDelay(config) {
|
||||||
|
const response = await this.request(config);
|
||||||
|
const jitter = Math.random() * this.delay * 0.5;
|
||||||
|
await this.sleep(this.delay + jitter);
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { RequestClient, DEFAULT_HEADERS };
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
/**
|
||||||
|
* 主脚本模板 - VM 沙箱执行
|
||||||
|
*
|
||||||
|
* 适用场景:服务端返回混淆JS用于生成Cookie/Token
|
||||||
|
* 流程:
|
||||||
|
* 1. 请求接口 → 返回混淆 JS
|
||||||
|
* 2. VM 沙箱执行 JS → 提取 Cookie
|
||||||
|
* 3. 携带 Cookie 请求数据接口
|
||||||
|
*
|
||||||
|
* TODO: 根据逆向分析结果修改配置和逻辑
|
||||||
|
*/
|
||||||
|
|
||||||
|
const { executeAndExtractCookie } = require('./utils/sandbox');
|
||||||
|
const { TwoPhaseClient } = require('./utils/request');
|
||||||
|
|
||||||
|
// ============ 配置区域 ============
|
||||||
|
|
||||||
|
const CONFIG = {
|
||||||
|
name: '示例项目 - 动态Cookie',
|
||||||
|
description: '采集全部5页数据',
|
||||||
|
|
||||||
|
baseURL: 'https://target.com',
|
||||||
|
|
||||||
|
// 第一阶段:获取动态JS的接口
|
||||||
|
jsEndpoint: '/api/init',
|
||||||
|
|
||||||
|
// 第二阶段:数据接口
|
||||||
|
dataEndpoint: '/api/data',
|
||||||
|
|
||||||
|
referer: 'https://target.com/page',
|
||||||
|
totalPages: 5,
|
||||||
|
delay: 2000,
|
||||||
|
|
||||||
|
cookies: {
|
||||||
|
// sessionid: 'your_session_id',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// ============ 主逻辑 ============
|
||||||
|
|
||||||
|
async function getDynamicCookie(client) {
|
||||||
|
console.log('[*] 获取动态Cookie...');
|
||||||
|
|
||||||
|
// 第一阶段:请求返回JS代码的接口
|
||||||
|
const jsCode = await client.fetchDynamicJS(CONFIG.jsEndpoint);
|
||||||
|
|
||||||
|
// 在VM沙箱中执行,提取Cookie
|
||||||
|
const result = executeAndExtractCookie(jsCode, {
|
||||||
|
url: CONFIG.baseURL + CONFIG.referer,
|
||||||
|
initialCookie: client.getCookieString(),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.success && Object.keys(result.cookies).length > 0) {
|
||||||
|
console.log(`[+] Cookie 提取成功: ${result.cookieString.substring(0, 100)}...`);
|
||||||
|
client.setCookies(result.cookies);
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
console.error('[-] Cookie 提取失败:', result.error || '未生成Cookie');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchPage(client, page) {
|
||||||
|
// TODO: 根据实际接口修改参数
|
||||||
|
const params = { page };
|
||||||
|
return client.fetchWithDelay(CONFIG.dataEndpoint, params);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
console.log(`[*] 项目:${CONFIG.name}`);
|
||||||
|
console.log(`[*] 目标:${CONFIG.description}`);
|
||||||
|
console.log('');
|
||||||
|
|
||||||
|
const client = new TwoPhaseClient({
|
||||||
|
baseURL: CONFIG.baseURL,
|
||||||
|
cookies: CONFIG.cookies,
|
||||||
|
delay: CONFIG.delay,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 获取动态Cookie
|
||||||
|
const cookieOk = await getDynamicCookie(client);
|
||||||
|
if (!cookieOk) {
|
||||||
|
console.error('[!] 无法获取动态Cookie,退出');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 采集数据
|
||||||
|
const allData = [];
|
||||||
|
|
||||||
|
for (let page = 1; page <= CONFIG.totalPages; page++) {
|
||||||
|
try {
|
||||||
|
// 某些网站每次请求都需要刷新Cookie
|
||||||
|
// await getDynamicCookie(client);
|
||||||
|
|
||||||
|
const responseData = await fetchPage(client, page);
|
||||||
|
const pageData = responseData.data || [];
|
||||||
|
allData.push(...pageData);
|
||||||
|
console.log(`[+] 正在采集第 ${page}/${CONFIG.totalPages} 页... ✓ 获取 ${pageData.length} 条数据`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`[-] 第 ${page} 页采集失败: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`\n[+] 采集完成,共 ${allData.length} 条数据`);
|
||||||
|
|
||||||
|
// TODO: 根据需求计算结果
|
||||||
|
const result = allData.reduce((sum, item) => sum + (item.value || 0), 0);
|
||||||
|
|
||||||
|
console.log('\n' + '='.repeat(30) + ' 计算结果 ' + '='.repeat(30));
|
||||||
|
console.log(`答案:${result}`);
|
||||||
|
console.log('='.repeat(70));
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch(error => {
|
||||||
|
console.error('\n[!] 运行错误:', error.message);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"name": "js-reverse-vm-sandbox",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "JS逆向 - VM沙箱执行模板",
|
||||||
|
"main": "main.js",
|
||||||
|
"scripts": {
|
||||||
|
"start": "node main.js",
|
||||||
|
"test": "node main.js --test"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"axios": "^1.7.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
/**
|
||||||
|
* 请求封装模块(VM沙箱版本)
|
||||||
|
* 支持两阶段请求:先获取动态JS生成Cookie,再携带Cookie请求数据
|
||||||
|
*/
|
||||||
|
|
||||||
|
const axios = require('axios');
|
||||||
|
|
||||||
|
const DEFAULT_HEADERS = {
|
||||||
|
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
||||||
|
'Accept': 'application/json, text/javascript, */*; q=0.01',
|
||||||
|
'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
|
||||||
|
'X-Requested-With': 'XMLHttpRequest',
|
||||||
|
};
|
||||||
|
|
||||||
|
class TwoPhaseClient {
|
||||||
|
constructor(options = {}) {
|
||||||
|
this.baseURL = options.baseURL || '';
|
||||||
|
this.cookies = options.cookies || {};
|
||||||
|
this.headers = { ...DEFAULT_HEADERS, ...options.headers };
|
||||||
|
this.delay = options.delay || 1500;
|
||||||
|
|
||||||
|
this.client = axios.create({
|
||||||
|
baseURL: this.baseURL,
|
||||||
|
timeout: options.timeout || 30000,
|
||||||
|
headers: this.headers,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getCookieString() {
|
||||||
|
return Object.entries(this.cookies).map(([k, v]) => `${k}=${v}`).join('; ');
|
||||||
|
}
|
||||||
|
|
||||||
|
setCookies(cookieObj) {
|
||||||
|
Object.assign(this.cookies, cookieObj);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 第一阶段:获取动态JS代码
|
||||||
|
* @param {string} url - 返回JS代码的接口
|
||||||
|
* @returns {string} JS 代码字符串
|
||||||
|
*/
|
||||||
|
async fetchDynamicJS(url, params = {}) {
|
||||||
|
const response = await this.client.get(url, {
|
||||||
|
params,
|
||||||
|
headers: { Cookie: this.getCookieString() },
|
||||||
|
transformResponse: [(data) => data],
|
||||||
|
});
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 第二阶段:携带Cookie请求数据
|
||||||
|
*/
|
||||||
|
async fetchData(url, params = {}) {
|
||||||
|
const response = await this.client.get(url, {
|
||||||
|
params,
|
||||||
|
headers: { Cookie: this.getCookieString() },
|
||||||
|
});
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
async post(url, data = {}) {
|
||||||
|
const response = await this.client.post(url, data, {
|
||||||
|
headers: { Cookie: this.getCookieString() },
|
||||||
|
});
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
sleep(ms) {
|
||||||
|
return new Promise(resolve => setTimeout(resolve, ms));
|
||||||
|
}
|
||||||
|
|
||||||
|
async fetchWithDelay(url, params = {}) {
|
||||||
|
const data = await this.fetchData(url, params);
|
||||||
|
const jitter = Math.random() * this.delay * 0.3;
|
||||||
|
await this.sleep(this.delay + jitter);
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { TwoPhaseClient, DEFAULT_HEADERS };
|
||||||
@@ -0,0 +1,152 @@
|
|||||||
|
/**
|
||||||
|
* VM 沙箱模块
|
||||||
|
* 用于执行服务端返回的混淆 JS 代码,提取动态生成的 Cookie/Token
|
||||||
|
*/
|
||||||
|
|
||||||
|
const vm = require('vm');
|
||||||
|
|
||||||
|
function createSandbox(options = {}) {
|
||||||
|
const cookies = {};
|
||||||
|
const logs = [];
|
||||||
|
|
||||||
|
const sandbox = {
|
||||||
|
window: null,
|
||||||
|
self: null,
|
||||||
|
globalThis: null,
|
||||||
|
top: null,
|
||||||
|
parent: null,
|
||||||
|
|
||||||
|
document: {
|
||||||
|
cookie: options.initialCookie || '',
|
||||||
|
createElement: (tag) => ({
|
||||||
|
tagName: tag.toUpperCase(), style: {}, src: '',
|
||||||
|
setAttribute: () => {}, getAttribute: () => null,
|
||||||
|
appendChild: () => {}, innerHTML: '',
|
||||||
|
}),
|
||||||
|
getElementById: () => null,
|
||||||
|
getElementsByTagName: (tag) => tag === 'script' ? [1, 2, 3] : [],
|
||||||
|
getElementsByClassName: () => [],
|
||||||
|
querySelector: () => null,
|
||||||
|
querySelectorAll: () => [],
|
||||||
|
head: { appendChild: () => {} },
|
||||||
|
body: { appendChild: () => {} },
|
||||||
|
location: { href: options.url || 'https://example.com' },
|
||||||
|
referrer: '',
|
||||||
|
readyState: 'complete',
|
||||||
|
},
|
||||||
|
|
||||||
|
navigator: {
|
||||||
|
userAgent: options.userAgent || 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
||||||
|
appCodeName: 'Mozilla', appName: 'Netscape', appVersion: '5.0',
|
||||||
|
platform: 'MacIntel', language: 'zh-CN', languages: ['zh-CN', 'zh', 'en'],
|
||||||
|
cookieEnabled: true, onLine: true, webdriver: false,
|
||||||
|
plugins: { length: 3 }, mimeTypes: { length: 4 },
|
||||||
|
hardwareConcurrency: 8, maxTouchPoints: 0,
|
||||||
|
},
|
||||||
|
|
||||||
|
location: {
|
||||||
|
href: options.url || 'https://example.com',
|
||||||
|
protocol: 'https:', host: 'example.com',
|
||||||
|
hostname: 'example.com', pathname: '/', search: '', hash: '',
|
||||||
|
origin: 'https://example.com',
|
||||||
|
},
|
||||||
|
|
||||||
|
screen: { width: 1920, height: 1080, availWidth: 1920, availHeight: 1055, colorDepth: 24, pixelDepth: 24 },
|
||||||
|
|
||||||
|
setTimeout: (fn, ms) => setTimeout(fn, Math.min(ms || 0, 3000)),
|
||||||
|
setInterval: (fn, ms) => { /* 不执行定时器避免死循环 */ return -1; },
|
||||||
|
clearTimeout, clearInterval,
|
||||||
|
|
||||||
|
String, Array, Object, Math, Date, RegExp, JSON, Map, Set,
|
||||||
|
parseInt, parseFloat, isNaN, isFinite, NaN, Infinity, undefined,
|
||||||
|
encodeURIComponent, decodeURIComponent, encodeURI, decodeURI,
|
||||||
|
escape, unescape,
|
||||||
|
Error, TypeError, RangeError, SyntaxError, ReferenceError,
|
||||||
|
ArrayBuffer, Uint8Array, Int32Array, Float64Array, DataView,
|
||||||
|
Promise, Proxy, Reflect, Symbol, Number, Boolean,
|
||||||
|
|
||||||
|
btoa: (str) => Buffer.from(str, 'binary').toString('base64'),
|
||||||
|
atob: (b64) => Buffer.from(b64, 'base64').toString('binary'),
|
||||||
|
|
||||||
|
console: {
|
||||||
|
log: (...args) => logs.push(args.join(' ')),
|
||||||
|
warn: () => {}, error: () => {}, info: () => {},
|
||||||
|
debug: () => {}, trace: () => {}, clear: () => {},
|
||||||
|
},
|
||||||
|
|
||||||
|
XMLHttpRequest: class {
|
||||||
|
open() {} setRequestHeader() {} send() {
|
||||||
|
this.readyState = 4; this.status = 200; this.responseText = '{}';
|
||||||
|
if (this.onreadystatechange) this.onreadystatechange();
|
||||||
|
}
|
||||||
|
addEventListener(e, fn) { this['on' + e] = fn; }
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
sandbox.window = sandbox;
|
||||||
|
sandbox.self = sandbox;
|
||||||
|
sandbox.globalThis = sandbox;
|
||||||
|
sandbox.top = sandbox;
|
||||||
|
sandbox.parent = sandbox;
|
||||||
|
sandbox.frames = sandbox;
|
||||||
|
|
||||||
|
// Cookie 拦截
|
||||||
|
Object.defineProperty(sandbox.document, 'cookie', {
|
||||||
|
get() {
|
||||||
|
return Object.entries(cookies).map(([k, v]) => `${k}=${v}`).join('; ');
|
||||||
|
},
|
||||||
|
set(val) {
|
||||||
|
const mainPart = val.split(';')[0];
|
||||||
|
const eqIdx = mainPart.indexOf('=');
|
||||||
|
if (eqIdx > 0) {
|
||||||
|
const name = mainPart.substring(0, eqIdx).trim();
|
||||||
|
const value = mainPart.substring(eqIdx + 1).trim();
|
||||||
|
cookies[name] = value;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
configurable: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (options.initialCookie) {
|
||||||
|
options.initialCookie.split(';').forEach(pair => {
|
||||||
|
const [k, ...rest] = pair.trim().split('=');
|
||||||
|
if (k) cookies[k.trim()] = rest.join('=');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return { sandbox, cookies, logs };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行混淆 JS 代码并提取 Cookie
|
||||||
|
* @param {string} code - 混淆的 JS 代码
|
||||||
|
* @param {Object} options - 配置选项
|
||||||
|
* @returns {Object} { cookies, logs, success }
|
||||||
|
*/
|
||||||
|
function executeAndExtractCookie(code, options = {}) {
|
||||||
|
const { sandbox, cookies, logs } = createSandbox(options);
|
||||||
|
vm.createContext(sandbox);
|
||||||
|
|
||||||
|
try {
|
||||||
|
vm.runInContext(code, sandbox, {
|
||||||
|
timeout: options.timeout || 5000,
|
||||||
|
filename: 'dynamic.js',
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
cookies: { ...cookies },
|
||||||
|
cookieString: Object.entries(cookies).map(([k, v]) => `${k}=${v}`).join('; '),
|
||||||
|
logs,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: error.message,
|
||||||
|
cookies: { ...cookies },
|
||||||
|
logs,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { createSandbox, executeAndExtractCookie };
|
||||||
@@ -0,0 +1,160 @@
|
|||||||
|
/**
|
||||||
|
* 主脚本模板 - WASM 加载还原
|
||||||
|
*
|
||||||
|
* 适用场景:加密函数通过 WebAssembly 实现
|
||||||
|
* 流程:
|
||||||
|
* 1. 下载/加载 .wasm 文件
|
||||||
|
* 2. 补全运行环境
|
||||||
|
* 3. 调用 WASM 导出的加密函数
|
||||||
|
* 4. 使用加密结果构造请求
|
||||||
|
*
|
||||||
|
* TODO: 根据逆向分析结果修改配置和逻辑
|
||||||
|
*/
|
||||||
|
|
||||||
|
const { loadWasmFromFile, loadWasmFromURL, analyzeWasm } = require('./utils/wasm-loader');
|
||||||
|
const { patchWasmBindgen } = require('./utils/env-patch');
|
||||||
|
const axios = require('axios');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
// ============ 配置区域 ============
|
||||||
|
|
||||||
|
const CONFIG = {
|
||||||
|
name: '示例项目 - WASM加密',
|
||||||
|
description: '采集全部5页数据',
|
||||||
|
|
||||||
|
baseURL: 'https://target.com',
|
||||||
|
dataEndpoint: '/api/data',
|
||||||
|
|
||||||
|
// WASM 文件路径或 URL
|
||||||
|
wasmSource: './main.wasm', // 本地文件
|
||||||
|
// wasmSource: 'https://target.com/main.wasm', // 远程 URL
|
||||||
|
|
||||||
|
// WASM 导出函数名
|
||||||
|
encryptFunction: 'encode',
|
||||||
|
|
||||||
|
// WASM 类型: 'plain' | 'emscripten' | 'wasm-bindgen' | 'go'
|
||||||
|
wasmType: 'plain',
|
||||||
|
|
||||||
|
totalPages: 5,
|
||||||
|
delay: 1500,
|
||||||
|
|
||||||
|
cookies: {},
|
||||||
|
headers: {
|
||||||
|
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// ============ 主逻辑 ============
|
||||||
|
|
||||||
|
let wasmExports = null;
|
||||||
|
|
||||||
|
async function initWasm() {
|
||||||
|
console.log('[*] 加载 WASM 模块...');
|
||||||
|
|
||||||
|
// 根据 WASM 类型补全环境
|
||||||
|
if (CONFIG.wasmType === 'wasm-bindgen') {
|
||||||
|
patchWasmBindgen();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分析 WASM 结构
|
||||||
|
if (CONFIG.wasmSource.startsWith('http')) {
|
||||||
|
console.log('[*] 从远程下载 WASM...');
|
||||||
|
const result = await loadWasmFromURL(CONFIG.wasmSource);
|
||||||
|
wasmExports = result.exports;
|
||||||
|
console.log('[+] WASM 导出函数:', Object.keys(result.exports).filter(k => typeof result.exports[k] === 'function'));
|
||||||
|
} else {
|
||||||
|
const wasmPath = path.resolve(CONFIG.wasmSource);
|
||||||
|
|
||||||
|
// 先分析 WASM 结构
|
||||||
|
const analysis = await analyzeWasm(wasmPath);
|
||||||
|
console.log('[*] WASM 导出:', analysis.exports.map(e => `${e.name}(${e.kind})`).join(', '));
|
||||||
|
console.log('[*] WASM 导入:', analysis.imports.map(i => `${i.module}.${i.name}`).join(', '));
|
||||||
|
|
||||||
|
const result = await loadWasmFromFile(wasmPath);
|
||||||
|
wasmExports = result.exports;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证加密函数存在
|
||||||
|
if (typeof wasmExports[CONFIG.encryptFunction] !== 'function') {
|
||||||
|
throw new Error(`WASM 导出函数 "${CONFIG.encryptFunction}" 不存在。可用函数: ${Object.keys(wasmExports).filter(k => typeof wasmExports[k] === 'function').join(', ')}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[+] WASM 加载成功,加密函数: ${CONFIG.encryptFunction}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateEncryptedParam(page) {
|
||||||
|
// TODO: 根据逆向分析修改参数生成逻辑
|
||||||
|
const timestamp = Math.floor(Date.now() / 1000);
|
||||||
|
|
||||||
|
// 调用 WASM 导出的加密函数
|
||||||
|
const encrypted = wasmExports[CONFIG.encryptFunction](timestamp, page);
|
||||||
|
|
||||||
|
// TODO: 根据实际格式组装参数
|
||||||
|
return `${encrypted}|${timestamp}|${page}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchPage(page) {
|
||||||
|
const m = generateEncryptedParam(page);
|
||||||
|
|
||||||
|
const response = await axios.get(`${CONFIG.baseURL}${CONFIG.dataEndpoint}`, {
|
||||||
|
params: { page, m },
|
||||||
|
headers: {
|
||||||
|
...CONFIG.headers,
|
||||||
|
Cookie: Object.entries(CONFIG.cookies).map(([k, v]) => `${k}=${v}`).join('; '),
|
||||||
|
Referer: CONFIG.baseURL,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
function sleep(ms) {
|
||||||
|
return new Promise(resolve => setTimeout(resolve, ms));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
console.log(`[*] 项目:${CONFIG.name}`);
|
||||||
|
console.log(`[*] 目标:${CONFIG.description}`);
|
||||||
|
console.log('');
|
||||||
|
|
||||||
|
// 初始化 WASM
|
||||||
|
await initWasm();
|
||||||
|
console.log('');
|
||||||
|
|
||||||
|
// 验证:测试加密函数
|
||||||
|
const testResult = generateEncryptedParam(1);
|
||||||
|
console.log(`[*] 加密测试: page=1 → m=${testResult}`);
|
||||||
|
console.log('');
|
||||||
|
|
||||||
|
// 采集数据
|
||||||
|
const allData = [];
|
||||||
|
|
||||||
|
for (let page = 1; page <= CONFIG.totalPages; page++) {
|
||||||
|
try {
|
||||||
|
const responseData = await fetchPage(page);
|
||||||
|
const pageData = responseData.data || [];
|
||||||
|
allData.push(...pageData);
|
||||||
|
console.log(`[+] 正在采集第 ${page}/${CONFIG.totalPages} 页... ✓ 获取 ${pageData.length} 条数据`);
|
||||||
|
|
||||||
|
if (page < CONFIG.totalPages) {
|
||||||
|
await sleep(CONFIG.delay + Math.random() * 500);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`[-] 第 ${page} 页采集失败: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`\n[+] 采集完成,共 ${allData.length} 条数据`);
|
||||||
|
|
||||||
|
// TODO: 计算结果
|
||||||
|
const result = allData.reduce((sum, item) => sum + (item.value || 0), 0);
|
||||||
|
|
||||||
|
console.log('\n' + '='.repeat(30) + ' 计算结果 ' + '='.repeat(30));
|
||||||
|
console.log(`答案:${result}`);
|
||||||
|
console.log('='.repeat(70));
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch(error => {
|
||||||
|
console.error('\n[!] 运行错误:', error.message);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"name": "js-reverse-wasm",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "JS逆向 - WASM加载还原模板",
|
||||||
|
"main": "main.js",
|
||||||
|
"scripts": {
|
||||||
|
"start": "node main.js",
|
||||||
|
"test": "node main.js --test"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"axios": "^1.7.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
/**
|
||||||
|
* WASM 环境补全模块
|
||||||
|
* 为不同类型的 WASM(Emscripten、wasm-bindgen 等)提供最小运行环境
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 补全 wasm-bindgen (Rust) 生成的 WASM 所需的环境
|
||||||
|
* 常见于 Rust 编写的加密模块
|
||||||
|
*/
|
||||||
|
function patchWasmBindgen() {
|
||||||
|
class Window {
|
||||||
|
constructor() {
|
||||||
|
this.document = {
|
||||||
|
body: {},
|
||||||
|
head: {},
|
||||||
|
createElement: () => ({}),
|
||||||
|
getElementById: () => null,
|
||||||
|
querySelector: () => null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const win = new Window();
|
||||||
|
win.window = win;
|
||||||
|
win.self = win;
|
||||||
|
win.globalThis = win;
|
||||||
|
|
||||||
|
globalThis.Window = Window;
|
||||||
|
globalThis.window = win;
|
||||||
|
globalThis.self = win;
|
||||||
|
globalThis.document = win.document;
|
||||||
|
|
||||||
|
// wasm-bindgen 可能使用 TextEncoder/TextDecoder
|
||||||
|
if (typeof TextEncoder === 'undefined') {
|
||||||
|
const { TextEncoder, TextDecoder } = require('util');
|
||||||
|
globalThis.TextEncoder = TextEncoder;
|
||||||
|
globalThis.TextDecoder = TextDecoder;
|
||||||
|
}
|
||||||
|
|
||||||
|
return win;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 补全 Emscripten 生成的 WASM 所需的环境
|
||||||
|
*/
|
||||||
|
function patchEmscripten() {
|
||||||
|
return {
|
||||||
|
print: console.log,
|
||||||
|
printErr: console.error,
|
||||||
|
TOTAL_MEMORY: 16777216,
|
||||||
|
TOTAL_STACK: 5242880,
|
||||||
|
noInitialRun: true,
|
||||||
|
noExitRuntime: true,
|
||||||
|
onRuntimeInitialized: null,
|
||||||
|
preRun: [],
|
||||||
|
postRun: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 补全 Go WASM 所需的环境
|
||||||
|
*/
|
||||||
|
function patchGoWasm() {
|
||||||
|
globalThis.process = globalThis.process || { argv: ['node'], env: {} };
|
||||||
|
globalThis.fs = globalThis.fs || require('fs');
|
||||||
|
|
||||||
|
// Go WASM 需要 performance.now
|
||||||
|
if (typeof performance === 'undefined') {
|
||||||
|
globalThis.performance = {
|
||||||
|
now: () => Date.now(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Go WASM 需要 crypto.getRandomValues
|
||||||
|
if (typeof globalThis.crypto === 'undefined') {
|
||||||
|
const nodeCrypto = require('crypto');
|
||||||
|
globalThis.crypto = {
|
||||||
|
getRandomValues: (arr) => {
|
||||||
|
const bytes = nodeCrypto.randomBytes(arr.length);
|
||||||
|
arr.set(bytes);
|
||||||
|
return arr;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清理环境补丁(测试完成后调用)
|
||||||
|
*/
|
||||||
|
function cleanupPatches() {
|
||||||
|
delete globalThis.Window;
|
||||||
|
delete globalThis.window;
|
||||||
|
delete globalThis.self;
|
||||||
|
delete globalThis.document;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { patchWasmBindgen, patchEmscripten, patchGoWasm, cleanupPatches };
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
/**
|
||||||
|
* WASM 加载与执行模块
|
||||||
|
* 在 Node.js 中加载 WebAssembly 模块并调用导出函数
|
||||||
|
*/
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加载本地 WASM 文件
|
||||||
|
* @param {string} wasmPath - WASM 文件路径
|
||||||
|
* @param {Object} importObject - 导入对象(环境依赖)
|
||||||
|
* @returns {Object} WASM 实例的 exports
|
||||||
|
*/
|
||||||
|
async function loadWasmFromFile(wasmPath, importObject = {}) {
|
||||||
|
const wasmBuffer = fs.readFileSync(wasmPath);
|
||||||
|
return loadWasm(wasmBuffer, importObject);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从 URL 下载并加载 WASM
|
||||||
|
* @param {string} url - WASM 文件 URL
|
||||||
|
* @param {Object} importObject - 导入对象
|
||||||
|
* @returns {Object} WASM 实例的 exports
|
||||||
|
*/
|
||||||
|
async function loadWasmFromURL(url, importObject = {}) {
|
||||||
|
const axios = require('axios');
|
||||||
|
const response = await axios.get(url, { responseType: 'arraybuffer' });
|
||||||
|
return loadWasm(response.data, importObject);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从二进制数据加载 WASM
|
||||||
|
*/
|
||||||
|
async function loadWasm(wasmBuffer, importObject = {}) {
|
||||||
|
const defaultImports = {
|
||||||
|
env: {
|
||||||
|
memory: new WebAssembly.Memory({ initial: 256, maximum: 512 }),
|
||||||
|
table: new WebAssembly.Table({ initial: 0, element: 'anyfunc' }),
|
||||||
|
abort: (msg, file, line, col) => {
|
||||||
|
console.error(`WASM abort at ${file}:${line}:${col}: ${msg}`);
|
||||||
|
},
|
||||||
|
__memory_base: 0,
|
||||||
|
__table_base: 0,
|
||||||
|
...importObject.env,
|
||||||
|
},
|
||||||
|
wasi_snapshot_preview1: {
|
||||||
|
fd_write: () => 0,
|
||||||
|
fd_read: () => 0,
|
||||||
|
fd_close: () => 0,
|
||||||
|
fd_seek: () => 0,
|
||||||
|
fd_fdstat_get: () => 0,
|
||||||
|
environ_sizes_get: () => 0,
|
||||||
|
environ_get: () => 0,
|
||||||
|
proc_exit: (code) => { throw new Error(`WASM proc_exit(${code})`); },
|
||||||
|
clock_time_get: () => BigInt(Date.now()) * 1000000n,
|
||||||
|
...importObject.wasi_snapshot_preview1,
|
||||||
|
},
|
||||||
|
...importObject,
|
||||||
|
};
|
||||||
|
|
||||||
|
// 检查 WASM 需要哪些导入
|
||||||
|
const module = await WebAssembly.compile(wasmBuffer);
|
||||||
|
const requiredImports = WebAssembly.Module.imports(module);
|
||||||
|
|
||||||
|
// 确保所有需要的导入都存在
|
||||||
|
for (const imp of requiredImports) {
|
||||||
|
if (!defaultImports[imp.module]) {
|
||||||
|
defaultImports[imp.module] = {};
|
||||||
|
}
|
||||||
|
if (!defaultImports[imp.module][imp.name]) {
|
||||||
|
switch (imp.kind) {
|
||||||
|
case 'function':
|
||||||
|
defaultImports[imp.module][imp.name] = () => {};
|
||||||
|
break;
|
||||||
|
case 'global':
|
||||||
|
defaultImports[imp.module][imp.name] = new WebAssembly.Global({ value: 'i32', mutable: true }, 0);
|
||||||
|
break;
|
||||||
|
case 'memory':
|
||||||
|
defaultImports[imp.module][imp.name] = new WebAssembly.Memory({ initial: 256 });
|
||||||
|
break;
|
||||||
|
case 'table':
|
||||||
|
defaultImports[imp.module][imp.name] = new WebAssembly.Table({ initial: 0, element: 'anyfunc' });
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const instance = await WebAssembly.instantiate(module, defaultImports);
|
||||||
|
|
||||||
|
return {
|
||||||
|
exports: instance.exports,
|
||||||
|
module,
|
||||||
|
instance,
|
||||||
|
imports: requiredImports,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分析 WASM 模块的导出和导入
|
||||||
|
*/
|
||||||
|
async function analyzeWasm(wasmPath) {
|
||||||
|
const wasmBuffer = fs.readFileSync(wasmPath);
|
||||||
|
const module = await WebAssembly.compile(wasmBuffer);
|
||||||
|
|
||||||
|
const exports = WebAssembly.Module.exports(module);
|
||||||
|
const imports = WebAssembly.Module.imports(module);
|
||||||
|
|
||||||
|
return {
|
||||||
|
exports: exports.map(e => ({ name: e.name, kind: e.kind })),
|
||||||
|
imports: imports.map(i => ({ module: i.module, name: i.name, kind: i.kind })),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { loadWasmFromFile, loadWasmFromURL, loadWasm, analyzeWasm };
|
||||||
Reference in New Issue
Block a user