JS 逆向天花板姿势:JSRPC 远程调用实战

📅 2026/7/10 5:53:05
JS 逆向天花板姿势:JSRPC 远程调用实战
JS 逆向走到深处最终都会撞上一堵墙核心加密逻辑被 VMP 虚拟机保护、控制流平坦化、反调试陷阱层层包裹静态分析几乎无法还原AST 解析也无从下手。传统抠代码 补环境策略在此彻底失效。JSRPCJavaScript Remote Procedure Call换了一个思路不逆向算法本身把浏览器变成远程加密服务。加密函数在浏览器原环境中正常执行Python 爬虫通过 WebSocket 远程调用。本文从原理到完整实现构建一套生产可用的 JSRPC 框架。一、核心思想不还原算法远程调用原函数维度传统扣代码JSRPC核心动作抠出加密 JS在 Node.js 中补环境运行函数留在浏览器内RPC 协议远程调用适用场景算法清晰、依赖少深度混淆、VMP、补环境成本极高维护成本站点更新后重新扣代码浏览器实时运行最新代码近乎零维护执行速度Node.js 直执极快受网络 RTT 影响约 50~200ms/次环境还原依赖补环境完整度原生浏览器100% 还原数据流Python 爬虫 浏览器真实环境 │ WS: {func:getSign, args:[token]} │ │ ──────────────────────────────────────────→ │ │ 执行 window.getSign(token) │ WS: {result:a8f3b2c1} │ │ ←────────────────────────────────────────── │二、Python 端WebSocket Serverimport asyncio, websockets, json, logging from typing import Any, Dict, Optional logging.basicConfig(levellogging.INFO, format%(asctime)s [%(levelname)s] %(message)s) logger logging.getLogger(JSRPC) class JSRPCServer: def __init__(self, host127.0.0.1, port8080): self.host, self.port host, port self.browser_ws: Optional[websockets.WebSocketServerProtocol] None self.pending: Dict[str, asyncio.Future] {} self._req_id 0 async def start(self): server await websockets.serve(self._handler, self.host, self.port) logger.info(fJSRPC 服务就绪: ws://{self.host}:{self.port}) await server.wait_closed() async def _handler(self, ws, pathNone): self.browser_ws ws logger.info(f浏览器已连接: {ws.remote_address}) try: async for msg in ws: await self._on_message(msg) except websockets.exceptions.ConnectionClosed: logger.warning(浏览器断开) self.browser_ws None async def _on_message(self, msg: str): data json.loads(msg) req_id data.get(id) if req_id in self.pending: fut self.pending.pop(req_id) if data.get(error): fut.set_exception(RuntimeError(data[error])) else: fut.set_result(data.get(result)) async def call(self, func_name: str, *args, timeout10.0) - Any: 远程调用浏览器中的全局函数 if not self.browser_ws: raise RuntimeError(无浏览器连接) self._req_id 1 req_id str(self._req_id) fut asyncio.get_event_loop().create_future() self.pending[req_id] fut await self.browser_ws.send(json.dumps({ action: call, id: req_id, func: func_name, args: list(args), })) try: return await asyncio.wait_for(fut, timeout) except asyncio.TimeoutError: self.pending.pop(req_id, None) raise TimeoutError(f{func_name} 超时)三、浏览器端注入脚本通过控制台或 Playwright 注入建立 WebSocket 长连接并监听调用指令// jsrpc_inject.js (function() { const ws new WebSocket(ws://127.0.0.1:8080); ws.onopen () console.log([JSRPC] 已连接); ws.onclose () setTimeout(() location.reload(), 5000); // 断开则刷新重连 ws.onmessage async (event) { const msg JSON.parse(event.data); if (msg.action ! call) return; try { // 按路径取函数支持 getSign 或 obj.encrypt const path msg.func.split(.); let func window; for (const k of path) func func[k]; // 原生环境执行支持 async 函数 const result await func.apply(null, msg.args || []); ws.send(JSON.stringify({ id: msg.id, result })); } catch (err) { ws.send(JSON.stringify({ id: msg.id, error: err.toString() })); } }; })();四、完整调用链async def main(): rpc JSRPCServer(port8080) server_task asyncio.create_task(rpc.start()) # 等待浏览器注入脚本后连接 while rpc.browser_ws is None: await asyncio.sleep(0.5) # 远程调用浏览器中的加密函数 sign await rpc.call(getSign, user_token_12345) print(f签名: {sign}) # 带签名发起业务请求 import aiohttp async with aiohttp.ClientSession() as session: async with session.get(https://target.com/api/data, headers{X-Sign: sign}) as resp: data await resp.json() print(f业务数据: {data}) server_task.cancel() asyncio.run(main())配合 Playwright 自动注入from playwright.async_api import async_playwright async def inject_jsrpc(): async with async_playwright() as p: browser await p.chromium.launch(headlessFalse) page await browser.new_page() await page.goto(https://target.com/login) with open(jsrpc_inject.js) as f: await page.evaluate(f.read()) await page.wait_for_timeout(600000) asyncio.run(inject_jsrpc())五、进阶优化批量调用减少 RTT 开销async def batch_call(self, calls: list) - list: 并发发送多个调用请求一次性等待全部结果 loop asyncio.get_event_loop() futures, req_ids [], [] for spec in calls: self._req_id 1 rid str(self._req_id) fut loop.create_future() self.pending[rid] fut futures.append(fut) await self.browser_ws.send(json.dumps({ action: call, id: rid, func: spec[func], args: spec.get(args, []), })) return await asyncio.gather(*futures, return_exceptionsTrue)结果缓存相同参数零延迟import time class CachedJSRPC(JSRPCServer): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._cache {} async def cached_call(self, func_name: str, *args, ttl300): key f{func_name}:{hash(args)} if key in self._cache: ts, val self._cache[key] if time.time() - ts ttl: return val result await self.call(func_name, *args) self._cache[key] (time.time(), result) return result六、生产部署与代理配合容器化部署时浏览器和 Python 服务端打包在同一镜像中。浏览器侧需配合代理 IP 轮换以规避风控from playwright.async_api import async_playwright import requests, random def get_proxy(): 亿牛云 API 代理提取出口 IP url http://ip.16yun.cn:817/myip/pl/ORDER_ID/?sSIGNuUSERformatjson resp requests.get(url, timeout10) if resp.status_code 429: return None # 提取频率过快 d resp.json() return fhttp://{d[0][ip]}:{d[0][port]} async def launch_with_proxy(): proxy get_proxy() async with async_playwright() as p: browser await p.chromium.launch( headlessTrue, proxy{server: proxy} if proxy else None, ) page await browser.new_page() await page.goto(https://target.com) with open(jsrpc_inject.js) as f: await page.evaluate(f.read()) # 保持运行 await page.wait_for_timeout(3600000)浏览器场景下登录态维持和加密调用必须在同一会话中完成。亿牛云爬虫代理通过**font stylecolor:rgb(0, 206, 185);background-color:rgb(252, 252, 252);Proxy-Tunnel/font**头绑定出口 IP——相同随机数保持同一 IP确保登录与后续 RPC 调用的会话一致性。遇到403检查白名单遇到429降低采集频率。七、选型决策场景推荐方案原因算法清晰MD5/AES扣代码Node.js 直执无 RTT 开销轻度混淆AST 还原 扣代码可还原长期成本可控VMP / 深度混淆JSRPC静态分析不可行高频调用1000 QPS扣代码JSRPC 受 RTT 限制中低频100 QPSJSRPC性能足够零维护站点频繁更新加密JSRPC浏览器自动运行最新代码八、踩坑速查陷阱解法异步函数返回 Promise注入脚本用**font stylecolor:rgb(0, 206, 185);await func.apply()/font****font stylecolor:rgb(0, 206, 185);this/font**指向错误**font stylecolor:rgb(0, 206, 185);func.apply(originalThis, args)/font**页面跳转导致断连**font stylecolor:rgb(0, 206, 185);onclose/font**触发自动重连CSP 阻止脚本注入通过 CDP 协议注入绕过 CSPCookie 与 IP 不匹配登录和 RPC 调用绑定同一**font stylecolor:rgb(0, 206, 185);Proxy-Tunnel/font**九、总结JSRPC 的核心价值在于将对不可逆向加密的应对策略从还原算法转变为调用原函数。三个结构性优势零逆向成本不分析内部实现、零维护成本站点更新自动生效、100% 环境还原原生浏览器执行。代价是每次调用约 50~200ms 的 RTT 开销不适合万级 QPS 场景。选型原则很简单加密算法能在 2 小时内逆向还原走扣代码超过 2 天搞不定上 JSRPC。在 VMP 保护日益普及的当下JSRPC 正从非常规手段变成 JS 逆向的标配技能。