前言:
因为最近工作需要爬取APP应用
的信息,考虑到目前市场上比较成熟的应用市场整合网站,因此选择了七麦
来下手,也由此发现了七麦
的反爬策略,所以这次我们来分析一下七麦
网站的接口的参数的由来。
开始:
我们首先来看看七麦
的接口,如下图所示:
我们可以看到这是正常情况下的请求,看到了一个很有趣的参数。
参数构成是这样的:
analysis: IRIdEVEIChkIDF1USWkOFkofB0YADVNoWVQYCHZCBgBQAgRaAlNRAFQiGgA=
打眼一看,这个加密的值有点像Base64
加密之后的效果,并且我们间隔一段时间使用这个相同的值我们会发现,返回的响应是如下这样:
{'code': 10602, 'msg': 'Access Error'}
因此,我们可以断定这个应该是加了时间部分的Salt
,我们直接去寻找相关的加密方法。
我们一般会通过每个请求后面的Initiator
部分来跳转,如下图所示:
不过这样的意义不是很大,因为我们直接进入了一个产生这个请求的Js
部分,让人看的一脸蒙,所以我这里推荐使用XHR Breakpoints
打断点去拦截请求,这样我们就可以看到一个完整的调用栈CallBack Stack
,此处填入 URL 包含的关键词 indexPlus
。
增加了断点之后我们重新刷新页面,此时会卡在Debug
的位置,如图:
我们可以通过右边的
Watch
机制查看到这里的h
是一个XHR
对象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
,如图:
我们全局搜索7GwW
这个模块,发现它只存在一个Js
文件当中,我们接着在这个Js
文件当中寻找7GwW
,发现它是被KCLY
这个模块所引用,同理,继续全局找,如图:
我们可以发现,有三个模块引用了它,没事,我们一个个分析:
我們先分析XmWM
,這個模塊是有tIFN
引入的,如圖:
接着我们再顺着tIFN
,接着找,找到了mtWM
模块,然后继续引入,最终找到了gXmS
,如图所示:
我们可以看到了在这个模块请求被打包,封装。
至此,我们费劲脑子终于找到了封装请求的模块,不过倒是很费时,但这只是为了让人理解模块化的代码的含义,真正我们在分析一个请求的时候,我们是可以使用一个更简单的方法,
Callback Stack
调用栈,我们可以分析出,这个请求是发送的
get
请求,那我们就可以认为get
这个部分是调用的模块,如图: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)
}),
我们加上断点来试试,如图:
分析 分析
其实我们发现整个加密过程无非是两个加密函数比较重要,l.d
和l.h
,我们看看这两个函数的方法,如图:
接下来就没有什么难度了,就是自定义一些加密算法,可以打断点看出来,比如如图:
i的值
base64加密的l.d方法
至此,一个完整的分析就是这样出来,我们可以看到我们整个的分析流程就是根据每个包追溯上层包一个个追溯过来的,恶心的就是代码被混淆让人看的烦,不过其实掌握好规律之后就会发现原理还是很容易的。
话不多说,上代码
我们按照组装的步骤:
- 设置一个时间差变量
- 提取查询参数值(除了 analysis)
- 排序拼接参数值字符串并 Base64 编码
- 拼接自定义字符串
- 自定义加密后再 Base64 编码
- 拼接 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()
网友评论