
關注微信公眾號:K哥爬蟲,QQ交流群:808574309,持續分享爬蟲進階、JS/安卓逆向等技術干貨!
聲明
本文章中所有內容僅供學習交流,抓包內容、敏感網址、數據接口均已做脫敏處理,嚴禁用於商業用途和非法用途,否則由此產生的一切后果均與作者無關,若有侵權,請聯系我立即刪除!
逆向目標
-
目標:醫療保障局公共查詢
-
主頁:
aHR0cHM6Ly9mdXd1Lm5oc2EuZ292LmNuL25hdGlvbmFsSGFsbFN0LyMvc2VhcmNoL21lZGljYWw= -
接口:
aHR0cHM6Ly9mdXd1Lm5oc2EuZ292LmNuL2VidXMvZnV3dS9hcGkvbnRobC9hcGkvZml4ZWQvcXVlcnlGaXhlZEhvc3BpdGFs -
逆向參數:Request Payload 的
encData和signData、Request Headers 的x-tif-nonce和x-tif-signature
逆向過程
抓包分析
來到公共查詢頁面,點擊翻頁,就可以看到一個 POST 請求,Request Payload 的參數部分是加密的,主要是 appCode、encData 和 signData 參數,同樣返回的數據也有這些參數,其加密解密方法是一樣的,其中 encType 和 signType 分別為 SM4 和 SM2,所以大概率這是國密算法了,有關國密算法 K 哥前期文章有介紹:《爬蟲逆向基礎,認識 SM1-SM9、ZUC 國密算法》,此外請求頭還有 x-tif-nonce 和 x-tif-signature 參數,如下圖所示:

參數逆向
直接全局搜索 encData 或 signData,搜索結果僅在 app.1634197175801.js 有,非常明顯,上面還有設置 header 的地方,所有參數都在這里,埋下斷點,可以看到這里就是加密的地方,如下圖所示:

這里的加密函數,主要都傳入了一個 e 參數,我們可以先看一下這個 e,里面的參數含義如下:
- addr:醫療機構詳細地址,默認空;
- medinsLvCode:醫療機構等級代碼,默認空;
- medinsName:醫療機構名稱,默認空;
- medinsTypeCode:醫療機構類型代碼,默認空;
- pageNum:頁數,默認 1;
- pageSize:每頁數據條數,默認 10;
- regnCode:醫療機構所在地代碼,默認 110000(北京市);
- sprtEcFlag:暫時不知其含義,默認空。
等級代碼、類型代碼、所在地代碼,都是通過請求加密接口得到的,他們的加密和解密方法都一樣,在最后的完整代碼里有分享,這里不再贅述。其他參數比如 appCode,是在 JS 里寫死的。

我們再觀察一下整個 JS 文件,在頭部可以看到 .call 語句,並且有 exports 關鍵字,很明顯是一個 webpack 形式的寫法。

我們回到加密的地方,從上往下看,整個函數引用了很多其他模塊,如果想整個扣下來,花費時間肯定是無比巨大的,如果想直接拿下整個 JS,再將參數導出,這種暴力做法可是可以,但是整個 JS 有七萬多行,運行效率肯定是有所影響的,所以觀察函數,將不用的函數去掉,有用的留下來,是比較好的做法,觀察 function d,第一行 var t = n("6c27").sha256,點進去來到 createOutputMethod 方法,這里整個是一個 SHA256 算法,從這個方法往下整個 copy 下來即可,如下圖所示:


這里要注意的是,觀察這個函數后面導出的 sha256 實際上是調用了 createMethod 這個方法,那么我們 copy 下來的方法直接調用 createMethod 即可,即 var t = createMethod(),不需要這些 exports 了。

另外還有一些變量需要定義,整個 copy 下來的結構如下:

接着前面的繼續往下看,還有一句 o = Object(i.a)(),同樣點進去直接 copy 下來即可,這里沒有什么需要注意的地方。

再往下看就來到了 e.data.signData = p(e),點進 function p,將整個函數 copy 下來,這時候你本地調試會發現沒有任何錯誤,實際上他這里使用了 try-catch 語句,捕獲到了異常之后就沒有任何處理,可以自己加一句 console.log(e) 來輸出異常,實際上他這里會在 o.doSignature、e.from 兩個位置提示未定義,同樣的我們可以點進去將函數扣出來,但是后面會遇到函數不斷引用其他函數,為了方便,我們可以將其寫到 webpack 里,下面的 e.from 也是一樣。


