CircuitSavage/hcaptcha-hsj-hsw-reversed
GitHub: CircuitSavage/hcaptcha-hsj-hsw-reversed
针对 hCaptcha 的 hsj.js 和 hsw.js 进行字节级逆向,自动化提取六个静态 AES-256 主密钥及构建指纹标识符。
Stars: 14 | Forks: 3
HCAPTCHA HSJ HSW 逆向
针对 hCaptcha 的 hsj.js 和 hsw.js 进行字节级精准主密钥提取 — 提取六个编译时静态固定的 AES-256 主密钥(全部已提取;其中五个具有端到端的加密→解密验证,第六个在 AES 加密调用点完成了结构性验证),以及一个针对每个构建版本的指纹标识符,整个过程耗时不到二十五秒。
快速开始 ·
工作原理 ·
SDK ·
结构 ·
文档
### 需要验证码解决工具?
针对 **Cloudflare Turnstile**、**Cloudflare 5秒盾**、**AWS WAF** 以及 **DataDome**:
API 优先解决方案 · $0.50 – $1.20 / 1K · 阶梯定价 · 亚秒级响应 · peak.fo
## 介绍
hCaptcha 会向每个浏览器分发两个编译过的 bundle。两者都使用内嵌于各个构建版本中的静态 **AES-256-GCM** 主密钥对其网络流量进行加密。
| Bundle | 编译目标 | 密钥 |
hsj.js |
asm.js 风格的编译 JS |
n_key · response_decrypt_key · payload_encrypt_key |
hsw.js |
wasm-bindgen Rust → WebAssembly |
encrypt_key · decrypt_key · n_key · fingerprint_blob_key |
本包可恢复每个构建版本的 **六个** AES-256 主密钥(全为构建时静态),以及一个确定性的构建级指纹标识符 —— 无需候选猜测,也无需硬编码索引。每次构建的 `hsw.js` 都会随机化其 WASM 函数索引、魔数、局部变量和栈偏移量;抓取器会根据结构角色来定位各个部分。
**验证范围。** 六个密钥中有五个具备完整的端到端验证(加密 → 解密 → 匹配明文):三个 HSJ 密钥通过 AST patch 在 bundle 内部得到验证,而 `hsw.encrypt_key` / `hsw.decrypt_key` 则通过针对线上 bundle 的 AES-256-GCM 往返测试进行了验证。第六个密钥 `hsw.n_key` 在 AES 加密调用点完成了**结构性验证**(捕获的 32 字节在环形缓冲区的每条记录中以及预热 + JWT 调用间均保持一致,直接读取自 n-token AES 加密入口的 arg0),但**使用该密钥对线上 n-token 明文进行端到端恢复仍未解决** —— 传输格式信封的明文恢复路径尚未被攻克。有关确切的范围及剩余的开放性假设,请参阅 [`docs/12-hsw-complete-summary.md`](docs/12-hsw-complete-summary.md)。
| 密钥 | 验证 |
| --- | --- |
| `hsj.n_key` / `response_decrypt_key` / `payload_encrypt_key` | 通过 AST patch 在 bundle 内部验证 |
| `hsw.encrypt_key` | 端到端 AES-256-GCM 往返验证(误报率 **2⁻¹²⁸**) |
| `hsw.decrypt_key` | 通过 `HSWBridge.decrypt` 进行端到端 bundle 往返验证 |
| `hsw.n_key` | **结构性验证** —— 在 bundle 触发其 AES 密钥扩展的那一刻,从 n-token AES 加密入口(当前构建版本中为 fn 330)的 arg0(AES 主密钥缓冲区)读取的 32 字节。构建静态:在预热 + JWT 调用中捕获到的字节相同,在环形缓冲区的每条记录中也相同(`extraction_status = captured-from-f330_a0-Nrecords-static`)。调用图 BFS 证明该位置可从 `ec`/`pc`(n-token Promise 执行器路径)到达,但**无法**从 `vc`(请求/响应调度器)到达 —— 从而在结构上将其标识为 n-token 的 AES 主密钥。**使用该密钥对线上 n-token 的端到端明文恢复仍然悬而未决**(参见 [`docs/12`](docs/12-hsw-complete-summary.md) § “Open: n-token plaintext recovery”)。 |
| `hsw.fingerprint_blob_key` | `SHA-256(hsw.n_key)` —— 从捕获的 `n_key` 派生的确定性构建级标识符。继承 `n_key` 的注意事项:结构派生,未针对任何消费者端的输出进行端到端验证。 |
示例输出(点击展开)
```
{
"version": "3441ba6850bebb5729a3e9698c8c5419272f07785b9fbb4178d928bd2bde44c9",
"hsj": {
"n_key": "fe1ba43f33813dbac034ef12f34f3ee371b09057e2a25346a652c681edb2104b",
"response_decrypt_key": "2fb5e0f6aab9596b2001c45ce12cad34e82d579dfea24409fe9b7de4b82d4028",
"payload_encrypt_key": "b2837807eecf9221db94d24337f122d093f70c93efb7d7fc1356e57363e27e28"
},
"hsw": {
"encrypt_key": "8f0b403bcce17f2c417f5e61541cb40440f6b3910f3f59b9e0afb94ed41aef9f",
"decrypt_key": "c55c34ca71b617181f135ed45ded08b70e104366cc1daac5e275f8408149392c",
"n_key": "074cb68ffa72374113adf20618418085a0e853e85cf80ccbf4558a341a6fcc38",
"fingerprint_blob_key": "1a19251e0999f060ad6c968c7b4f48fb95d4f7a4982382f5b3327d09ec56debb"
},
"cipher": "AES-256-GCM",
"wire_format": {
"hsj": "ct(N) || tag(16) || iv(12) || 0x00",
"hsw": "iv(12) || ct(N) || tag(16)"
},
"verified": {
"hsw_encrypt_key": true,
"hsw_decrypt_key": true,
"hsw_n_key": true,
"hsw_fingerprint_blob_key": true
},
"extraction_status": {
"hsw_n_key": "captured-from-f330_a0-2records-static",
"hsw_fingerprint_blob_key": {
"construction": "sha256(hsw.n_key)",
"source_ring": "f330_a0"
}
}
}
```
`hsw.n_key` 在**结构性**意义上被报告为 `verified: true`:
这些字节是在 bundle 调用 AES 密钥扩展的那一刻,
直接从 AES 主密钥缓冲区(当前构建版本中 n-token AES 加密
入口 fn 330 的 arg0)读取的,并且在同一构建的每次调用
(预热 + JWT 调用)中都观察到相同的 32 字节,证明
该密钥是构建静态的,并且这就是 bundle 在 n-token 位置
提供给其 AES 加密的具体值。这**不**等同于
端到端的明文恢复。在我们尝试使用该密钥进行的所有
AES-GCM 传输格式测试中,线上的 n-token 均**无法**
解密 —— 包括用户确认的 HSJ 风格尾块 `ct(N) ‖ tag(16) ‖ iv(12) ‖ 0x00`
—— 因此消费者端的明文恢复路径仍然悬而未决。我们
针对线上 token 暴力测试了 48 个独立捕获的 32 字节值 × 5 种尾部长度
× 2 种布局,解密成功数为 0。
目前还有三个可行的
假设(密钥处于 fixslice32 位切片形式需要逆正交化、
选取了错误的 arg 索引 / 错误的 KS 候选,
或者是非平凡的外部信封,如按调用包装、长度前缀或
多块分帧)。详情请参见
[`docs/12-hsw-complete-summary.md`](docs/12-hsw-complete-summary.md)
§ “Open: n-token plaintext recovery”。
## 快速开始
```
git clone https://github.com/CircuitSavage/hcaptcha-hsj-hsw-reversed
cd hcaptcha-hsj-hsw-reversed
pip install pycryptodome xxhash msgpack jsbeautifier requests
npm install
PYTHONPATH=src python -m hcaptcha
```
或通过 SDK:
```
from hcaptcha import KeyFetcher
keys = KeyFetcher().fetch()
print(keys["hsj"]["n_key"])
print(keys["hsw"]["encrypt_key"])
```
端到端运行时间:获取全部 6 个密钥约 22 秒(5 个加解密 + HSJ 密钥约 8 秒
+ HSW N-key 直接 AES 现场捕获约 14 秒)。如果只需要 5 个加解密 + HSJ 密钥,
可以通过直接调用 `HSJKeyFetcher`/`HSWKeyFetcher` 来跳过捕获步骤。
### 自动刷新
GitHub Actions 工作流每 12 小时运行一次,重新提取当前构建版本的密钥,并将快照提交到 [`data/keys.json`](data/keys.json)。历史的各构建版本快照积聚在 [`data/archive/`](data/archive/) 下。可通过 [Actions 选项卡](https://github.com/CircuitSavage/hcaptcha-hsj-hsw-reversed/actions/workflows/refresh-keys.yml) 进行手动运行。
```
# 始终最新的 keys,无需自行运行 fetcher:
curl -L https://raw.githubusercontent.com/CircuitSavage/hcaptcha-hsj-hsw-reversed/main/data/keys.json
```
## 工作原理
```
sequenceDiagram
participant App
participant V as version
participant J as hsj
participant H as hsw
participant N as Node + jsdom + WASM
App->>V: latest_version()
V-->>App: build hash
App->>J: extract HSJ keys
J->>N: AST-patch hsj.js, drive 3 entry points
N-->>J: 3 dumped 32-byte keys
J-->>App: n_key, response_decrypt_key, payload_encrypt_key
App->>H: extract HSW keys
H->>H: deobf -> magics, structural locate (vc, key-schedule, deobf-helper)
H->>H: build patched WASM (wasm_writer)
H->>N: hook WebAssembly.instantiate, drive encrypt + decrypt
N-->>H: 2 master keys + sentinel
H->>H: AES-256-GCM round-trip verify
H-->>App: encrypt_key, decrypt_key (verified)
```
| 步骤 | 模块 | 输出 |
|------|--------|--------|
| 版本发现 | `hcaptcha.version` | 带有构建哈希的资产 URL |
| HSJ 提取 | `hcaptcha.hsj` | 3 个 AES-256 密钥(在密钥扩展上进行 AST patch) |
| HSW 提取 | `hcaptcha.hsw` | 2 个 AES-256 密钥(在密钥扩展上进行 WASM 字节码 patch) |
| HSW N-key 捕获 | `hcaptcha.hsw_n_key_capture` | 1 个 AES-256 密钥(在 n-token AES 加密入口直接捕获,构建静态) |
| 统一入口 | `hcaptcha.keyfetcher` | 所有 6 个密钥 + 指纹标识符 + 密码 / 线上传输元数据 + extraction_status |
**HSJ —— AST patch。** `hsj.js` 将其 AES 密钥保存在一个由 JS 管理的 `Int8Array` 堆中。密钥扩展总是分配一个 480 字节的栈帧,其中 32 字节的主密钥位于偏移量 0 处。我们对该 prologue 进行 AST patch,使其在每次触发时将这 32 字节复制到一个 JS 数组中,然后驱动这三个入口点。
**HSW —— WASM 字节码 patch。** `hsw.js` 使用 RustCrypto `aes-soft` fixslice32。主密钥永远不会以 32 个连续纯字节的形式存在于线性内存中。我们对 WASM 字节码本身进行 patch:在密钥扩展的入口处对构建的 XOR 去混淆 helper 进行 8 次调用,每次将一个去混淆的密钥字复制到固定的暂存区。JS 通过添加到同一 patch 过的二进制文件中的新 `__peek32` 导出读取暂存区。
**HSW N-key —— 直接 AES 现场捕获。** 早期的尝试追踪了 `vc` 内部的 LCG 字节存储 helper,并恢复了中间状态字节 —— 这些字节在每次调用时都不同,且无法解密 n-token,因为 **`vc` 并不是 n-token 的路径**。对 `hsw.wasm` 的调用图 BFS 证明,n-token AES 加密是从 Promise 执行器导出(当前构建中为 `pc` → 389 → 250 → 548 → `fn 330`)到达的,而**不是**从 `vc`(它仅承载 `encrypt_req_data` / `decrypt_resp_data`)到达的。`hsw_n_key_capture` 对 fn 330(AES 加密入口,签名 `(i32,i32,i32) → i32`)的 prologue 进行 patch,将 `arg0`(AES 主密钥缓冲区指针)处的 32 字节转储到环形缓冲区中,然后运行一次 `window.hsw(jwt)`。捕获的密钥是构建静态的(在预热 + JWT 调用中字节相同,在环形缓冲区的每条记录中字节相同)。`fingerprint_blob_key` 是 `sha256(hsw.n_key)` —— 一个确定性的构建级标识符。请参阅 [`docs/09-hsw-keys-derivation.md`](docs/09-hsw-keys-derivation.md) 和 [`docs/11-hsw-function-map.md`](docs/11-hsw-function-map.md)。
## 安装说明
要求 **Python 3.10+** 和 **Node 18+**。
```
# Python deps
pip install pycryptodome xxhash msgpack jsbeautifier requests
# Node deps (acorn, astring, jsdom, canvas)
npm install
```
可选 —— 安装 Python 包本身:
```
pip install -e .
hcaptcha # CLI prints all 6 keys + fingerprint identifier as JSON
```
## SDK
Python 包暴露了四个类。按需导入。
| API | 返回值 | 用例 |
|-----|---------|----------|
| `KeyFetcher().fetch()` | 所有 6 个密钥 + 指纹标识符 + 元数据 + extraction_status | 多数用户 —— 单次调用 |
| `HSJKeyFetcher().fetch_keys()` | 3 个 HSJ 密钥 | 仅 HSJ 工作负载 |
| `HSWKeyFetcher().fetch()` | 2 个 HSW 密钥(加解密)+ 验证 | 仅 HSW 工作负载 |
| `hsw_n_key_capture.capture()` | HSW N-key + 环形缓冲区捕获 + 线上 n-token | 直接 AES 现场捕获(当前构建) |
| `HSWBridge()` | 作为服务进行加密 / 解密 / 求解 | 黑盒线上兼容流量 |
每个密钥均可作为标准 AES-256-GCM 密钥与任何库配合使用:
```
from Crypto.Cipher import AES
from hcaptcha import KeyFetcher
keys = KeyFetcher().fetch()
hsw_encrypt = bytes.fromhex(keys["hsw"]["encrypt_key"])
# wire: iv(12) || ct(N) || tag(16)
iv, ct, tag = blob[:12], blob[12:-16], blob[-16:]
pt = AES.new(hsw_encrypt, AES.MODE_GCM, nonce=iv).decrypt_and_verify(ct, tag)
```
## 线上传输格式
```
HSJ: ct(N) ‖ tag(16) ‖ iv(12) ‖ 0x00 ← trailing version byte
HSW: iv(12) ‖ ct(N) ‖ tag(16) ← no trailer
```
两个 bundle 都使用 AES-256-GCM,且具有**空的 AAD**,并且每次调用都有一个 12 字节的随机 IV。
## 仓库结构
```
hcaptcha-hsj-hsw-reversed/
├── README.md
├── LICENSE
├── pyproject.toml
├── package.json
│
├── docs/ ← deep-dive documentation
│ ├── 00-architecture.md overall flow + pipeline
│ ├── 01-hsj-bundle.md HSJ internals
│ ├── 02-hsw-bundle.md HSW internals + 6-key inventory
│ ├── 03-deobfuscation.md 12-pass pipeline
│ ├── 04-key-extraction.md method per key
│ ├── 05-wasm-internals.md WASM 1.0 format
│ ├── 06-fixslice32.md bit-sliced AES math
│ ├── 07-wasm-patching.md bytecode-patching technique
│ ├── 08-hsw-dispatch-table.md vc dispatcher anatomy
│ ├── 09-hsw-keys-derivation.md HSW N-key direct AES-site capture
│ ├── 10-architecture-eras.md four hsw.js generations (a–d) + n-token sub-dispatcher
│ ├── 11-hsw-function-map.md per-function role labels
│ ├── 12-hsw-complete-summary.md canonical end-to-end summary
│ ├── 13-hsw-rust-crates.md Rust-crate scan (phase 2 placeholder)
│ └── hsw_function_labels.json machine-readable per-build labels
│
├── examples/
│ └── fetch_all.py
│
└── src/hcaptcha/ Python package
├── __init__.py
├── __main__.py python -m hcaptcha
├── keyfetcher.py unified — 6 keys + fingerprint identifier
├── hsj.py HSJ extractor
├── hsw.py HSW encrypt/decrypt extractor (bytecode patch)
├── hsw_n_key_capture.py HSW N-key — direct AES-site capture (the working extractor)
├── hsw_bridge.py HSWBridge + HSWAnalyzer
├── hsw_client.py end-to-end client (encrypt + PoW + decrypt)
├── hsw_crypto.py pure-Python AES-256-GCM crypto
├── hsw_pow.py Hashcash v1 + SHA-1 PoW solver
├── algorithm.py AES helpers
├── log.py Logger
├── version.py build-version discovery
│
└── tools/ internal infrastructure
├── wasm_disasm.py WASM 1.0 disassembler
├── wasm_writer.py WASM 1.0 byte-perfect re-emitter
├── fixslice_inverse.py fixslice32 reference
├── deobf.py + deobf.js 12-pass deobfuscator
├── sandbox_polyfill.js jsdom polyfill for n-token path
└── js_runtime.py + _js_runner.js Node + jsdom sandbox
```
## 文档
| 文档 | 内容 |
|-----|----------|
| [`docs/00-architecture.md`](docs/00-architecture.md) | 总体流程、仓库映射、流水线 |
| [`docs/01-hsj-bundle.mddocs/01-hsj-bundle.md) | HSJ 内部原理、AST patch 提取 |
| [`docs/02-hsw-bundle.md`](docs/02-hsw-bundle.md) | HSW 调度器、wbg shim、线上传输格式 |
| [`docs/03-deobfuscation.md`](docs/03-deobfuscation.md) | 12 阶段去混淆流水线 |
| [`docs/04-key-extraction.md`](docs/04-key-extraction.md) | 每个密钥的提取方法及有效方案 |
| [`docs/05-wasm-internals.md`](docs/05-wasm-internals.md) | WASM 1.0 二进制格式参考 |
| [`docs/06-fixslice32.md`](docs/06-fixslice32.md) | 位切片 AES 数学原理 |
| [`docs/07-wasm-patching.md`](docs/07-wasm-patching.md) | 字节码 patch 技术 |
| [`docs/08-hsw-dispatch-table.md`](docs/08-hsw-dispatch-table.md) | `vc` 调度器剖析 |
| [`docs/09-hsw-keys-derivation.md`](docs/09-hsw-keys-derivation.md) | HSW N-key —— 直接 AES 现场捕获(当前)+ LCG/PCG 派生(旧版) |
| [`docs/10-architecture-eras.md`](docs/10-architecture-eras.md) | 四代 `hsw.js`(a–d)+ n-token 子调度器路径 |
| [`docs/11-hsw-function-map.md`](docs/11-hsw-function-map.md) | 函数级角色标签(`hsw_function_labels.json`) |
| [`docs/12-hsw-complete-summary.md`](docs/12-hsw-complete-summary.md) | 规范的端到端 HSW 总结(6 个密钥 + PoW + 调度器) |
| [`docs/13-hsw-rust-crates.md`](docs/13-hsw-rust-crates.md) | Rust crate 清单 + 完整的 XOR 密钥与混淆常量参考 |
| [`deobfuscated/`](deobfuscated/) | 冻结的、完全去混淆的 HSW + HSJ JS bundle + 提取出的 WASM blob(可通过 `tools/deobf.py` 重新生成) |
## 免责声明
仅供授权的安全研究和教育使用。请勿在未经许可的系统上使用。与 [hCaptcha](https://www.hcaptcha.com) 没有任何附属、认可或关联关系。移除请求:通过 Telegram 联系。
## 联系方式
标签:AI工具, GNU通用公共许可证, JavaScript逆向, MITM代理, Node.js, Python, 云资产清单, 密码学, 手动系统调用, 无后门, 网络调试, 自动化, 自定义脚本, 逆向工具, 逆向工程, 验证码绕过