js压缩混淆工具_反爬解决方案:七麦请求加密参数以及压缩混淆Js的逆向分析

b95bdd7e8a77e711b92f2bee756732fd.png

前言:

因为最近工作需要爬取APP应用的信息,考虑到目前市场上比较成熟的应用市场整合网站,因此选择了七麦来下手,也由此发现了七麦的反爬策略,所以这次我们来分析一下七麦网站的接口的参数的由来。

开始:

我们首先来看看七麦的接口,如下图所示:

fe289a5fa98680664ae6a13e51660325.png


正常请求

e4bcd4c7322e8af6e68f685878b56567.png


正常响应

我们可以看到这是正常情况下的请求,看到了一个很有趣的参数。
参数构成是这样的:

analysis: IRIdEVEIChkIDF1USWkOFkofB0YADVNoWVQYCHZCBgBQAgRaAlNRAFQiGgA=

打眼一看,这个加密的值有点像Base64加密之后的效果,并且我们间隔一段时间使用这个相同的值我们会发现,返回的响应是如下这样:

{'code': 10602, 'msg': 'Access Error'}

因此,我们可以断定这个应该是加了时间部分的Salt,我们直接去寻找相关的加密方法。
我们一般会通过每个请求后面的Initiator部分来跳转,如下图所示:

ba37df76d67b0789e9b120979cba75f7.png


Initiator部分

09adf86879f1239b8f3a040e48b0e312.png


跳转之后

不过这样的意义不是很大,因为我们直接进入了一个产生这个请求的Js部分,让人看的一脸蒙,所以我这里推荐使用XHR Breakpoints打断点去拦截请求,这样我们就可以看到一个完整的调用栈CallBack Stack,此处填入 URL 包含的关键词 indexPlus
增加了断点之后我们重新刷新页面,此时会卡在Debug的位置,如图:

879cdeb28b51d04b648ce3d80317bcde.png


XHR断点位置

我们可以通过右边的Watch机制查看到这里的h是一个XHR对象

1b93fa51dfa2a61960f48487fb202690.png


watch h对象

我们大致翻阅这段代码,发现这里面的代码大致上都是很混乱的名字,猜想应该是经过代码混淆了,我们来观察下面这段代码,也就是我们断点位置的下面几行。

"7O1s": function(t, e, n) {
    var r = n("DIVP")
        , i = n("XSOZ")
        , o = n("kkCw")("species");
    t.exports = function(t, e) {
        var n, a = r(t).constructor;
        return void 0 === a || void 0 == (n = r(a)[o]) ? e : i(n)
    }
},
"7UMu": function(t, e, n) {
    var r = n("R9M2");
    t.exports = Array.isArray || function(t) {
        return "Array" == r(t)
    }
},
"7gX0": function(t, e) {
    var n = t.exports = {
        version: "2.5.5"
    };
    "number" == typeof __e && (__e = n)
},

虽然这段代码经过了一定程度的混淆,但是我们还是大致能看出来一点规律,比如类似701s的随机字符应该是某个方法的名称,而 var r = n("DIVP") 即引入模块,正常的写法可能是import a from 'b'或者 const a = require('b')
这里发起 Ajax 请求的函数很可能只是一个被封装了的模块供整个项目调用,粗略看一下函数代码也没有发现计算加密的部分。针对这种模块化开发,一个逆向的思路是,只要查看该模块被引用的情况,不断向上追溯,总能找到最初发起请求和加密的函数。
PS: 插一嘴,在如今前端开发也是大部分基于一些成熟的框架进行模块化的开发,并有一整套完整的打包发布、压缩混淆工具,这同时意味着他们的请求一般都会封装起来,因此我们在逆向的时候只有不断前溯,就能够发现模块的根源。

我们在这里检索断点所在的模块名 7GwW,如图:

2fd40b09a589a6894919b8402c002324.png


追溯7GwW

我们全局搜索7GwW这个模块,发现它只存在一个Js文件当中,我们接着在这个Js文件当中寻找7GwW,发现它是被KCLY这个模块所引用,同理,继续全局找,如图:

f43b9853a05219b007a1eea8c7803d3c.png


寻找KCLY

我们可以发现,有三个模块引用了它,没事,我们一个个分析:

我們先分析XmWM,這個模塊是有tIFN引入的,如圖:

4ffae2d525a760359f05123b2c384f63.png


尋找XmWM

接着我们再顺着tIFN,接着找,找到了mtWM模块,然后继续引入,最终找到了gXmS,如图所示:

6d36baa0bc3cb9be35b2b46581bcdeda.png


gXmS

我们可以看到了在这个模块请求被打包,封装。

至此,我们费劲脑子终于找到了封装请求的模块,不过倒是很费时,但这只是为了让人理解模块化的代码的含义,真正我们在分析一个请求的时候,我们是可以使用一个更简单的方法,Callback Stack调用栈,