將模塊寫成 webpack 形式,在自執行方法里調用,然后定義全局變量來接收,再將原來的 o, e 換成全局變量即可,這里還需要注意的一個地方,那就是 o.doSignature 傳入的 h,是一個定值,需要定義一下,不然后面解密是失敗的。如下圖所示:


這里扣 webpack 模塊的時候也需要注意,不要把所有原方法里有的模塊都扣出來,有些根本沒用到,可以直接注釋掉,這個過程是需要有耐心的,你如果全部扣,那將會是無窮無盡的,還不如直接使用整個 JS 文件,所有有用的模塊如下(可能會多,但不會少):

接着原來的說,encData: v("SM4", e) 這里用到了 function v,v 里面又用到了 A、g 等函數,全部扣下來即可,同時還需要注意,前面所說的 e 在 A 函數里也用到了,同樣需要換成我們自己定義的全局變量,如下圖所示:


到此加密用到的函數都扣完了,此時我們可以寫一個方法,對加密的過程進行封裝,使用時只需要傳入類似以下參數即可:
{
"addr": "",
"regnCode": "110000",
"medinsName": "",
"sprtEcFlag": "",
"medinsLvCode": "",
"medinsTypeCode": "",
"pageNum": 1,
"pageSize": 10
}
如下圖所示 getEncryptedData 就是加密方法:

那么解密方法呢?很明顯返回的數據是 encData,直接搜索 encData 就只有三個結果,很容易找到就行 function y,同樣的,這里要注意把 e.from 改成我們自定義的 e_.Buffer.from,另外我們也可以將 header 參數的生成方法也封裝成一個函數,便於調用。


