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.jshsw.js 进行字节级精准主密钥提取 — 提取六个编译时静态固定的 AES-256 主密钥(全部已提取;其中五个具有端到端的加密→解密验证,第六个在 AES 加密调用点完成了结构性验证),以及一个针对每个构建版本的指纹标识符,整个过程耗时不到二十五秒。

Python 3.10+ Node 18+ MIT License

Refresh keys CI

Telegram @jujucodings

快速开始  ·  工作原理  ·  SDK  ·  结构  ·  文档

### 需要验证码解决工具? 针对 **Cloudflare Turnstile**、**Cloudflare 5秒盾**、**AWS WAF** 以及 **DataDome**:
Peak Solutions
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 联系。 ## 联系方式

Telegram @jujucodings

标签:AI工具, GNU通用公共许可证, JavaScript逆向, MITM代理, Node.js, Python, 云资产清单, 密码学, 手动系统调用, 无后门, 网络调试, 自动化, 自定义脚本, 逆向工具, 逆向工程, 验证码绕过