早闻 RPC(Remote Procedure Call)远程过程调用,这一词了,应该是在安卓逆向的时候听闻的,当时吹嘘的意思是这样的,通过另一个远端服务器来调用安卓代码中的函数,并将执行后的结果返回。比如有一个加密算法,如果要实现脱机(脱离当前环境)运行的话,就需要扣除相对应的代码,补齐对应的环境(模块,上下文,语言),然而要在补齐该加密算法的环境可不好实现,而通过 RPC 则可以免除扣代码,通过数据通信来达到远程调用的目的,听起来是挺牛逼的,实际上也确实挺骚的。这里我将以浏览器与本地搭建一个 websocket 来实现调用浏览器内的函数。
算法例子
这里我所采用的是百度登录的密码加密算法,具体逆向实现就不细写了,借用视频教程志远 2021 全新 js 逆向 RPC
通过关键词password:
便可找到对应的加密地点,找到加密调用的函数所出现的位置(loginv5.js 8944 行),发现通过调用e.RSA.encrypt(s)
(其中 s 为明文 a123456
),便可得到加密后的结果。
e.RSA.encrypt(s)
'Zhge9q9jkiMA0UTfHxwNeyafnuUG8rcAh/gKfQpZiOQq8EYI/tJO83lKr52c4Im3cew3wVcINf2jEGEqH5EimnMI3g6eOjcdqduGyqynA4JjMJ0wltGdL8VUTTJsknsHUQlJXHOm/7zqx4NaBvOzhWzdDBk5cAOJ2DXgPaqoygg='
按照往常的做法,需要将e.RSA.encrypt(s)
所用的代码处单独抠出来,放在 V8 引擎上测试或使用现有的加密库 如 CryptoJS,找到对应的密钥来进行加密。不过这里使用 RPC 来实现该算法的调用。
实现
目前调用的环境有了(浏览器环境),只要我们这个浏览器不停止(使用无头浏览器运行),控制台便能一直输出我们想要的加密后结果。所以要实现的目的很简单,就是其他窗口(指其他语言所实现的程序),能远程调用e.RSA.encrypt(s)
并将结果输出到其他窗口。
那么就需要建立通信协议了,这里我所采用的是浏览器自带的 Websocket 客户端与 Nodejs 搭建的 Websocket 服务端来进行通信,众所周知 HTTP 请求是无法双向传输的。所以使用 websocket 这样服务端就可以主动向浏览器发送请求,同时 websocket 在当前这个环境下好实现。
Nodejs 实现 Websocket 服务端
安装 ws 模块
npm install ws -S
npm install @types/ws -D
这里之所以选 ws,是因为 ws 对于 Websocket 协议而已,实现方便,且速度最快,并且浏览器可以通过let ws = new Websocket()
来创建客户端直接连接,而使用 socket.io 的话,浏览器则需要载入 socket.io 客户端文件,繁琐。
代码例子
import WebSocket, { WebSocketServer } from 'ws'
let ws = new WebSocketServer({
port: 8080,
})
ws.on('connection', socket => {
function message(msg) {
console.log('接受到的msg: ' + msg)
socket.send('我接受到你的数据: ' + msg)
}
socket.on('message', message)
})
使用 WebSocket 在线测试网站websocket 在线测试 (websocket-test.com)
测试结果如下
上面代码写的很简陋,尤其是数据交互的地方,这里可以使用 json 来改进一下。像这样,至于为啥用 try 是防止 json 数据不对导致解析错误(具体代码就不解读了)
import WebSocket, { WebSocketServer } from 'ws'
let ws = new WebSocketServer({ port: 8080 })
ws.on('connection', socket => {
console.log('有人连接了')
function message(data) {
try {
let json = JSON.parse(data) // data: {"type":"callbackPasswordEnc","value":"a123456"}
let { type, value } = json
switch (type) {
case 'callbackPasswordEnc':
// doSomething()
console.log('得到的加密密文为:' + value)
break
}
} catch (error) {
console.error(error)
}
}
socket.on('message', message)
// 浏览器通信1秒后向浏览器调用加密算法
setTimeout(() => {
let jsonStr = JSON.stringify({
type: 'getPasswordEnc',
value: 'a123456',
})
socket.send(jsonStr)
}, 1000)
})
浏览器实现 websocket
既然要实现我们的代码,那么就需要将我们的代码注入到原来的代码上,这里我使用的是 Chrome 的开发者工具中的覆盖功能,选择一个本地文件夹,并允许权限。
选择要替换代码的文件,选择保存以备替换(前提得开启覆盖)
接着在覆盖中找到文件,找到加密的代码块,添加如下代码
!(function () {
let url = 'ws://127.0.0.1:8080'
let ws = new WebSocket(url)
// 浏览器连接后告诉服务端是浏览器
ws.onopen = function (event) {
ws.send(JSON.stringify({ type: 'isBrowser', value: true }))
}
ws.onmessage = function (event) {
let json = JSON.parse(event.data)
let { type, value } = json
switch (type) {
case 'getPasswordEnc':
let passwordEnc = e.RSA.encrypt(value)
let jsonStr = JSON.stringify({
type: 'callbackPasswordEnc',
value: passwordEnc,
})
console.log(jsonStr)
ws.send(jsonStr)
break
}
}
})()
然后就是最关键的地方了,触发加密函数,并将结果返回。触发加密函数只需要向浏览器发送指定数据{"type":"getPasswordEnc","value":"a123456"}
,浏览器接受到对应的类型与数据,便调用相应的函数,并将结果{"type":"callbackPasswordEnc","value":"FM6SK3XiL5X0RF9NZi7qhIsu7Pd46mfKnn6YkWUNSGrJO+XXhiXyoG8huaqQW4BnmYuo0JVVQj28C+BK/r6NTNbLcV4gMSREB2hYU/oIYedCJsZ9sbZQ89p1aI9kVcDeRlXBhjNUxkcS9Rh+vKzyNApwpbPcAuGTCSZhKst8vVo="}
返回即可。
服务端的效果如下图
优化执行流程
实现是实现了,但是代码貌似很不优雅,甚至有点别扭。按理来说因为是浏览器作为 websocket 服务端,我们作为客户端,客户端向服务器获取数据才合理,但在这里浏览器当不了 websocket 服务端这个角色,所以只能使用如此别扭的方式来调用。像上面例子的话,如果我的程序要实现一个某度登录的话,那么我这个程序就需要搭建一个 ws 服务器来进行两者的通信,有没有好的办法又不太依赖于 ws 服务端,就像 http 那样,程序只需要发送一个请求,给定类型和数值进行加密处理后返回即可。于是我处理的思路是这样的。
思路
我的做法是将 websocket 服务端当个中转站,而浏览器的 websocket 客户端作为一个加密算法的服务,再添加一个登录算法实现的客户端简称为用户调用的,所以现在一共有三份代码(websocket 服务端,浏览器端,用户调用端)。这里我还是以 nodejs 为例。
浏览器端
浏览器 websocket 客户端的代码,在初次连接的时候,告诉 websocket 服务端是不是浏览器。并将于浏览器连接的 socket 句柄存入全局对象,以便用户获取加密参数的时候向浏览器调用。
ws.onopen = function (event) {
ws.send(JSON.stringify({ type: 'isBrowser', value: true }))
}