完整代碼
GitHub 關注 K 哥爬蟲,持續分享爬蟲相關代碼!歡迎 star !https://github.com/kgepachong/
以下只演示部分關鍵代碼,不能直接運行! 完整代碼倉庫地址:https://github.com/kgepachong/crawler/
JavaScript 加密關鍵代碼架構
var sm2, sm4, e_;
!function (e) {
var n = {},
i = {app: 0},
r = {app: 0};
function o(t) {}
o.e = function (e) {}
o.m = e
o.c = n
o.d = function (e, t, n) {}
o.r = function (e) {}
o.n = function (e) {}
o.o = function (e, t) {}
sm2 = o('4d09')
e_ = o('b639')
sm4 = o('e04e')
}({
"4d09": function (e, t, n) {},
'f33e': function (e, t, n) {},
"4d2d": function (e, t, n) {},
'b381': function (e, t, n) {},
// 此處省略 N 個模塊
})
// 此處省略 N 個變量
var createOutputMethod = function (e, t) {},
createMethod = function (e) {},
nodeWrap = function (method, is224) {},
createHmacOutputMethod = function (e, t) {},
createHmacMethod = function (e) {};
function Sha256(e, t) {}
function HmacSha256(e, t, n) {}
// 此處省略 N 個方法
function i() {}
function p(t) {}
function m(e) {}
var c = {
paasId: undefined,
appCode: "T98HPCGN5ZVVQBS8LZQNOAEXVI9GYHKQ",
version: "1.0.0",
appSecret: "NMVFVILMKT13GEMD3BKPKCTBOQBPZR2P",
publicKey: "BEKaw3Qtc31LG/hTPHFPlriKuAn/nzTWl8LiRxLw4iQiSUIyuglptFxNkdCiNXcXvkqTH79Rh/A2sEFU6hjeK3k=",
privateKey: "AJxKNdmspMaPGj+onJNoQ0cgWk2E3CYFWKBJhpcJrAtC",
publicKeyType: "base64",
privateKeyType: "base64"
},
l = c.appCode,
u = c.appSecret,
f = c.publicKey,
h = c.privateKey,
t = createMethod(),
// t = n("6c27").sha256,
r = Math.ceil((new Date).getTime() / 1e3),
o = i(),
a = r + o + r;
function getEncryptedData(data) {
var e = {"data": data}
return e.data = {
data: e.data || {}
},
e.data.appCode = c.appCode,
e.data.version = c.version,
e.data.encType = "SM4",
e.data.signType = "SM2",
e.data.timestamp = r,
e.data.signData = p(e),
e.data.data = {
encData: v("SM4", e)
},
// e.data = JSON.stringify({
// data: e.data
// }),
e
}
function getDecryptedData(t) {
if (!t)
return null;
var n = e_.Buffer.from(t.data.data.encData, "hex")
, i = function(t, n) {
var i = sm4.decrypt(n, t)
, r = i[i.length - 1];
return i = i.slice(0, i.length - r),
e_.Buffer.from(i).toString("utf-8")
}(g(l, u), n);
return JSON.parse(i)
}
function getHeaders(){
var headers = {}
return headers["x-tif-paasid"] = c.paasId,
headers["x-tif-signature"] = t(a),
headers["x-tif-timestamp"] = r.toString(),
headers["x-tif-nonce"] = o,
headers["Accept"] = "application/json",
headers["contentType"] = "application/x-www-form-urlencoded",
headers
}
Python 獲取數據關鍵代碼
# ==================================
# --*-- coding: utf-8 --*--
# @Time : 2021-11-03
# @Author : 微信公眾號:K哥爬蟲
# @FileName: nhsa.py
# @Software: PyCharm
# ==================================
import execjs
import requests
regn_code_url = "脫敏處理,完整代碼關注 GitHub:https://github.com/kgepachong/crawler"
lv_and_type_url = "脫敏處理,完整代碼關注 GitHub:https://github.com/kgepachong/crawler"
result_url = "脫敏處理,完整代碼關注 GitHub:https://github.com/kgepachong/crawler"
UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.4606.81 Safari/537.36"
with open('nhsa.js', 'r', encoding='utf-8') as f:
nhsa_js = execjs.compile(f.read())
def get_headers():
"""獲取 header 參數,每次請求改變"""
headers = nhsa_js.call("getHeaders")
headers["User-Agent"] = UA
headers["Content-Type"] = "application/json"
headers["Host"] = "脫敏處理,完整代碼關注 GitHub:https://github.com/kgepachong/crawler"
headers["Origin"] = "脫敏處理,完整代碼關注 GitHub:https://github.com/kgepachong/crawler"
headers["Referer"] = "脫敏處理,完整代碼關注 GitHub:https://github.com/kgepachong/crawler"
# print(headers)
return headers
def get_regn_code():
"""獲取城市代碼,返回結果無加密"""
payload = {"data": {"transferFlag": ""}}
response = requests.post(url=regn_code_url, json=payload, headers=get_headers())
print(response.text)
def get_medins_lv_or_type_code(key):
"""獲取醫療機構等級 (LV) or 類型 (TYPE) 代碼"""
if key == "LV":
payload = {"type": "MEDINSLV"}
elif key == "TYPE":
payload = {"type": "MEDINS_TYPE"}
else:
print("輸入有誤!")
return
encrypted_payload = nhsa_js.call("getEncryptedData", payload)
encrypted_data = requests.post(url=lv_and_type_url, json=encrypted_payload, headers=get_headers()).json()
decrypted_data = nhsa_js.call("getDecryptedData", encrypted_data)
print(decrypted_data)
def get_result():
addr = input("請輸入醫療機構詳細地址(默認無): ") or ""
medins_lv_code = input("請輸入醫療機構等級代碼(默認無): ") or ""
medins_name = input("請輸入醫療機構名稱(默認無): ") or ""
medins_type_code = input("請輸入醫療機構類型代碼(默認無): ") or ""
regn_code = input("請輸入醫療機構所在地代碼(默認北京市): ") or "110000"
page_num = input("請輸入要爬取的頁數(默認1): ") or 1
for page in range(1, int(page_num)+1):
payload = {
"addr": addr,
"medinsLvCode": medins_lv_code,
"medinsName": medins_name,
"medinsTypeCode": medins_type_code,
"pageNum": page,
"pageSize": 10,
"regnCode": regn_code,
"sprtEcFlag": ""
}
page += 1
encrypted_payload = nhsa_js.call("getEncryptedData", payload)
encrypted_data = requests.post(url=result_url, json=encrypted_payload, headers=get_headers()).json()
decrypted_data = nhsa_js.call("getDecryptedData", encrypted_data)
print(decrypted_data)
def main():
# 獲取城市代碼
# get_regn_code()
# 獲取醫療機構等級代碼
# get_medins_lv_or_type_code("LV")
# 獲取醫療機構類型代碼
# get_medins_lv_or_type_code("TYPE")
# 獲取搜索結果
get_result()
if __name__ == "__main__":
main()

