RPC 远程调用
RPC(Remote Procedure Call)方案不需要将加密代码扣出来补环境,而是让加密代码继续跑在浏览器原始环境中,通过 WebSocket 通道暴露给外部调用。 适合:加密逻辑极其复杂、补环境成本过高、或对运行速度要求不高的场景。 Sekiro(https://github.com/virjar/sekiro) 是专为 RPC 场景设计的框架,支持 Java/Node.js 服务端,提供完整的路由和多客户端管理。 浏览器端注入(使用 Sekiro JS SDK): Python 调用: Python 服务端用 asyncio + websoc
官方文档:https://developer.mozilla.org/zh-CN/docs/Web/API/WebSockets_API
适用场景:加密逻辑过于复杂难以扣代码时,通过 WebSocket 让浏览器直接执行加密并返回结果
RPC(Remote Procedure Call)方案不需要将加密代码扣出来补环境,而是让加密代码继续跑在浏览器原始环境中,通过 WebSocket 通道暴露给外部调用。
适合:加密逻辑极其复杂、补环境成本过高、或对运行速度要求不高的场景。
核心架构
Python 脚本 (客户端)
|
| WebSocket 发送参数
v
本地 WebSocket 服务端 (Node.js 或 Python)
|
| 转发给注入的浏览器端代码
v
浏览器 JS 环境 (已注入 RPC 客户端)
|
| 调用目标加密函数,返回结果
v
Python 脚本拿到加密结果
最简实现
服务端(Node.js WebSocket)
// server.js
const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 23333 });
const clients = new Set();
wss.on('connection', function(ws) {
clients.add(ws);
console.log('浏览器已连接');
ws.on('message', function(message) {
// 将浏览器的响应转发给等待的 Python 请求
console.log('收到响应:', message.toString());
});
ws.on('close', function() {
clients.delete(ws);
});
});
console.log('RPC Server 运行在 ws://localhost:23333');
注入浏览器端(在 Console 或 Tampermonkey 中执行)
// browser_client.js - 注入到目标页面
(function() {
var ws = new WebSocket('ws://localhost:23333');
ws.onopen = function() {
console.log('RPC 客户端已连接');
};
ws.onmessage = function(event) {
var msg = JSON.parse(event.data);
var result;
try {
// 调用页面中的加密函数
// 根据实际情况修改
result = window._encrypt(msg.params);
} catch(e) {
result = { error: e.message };
}
ws.send(JSON.stringify({
id: msg.id,
result: result
}));
};
})();
Python 调用端
import asyncio
import websockets
import json
import uuid
async def call_browser_encrypt(params):
async with websockets.connect('ws://localhost:23333') as ws:
msg_id = str(uuid.uuid4())
await ws.send(json.dumps({'id': msg_id, 'params': params}))
response = await ws.recv()
return json.loads(response)['result']
# 使用示例
result = asyncio.run(call_browser_encrypt({'data': 'hello'}))
print(result)
Sekiro 框架
Sekiro 是专为 RPC 场景设计的框架,支持 Java/Node.js 服务端,提供完整的路由和多客户端管理。
浏览器端注入(使用 Sekiro JS SDK):
// 引入 sekiro 客户端脚本后
var client = new SekiroClient("ws://localhost:5620/business-demo/register?group=ws-group&clientId=" + Math.random());
client.registerAction("getEncryptedSign", function(request, resolve, reject) {
try {
var data = request['data'];
var sign = window._generateSign(data); // 调用页面加密函数
resolve(sign);
} catch(e) {
reject(e.message);
}
});
Python 调用:
import requests
def get_sign(data):
resp = requests.get('http://localhost:5620/business-demo/invoke', params={
'group': 'ws-group',
'action': 'getEncryptedSign',
'data': data
})
return resp.json()['data']
优缺点对比
| 维度 | RPC 方案 | 扣代码补环境 |
|---|---|---|
| 开发成本 | 低(无需分析算法细节) | 高(需完整还原环境) |
| 运行速度 | 慢(网络延迟) | 快(本地直接调用) |
| 稳定性 | 依赖浏览器保持打开 | 独立运行,稳定 |
| 可维护性 | 网站更新后通常自动适配 | 网站更新后需重新分析 |
| 适用场景 | 临时脚本、复杂环境 | 生产环境、高并发 |
注意事项
- 浏览器端 WebSocket 连接到本地服务,不会产生跨域问题(ws 协议不受同源策略限制)
- 如果网站 CSP 限制了 WebSocket 连接,可通过 mitmproxy 修改响应头绕过
- 多并发场景下需在服务端维护请求队列,避免响应错位
最佳实践
Python 服务端用 asyncio + websockets 库:异步 WebSocket 服务可以轻松处理多个并发请求;用 asyncio.Queue 维护请求队列,保证顺序;每个请求带唯一 ID,响应中携带同一 ID 匹配,避免并发时响应错位。
Tampermonkey 脚本注入 WebSocket 客户端:在 Tampermonkey 脚本中 @run-at document-start 建立 WebSocket 连接,Hook 目标函数后通过 WebSocket 接收参数、调用函数、发送结果,整个流程在浏览器内运行,完全真实环境。
用 requestId 匹配请求和响应:高并发时 Python 同时发出多个请求,浏览器按事件循环顺序处理不保证顺序;每个请求带 { "id": uuid, "params": ... },响应带同一 id,Python 用字典匹配。
设置超时机制保护主流程:浏览器 tab 关闭或卡住时 WebSocket 不一定立即报错;Python 服务端每次请求设置 asyncio.wait_for(timeout=10) 超时,超时后重试或降级处理。
RPC 适合打 PoC,补环境适合生产:RPC 依赖浏览器实例持续运行,无法水平扩展;验证可行性后,对高频调用的签名函数仍然值得投入时间做补环境,性能和稳定性会更好。
常见陷阱
陷阱:WebSocket 建立后过一会儿自动断开
现象: RPC 服务正常运行一段时间后,突然开始报连接关闭错误。
原因: 部分 WebSocket 服务器(或浏览器)在空闲超时后关闭连接;或网站有 CSRF/token 检测,发现 WebSocket 来源可疑后断开。
解决: 在 Tampermonkey 脚本中加心跳(每 30 秒发送 ping),断开后自动重连;Python 服务端监听 close 事件并重建连接。
陷阱:CSP 阻止了 WebSocket 连接
现象: Tampermonkey 脚本中 new WebSocket('ws://localhost:8080') 报错 Refused to connect。
原因: 目标网站的 Content-Security-Policy 响应头限制了 connect-src,不允许连接到 localhost。
解决: 用 mitmproxy 拦截目标站响应,在 response 钩子中修改 Content-Security-Policy 头,移除或放宽 connect-src 限制。
陷阱:并发请求时响应错位
现象: 高并发时部分请求拿到了其他请求的签名结果,导致业务逻辑出错。
原因: 没有用请求 ID 做匹配,响应按到达顺序返回,并发时顺序不保证。
解决: 每个请求携带唯一 requestId(UUID),响应带同一 requestId,Python 用 asyncio.Event 或 Future 等待特定 ID 的响应。