前言:
因?yàn)樽罱ぷ餍枰廊?code>APP應(yīng)用的信息躺坟,考慮到目前市場上比較成熟的應(yīng)用市場整合網(wǎng)站,因此選擇了七麥
來下手和二,也由此發(fā)現(xiàn)了七麥
的反爬策略趾牧,所以這次我們來分析一下七麥
網(wǎng)站的接口的參數(shù)的由來。
開始:
我們首先來看看七麥
的接口傀广,如下圖所示:
我們可以看到這是正常情況下的請求颁独,看到了一個很有趣的參數(shù)。
參數(shù)構(gòu)成是這樣的:
analysis: IRIdEVEIChkIDF1USWkOFkofB0YADVNoWVQYCHZCBgBQAgRaAlNRAFQiGgA=
打眼一看伪冰,這個加密的值有點(diǎn)像Base64
加密之后的效果誓酒,并且我們間隔一段時間使用這個相同的值我們會發(fā)現(xiàn),返回的響應(yīng)是如下這樣:
{'code': 10602, 'msg': 'Access Error'}
因此贮聂,我們可以斷定這個應(yīng)該是加了時間部分的Salt
靠柑,我們直接去尋找相關(guān)的加密方法。
我們一般會通過每個請求后面的Initiator
部分來跳轉(zhuǎn)吓懈,如下圖所示:
不過這樣的意義不是很大歼冰,因?yàn)槲覀冎苯舆M(jìn)入了一個產(chǎn)生這個請求的Js
部分,讓人看的一臉蒙耻警,所以我這里推薦使用XHR Breakpoints
打斷點(diǎn)去攔截請求隔嫡,這樣我們就可以看到一個完整的調(diào)用棧CallBack Stack
,此處填入 URL 包含的關(guān)鍵詞 indexPlus
甘穿。
增加了斷點(diǎn)之后我們重新刷新頁面腮恩,此時會卡在Debug
的位置,如圖:
我們可以通過右邊的
Watch
機(jī)制查看到這里的h
是一個XHR
對象我們大致翻閱這段代碼温兼,發(fā)現(xiàn)這里面的代碼大致上都是很混亂的名字秸滴,猜想應(yīng)該是經(jīng)過代碼混淆了,我們來觀察下面這段代碼募判,也就是我們斷點(diǎn)位置的下面幾行荡含。
"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)
},
雖然這段代碼經(jīng)過了一定程度的混淆,但是我們還是大致能看出來一點(diǎn)規(guī)律届垫,比如類似701s
的隨機(jī)字符應(yīng)該是某個方法的名稱释液,而 var r = n("DIVP")
即引入模塊,正常的寫法可能是import a from 'b'
或者 const a = require('b')
敦腔。
這里發(fā)起 Ajax 請求的函數(shù)很可能只是一個被封裝了的模塊供整個項(xiàng)目調(diào)用均澳,粗略看一下函數(shù)代碼也沒有發(fā)現(xiàn)計算加密的部分。針對這種模塊化開發(fā)符衔,一個逆向的思路是找前,只要查看該模塊被引用的情況,不斷向上追溯判族,總能找到最初發(fā)起請求和加密的函數(shù)躺盛。
PS: 插一嘴,在如今前端開發(fā)也是大部分基于一些成熟的框架進(jìn)行模塊化的開發(fā)形帮,并有一整套完整的打包發(fā)布槽惫、壓縮混淆工具,這同時意味著他們的請求一般都會封裝起來辩撑,因此我們在逆向的時候只有不斷前溯界斜,就能夠發(fā)現(xiàn)模塊的根源。
我們在這里檢索斷點(diǎn)所在的模塊名 7GwW
合冀,如圖:
我們?nèi)炙阉?code>7GwW這個模塊各薇,發(fā)現(xiàn)它只存在一個Js
文件當(dāng)中,我們接著在這個Js
文件當(dāng)中尋找7GwW
君躺,發(fā)現(xiàn)它是被KCLY
這個模塊所引用峭判,同理,繼續(xù)全局找棕叫,如圖:
我們可以發(fā)現(xiàn)林螃,有三個模塊引用了它,沒事俺泣,我們一個個分析:
我們先分析XmWM
疗认,這個模塊是有tIFN
引入的,如圖:
接著我們再順著tIFN
伏钠,接著找侮邀,找到了mtWM
模塊,然后繼續(xù)引入贝润,最終找到了gXmS
绊茧,如圖所示:
我們可以看到了在這個模塊請求被打包,封裝打掘。
至此华畏,我們費(fèi)勁腦子終于找到了封裝請求的模塊,不過倒是很費(fèi)時尊蚁,但這只是為了讓人理解模塊化的代碼的含義亡笑,真正我們在分析一個請求的時候,我們是可以使用一個更簡單的方法横朋,
Callback Stack
調(diào)用棧仑乌,我們可以分析出,這個請求是發(fā)送的
get
請求,那我們就可以認(rèn)為get
這個部分是調(diào)用的模塊晰甚,如圖:分析的方法其實(shí)和之前的都是差不多的衙传,我們看Callback Stack
調(diào)用棧每個調(diào)用方法的細(xì)節(jié)就能找到。
我們可以深挖這個加密的流程厕九,也就是整個請求組裝的過程蓖捶,如圖:
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)
}),
我們加上斷點(diǎn)來試試,如圖:
其實(shí)我們發(fā)現(xiàn)整個加密過程無非是兩個加密函數(shù)比較重要扁远,l.d
和l.h
俊鱼,我們看看這兩個函數(shù)的方法,如圖:
接下來就沒有什么難度了畅买,就是自定義一些加密算法并闲,可以打斷點(diǎn)看出來尖淘,比如如圖:
至此齿税,一個完整的分析就是這樣出來,我們可以看到我們整個的分析流程就是根據(jù)每個包追溯上層包一個個追溯過來的话速,惡心的就是代碼被混淆讓人看的煩洒宝,不過其實(shí)掌握好規(guī)律之后就會發(fā)現(xiàn)原理還是很容易的购公。
話不多說,上代碼
我們按照組裝的步驟:
- 設(shè)置一個時間差變量
- 提取查詢參數(shù)值(除了 analysis)
- 排序拼接參數(shù)值字符串并 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
}
# 自定義加密函數(shù)
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 免費(fèi)榜單
# 步驟一:時間差
t = str(int((time.time() * 1000 - 1515125653845)))
# 步驟二:提取查詢參數(shù)值并排序
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))
# 測試:發(fā)起請求
res = requests.get(url, headers=headers)
rsp = json.loads(res.text)
print(rsp)
if __name__ == '__main__':
main()