閱讀文本大概需要 10 分鐘。
目 標(biāo) 場 景
在移動互聯(lián)網(wǎng)時代,很大一部分企業(yè)拋棄了傳統(tǒng)的網(wǎng)站,選擇將數(shù)據(jù)、服務(wù)整合到 App 端,因此 App 端無論是爬蟲還是反反爬都顯得尤為重要。
常見的 App 端的爬蟲方式是利用 Appium 和 Airtest 驅(qū)動手機(jī)打開應(yīng)用,操作頁面,然后通過元素 ID 獲取元素的內(nèi)容,又或者借助 mitmproxy 捕獲到請求的數(shù)據(jù),最后將數(shù)據(jù)保存下來。
?如果要完成復(fù)雜的操作,加快爬蟲的效率,就必須破解 App 端的登錄,獲取一些關(guān)鍵的數(shù)據(jù),直接模擬接口請求,達(dá)到快速高效地爬取數(shù)據(jù)的目的。
本篇文章的目的是帶大家「破解 App 端的登錄」這一操作。
ps:本文僅限技術(shù)交流,請勿用于其他用途。
準(zhǔn) 備 工 作
在開始編寫腳本之前,需要做好如下準(zhǔn)備工作
待破解的 APK 應(yīng)用,可去官網(wǎng)或者各大應(yīng)用市場去下載,然后安裝應(yīng)用到手機(jī)中
反編譯工具,MAC OSX 推薦 Android Crack Tool 工具集,Win OS 可以使用 dex2jar 來反編譯 APK 應(yīng)用包
源碼瀏覽工具:jadx-gui
抓包工具:Charles 或者 Fiddler
編 寫 腳 本
第 1 步,確保手機(jī)配置好代理之后,就可以利用 Charles 對「獲取****驗(yàn)證碼」和「登錄」進(jìn)行抓包操作,得到請求地址、請求參數(shù)和請求頭等數(shù)據(jù)。
第 2 步,對請求參數(shù)、請求頭中「沒有規(guī)律的數(shù)據(jù)」尋找生成的規(guī)律,并用 Python 代碼來生成這些數(shù)據(jù)。
首先,我們查看獲取驗(yàn)證碼這一請求的參數(shù),發(fā)現(xiàn)除了手機(jī)號碼外,參數(shù) t 可以很容易想到是請求的時間戳,唯獨(dú)參數(shù) token 在沒有其他網(wǎng)絡(luò)請求的情況下生成了。
def get_unix_time(type_13):
"""
獲取時間戳
:param type_13:10位、13位,是否是13位
:return:
"""
t = time.time()
if type_13:
millis = int(round(t * 1000))
else:
millis = int(t)
return millis
所以,我們大膽猜測:這個 token 是 App 端通過一定的邏輯生成的;****同理,請求頭中 token 也是由 App 端生成。
在我們多次發(fā)起獲取驗(yàn)證碼的操作之后,我們得出一個規(guī)律:參數(shù)中的 token 保持不變,與請求時間沒有關(guān)系;請求頭的 token 會隨著時間的變化的也會發(fā)生變化。
我們利用 Android Crack Tool 對 APK 應(yīng)用進(jìn)行反編譯,得到源碼 Jar 包。
然后就可以使用 jadx-gui 工具打開源碼 Jar 包,通過請求地址中的「關(guān)鍵詞:login」搜索源碼,就能找到請求發(fā)送的位置。
由于應(yīng)用源碼打包的時候混淆了代碼,因此,我們需要根據(jù)上面的搜索結(jié)果去定位參數(shù)初始化位置及實(shí)現(xiàn)邏輯。
逐步往上追溯應(yīng)用源碼,可以找到按鈕點(diǎn)擊事件的監(jiān)聽函數(shù)。
具體實(shí)現(xiàn)邏輯是把用戶輸入的手機(jī)函數(shù)傳給混淆后的函數(shù) :b()
點(diǎn)擊查看函數(shù) b() 的實(shí)現(xiàn)邏輯,會發(fā)現(xiàn)方法中對手機(jī)號碼進(jìn)行了截取,獲取當(dāng)前日期時間,進(jìn)行字符串的「第一次拼接」操作。
對第一部分的拼接我們用 Python 代碼進(jìn)行實(shí)現(xiàn)。
def __get_param_token(self, phone_num):
"""
獲取參數(shù)Token
:return: BNpK8SMDiV6jTU4DR99A9vYoN9e90yBd
"""
today = datetime.date.today()
formatted_today = today.strftime('%Y%m%d')
formatted_day = today.strftime('%m%d')
# 參數(shù)1 手機(jī)號碼|完整日期6位
arg1 = phone_num + "|" + formatted_today
# 手機(jī)號碼后4位+日期包含月、日
# 參數(shù)2 64230704
# 字符串轉(zhuǎn)為bytes
arg2 = bytes(phone_num[7:] + formatted_day, encoding="utf8")
第一次拼接完成之后,我們發(fā)現(xiàn)又調(diào)用了一個函數(shù) a(),參數(shù)為上面拼接生成的兩個變量。
函數(shù) a() 的內(nèi)部使用「DES + Base64」加密算法來進(jìn)行第二步的處理。
加密的操作用 Python 可以很輕松的實(shí)現(xiàn)。
def encode(arg1, arg2):
"""
加密
:param arg1:11位手機(jī)號碼|完整日期 string
:param arg2:手機(jī)后4位+日期4位 bytes
:return:
"""
des = DES.new(arg2, mode=DES.MODE_CBC, iv=bytearray([1, 2, 3, 4, 5, 6, 7, 8]))
msg = des.encrypt(pad(arg1.encode(), DES.block_size))
# 加密后的結(jié)果,bytes
encode_result = base64.b64encode(msg)
# 轉(zhuǎn)為string
return str(encode_result, encoding='utf-8')
需要注意的是,b()函數(shù)的最后一行,對第二步生成的字符串進(jìn)行了特殊字符的替換操作,生成 Token 之前需要對數(shù)據(jù)進(jìn)行同樣的處理。
通過以上三步操作,就可以生成網(wǎng)絡(luò)請求中的參數(shù) Token。
同樣的方式,針對請求中的 Token,我們通過查詢 token 關(guān)鍵字查詢源碼。
通過觀察,我們發(fā)現(xiàn)類 e 中的 b()函數(shù)的功能就是往請求中添加請求頭,繼續(xù)查看函數(shù) b() 的實(shí)現(xiàn)類,發(fā)現(xiàn)這個類也全部被混淆了。
如果你細(xì)心一點(diǎn),一定會發(fā)現(xiàn)當(dāng)前實(shí)現(xiàn)類的包名是 Okhttp3,我們可以從 Github 下載 Okhttp3 的源碼,然后進(jìn)行對比,就能很清晰的知道里面的實(shí)現(xiàn)邏輯了。
ps:okhttp 是 Android 使用很多一個網(wǎng)絡(luò)請求庫。
通過對比沒有混淆過的代碼,可以很容易的編寫出生成請求頭中 Token 的邏輯。
def __get_head_token(self, method, url, data):
"""
獲取請求頭Token
分為Get和Post請求方式
:param method: 請求方式
:param url: 請求URL
:param data: Post請求中的參數(shù)
:return:
"""
today = datetime.date.today()
formatted_today = today.strftime('%Y%m%d')
if method == Method.GET:
# 請求的URL的query部分
query_content = url.split('?')[1]
else:
query_content = urlencode(data)
print('query_content:' + query_content)
# 根據(jù)反編譯后的源碼增加對應(yīng)的邏輯
token_pro = query_content + "|" + formatted_today + '|zxw'
# MD5計算
token = md5(token_pro)
return token
至此,這一步就完成兩個 Token 的生成。
第 3 步就可以利用 Python「模擬發(fā)起一個請求」,來獲取手機(jī)驗(yàn)證碼了。
def get_code(self, timestamp):
"""
獲取驗(yàn)證碼
:return:
"""
# 1.1 獲取參數(shù)Token,與日期有關(guān)
self.param_token = self.__get_param_token(self.phone)
print("parm_token:" + self.param_token)
# 1.2 獲取請求頭Token,與時間有關(guān)
url = self.code_url.format(self.phone, timestamp, self.param_token)
# 獲取請求頭中的Token
self.head_token = self.__get_head_token(Method.GET, url, None)
print('head_token【獲取驗(yàn)證碼】:' + self.head_token)
# 2.獲取手機(jī)驗(yàn)證碼的URL
get_code_url = self.code_url.format(self.phone, timestamp, self.param_token)
# 3.修改Head中的token
HEADERS['token'] = self.head_token
print(get_code_url)
# 4.發(fā)起【獲取驗(yàn)證碼】的請求
resp = requests.get(get_code_url, headers=HEADERS)
print('==' * 60)
print(resp.text)
同理,后面的登錄請求也是先通過抓包,使用上面生成的 Token 邏輯去修改請求頭中 Token,然后模擬請求,就可以正常登錄了。
def login(self, code, timestamp):
"""
登錄
:return:
"""
# 修改參數(shù)
self.login_params['loginCode'] = code
self.login_params['t'] = timestamp
# 請求token
# url = self.code_url.format(self.phone, timestamp, self.param_token)
self.head_token = self.__get_head_token(Method.POST, None, self.login_params)
print('head_token【登錄】:' + self.head_token)
HEADERS['token'] = self.head_token
# 登錄
resp = requests.post(self.login_url, data=self.login_params, headers=HEADERS)
print(resp.text)
結(jié) 果 結(jié) 論
通過模擬獲取驗(yàn)證碼的請求,等待手機(jī)收到驗(yàn)證碼之后,輸入驗(yàn)證碼,然后再模擬登錄的請求,就可以獲取登錄成功后的令牌。
由于驗(yàn)證碼是由服務(wù)器產(chǎn)生的,這里沒法獲取生成邏輯,但是針對安卓手機(jī)可以監(jiān)聽通知欄消息元素,拿到短信驗(yàn)證碼進(jìn)行自動填入,就不需要人工輸入了。
拿到登錄令牌之后,理論上 App 上頁面的各類網(wǎng)絡(luò)請求都可以利用 Python 去模擬,后面提供的源碼包含了一個完整搶票的流程。
本文首發(fā)于公眾號「 AirPython 」,關(guān)注公眾號后,回復(fù)「 App登錄 」即可獲得所有源碼。
如果你覺得文章還不錯,請大家點(diǎn)贊分享下。你的肯定是我最大的鼓勵和支持。
推薦閱讀: