找回密码
 立即注册
首页 业界区 安全 js逆向实战之某多多anti_content参数加密

js逆向实战之某多多anti_content参数加密

晚能 2025-6-29 16:43:27
声明:本篇文章仅用于知识分享,不得用于其他用途

网址: aHR0cHM6Ly9waW5kdW9kdW8uY29tL2hvbWUvZ2lybGNsb3RoZXMv
前置知识

1. RPC原理

RPC(Remote Procedure Call Protocol)远程过程调用协议。一个通俗的描述是:客户端在不知道调用细节的情况下,调用存在于远程计算机上的某个对象,就像调用本地应用程序中的对象一样。
具体介绍可以参考RPC原理详解,目前我们只需要了解一下RPC的作用就可以了。
在爬虫中,我们可以在浏览器中植入一段代码,通过RPC启动一个websocket,让浏览器帮助我们完成加解密的逻辑,然后把结果返回给我们。
总共包括三部分:

  • 浏览器端:负责执行js代码,回传结果。
  • python的websocket服务器端:相当于一个中间商,既负责和浏览器进行任务交互,调用浏览器中的js,完成数据返回;又和用户进行交互,给web端提供返回数据。
  • web端:和爬虫交互。
整个流程:爬虫请求到web端,web端把指令发送给websocket服务器端,websocket服务器端发送指令给浏览器端,浏览器执行完之后,将结果通过websocket服务器端返回给web端,最终返回到爬虫。
1.png

2. RPC代码

代码中会调用websockets和asyncio库,安装命令pip install websockets和pip install asyncio
websocket服务器端
  1. # 中间商
  2. import websockets
  3. import asyncio
  4. import re
  5. browser_info = {}
  6. client_info = {}
  7. async def regist(ws, path):
  8.     # 注意正则最后了,用.*?会什么都匹配不到
  9.     obj = re.compile(r"/(?P.*?)\.ws\?name=(?P<name>.*)")
  10.     search_result = obj.search(path)
  11.     # print(search_result)
  12.     action = search_result.group("action")
  13.     name = search_result.group("name")
  14.     if action == "regist":  # 来自浏览器
  15.         browser_info[name] = ws     # 保存该连接     {"test": 和test浏览器之间的连接}
  16.         return "browser", name
  17.     elif action == "invoke":    # 来自客户端
  18.         client_info[name] = ws      # 保存该连接     {"test": 和test客户端之间的连接}
  19.         return "client", name
  20. # ws表示服务器与客户端的连接
  21. # path表示请求过来的路径
  22. async def handle(ws, path):
  23.     # 建立链接的时候走这里
  24.     # regist.ws?name=test    =>  浏览器
  25.     # invoke.ws?name=test    =>  python客户端
  26.     t, name = await regist(ws, path)
  27.     print(t, name)
  28.     async for msg in ws:
  29.         print(msg)
  30.         if t == "browser":  # 浏览器
  31.             await client_info[name].send(msg)
  32.         elif t == "client":
  33.             await browser_info[name].send(msg)
  34. async def main():
  35.     # 启动websocket服务
  36.     async with websockets.serve(handle, "127.0.0.1", 8848) as ws:
  37.         print("已成功")
  38.         await asyncio.Future()  # 永远停在这
  39. if __name__ == '__main__':
  40.     asyncio.run(main())
复制代码
浏览器端
  1. // 浏览器逻辑:
  2. ws = new WebSocket("ws://127.0.0.1:8848/regist.ws?name=test");
  3. // 有人传输数据过来的时候自动执行的函数
  4. ws.onmessage = function (msg){
  5.     // console.log("很高兴", msg.data);
  6.     // 当接受到消息后,返回结果
  7.     ws.send("天气有点凉")
  8. };
复制代码
web端
  1. import websockets
  2. import asyncio
  3. async def main():
  4.     # python这边连接是为了什么?为了让ws调用js,完成加密
  5.     # python连接websocket服务器的逻辑
  6.     async with websockets.connect("ws://127.0.0.1:8848/invoke.ws?name=test") as ws:
  7.         await ws.send("你好,lllll")
  8.         print("链接成功了")
  9.         ret = await ws.recv()
  10.         print(ret)
  11. if __name__ == '__main__':
  12.     asyncio.run(main())
