js逆向实战之某多多anti_content参数加密
声明:本篇文章仅用于知识分享,不得用于其他用途
网址: aHR0cHM6Ly9waW5kdW9kdW8uY29tL2hvbWUvZ2lybGNsb3RoZXMv
前置知识
1. RPC原理
RPC(Remote Procedure Call Protocol)远程过程调用协议。一个通俗的描述是:客户端在不知道调用细节的情况下,调用存在于远程计算机上的某个对象,就像调用本地应用程序中的对象一样。
具体介绍可以参考RPC原理详解,目前我们只需要了解一下RPC的作用就可以了。
在爬虫中,我们可以在浏览器中植入一段代码,通过RPC启动一个websocket,让浏览器帮助我们完成加解密的逻辑,然后把结果返回给我们。
总共包括三部分:
- 浏览器端:负责执行js代码,回传结果。
- python的websocket服务器端:相当于一个中间商,既负责和浏览器进行任务交互,调用浏览器中的js,完成数据返回;又和用户进行交互,给web端提供返回数据。
- web端:和爬虫交互。
整个流程:爬虫请求到web端,web端把指令发送给websocket服务器端,websocket服务器端发送指令给浏览器端,浏览器执行完之后,将结果通过websocket服务器端返回给web端,最终返回到爬虫。
2. RPC代码
代码中会调用websockets
和asyncio
库,安装命令pip install websockets
和pip install asyncio
websocket服务器端
# 中间商
import websockets
import asyncio
import re
browser_info = {}
client_info = {}
async def regist(ws, path):
# 注意正则最后了,用.*?会什么都匹配不到
obj = re.compile(r"/(?P<action>.*?)\.ws\?name=(?P<name>.*)")
search_result = obj.search(path)
# print(search_result)
action = search_result.group("action")
name = search_result.group("name")
if action == "regist": # 来自浏览器
browser_info[name] = ws # 保存该连接 {"test": 和test浏览器之间的连接}
return "browser", name
elif action == "invoke": # 来自客户端
client_info[name] = ws # 保存该连接 {"test": 和test客户端之间的连接}
return "client", name
# ws表示服务器与客户端的连接
# path表示请求过来的路径
async def handle(ws, path):
# 建立链接的时候走这里
# regist.ws?name=test => 浏览器
# invoke.ws?name=test => python客户端
t, name = await regist(ws, path)
print(t, name)
async for msg in ws:
print(msg)
if t == "browser": # 浏览器
await client_info[name].send(msg)
elif t == "client":
await browser_info[name].send(msg)
async def main():
# 启动websocket服务
async with websockets.serve(handle, "127.0.0.1", 8848) as ws:
print("已成功")
await asyncio.Future() # 永远停在这
if __name__ == '__main__':
asyncio.run(main())
浏览器端
// 浏览器逻辑:
ws = new WebSocket("ws://127.0.0.1:8848/regist.ws?name=test");
// 有人传输数据过来的时候自动执行的函数
ws.onmessage = function (msg){
// console.log("很高兴", msg.data);
// 当接受到消息后,返回结果
ws.send("天气有点凉")
};
web端
import websockets
import asyncio
async def main():
# python这边连接是为了什么?为了让ws调用js,完成加密
# python连接websocket服务器的逻辑
async with websockets.connect("ws://127.0.0.1:8848/invoke.ws?name=test") as ws:
await ws.send("你好,lllll")
print("链接成功了")
ret = await ws.recv()
print(ret)
if __name__ == '__main__':
asyncio.run(main())
3. 代码执行顺序
-
首先启动websocket服务器端,看到控制台输出“已成功”。
-
将浏览器端的代码复制到浏览器的控制台运行。
可以看到websocket得到了浏览器的相应。
-
启动web端,看到web端和websocket端都得到了浏览器端发送的信息。
通过RPC,我们就可以不用一点点的去抠代码了,只需找到加密函数或者解密函数的入口,然后在浏览器的代码里定义一个变量来接收结果即可。
固定逻辑
先看一段示例代码。
return o.a.wrap(function(t) {
for (; ; )
switch (t.prev = t.next) {
case 0:
return t.abrupt("return");
case 3:
return t.t0 = "".concat(s, "&anti_content="),
t.next = 10,
Object(x.a)();
case 10:
t.t1 = t.sent,
s = t.t0.concat.call(t.t0, t.t1);
看到上面格式的代码就得第一反应是一个异步逻辑,这是为了要适配所有的浏览器才进行的改编。普遍的代码应该如下
async def function(){
await xxxxxx
}
现在来解释改编后的代码。如果进了case 0
,会执行t.abrupt("return")
,这是真正的退出代码;如果进了case 3
,会执行t.t0 = "".concat(s, "&anti_content="),t.next = 10,Object(x.a)();
,这行代码最终返回的结果是Object(x.a)()
;如果进了case 10
,会执行t.t1=t.sent
,重点来了t.sent的值其实是Object(x.a)()执行完的结果,本质上就是t.t1=t.sent=Object(x.a)()
。所以这里最重要的代码其实是Object(x.a)()
。
某多多anti_content加密
-
访问网址,需要关注的数据包如下图所示。
需要知道加密的参数是anti_content
.
-
全局搜索
anti_content
,只有一处,非常好定位。
-
打断点,刷新(ctrl+shift+r)页面,看逻辑。
t.t0
值为s
拼接&anti_content=
,s
的值如下图,就跟数据包的url一致。
-
现在只要关注
t.t0
后面拼接了什么东西,这就是我们想要anti_content
的值。往下看几行,就可以看到关键代码。
t.t1
的值跟预想中的一致。
-
这里明显是一个固定逻辑代码,按照前置知识点的讲解,只需要知道
Object(x.a)()
的逻辑就能真相大白,定位x.a
。
-
y.apply(this, arguments)
,直接去找y
,就在它下方。
如果没定位到,就多刷新界面。
-
又看到了熟悉的固定逻辑代码,打断点,看触发哪个case。
-
不管
r
有没有值,都会走到case 3
。case 5
里e.sent
的值是r.messagePackSync()
的结果。所以在case 3
里打断点,重点关注r.messagePackSync();
。
-
看下
messagePackSync
函数。_("0x7f", "!9fm")="prototype"
,_("0x37", "^yZA")="messagePackSync"
,相当于在ut
的原型链上添加messagePackSync
函数。
-
r.messagePackSync()
里不需要任何参数,它自己就能得到结果,非常适合使用rpc,但首先需要找到r
对象是哪里定义的。这里直接搜索r
肯定会得到很多结果,不能这样搜。想一下平常的写法。var r = new xx(); r.prototype.messagePackSync = messagePackSync();
搜索
r = new
,总共7处,候选项只有3处。
暂时不能确定,在这三处都打上断点,刷新界面,确定r
的定义如下。
-
又看到非常熟悉的固定逻辑代码了,关键代码下面三行。
t = e.sent
,r = new s({serverTime: t})
,return l = !0,e.next = 4,_();
,
r
的创建需要serverTime
参数,serverTime
由e.sent
赋值,e.sent
为_()
运行得到的值,故关键为_()
函数,定位一下。
继续找m
。
-
可以看到
Object(o.a)("/api/server/_stm", "get", {}, "https://apiv2.pinduoduo.com");
这行代码,结合_stm
流量包的响应数据就是serverTime
,逻辑就理清了。
-
已经找到了入口处,只要让浏览器把
r
对象创建出来之后,我们自己定义一个window
对象即可。
只要调用window.pinduoduo.messagePackSync()
就可以得到想要的内容了。
这里还有个小彩蛋,查看window.pinduoduo
里包含哪些方法。
messagePack
和messagePackSync
两个方法的功能是一致的,只是一个为异步。
在注入的时候使用messagePack
方法更为方便。注入代码如下:(function () { // 浏览器逻辑: let ws = new WebSocket("ws://127.0.0.1:8848/regist.ws?name=pinduoduo"); // 有人传输数据过来的时候自动执行的函数 ws.onmessage = function (msg) { // console.log("很高兴", msg.data); let ret = window.pinduoduo.messagePack(); console.log("计算完毕,结果是", ret) // 当接受到消息后,返回结果 ws.send(ret) }; })();
-
将注入代码输入控制台,回车执行,却发现报错了。
提示违反了内容安全策略指令,再回去看流量包的响应头,有一个Content-Security-Policy-Report-Only
头,就是它导致的。
-
想要解决这个问题,需要利用代理工具在响应头中把
Content-Security-Policy-Report-Only
字段给删了。这里选用charles
工具。
- Tools->Rewrite,勾选
Enable Rewrite
- 点击
Add
,配置一个规则。
点击ok,会配置好对哪个域名执行操作。
再点击下面的Add
,配置规则。
点击ok,配置完成。
- 再次刷新界面,可以看到响应头中的
Content-Security-Policy-Report-Only
字段没了。
- 从13步开始重新创建
r
,设置一个变量接收,注入代码。启动websocketserver
和webserver
端的代码,在网页上访问127.0.0.1:8000/get?project_name=pinduoduo
就能拿到anti_content
的值了。(这里的端口号需要根据webserver的代码启动在哪个端口,project_name也要上下对应)
- websocketserver.py
# 中间商 import websockets import asyncio import re browser_info = {} client_info = {} async def regist(ws, path): # 注意正则最后了,用.*?会什么都匹配不到 obj = re.compile(r"/(?P<action>.*?)\.ws\?name=(?P<name>.*)") search_result = obj.search(path) action = search_result.group("action") name = search_result.group("name") if action == "regist": # 来自浏览器 browser_info[name] = ws # 保存该连接 {"iwencai": 和iwencai浏览器之间的连接} return "browser", name elif action == "invoke": # 来自客户端 client_info[name] = ws # 保存该连接 {"iwencai": 和iwencai客户端之间的连接} return "client", name # ws表示服务器与客户端的连接 # path表示请求过来的路径 async def handle(ws, path): # 建立链接的时候走这里 # regist.ws?name=iwencai => 浏览器 # invoke.ws?name=iwencai => python客户端 t, name = await regist(ws, path) async for msg in ws: if t == "browser": # 浏览器 await client_info[name].send(msg) elif t == "client": await browser_info[name].send(msg) async def main(): # 启动websocket服务 async with websockets.serve(handle, "127.0.0.1", 8848) as ws: print("已成功") await asyncio.Future() # 永远停在这 if __name__ == '__main__': asyncio.run(main())
- webserver.py
from sanic import Sanic, HTTPResponse import websockets app = Sanic(__name__) @app.route("/get") async def func(req): # 在这里可以接受参数,指定哪个项目 project_name = req.args.get("project_name") if project_name: async with websockets.connect(f"ws://127.0.0.1:8848/invoke.ws?name={project_name}") as ws: await ws.send("你好,lllll") print("链接成功了") ret = await ws.recv() return HTTPResponse(ret) else: return HTTPResponse("至少给我一个项目名称") if __name__ == '__main__': app.run()