This commit is contained in:
dela
2026-03-11 14:21:26 +08:00
parent f923257af6
commit 502307cbcd
14 changed files with 37495 additions and 125 deletions

1
.gitignore vendored
View File

@@ -2,3 +2,4 @@
CLAUDE.md
/docs
test_live.sh
node_modules

555
Cargo.lock generated
View File

@@ -2,19 +2,230 @@
# It is not intended for manual editing.
version = 4
[[package]]
name = "atomic-waker"
version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
[[package]]
name = "axum"
version = "0.8.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b52af3cb4058c895d37317bb27508dccc8e5f2d39454016b297bf4a400597b8"
dependencies = [
"axum-core",
"bytes",
"form_urlencoded",
"futures-util",
"http",
"http-body",
"http-body-util",
"hyper",
"hyper-util",
"itoa",
"matchit",
"memchr",
"mime",
"percent-encoding",
"pin-project-lite",
"serde_core",
"serde_json",
"serde_path_to_error",
"serde_urlencoded",
"sync_wrapper",
"tokio",
"tower",
"tower-layer",
"tower-service",
"tracing",
]
[[package]]
name = "axum-core"
version = "0.5.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1"
dependencies = [
"bytes",
"futures-core",
"http",
"http-body",
"http-body-util",
"mime",
"pin-project-lite",
"sync_wrapper",
"tower-layer",
"tower-service",
"tracing",
]
[[package]]
name = "base64"
version = "0.22.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
[[package]]
name = "bitflags"
version = "2.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af"
[[package]]
name = "bytes"
version = "1.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
[[package]]
name = "cfg-if"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "errno"
version = "0.3.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
dependencies = [
"libc",
"windows-sys 0.61.2",
]
[[package]]
name = "form_urlencoded"
version = "1.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf"
dependencies = [
"percent-encoding",
]
[[package]]
name = "futures-channel"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d"
dependencies = [
"futures-core",
]
[[package]]
name = "futures-core"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d"
[[package]]
name = "futures-task"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393"
[[package]]
name = "futures-util"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6"
dependencies = [
"futures-core",
"futures-task",
"pin-project-lite",
"slab",
]
[[package]]
name = "hcaptcha-pow"
version = "0.1.0"
dependencies = [
"axum",
"base64",
"serde",
"serde_json",
"tokio",
]
[[package]]
name = "http"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a"
dependencies = [
"bytes",
"itoa",
]
[[package]]
name = "http-body"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184"
dependencies = [
"bytes",
"http",
]
[[package]]
name = "http-body-util"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a"
dependencies = [
"bytes",
"futures-core",
"http",
"http-body",
"pin-project-lite",
]
[[package]]
name = "httparse"
version = "1.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
[[package]]
name = "httpdate"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
[[package]]
name = "hyper"
version = "1.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11"
dependencies = [
"atomic-waker",
"bytes",
"futures-channel",
"futures-core",
"http",
"http-body",
"httparse",
"httpdate",
"itoa",
"pin-project-lite",
"pin-utils",
"smallvec",
"tokio",
]
[[package]]
name = "hyper-util"
version = "0.1.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0"
dependencies = [
"bytes",
"http",
"http-body",
"hyper",
"pin-project-lite",
"tokio",
"tower-service",
]
[[package]]
@@ -23,12 +234,103 @@ version = "1.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2"
[[package]]
name = "libc"
version = "0.2.182"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112"
[[package]]
name = "lock_api"
version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
dependencies = [
"scopeguard",
]
[[package]]
name = "log"
version = "0.4.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
[[package]]
name = "matchit"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3"
[[package]]
name = "memchr"
version = "2.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
[[package]]
name = "mime"
version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
[[package]]
name = "mio"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc"
dependencies = [
"libc",
"wasi",
"windows-sys 0.61.2",
]
[[package]]
name = "once_cell"
version = "1.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
[[package]]
name = "parking_lot"
version = "0.12.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a"
dependencies = [
"lock_api",
"parking_lot_core",
]
[[package]]
name = "parking_lot_core"
version = "0.9.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
dependencies = [
"cfg-if",
"libc",
"redox_syscall",
"smallvec",
"windows-link",
]
[[package]]
name = "percent-encoding"
version = "2.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
[[package]]
name = "pin-project-lite"
version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b"
[[package]]
name = "pin-utils"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]]
name = "proc-macro2"
version = "1.0.106"
@@ -47,6 +349,27 @@ dependencies = [
"proc-macro2",
]
[[package]]
name = "redox_syscall"
version = "0.5.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
dependencies = [
"bitflags",
]
[[package]]
name = "ryu"
version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
[[package]]
name = "scopeguard"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]]
name = "serde"
version = "1.0.228"
@@ -90,6 +413,61 @@ dependencies = [
"zmij",
]
[[package]]
name = "serde_path_to_error"
version = "0.1.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457"
dependencies = [
"itoa",
"serde",
"serde_core",
]
[[package]]
name = "serde_urlencoded"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
dependencies = [
"form_urlencoded",
"itoa",
"ryu",
"serde",
]
[[package]]
name = "signal-hook-registry"
version = "1.4.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b"
dependencies = [
"errno",
"libc",
]
[[package]]
name = "slab"
version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
[[package]]
name = "smallvec"
version = "1.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
[[package]]
name = "socket2"
version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "86f4aa3ad99f2088c990dfa82d367e19cb29268ed67c574d10d0a4bfe71f07e0"
dependencies = [
"libc",
"windows-sys 0.60.2",
]
[[package]]
name = "syn"
version = "2.0.117"
@@ -101,12 +479,189 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "sync_wrapper"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263"
[[package]]
name = "tokio"
version = "1.49.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86"
dependencies = [
"bytes",
"libc",
"mio",
"parking_lot",
"pin-project-lite",
"signal-hook-registry",
"socket2",
"tokio-macros",
"windows-sys 0.61.2",
]
[[package]]
name = "tokio-macros"
version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "tower"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4"
dependencies = [
"futures-core",
"futures-util",
"pin-project-lite",
"sync_wrapper",
"tokio",
"tower-layer",
"tower-service",
"tracing",
]
[[package]]
name = "tower-layer"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e"
[[package]]
name = "tower-service"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"
[[package]]
name = "tracing"
version = "0.1.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
dependencies = [
"log",
"pin-project-lite",
"tracing-core",
]
[[package]]
name = "tracing-core"
version = "0.1.36"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
dependencies = [
"once_cell",
]
[[package]]
name = "unicode-ident"
version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
[[package]]
name = "wasi"
version = "0.11.1+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
[[package]]
name = "windows-link"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-sys"
version = "0.60.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb"
dependencies = [
"windows-targets",
]
[[package]]
name = "windows-sys"
version = "0.61.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
dependencies = [
"windows-link",
]
[[package]]
name = "windows-targets"
version = "0.53.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3"
dependencies = [
"windows-link",
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_gnullvm",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53"
[[package]]
name = "windows_aarch64_msvc"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006"
[[package]]
name = "windows_i686_gnu"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3"
[[package]]
name = "windows_i686_gnullvm"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c"
[[package]]
name = "windows_i686_msvc"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2"
[[package]]
name = "windows_x86_64_gnu"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1"
[[package]]
name = "windows_x86_64_msvc"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650"
[[package]]
name = "zmij"
version = "1.0.21"

View File

@@ -4,6 +4,8 @@ version = "0.1.0"
edition = "2021"
[dependencies]
axum = "0.8"
base64 = "0.22"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
tokio = { version = "1", features = ["full"] }

1503
asset/challenge.js Normal file

File diff suppressed because it is too large Load Diff

469
asset/deobfuscate.js Normal file
View File

@@ -0,0 +1,469 @@
/**
* hsw.js AST 解混淆脚本 v4
*
* 双解码器支持:
* - PW/QQ: base=311, 596条, rotation=275
* - Qg: base=438, 133条, 无旋转
*
* AST 遍历 passes:
* 1. 别名收集 (两个解码器各自的别名链)
* 2. 常量传播 + 解码替换
* 3. 属性访问简化: obj["prop"] → obj.prop
* 4. 字符串拼接折叠: "a".concat("b") → "ab"
* 5. 死代码清理
*/
const fs = require("fs");
const path = require("path");
const parser = require("@babel/parser");
const traverse = require("@babel/traverse").default;
const generate = require("@babel/generator").default;
const t = require("@babel/types");
const SRC = path.join(__dirname, "hsw.js");
const OUT = path.join(__dirname, "hsw.deobfuscated.js");
const src = fs.readFileSync(SRC, "utf-8");
// ═══════════════════════════════════════════
// Phase 1: 提取两个字符串数组 + 解码
// ═══════════════════════════════════════════
console.log("[*] Phase 1: 提取字符串数组...");
function decode(encoded) {
const chars =
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+/=";
let raw = "";
let bc = 0;
let bs = 0;
let ch;
let idx = 0;
while ((ch = encoded.charAt(idx++))) {
ch = chars.indexOf(ch);
if (~ch) {
bs = bc % 4 ? 64 * bs + ch : ch;
if (bc++ % 4)
raw += String.fromCharCode(255 & (bs >> ((-2 * bc) & 6)));
}
}
let uri = "";
for (let i = 0; i < raw.length; i++)
uri += "%" + ("00" + raw.charCodeAt(i).toString(16)).slice(-2);
return decodeURIComponent(uri);
}
// ── 解码器 1: tH / QQ / PW (base=311, 有旋转) ──
const arr1Match = src.match(
/tH\s*=\s*function\s*\(\)\s*\{\s*var\s+Ig\s*=\s*\[([\s\S]*?)\];\s*return\s*\(\s*tH/
);
if (!arr1Match) { console.error("[-] 无法提取 tH 数组"); process.exit(1); }
const raw1 = eval(`[${arr1Match[1]}]`);
const decoded1 = raw1.map((s) => { try { return decode(s); } catch { return null; } });
console.log(`[+] 解码器1 (PW/QQ): ${raw1.length} 条, base=311`);
// 暴力搜索旋转
const BASE1 = 311;
const LEN1 = decoded1.length;
const lookup1 = (rot, idx) => decoded1[((((idx - BASE1 + rot) % LEN1) + LEN1) % LEN1)];
const constraints1 = [
[814, "length"], [507, "concat"], [606, "createElement"],
[675, "canvas"], [440, "prototype"], [719, "toString"],
[874, "call"], [532, "create"], [716, "test"],
[897, "width"], [811, "height"], [873, "toDataURL"],
];
let rot1 = -1;
for (let r = 0; r < LEN1; r++) {
if (constraints1.every(([i, e]) => lookup1(r, i) === e)) { rot1 = r; break; }
}
if (rot1 === -1) { console.error("[-] PW/QQ 旋转搜索失败"); process.exit(1); }
console.log(`[+] PW/QQ 旋转偏移: ${rot1}`);
const map1 = new Map();
for (let i = BASE1; i < BASE1 + LEN1; i++) {
const v = lookup1(rot1, i);
if (v != null) map1.set(i, v);
}
// ── 解码器 2: xf / Qg (base=438, 无旋转) ──
const arr2Match = src.match(
/xf\s*=\s*function\s*\(\)\s*\{\s*var\s+Ig\s*=\s*\[([\s\S]*?)\];\s*return\s*\(\s*xf/
);
if (!arr2Match) { console.error("[-] 无法提取 xf 数组"); process.exit(1); }
const raw2 = eval(`[${arr2Match[1]}]`);
const decoded2 = raw2.map((s) => { try { return decode(s); } catch { return null; } });
console.log(`[+] 解码器2 (Qg): ${raw2.length} 条, base=438`);
const BASE2 = 438;
const map2 = new Map();
for (let i = 0; i < decoded2.length; i++) {
if (decoded2[i] != null) map2.set(i + BASE2, decoded2[i]);
}
// 验证 Qg 的一些已知值
const constraints2 = [
[441, "length"], // Qg(441) 在 wasm 段出现大量
[439, "buffer"], // Qg(439)
[453, "name"], // Qg(453)
[456, "isArray"], // Array[Qg(456)]
[457, "exec"], // regex.exec
[447, "string"], // typeof === Qg(447)
[449, "constructor"],// Ig[Qg(449)]
[462, "message"], // Ig[Qg(462)]
[463, "stack"], // Ig[Qg(463)]
[476, "set"], // cP().set()
[467, "subarray"], // Ig[Qg(467)]
];
let c2pass = 0;
for (const [idx, exp] of constraints2) {
const act = map2.get(idx);
if (act === exp) c2pass++;
else console.log(` [!] Qg(${idx}): 期望 "${exp}", 实际 "${act}"`);
}
if (c2pass < constraints2.length) {
// 需要旋转
console.log(`[!] Qg 直接映射验证 ${c2pass}/${constraints2.length}, 尝试旋转搜索...`);
const LEN2 = decoded2.length;
let rot2 = -1;
for (let r = 0; r < LEN2; r++) {
const lk = (idx) => decoded2[((((idx - BASE2 + r) % LEN2) + LEN2) % LEN2)];
if (constraints2.every(([i, e]) => lk(i) === e)) { rot2 = r; break; }
}
if (rot2 !== -1 && rot2 !== 0) {
console.log(`[+] Qg 旋转偏移: ${rot2}`);
map2.clear();
for (let i = BASE2; i < BASE2 + LEN2; i++) {
const v = decoded2[((((i - BASE2 + rot2) % LEN2) + LEN2) % LEN2)];
if (v != null) map2.set(i, v);
}
}
}
console.log(`[+] 映射表: PW/QQ=${map1.size}条, Qg=${map2.size}条, 合计=${map1.size + map2.size}`);
// ── 合并映射: 根据解码器名来分派 ──
// decoderGroup1: PW, QQ, 以及它们的别名 → 查 map1
// decoderGroup2: Qg, 以及它的别名 → 查 map2
const group1Names = new Set(["PW", "QQ"]);
const group2Names = new Set(["Qg"]);
function getMapForDecoder(name) {
if (group1Names.has(name)) return map1;
if (group2Names.has(name)) return map2;
return null;
}
// ═══════════════════════════════════════════
// Phase 2: 解析 AST
// ═══════════════════════════════════════════
console.log("[*] Phase 2: 解析 AST...");
const ast = parser.parse(src, {
sourceType: "script",
plugins: [],
errorRecovery: true,
});
console.log("[+] AST 解析完成");
// ═══════════════════════════════════════════
// Phase 3: Pass 1 — 别名收集 (两个解码器)
// ═══════════════════════════════════════════
console.log("[*] Phase 3: 收集解码器别名...");
// 追踪: var X = PW → X 属于 group1
// var Y = Qg → Y 属于 group2
// var Z = X → Z 属于 X 的 group
const allDecoders = new Set([...group1Names, ...group2Names]);
traverse(ast, {
VariableDeclarator(path) {
if (
t.isIdentifier(path.node.id) &&
t.isIdentifier(path.node.init) &&
allDecoders.has(path.node.init.name)
) {
const src = path.node.init.name;
const dst = path.node.id.name;
if (group1Names.has(src)) group1Names.add(dst);
else if (group2Names.has(src)) group2Names.add(dst);
allDecoders.add(dst);
}
},
AssignmentExpression(path) {
if (
t.isIdentifier(path.node.left) &&
t.isIdentifier(path.node.right) &&
allDecoders.has(path.node.right.name)
) {
const src = path.node.right.name;
const dst = path.node.left.name;
if (group1Names.has(src)) group1Names.add(dst);
else if (group2Names.has(src)) group2Names.add(dst);
allDecoders.add(dst);
}
},
});
// 多轮别名链传播
let prevSize;
do {
prevSize = allDecoders.size;
traverse(ast, {
noScope: true,
VariableDeclarator(path) {
if (
t.isIdentifier(path.node.id) &&
t.isIdentifier(path.node.init) &&
allDecoders.has(path.node.init.name) &&
!allDecoders.has(path.node.id.name)
) {
const s = path.node.init.name;
const d = path.node.id.name;
if (group1Names.has(s)) group1Names.add(d);
else if (group2Names.has(s)) group2Names.add(d);
allDecoders.add(d);
}
},
AssignmentExpression(path) {
if (
t.isIdentifier(path.node.left) &&
t.isIdentifier(path.node.right) &&
allDecoders.has(path.node.right.name) &&
!allDecoders.has(path.node.left.name)
) {
const s = path.node.right.name;
const d = path.node.left.name;
if (group1Names.has(s)) group1Names.add(d);
else if (group2Names.has(s)) group2Names.add(d);
allDecoders.add(d);
}
},
});
} while (allDecoders.size > prevSize);
console.log(`[+] 解码器1别名 (${group1Names.size}): ${[...group1Names].join(", ")}`);
console.log(`[+] 解码器2别名 (${group2Names.size}): ${[...group2Names].join(", ")}`);
// ═══════════════════════════════════════════
// Phase 4: Pass 2 — 常量传播 + 解码替换
// ═══════════════════════════════════════════
console.log("[*] Phase 4: 常量传播 + 字符串解码...");
let resolvedCount = 0;
let constPropCount = 0;
function resolveNumericValue(node, scopePath) {
if (t.isNumericLiteral(node)) return node.value;
if (t.isUnaryExpression(node) && node.operator === "-" && t.isNumericLiteral(node.argument)) {
return -node.argument.value;
}
if (t.isIdentifier(node)) {
const binding = scopePath.scope.getBinding(node.name);
if (binding && binding.constant && binding.path.isVariableDeclarator()) {
const init = binding.path.node.init;
if (t.isNumericLiteral(init)) return init.value;
}
// 非 constant binding 但 init 是数字且只赋值一次
if (binding && binding.path.isVariableDeclarator()) {
const init = binding.path.node.init;
if (t.isNumericLiteral(init) && !binding.constantViolations.length) {
return init.value;
}
}
}
return null;
}
traverse(ast, {
CallExpression(path) {
const callee = path.node.callee;
if (
t.isIdentifier(callee) &&
allDecoders.has(callee.name) &&
path.node.arguments.length >= 1
) {
const arg = path.node.arguments[0];
const numVal = resolveNumericValue(arg, path);
if (numVal === null) return;
const map = getMapForDecoder(callee.name);
if (!map) return;
if (map.has(numVal)) {
path.replaceWith(t.stringLiteral(map.get(numVal)));
resolvedCount++;
if (!t.isNumericLiteral(arg)) constPropCount++;
}
}
},
});
console.log(`[+] 解码替换: ${resolvedCount} (常量传播: ${constPropCount})`);
// ═══════════════════════════════════════════
// Phase 5: Pass 3 — 属性简化
// ═══════════════════════════════════════════
console.log("[*] Phase 5: 属性访问简化...");
let memberCount = 0;
const RESERVED = new Set([
"break","case","catch","continue","debugger","default","delete","do",
"else","finally","for","function","if","in","instanceof","new","return",
"switch","this","throw","try","typeof","var","void","while","with",
"class","const","enum","export","extends","import","super","implements",
"interface","let","package","private","protected","public","static","yield",
]);
traverse(ast, {
MemberExpression(path) {
if (
path.node.computed &&
t.isStringLiteral(path.node.property) &&
/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(path.node.property.value) &&
!RESERVED.has(path.node.property.value)
) {
path.node.computed = false;
path.node.property = t.identifier(path.node.property.value);
memberCount++;
}
},
});
console.log(`[+] 属性简化: ${memberCount}`);
// ═══════════════════════════════════════════
// Phase 6: Pass 4 — 字符串拼接折叠
// ═══════════════════════════════════════════
console.log("[*] Phase 6: 字符串拼接折叠...");
let concatCount = 0;
let round;
do {
round = 0;
traverse(ast, {
CallExpression(path) {
if (
t.isMemberExpression(path.node.callee) &&
!path.node.callee.computed &&
t.isIdentifier(path.node.callee.property, { name: "concat" }) &&
t.isStringLiteral(path.node.callee.object) &&
path.node.arguments.length === 1 &&
t.isStringLiteral(path.node.arguments[0])
) {
path.replaceWith(
t.stringLiteral(
path.node.callee.object.value + path.node.arguments[0].value
)
);
round++;
}
},
});
concatCount += round;
} while (round > 0);
console.log(`[+] 拼接折叠: ${concatCount}`);
// ═══════════════════════════════════════════
// Phase 7: Pass 5 — 常量折叠 (typeof, 简单比较)
// ═══════════════════════════════════════════
console.log("[*] Phase 7: 常量表达式折叠...");
let constFoldCount = 0;
traverse(ast, {
// "string" == typeof x → 保留 (运行时), 但 typeof "xxx" → "string"
UnaryExpression(path) {
if (path.node.operator === "typeof" && t.isStringLiteral(path.node.argument)) {
path.replaceWith(t.stringLiteral("string"));
constFoldCount++;
}
},
// void 0 → undefined (可选, 增加可读性)
// 暂时不做, 可能影响语义
});
console.log(`[+] 常量折叠: ${constFoldCount}`);
// ═══════════════════════════════════════════
// Phase 8: Pass 6 — 清理无用别名声明
// ═══════════════════════════════════════════
console.log("[*] Phase 8: 清理无用别名...");
let deadCount = 0;
traverse(ast, {
VariableDeclarator(path) {
if (
t.isIdentifier(path.node.id) &&
t.isIdentifier(path.node.init) &&
allDecoders.has(path.node.init.name) &&
allDecoders.has(path.node.id.name)
) {
const binding = path.scope.getBinding(path.node.id.name);
if (binding && binding.references === 0) {
path.remove();
deadCount++;
}
}
},
});
console.log(`[+] 清理: ${deadCount}`);
// ═══════════════════════════════════════════
// Phase 9: 生成输出
// ═══════════════════════════════════════════
console.log("[*] Phase 9: 生成代码...");
const output = generate(ast, {
comments: true,
compact: false,
retainLines: false,
concise: false,
}).code;
fs.writeFileSync(OUT, output, "utf-8");
const origKB = (fs.statSync(SRC).size / 1024).toFixed(1);
const newKB = (Buffer.byteLength(output) / 1024).toFixed(1);
console.log(`\n${"═".repeat(50)}`);
console.log(`[=] 解混淆完成`);
console.log(` 原始: ${origKB} KB`);
console.log(` 输出: ${newKB} KB`);
console.log(` 解码替换: ${resolvedCount} (常量传播 ${constPropCount})`);
console.log(` 属性简化: ${memberCount}`);
console.log(` 拼接折叠: ${concatCount}`);
console.log(` 常量折叠: ${constFoldCount}`);
console.log(` 死代码清理: ${deadCount}`);
console.log(` PW/QQ 别名: ${group1Names.size} | Qg 别名: ${group2Names.size}`);
console.log(` 输出: ${OUT}`);
console.log(`${"═".repeat(50)}`);
// 导出映射表
const mapOut = path.join(__dirname, "hsw.string-map.json");
const mapObj = { "decoder1_PW_QQ": {}, "decoder2_Qg": {} };
for (const [k, v] of map1) mapObj.decoder1_PW_QQ[k] = v;
for (const [k, v] of map2) mapObj.decoder2_Qg[k] = v;
fs.writeFileSync(mapOut, JSON.stringify(mapObj, null, 2), "utf-8");
console.log(` 映射: ${mapOut}`);

19969
asset/hcaptcha.html Normal file

File diff suppressed because one or more lines are too long

4711
asset/hsw.deobfuscated.js Normal file

File diff suppressed because one or more lines are too long

9102
asset/hsw.js Normal file

File diff suppressed because one or more lines are too long

734
asset/hsw.string-map.json Normal file

File diff suppressed because one or more lines are too long

218
asset/package-lock.json generated Normal file
View File

@@ -0,0 +1,218 @@
{
"name": "asset",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "asset",
"version": "1.0.0",
"license": "ISC",
"dependencies": {
"@babel/generator": "^7.29.1",
"@babel/parser": "^7.29.0",
"@babel/traverse": "^7.29.0",
"@babel/types": "^7.29.0"
}
},
"node_modules/@babel/code-frame": {
"version": "7.29.0",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz",
"integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==",
"license": "MIT",
"dependencies": {
"@babel/helper-validator-identifier": "^7.28.5",
"js-tokens": "^4.0.0",
"picocolors": "^1.1.1"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/generator": {
"version": "7.29.1",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz",
"integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==",
"license": "MIT",
"dependencies": {
"@babel/parser": "^7.29.0",
"@babel/types": "^7.29.0",
"@jridgewell/gen-mapping": "^0.3.12",
"@jridgewell/trace-mapping": "^0.3.28",
"jsesc": "^3.0.2"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-globals": {
"version": "7.28.0",
"resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz",
"integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==",
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-string-parser": {
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
"integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-validator-identifier": {
"version": "7.28.5",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
"integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/parser": {
"version": "7.29.0",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz",
"integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==",
"license": "MIT",
"dependencies": {
"@babel/types": "^7.29.0"
},
"bin": {
"parser": "bin/babel-parser.js"
},
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/@babel/template": {
"version": "7.28.6",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz",
"integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==",
"license": "MIT",
"dependencies": {
"@babel/code-frame": "^7.28.6",
"@babel/parser": "^7.28.6",
"@babel/types": "^7.28.6"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/traverse": {
"version": "7.29.0",
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz",
"integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==",
"license": "MIT",
"dependencies": {
"@babel/code-frame": "^7.29.0",
"@babel/generator": "^7.29.0",
"@babel/helper-globals": "^7.28.0",
"@babel/parser": "^7.29.0",
"@babel/template": "^7.28.6",
"@babel/types": "^7.29.0",
"debug": "^4.3.1"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/types": {
"version": "7.29.0",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz",
"integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==",
"license": "MIT",
"dependencies": {
"@babel/helper-string-parser": "^7.27.1",
"@babel/helper-validator-identifier": "^7.28.5"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@jridgewell/gen-mapping": {
"version": "0.3.13",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
"integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
"license": "MIT",
"dependencies": {
"@jridgewell/sourcemap-codec": "^1.5.0",
"@jridgewell/trace-mapping": "^0.3.24"
}
},
"node_modules/@jridgewell/resolve-uri": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
"license": "MIT",
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/@jridgewell/sourcemap-codec": {
"version": "1.5.5",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
"license": "MIT"
},
"node_modules/@jridgewell/trace-mapping": {
"version": "0.3.31",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
"integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
"license": "MIT",
"dependencies": {
"@jridgewell/resolve-uri": "^3.1.0",
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
"node_modules/debug": {
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
"license": "MIT"
},
"node_modules/jsesc": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
"integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
"license": "MIT",
"bin": {
"jsesc": "bin/jsesc"
},
"engines": {
"node": ">=6"
}
},
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT"
},
"node_modules/picocolors": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
"license": "ISC"
}
}
}

19
asset/package.json Normal file
View File

@@ -0,0 +1,19 @@
{
"name": "asset",
"version": "1.0.0",
"description": "",
"main": "challenge.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"type": "commonjs",
"dependencies": {
"@babel/generator": "^7.29.1",
"@babel/parser": "^7.29.0",
"@babel/traverse": "^7.29.0",
"@babel/types": "^7.29.0"
}
}

86
asset/wasm.js Normal file

File diff suppressed because one or more lines are too long

View File

@@ -5,3 +5,108 @@ pub mod gift256;
pub mod hash;
pub mod solver;
pub mod feistel;
use base64::{engine::general_purpose::STANDARD, Engine};
use serde::Deserialize;
#[derive(Deserialize)]
#[allow(dead_code)]
struct JwtPayload {
#[serde(default)]
f: u32,
#[serde(default)]
s: u32,
#[serde(default)]
t: String,
d: String,
#[serde(default)]
l: String,
#[serde(default)]
i: String,
#[serde(default)]
e: u64,
#[serde(default)]
n: String,
#[serde(default)]
c: u32,
}
/// Takes a raw JWT string, runs the full PoW pipeline, returns the base64 proof.
pub fn solve_jwt(jwt_req: &str) -> Result<String, String> {
// 1. Decode JWT payload (base64url, no signature verification)
let parts: Vec<&str> = jwt_req.split('.').collect();
if parts.len() < 2 {
return Err("Invalid JWT format".into());
}
let payload_b64 = parts[1];
let payload_bytes = base64::engine::general_purpose::URL_SAFE_NO_PAD
.decode(payload_b64)
.map_err(|e| format!("Invalid base64 in JWT payload: {e}"))?;
let payload: JwtPayload = serde_json::from_slice(&payload_bytes)
.map_err(|e| format!("Invalid JSON in JWT payload: {e}"))?;
// 2. Decode challenge data from `d` field
let challenge_data = if let Some(eq_pos) = payload.d.find('=') {
let split_pos = eq_pos + payload.d[eq_pos..].chars().take_while(|&c| c == '=').count();
let part1 = &payload.d[..split_pos];
let part2 = &payload.d[split_pos..];
let mut data = STANDARD.decode(part1)
.map_err(|e| format!("Invalid base64 in challenge part1: {e}"))?;
if !part2.is_empty() {
let p2 = STANDARD.decode(part2).unwrap_or_else(|_| {
let padded = format!("{}{}", part2, "=".repeat((4 - part2.len() % 4) % 4));
STANDARD.decode(&padded).expect("Invalid base64 in challenge part2")
});
data.extend_from_slice(&p2);
}
data
} else {
STANDARD.decode(&payload.d)
.map_err(|e| format!("Invalid base64 in challenge data: {e}"))?
};
// 3. Split into encrypted_payload and seed_bytes
let (encrypted_payload, seed_bytes) = if let Some(eq_pos) = payload.d.find('=') {
let split_pos = eq_pos + payload.d[eq_pos..].chars().take_while(|&c| c == '=').count();
let part1_len = STANDARD.decode(&payload.d[..split_pos])
.map_err(|e| format!("Invalid base64 in part1: {e}"))?.len();
(&challenge_data[..part1_len], &challenge_data[part1_len..])
} else {
(challenge_data.as_slice(), &[] as &[u8])
};
// 4. Feistel decrypt
if encrypted_payload.len() < 33 {
return Err(format!(
"Encrypted payload too short: {} bytes (need >= 33)",
encrypted_payload.len()
));
}
let (key_material, _control) = feistel::feistel_decrypt(encrypted_payload, 0);
let difficulty = if payload.c > 0 { payload.c } else { 1000 };
let challenge = solver::Challenge {
key_material,
difficulty,
};
// 5. Derive PCG seed from seed_bytes
let seed = if seed_bytes.len() >= 8 {
u64::from_le_bytes([
seed_bytes[0], seed_bytes[1], seed_bytes[2], seed_bytes[3],
seed_bytes[4], seed_bytes[5], seed_bytes[6], seed_bytes[7],
])
} else {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos() as u64
};
// 6. Solve and encode
let proof = solver::solve(&challenge, seed);
Ok(STANDARD.encode(&proof))
}

View File

@@ -1,132 +1,28 @@
//! hCaptcha PoW solver CLI
//! Parses JWT challenge -> calls solver -> outputs base64 proof
use base64::{engine::general_purpose::STANDARD, Engine};
use axum::{extract::Json, http::StatusCode, response::IntoResponse, routing::post, Router};
use serde::Deserialize;
mod pcg;
mod sbox;
mod gift256;
mod hash;
mod solver;
mod util;
mod feistel;
#[derive(Deserialize)]
#[allow(dead_code)]
struct JwtPayload {
#[serde(default)]
f: u32,
#[serde(default)]
s: u32,
#[serde(default)]
t: String,
d: String,
#[serde(default)]
l: String,
#[serde(default)]
i: String,
#[serde(default)]
e: u64,
#[serde(default)]
n: String,
#[serde(default)]
c: u32,
struct SolveRequest {
jwt: String,
}
fn main() {
// 1. Read JWT from command line
let jwt_req = std::env::args().nth(1).expect("Usage: hcaptcha-pow <jwt>");
// 2. Decode JWT payload (base64url, no signature verification)
let parts: Vec<&str> = jwt_req.split('.').collect();
if parts.len() < 2 {
eprintln!("Invalid JWT format");
std::process::exit(1);
async fn solve_handler(Json(req): Json<SolveRequest>) -> impl IntoResponse {
let jwt = req.jwt;
match tokio::task::spawn_blocking(move || hcaptcha_pow::solve_jwt(&jwt)).await {
Ok(Ok(proof)) => (StatusCode::OK, Json(serde_json::json!({ "n": proof }))),
Ok(Err(e)) => (StatusCode::BAD_REQUEST, Json(serde_json::json!({ "error": e }))),
Err(e) => (
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({ "error": format!("Task panicked: {e}") })),
),
}
let payload_b64 = parts[1];
let payload_bytes = base64::engine::general_purpose::URL_SAFE_NO_PAD
.decode(payload_b64)
.expect("Invalid base64 in JWT payload");
let payload: JwtPayload = serde_json::from_slice(&payload_bytes).expect("Invalid JSON in JWT payload");
println!("Algorithm: {}", payload.n);
println!("Difficulty: {}", payload.c);
println!("Expiration: {}", payload.e);
// 3. Decode challenge data from `d` field
// The `d` field is two concatenated base64 strings:
// encrypted_payload (with = padding) + seed (no padding)
// Split at the padding boundary and decode each part separately.
let challenge_data = if let Some(eq_pos) = payload.d.find('=') {
let split_pos = eq_pos + payload.d[eq_pos..].chars().take_while(|&c| c == '=').count();
let part1 = &payload.d[..split_pos];
let part2 = &payload.d[split_pos..];
let mut data = STANDARD.decode(part1).expect("Invalid base64 in challenge part1");
if !part2.is_empty() {
let p2 = STANDARD.decode(part2).unwrap_or_else(|_| {
// part2 may lack padding — add it
let padded = format!("{}{}", part2, "=".repeat((4 - part2.len() % 4) % 4));
STANDARD.decode(&padded).expect("Invalid base64 in challenge part2")
});
data.extend_from_slice(&p2);
}
data
} else {
STANDARD.decode(&payload.d).expect("Invalid base64 in challenge data")
};
// Split challenge_data into encrypted_payload (part1) and seed_bytes (part2)
// Part1 = first decode (203 bytes), Part2 = second decode (12 bytes)
let (encrypted_payload, seed_bytes) = if let Some(eq_pos) = payload.d.find('=') {
let split_pos = eq_pos + payload.d[eq_pos..].chars().take_while(|&c| c == '=').count();
let part1_len = STANDARD.decode(&payload.d[..split_pos])
.expect("Invalid base64 in part1").len();
(&challenge_data[..part1_len], &challenge_data[part1_len..])
} else {
(challenge_data.as_slice(), &[] as &[u8])
};
println!("Encrypted payload: {} bytes, Seed: {} bytes", encrypted_payload.len(), seed_bytes.len());
// 4. Feistel decrypt 33 bytes → 32 byte key + 1 byte control
if encrypted_payload.len() < 33 {
eprintln!("Encrypted payload too short: {} bytes (need >= 33)", encrypted_payload.len());
std::process::exit(1);
}
let (key_material, control) = feistel::feistel_decrypt(encrypted_payload, 0);
println!("Feistel decrypted: key[0..4]={:02x}{:02x}{:02x}{:02x}, control=0x{:02x}",
key_material[0], key_material[1], key_material[2], key_material[3], control);
let difficulty = if payload.c > 0 { payload.c } else { 1000 };
let challenge = solver::Challenge {
key_material,
difficulty,
};
// 5. Derive PCG seed from seed_bytes (part2 of d)
let seed = if seed_bytes.len() >= 8 {
u64::from_le_bytes([
seed_bytes[0], seed_bytes[1], seed_bytes[2], seed_bytes[3],
seed_bytes[4], seed_bytes[5], seed_bytes[6], seed_bytes[7],
])
} else {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos() as u64
};
println!("PCG seed: 0x{:016x}", seed);
println!("Running {} iterations...", difficulty);
let proof = solver::solve(&challenge, seed);
// 6. Output base64-encoded proof
let proof_b64 = STANDARD.encode(&proof);
println!("Proof size: {} bytes ({} digests)", proof.len(), proof.len() / 16);
println!("n={}", proof_b64);
#[tokio::main]
async fn main() {
let app = Router::new().route("/solve", post(solve_handler));
let addr = "0.0.0.0:3000";
println!("hcaptcha-pow service listening on {addr}");
let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
axum::serve(listener, app).await.unwrap();
}