复制代码
3. 代码执行顺序


  • 首先启动websocket服务器端,看到控制台输出“已成功”。
    2.png

  • 将浏览器端的代码复制到浏览器的控制台运行。
    3.png

    可以看到websocket得到了浏览器的相应。
    4.png

  • 启动web端,看到web端和websocket端都得到了浏览器端发送的信息。
    5.png

    6.png

    通过RPC,我们就可以不用一点点的去抠代码了,只需找到加密函数或者解密函数的入口,然后在浏览器的代码里定义一个变量来接收结果即可。
固定逻辑

先看一段示例代码。
  1. return o.a.wrap(function(t) {
  2.                         for (; ; )
  3.                             switch (t.prev = t.next) {
  4.                             case 0:
  5.                                 return t.abrupt("return");
  6.                             case 3:
  7.                                 return t.t0 = "".concat(s, "&anti_content="),
  8.                                 t.next = 10,
  9.                                 Object(x.a)();
  10.                             case 10:
  11.                                 t.t1 = t.sent,
  12.                                 s = t.t0.concat.call(t.t0, t.t1);
复制代码
看到上面格式的代码就得第一反应是一个异步逻辑,这是为了要适配所有的浏览器才进行的改编。普遍的代码应该如下
  1. async def function(){
  2.         await xxxxxx
  3. }
复制代码
现在来解释改编后的代码。如果进了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加密


  • 访问网址,需要关注的数据包如下图所示。
    7.png

    需要知道加密的参数是anti_content.
    8.png

  • 全局搜索anti_content,只有一处,非常好定位。
    9.png

  • 打断点,刷新(ctrl+shift+r)页面,看逻辑。t.t0值为s拼接&anti_content=,s的值如下图,就跟数据包的url一致。
    10.png

  • 现在只要关注t.t0后面拼接了什么东西,这就是我们想要anti_content的值。往下看几行,就可以看到关键代码。
    11.png

    t.t1的值跟预想中的一致。
    12.png

  • 这里明显是一个固定逻辑代码,按照前置知识点的讲解,只需要知道Object(x.a)()的逻辑就能真相大白,定位x.a。
    13.png

  • y.apply(this, arguments),直接去找y,就在它下方。
    14.png

    如果没定位到,就多刷新界面。

  • 又看到了熟悉的固定逻辑代码,打断点,看触发哪个case。
    15.png

  • 不管r有没有值,都会走到case 3。case 5里e.sent的值是r.messagePackSync()的结果。所以在case 3里打断点,重点关注r.messagePackSync();。
    16.png

  • 看下messagePackSync函数。_("0x7f", "!9fm")="prototype",_("0x37", "^yZA")="messagePackSync",相当于在ut的原型链上添加messagePackSync函数。
    17.png

    18.png

  • r.messagePackSync()里不需要任何参数,它自己就能得到结果,非常适合使用rpc,但首先需要找到r对象是哪里定义的。这里直接搜索r肯定会得到很多结果,不能这样搜。想一下平常的写法。
    1. var r = new xx();
    2. r.prototype.messagePackSync = messagePackSync();
    复制代码
    搜索r = new,总共7处,候选项只有3处。
    19.png

    20.png

    21.png

    暂时不能确定,在这三处都打上断点,刷新界面,确定r的定义如下。
    22.png

  • 又看到非常熟悉的固定逻辑代码了,关键代码下面三行。
    t = e.sent,r = new s({serverTime: t}),return l = !0,e.next = 4,_();,
    r的创建需要serverTime参数,serverTime由e.sent赋值,e.sent为_()运行得到的值,故关键为_()函数,定位一下。
    23.png

    继续找m。
    24.png

  • 可以看到Object(o.a)("/api/server/_stm", "get", {}, "https://apiv2.pinduoduo.com");这行代码,结合_stm流量包的响应数据就是serverTime,逻辑就理清了。
    25.png

  • 已经找到了入口处,只要让浏览器把r对象创建出来之后,我们自己定义一个window对象即可。
    26.png

    只要调用window.pinduoduo.messagePackSync()就可以得到想要的内容了。
    27.png

    这里还有个小彩蛋,查看window.pinduoduo里包含哪些方法。
    28.png

    messagePack和messagePackSync两个方法的功能是一致的,只是一个为异步。
    29.png

    在注入的时候使用messagePack方法更为方便。注入代码如下:
    1. (function () {
    2.         // 浏览器逻辑:
    3.         let ws = new WebSocket("ws://127.0.0.1:8848/regist.ws?name=pinduoduo");
    4.         // 有人传输数据过来的时候自动执行的函数
    5.         ws.onmessage = function (msg) {
    6.                 // console.log("很高兴", msg.data);
    7.                 let ret = window.pinduoduo.messagePack();
    8.                 console.log("计算完毕,结果是", ret)
    9.                 // 当接受到消息后,返回结果
    10.                 ws.send(ret)
    11.         };
    12. })();
    复制代码
  • 将注入代码输入控制台,回车执行,却发现报错了。
    30.png

    提示违反了内容安全策略指令,再回去看流量包的响应头,有一个Content-Security-Policy-Report-Only头,就是它导致的。
    31.png

  • 想要解决这个问题,需要利用代理工具在响应头中把Content-Security-Policy-Report-Only字段给删了。这里选用charles工具。


  • Tools->Rewrite,勾选Enable Rewrite
    32.png

    33.png

  • 点击Add,配置一个规则。
    34.png

    35.png

    点击ok,会配置好对哪个域名执行操作。
    36.png

    再点击下面的Add,配置规则。
    37.png

    38.png

    点击ok,配置完成。
    39.png


  • 再次刷新界面,可以看到响应头中的Content-Security-Policy-Report-Only字段没了。
    40.png

  • 从13步开始重新创建r,设置一个变量接收,注入代码。启动websocketserver和webserver端的代码,在网页上访问127.0.0.1:8000/get?project_name=pinduoduo就能拿到anti_content的值了。(这里的端口号需要根据webserver的代码启动在哪个端口,project_name也要上下对应)


  • websocketserver.py
    1. # 中间商
    2. import websockets
    3. import asyncio
    4. import re
    5. browser_info = {}
    6. client_info = {}
    7. async def regist(ws, path):
    8.         # 注意正则最后了,用.*?会什么都匹配不到
    9.         obj = re.compile(r"/(?P.*?)\.ws\?name=(?P<name>.*)")
    10.         search_result = obj.search(path)
    11.         action = search_result.group("action")
    12.         name = search_result.group("name")
    13.         if action == "regist":  # 来自浏览器
    14.                 browser_info[name] = ws     # 保存该连接     {"iwencai": 和iwencai浏览器之间的连接}
    15.                 return "browser", name
    16.         elif action == "invoke":    # 来自客户端
    17.                 client_info[name] = ws      # 保存该连接     {"iwencai": 和iwencai客户端之间的连接}
    18.                 return "client", name
    19. # ws表示服务器与客户端的连接
    20. # path表示请求过来的路径
    21. async def handle(ws, path):
    22.         # 建立链接的时候走这里
    23.         # regist.ws?name=iwencai    =>  浏览器
    24.         # invoke.ws?name=iwencai    =>  python客户端
    25.         t, name = await regist(ws, path)
    26.         async for msg in ws:
    27.                 if t == "browser":  # 浏览器
    28.                         await client_info[name].send(msg)
    29.                 elif t == "client":
    30.                         await browser_info[name].send(msg)
    31. async def main():
    32.         # 启动websocket服务
    33.         async with websockets.serve(handle, "127.0.0.1", 8848) as ws:
    34.                 print("已成功")
    35.                 await asyncio.Future()  # 永远停在这
    36. if __name__ == '__main__':
    37.         asyncio.run(main())
    复制代码
  • webserver.py
    1. from sanic import Sanic, HTTPResponse
    2. import websockets
    3. app = Sanic(__name__)
    4. @app.route("/get")
    5. async def func(req):
    6.         # 在这里可以接受参数,指定哪个项目
    7.         project_name = req.args.get("project_name")
    8.         if project_name:
    9.                 async with websockets.connect(f"ws://127.0.0.1:8848/invoke.ws?name={project_name}") as ws:
    10.                         await ws.send("你好,lllll")
    11.                         print("链接成功了")
    12.                         ret = await ws.recv()
    13.                 return HTTPResponse(ret)
    14.         else:
    15.                 return HTTPResponse("至少给我一个项目名称")
    16. if __name__ == '__main__':
    17.         app.run()
    复制代码
    41.png


来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
您需要登录后才可以回帖 登录 | 立即注册