我们可以分析出,这个请求是发送的get请求,那我们就可以认为get这个部分是调用的模块,如图:

2d3b1691dd65b7defca16fd91e3d851c.png


get

分析的方法其实和之前的都是差不多的,我们看Callback Stack调用栈每个调用方法的细节就能找到。

我们可以深挖这个加密的流程,也就是整个请求组装的过程,如图:

d.a.interceptors.request.use(function(a) {
            try {
                if (void 0 == g.difftime && !v) {
                    var e = Object(l.f)("synct");
                    g.difftime = -Object(l.f)("syncd") || +new Date - 1e3 * e
                }
                var n = Object(l.h)(Object(l.a)("ElhBGlwHD1c="));
                n = n.split("").reverse().join("");
                var t = +new Date - (g.difftime ? g.difftime : 0) - 1515125653845
                  , r = ""
                  , o = [];
                return void 0 === a.params && (a.params = {}),
                p()(a.params).forEach(function(e) {
                    if (e == n)
                        return !1;
                    a.params.hasOwnProperty(e) && o.push(a.params[e])
                }),
                o = o.sort().join(""),
                o = Object(l.d)(o),
                o += "@#" + a.url.replace(a.baseURL, ""),
                o += "@#" + t,
                o += "@#1",
                r = Object(l.d)(Object(l.h)(o)),
                -1 == a.url.indexOf(n) && (a.url += (-1 != a.url.indexOf("?") ? "&" : "?") + n + "=" + encodeURIComponent(r)),
                a
            } catch (a) {}
        }, function(a) {
            return m.a.reject(a)
        }),

我们加上断点来试试,如图:

30137c1f33b547fc0da3761a03fd8108.png


分析

e7353950f9e06c4874d94687c2259d4f.png


分析

其实我们发现整个加密过程无非是两个加密函数比较重要,l.dl.h,我们看看这两个函数的方法,如图:

2ba1a6d98ec8f196e663da858c41ce0c.png


l.d

25d9358a13d5307e1a13cdb4adc4b180.png


l.h

接下来就没有什么难度了,就是自定义一些加密算法,可以打断点看出来,比如如图:

1afad32baed1bc25dbd70c5317ccd0c9.png


i的值

987197973221d4241e1dc621a5b53516.png


base64加密的l.d方法

至此,一个完整的分析就是这样出来,我们可以看到我们整个的分析流程就是根据每个包追溯上层包一个个追溯过来的,恶心的就是代码被混淆让人看的烦,不过其实掌握好规律之后就会发现原理还是很容易的。

话不多说,上代码

我们按照组装的步骤:

  1. 设置一个时间差变量
  2. 提取查询参数值(除了 analysis)
  3. 排序拼接参数值字符串并 Base64 编码
  4. 拼接自定义字符串
  5. 自定义加密后再 Base64 编码
  6. 拼接 URL
# -*- coding: utf-8 -*-

'''
------------------------------------------------------------
File Name: qimai.py
Description : 
Project: test
Last Modified: Friday, 25th January 2019 8:55:39 am
-------------------------------------------------------------
'''


import time
from urllib.parse import urlencode
import json
import base64

import requests


headers = {
    "Accept": "application/json, text/plain, */*",
    "Referer": "https://www.qimai.cn/rank",
    "User-Agent": "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:57.0) Gecko/20100101 Firefox/59.0"
}

params = {
    "brand": "all",
    "country": "cn",
    "date": "2019-01-20",
    "device": "iphone",
    "genre": "36",
    "page": 1
}


# 自定义加密函数
def encrypt(
    a: str,
    n="a12c0fa6ab9119bc90e4ac7700796a53"
) -> str:
    s, n = list(a), list(n)
    sl, nl = len(s), len(n)
    for i in range(0, sl):
        s[i] = chr(ord(s[i]) ^ ord(n[i % nl]))
    return "".join(s)


def main() -> None:
    # iPhone 免费榜单

    # 步骤一:时间差
    t = str(int((time.time() * 1000 - 1515125653845)))
    # 步骤二:提取查询参数值并排序
    s = "".join(sorted([str(v) for v in params.values()]))
    # 步骤三:Base64 Encode
    s = base64.b64encode(bytes(s, encoding="ascii"))
    # 步骤四:拼接自定义字符串
    s = "@#".join([s.decode(), "/rank/indexPlus/brand_id/1", t, "1"])
    # 步骤五:自定义加密 & Base64 Encode
    s = base64.b64encode(bytes(encrypt(s), encoding="ascii"))
    # 步骤六:拼接 URL
    params["analysis"] = s.decode()
    url = "https://api.qimai.cn/rank/indexPlus/brand_id/1?{}".format(
        urlencode(params))
    # 测试:发起请求
    res = requests.get(url, headers=headers)
    rsp = json.loads(res.text)
    print(rsp)


if __name__ == '__main__':
    main()

PS: 大家感兴趣的可以来我的github看源码,希望大家可以Star一波哦哦!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值