首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >JS 逆向天花板姿势:JSRPC 远程调用实战

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

原创
作者头像
小白学大数据
发布2026-07-09 16:50:01
发布2026-07-09 16:50:01
30
举报

JS 逆向走到深处,最终都会撞上一堵墙:核心加密逻辑被 VMP 虚拟机保护、控制流平坦化、反调试陷阱层层包裹,静态分析几乎无法还原,AST 解析也无从下手。传统"抠代码 + 补环境"策略在此彻底失效。JSRPC(JavaScript Remote Procedure Call)换了一个思路:不逆向算法本身,把浏览器变成远程加密服务。加密函数在浏览器原环境中正常执行,Python 爬虫通过 WebSocket 远程调用。本文从原理到完整实现,构建一套生产可用的 JSRPC 框架。一、核心思想:不还原算法,远程调用原函数

维度

传统扣代码

JSRPC

核心动作

抠出加密 JS,在 Node.js 中补环境运行

函数留在浏览器内,RPC 协议远程调用

适用场景

算法清晰、依赖少

深度混淆、VMP、补环境成本极高

维护成本

站点更新后重新扣代码

浏览器实时运行最新代码,近乎零维护

执行速度

Node.js 直执,极快

受网络 RTT 影响,约 50~200ms/次

环境还原

依赖补环境完整度

原生浏览器,100% 还原

数据流:

二、Python 端:WebSocket Server

代码语言:txt
复制
import asyncio, websockets, json, logging
from typing import Any, Dict, Optional

logging.basicConfig(level=logging.INFO, format='%(asctime)s [%(levelname)s] %(message)s')
logger = logging.getLogger('JSRPC')

class JSRPCServer:
    def __init__(self, host='127.0.0.1', port=8080):
        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(f"JSRPC 服务就绪: ws://{self.host}:{self.port}")
        await server.wait_closed()

    async def _handler(self, ws, path=None):
        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, timeout=10.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 长连接并监听调用指令:

代码语言:txt
复制
// 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() }));
        }
    };
})();

四、完整调用链

代码语言:txt
复制
async def main():
    rpc = JSRPCServer(port=8080)
    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 自动注入

代码语言:txt
复制
from playwright.async_api import async_playwright

async def inject_jsrpc():
    async with async_playwright() as p:
        browser = await p.chromium.launch(headless=False)
        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 开销)

代码语言:txt
复制
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_exceptions=True)

结果缓存(相同参数零延迟)

代码语言:txt
复制
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, ttl=300):
        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 轮换以规避风控:

浏览器场景下,登录态维持和加密调用必须在同一会话中完成。亿牛云爬虫代理通过 Proxy-Tunnel 头绑定出口 IP——相同随机数保持同一 IP,确保登录与后续 RPC 调用的会话一致性。遇到 403 检查白名单,遇到 429 降低采集频率。七、选型决策

场景

推荐方案

原因

算法清晰(MD5/AES)

扣代码

Node.js 直执,无 RTT 开销

轻度混淆

AST 还原 + 扣代码

可还原,长期成本可控

VMP / 深度混淆

JSRPC

静态分析不可行

高频调用(>1000 QPS)

扣代码

JSRPC 受 RTT 限制

中低频(<100 QPS)

JSRPC

性能足够,零维护

站点频繁更新加密

JSRPC

浏览器自动运行最新代码

八、踩坑速查

陷阱

解法

异步函数返回 Promise

注入脚本用 await func.apply()

this 指向错误

func.apply(originalThis, args)

页面跳转导致断连

onclose 触发自动重连

CSP 阻止脚本注入

通过 CDP 协议注入,绕过 CSP

Cookie 与 IP 不匹配

登录和 RPC 调用绑定同一 Proxy-Tunnel

九、总结JSRPC 的核心价值在于将对不可逆向加密的应对策略从"还原算法"转变为"调用原函数"。三个结构性优势:零逆向成本(不分析内部实现)、零维护成本(站点更新自动生效)、100% 环境还原(原生浏览器执行)。代价是每次调用约 50~200ms 的 RTT 开销,不适合万级 QPS 场景。选型原则很简单:加密算法能在 2 小时内逆向还原,走扣代码;超过 2 天搞不定,上 JSRPC。在 VMP 保护日益普及的当下,JSRPC 正从非常规手段变成 JS 逆向的标配技能。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档