| name | ruishu-reverse |
| description | Ruishu (Rivers Security) anti-bot pure-algorithm reversing — Cookie T generation + URL suffix handling |
| triggers | ["ruishu","rivers security","412 protection","Cookie T","dynamic JS anti-bot","anti-bot bypass"] |
Ruishu Anti-Bot Pure-Algorithm Reversing Skill
Goal: any Claude instance that reads this document should be able to independently produce a pure-algorithm Cookie T + URL suffix for a Ruishu-protected site.
Validated: 9+ sites returning HTTP 200.
Decision Tree
Once you have the target URL, choose an approach with this flow:
1. HTTP GET the target URL
├── Not 412 → not Ruishu protection, exit
└── 412 + $_ts.cd + $_ts.nsd → Ruishu confirmed
│
2. Do you only need GET requests?
├── Yes (80% of cases) → pure-algorithm approach [Stages 0→5]
└── POST required
│
3. Does the POST need a URL suffix?
├── No (99% of sites) → pure-algorithm Cookie + normal POST [Stages 0→5]
└── Suffix required
├── Stable approach → JsRpc (browser injection, works everywhere)
└── Lightweight approach → sdenv in-VM XHR (instance must be rebuilt after each POST)
Quick check for whether a suffix is needed: pure-algorithm Cookie T + normal POST → 200 means no suffix needed; 400/412 means a suffix is required.
Protection Overview (condensed)
Browser GET → 412 + HTML (contains $_ts.nsd, $_ts.cd, mainjs URL, Set-Cookie: xxxS=...)
↓
mainjs executes → decodes cd → extracts 45 key groups + VM bytecode
↓
Dynamically generates ~296KB of eval code (variable names determined by the nsd seed, different every time)
↓
eval executes → three-layer nested VM → collects fingerprints → assembles basearr (154-166B TLV)
↓
basearr → Huffman → XOR → AES-CBC → CRC32 → AES-CBC → Base64 → Cookie T
↓
Browser re-GETs with Cookie S+T → 200
The parts we replace with pure algorithm:
Stage Overview
| Stage | Input | Output | Validation criterion | Generic? |
|---|
| 0 Recon | target URL | 412 HTML + mainjs + Cookie S + sdenv reference data | sdenv Cookie → 200 | Generic |
| 1 Encryption chain | sdenv Cookie T + keys | generateCookie(basearr, keys) → Cookie T | sdenv basearr + pure-algorithm encryption → 200 | Generic, one-time |
| 2 Key extraction | $_ts.cd | keys[0..44] (45 groups) | keys match those extracted by sdenv | Generic, one-time |
| 3 Coder | mainjs + nsd + cd | eval code + codeUid + functionsNameSort | eval code byte-for-byte identical | Generic, one-time |
| 4 basearr | reference data + keys | buildBasearr(config, keys) → basearr | full pure-algorithm chain → 200 | ~1h per site |
| 5 End-to-end | everything | pure-algorithm HTTP GET → 200 | 3+ consecutive 200s | just assemble |
Execution order: strictly 0 → 1 → 2 → 3 → 4 → 5; only proceed to the next step after the current one passes validation.
Methodology
Data-driven (for Cookie T / basearr — the most important!)
Core idea: use sdenv to collect 3-5 sets of real data → compare byte by byte → find the source of each byte. Absolutely do not read the inner VM code (740 states, three-layer nesting — this is a trap).
Collect 5 sessions → split out each TLV field → annotate byte by byte:
Fixed (same across all sessions) → hardcode
From keys (matches keys[N]) → dynamic extraction
Time-related (varies predictably) → find the formula
Random (no pattern) → Math.random
Unknown → needs more data or deeper analysis
Real experience: spending 2 days reading the VM code was a complete waste. After switching to data-driven, all problems were solved within 1 day.
AST analysis (for URL suffix / eval code functions)
Core idea: the eval code is valid JS; parse the AST with acorn → build the rt[N] function map → recursively trace the call chain → extract the core algorithm. This accomplishes in a few hours what would take weeks by hand.
Output: 14 AST tools, ~20h, fully reversed the suffix core functions out of 296KB of obfuscated code. See ast_methodology.md for details.
Method Selection Table
| Which layer is the target in? | Which method to use |
|---|
| eval code JS functions | AST (precise and efficient) |
| basearr data structures | data-driven (fast and reliable) |
| r2mKa VM bytecode | AST to extract opcodes + automatic disassembly |
| runtime dynamic values (timestamps, etc.) | sdenv collection |
Pitfall Warnings (from real-world experience)
| Detour | Cost | Correct approach |
|---|
| Decompiling the inner VM to understand basearr | 2 days wasted | Data-driven: collect 5 sessions, solved in 10 minutes |
| Copying rs-reverse formulas (idx*7+6, etc.) | 1 day wasted | Data-driven: formulas are version-specific, not generic |
| Patching the environment to run eval code | document.all needs a C++ addon | Coder rewrites the mainjs logic |
| Hardcoding type=2 values | wrong as soon as the session changes | cp1 index→value mapping (reverse-derived from 5 sessions) |
| Skipping hybrid verification and going straight to basearr | a 400 with no idea which step is wrong | first sdenv basearr + pure-algorithm encryption = 200, proving encryption is correct |
| Reverse-deriving opcode semantics via runtime stack tracing | 80B/day throughput | AST static extraction: 400B/hour (80x more efficient) |
| HTTP-downloading mainjs with string concatenation | UTF-8 multi-byte characters get corrupted | Buffer concatenation + toString('utf-8') |
Troubleshooting Guide
Returns 412 (Cookie not accepted)
- Is the cookie name correct? →
keys[7].split(';')[5] + 'T', not hardcoded
- Was Cookie S sent along with it? → both S and T must be sent together
- Cookie T format? → must start with "0"
- Has the time expired? → cookies are usually valid for < 5 minutes, check the nonce timestamp
- Are cd and Cookie S matched? → they must come from the same 412 response
Returns 400 (Cookie format/content error)
- Did the encryption chain pass hybrid verification? → first validate with sdenv basearr + pure-algorithm encryption
- Does the basearr length match? → compare against the sdenv reference (usually 154-166B)
- Is the basearr TLV missing any fields? → compare field by field against the reference
- Was key extraction correct? → keys[0] should be "64", keys[2] should be 48B
- Does the POST need a URL suffix? → first confirm with an sdenv POST test
Coder Output Mismatch
- Compare byte by byte to find the first difference position:
for (let i = 0; i < Math.min(gen.length, ref.length); i++) {
if (gen[i] !== ref[i]) {
console.log('diff @' + i + ':', JSON.stringify(gen.substring(i, i+60)));
console.log('reference:', JSON.stringify(ref.substring(i, i+60)));
break;
}
}
- The 6 common bugs:
- opmate count: 5 named + 1 unnamed = 6 (not 7)
- gren(0) uses the global opmate, not the local one
- var declaration: uses mate index 1 (not 2)
- while(1): also uses the global opmate
- _ifElse: the start variable is modified inside the for loop, the else branch uses the modified start
- debugger: each gren segment rebuilds PRNG(seed=nsd), posis accumulates across segments
- Off by ~180 characters → most likely a debugger alignment issue
Key Extraction Failure
- Is keys[0] = "64" (ASCII [0x36, 0x34])?
- Yes → XOR offset is correct
- No → r2mka runTask must be implemented (high difficulty, prefer switching to another site for validation)
- keys.length < 45 → XOR offset calculation is wrong
- keys[29..32] are not 4B each → structural anomaly
type=2 Value Mismatch
- Do not hardcode! type=2 depends on the nsd → cp1 shuffle result
- Collect 5 sessions, record the keys[29..32] variable names + type=2 values
- Look up the variable name index in cp1=grenKeys(keynameNum, nsd)
- Build a cp1_index → value mapping table (the mapping is fixed for the same mainjs version)
- Use the script: scripts/collect_type2.js
Site Adaptation Checklist
When adapting a new site, check off each item:
Quick Reference: Common Constants
seed = 15679 * (seed & 0xFFFF) + 2531011
byte=0 → weight=45, byte=255 → weight=6, others → weight=1
outer: key=keys[16], IV=random 16B inner: key=keys[17], IV=all-zero 16B
0xEDB88320
'qrcklmDoExthWJiHAp1sVYKU3RFMQw8IGfPO92bvLNj.7zXBaSnu0TC6gy_4Ze5d'
'qrcklmDoExthWJiHAp1sVYKU3RFMQw8IGfPO92bvLNj.7zXBaSnu0TC6gy_4Ze5d{}|~ !#$%()*+,-;=?@[]^'
55295
'_$abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
[4, 16, 64, 256, 1024, 4096, 16384, 65536]
keys[7].split(';')[5] + 'T'
keys[7].split(';')[1]
Encryption Pipeline (7 steps)
basearr (154-166B)
→ Huffman encoding (~118B)
→ first 16 bytes XOR keys[2][0:15]
→ AES-128-CBC (key=keys[17], IV=all-zero, PKCS7) → ~128B
→ assemble packet: [2, 8, r2mkaTime(4B), now(4B), 48, keys[2](48B), lenEnc, cipher]
→ CRC32 → [crc(4B), packet] → ~193B
→ AES-128-CBC (key=keys[16], IV=random 16B, PKCS7) → ~224B
→ custom Base64 → "0" + 299 characters
Meaning of Key Keys
| key | Length | Meaning | Purpose |
|---|
| keys[2] | 48B | KEYS48 | XOR first 16B + all 48B embedded in packet |
| keys[7] | variable | config string (semicolon-separated) | Cookie name [5]+'T', suffix parameter name [1] |
| keys[16] | 16B | KEY2 | outer AES key |
| keys[17] | 16B | KEY1 | inner AES key |
| keys[19] | variable | timestamp string | type=10[6..9] |
| keys[21] | variable | r2mkaTime string | nonce time |
| keys[22] | variable | encrypted data | type=6 AES decryption |
| keys[24-26] | variable | numeric strings | type=10 parameters |
| keys[29-32] | 4B each | variable names | type=2 mapping (cp1 index→value) |
| keys[33-34] | variable | numeric strings | codeUid computation parameters |
Variable-Name Variation Warning
Ruishu's variable names are not fixed! Different nsd → different grenKeys(918, nsd) shuffle → all variable names in the eval code change.
Session 1 (nsd=84277): _$eX, _$hR, _$cR, _$bO ...
Session 2 (nsd=91234): _$f3, _$gT, _$aK, _$dP ...
Hook location must use structural features, not variable names:
const target = 'function _$hr(){var _$jZ=[324];';
const statePattern = /function\s+(_\$\w+)\(\)\{var\s+(_\$\w+)=\[324\]/;
if (code.length > 250000) { }
if (code.includes('15679') && code.includes('2531011')) { }
Tool Dependencies
| Tool | Install | Purpose | Stage |
|---|
| Node.js crypto/http | built-in | AES encrypt/decrypt, HTTP requests | all |
| sdenv | npx pnpm add sdenv | reference data collection, in-VM XHR | 0, 4, 6 |
| js-beautify | npm i js-beautify | format mainjs (optional) | 3 |
| acorn + acorn-walk | npm i acorn acorn-walk | AST analysis (suffix reversing) | 6 |
Note: npm 11.x + Node 24 has a dependency-resolution infinite-loop bug; installing sdenv must use pnpm.
Compiling native modules requires VS Build Tools (Windows) or gcc (Linux).
Companion Data Collection (core of Stage 0)
Ruishu's variable names differ on every load! You must collect the full companion dataset within the same session.
If you collect separately (first the 412, then the mainjs), nsd has already changed and the data won't line up!
Use scripts/collect_session.js to collect everything at once:
captured/
├── session.json nsd + cd + Cookie S/T + basearr + timestamps
├── keys_raw.json 45 key groups (index + length + data)
├── ts_init.js $_ts initialization script (contains cd)
├── eval_code.js 296KB eval code (matching variable names)
└── mainjs.js mainjs source (static, can be downloaded separately)
File Index
Detailed References (load on demand)
Executable Scripts
Reference Implementations
| File | Description |
|---|
| lib/coder.js | outer VM rewriter (362 lines, validated: eval code 100% byte-identical) |
| lib/basearr.js | basearr generator (304 lines, validated: HTTP 200) |