【JS 逆向百例】醫(yī)保局 SM2+SM4 國產(chǎn)加密算法實(shí)戰(zhàn)

聲明

本文章中所有內(nèi)容僅供學(xué)習(xí)交流,抓包內(nèi)容路翻、敏感網(wǎng)址狈癞、數(shù)據(jù)接口均已做脫敏處理,嚴(yán)禁用于商業(yè)用途和非法用途茂契,否則由此產(chǎn)生的一切后果均與作者無關(guān)蝶桶,若有侵權(quán),請聯(lián)系我立即刪除掉冶!

逆向目標(biāo)

  • 目標(biāo):醫(yī)療保障局公共查詢

  • 主頁:aHR0cHM6Ly9mdXd1Lm5oc2EuZ292LmNuL25hdGlvbmFsSGFsbFN0LyMvc2VhcmNoL21lZGljYWw=

  • 接口:aHR0cHM6Ly9mdXd1Lm5oc2EuZ292LmNuL2VidXMvZnV3dS9hcGkvbnRobC9hcGkvZml4ZWQvcXVlcnlGaXhlZEhvc3BpdGFs

  • 逆向參數(shù):Request Payload 的 encDatasignData真竖、Request Headers 的 x-tif-noncex-tif-signature

逆向過程

抓包分析

來到公共查詢頁面,點(diǎn)擊翻頁厌小,就可以看到一個(gè) POST 請求恢共,Request Payload 的參數(shù)部分是加密的,主要是 appCode璧亚、encData 和 signData 參數(shù)讨韭,同樣返回的數(shù)據(jù)也有這些參數(shù),其加密解密方法是一樣的癣蟋,其中 encType 和 signType 分別為 SM4 和 SM2透硝,所以大概率這是國密算法了,有關(guān)國密算法 K 哥前期文章有介紹:《爬蟲逆向基礎(chǔ)梢薪,認(rèn)識 SM1-SM9蹬铺、ZUC 國密算法》,此外請求頭還有 x-tif-nonce 和 x-tif-signature 參數(shù)秉撇,如下圖所示:

01.png

參數(shù)逆向

直接全局搜索 encData 或 signData甜攀,搜索結(jié)果僅在 app.1634197175801.js 有,非常明顯琐馆,上面還有設(shè)置 header 的地方规阀,所有參數(shù)都在這里,埋下斷點(diǎn)瘦麸,可以看到這里就是加密的地方谁撼,如下圖所示:

02.png

這里的加密函數(shù),主要都傳入了一個(gè) e 參數(shù)滋饲,我們可以先看一下這個(gè) e厉碟,里面的參數(shù)含義如下:

  • addr:醫(yī)療機(jī)構(gòu)詳細(xì)地址,默認(rèn)空屠缭;
  • medinsLvCode:醫(yī)療機(jī)構(gòu)等級代碼箍鼓,默認(rèn)空;
  • medinsName:醫(yī)療機(jī)構(gòu)名稱呵曹,默認(rèn)空款咖;
  • medinsTypeCode:醫(yī)療機(jī)構(gòu)類型代碼何暮,默認(rèn)空;
  • pageNum:頁數(shù)铐殃,默認(rèn) 1海洼;
  • pageSize:每頁數(shù)據(jù)條數(shù),默認(rèn) 10富腊;
  • regnCode:醫(yī)療機(jī)構(gòu)所在地代碼坏逢,默認(rèn) 110000(北京市);
  • sprtEcFlag:暫時(shí)不知其含義蟹肘,默認(rèn)空词疼。

等級代碼、類型代碼帘腹、所在地代碼,都是通過請求加密接口得到的许饿,他們的加密和解密方法都一樣阳欲,在最后的完整代碼里有分享,這里不再贅述陋率。其他參數(shù)比如 appCode球化,是在 JS 里寫死的。

03.png

我們再觀察一下整個(gè) JS 文件瓦糟,在頭部可以看到 .call 語句筒愚,并且有 exports 關(guān)鍵字,很明顯是一個(gè) webpack 形式的寫法菩浙。

04.png

我們回到加密的地方巢掺,從上往下看,整個(gè)函數(shù)引用了很多其他模塊劲蜻,如果想整個(gè)扣下來陆淀,花費(fèi)時(shí)間肯定是無比巨大的,如果想直接拿下整個(gè) JS先嬉,再將參數(shù)導(dǎo)出轧苫,這種暴力做法可是可以,但是整個(gè) JS 有七萬多行疫蔓,運(yùn)行效率肯定是有所影響的含懊,所以觀察函數(shù),將不用的函數(shù)去掉衅胀,有用的留下來岔乔,是比較好的做法,觀察 function d拗小,第一行 var t = n("6c27").sha256重罪,點(diǎn)進(jìn)去來到 createOutputMethod 方法,這里整個(gè)是一個(gè) SHA256 算法,從這個(gè)方法往下整個(gè) copy 下來即可剿配,如下圖所示:

05.png
06.png

這里要注意的是搅幅,觀察這個(gè)函數(shù)后面導(dǎo)出的 sha256 實(shí)際上是調(diào)用了 createMethod 這個(gè)方法,那么我們 copy 下來的方法直接調(diào)用 createMethod 即可呼胚,即 var t = createMethod()茄唐,不需要這些 exports 了。

07.png

另外還有一些變量需要定義蝇更,整個(gè) copy 下來的結(jié)構(gòu)如下:

08.png

接著前面的繼續(xù)往下看沪编,還有一句 o = Object(i.a)(),同樣點(diǎn)進(jìn)去直接 copy 下來即可年扩,這里沒有什么需要注意的地方蚁廓。

09.png

再往下看就來到了 e.data.signData = p(e),點(diǎn)進(jìn) function p厨幻,將整個(gè)函數(shù) copy 下來相嵌,這時(shí)候你本地調(diào)試會發(fā)現(xiàn)沒有任何錯(cuò)誤,實(shí)際上他這里使用了 try-catch 語句况脆,捕獲到了異常之后就沒有任何處理饭宾,可以自己加一句 console.log(e) 來輸出異常,實(shí)際上他這里會在 o.doSignature格了、e.from 兩個(gè)位置提示未定義看铆,同樣的我們可以點(diǎn)進(jìn)去將函數(shù)扣出來,但是后面會遇到函數(shù)不斷引用其他函數(shù)盛末,為了方便弹惦,我們可以將其寫到 webpack 里,下面的 e.from 也是一樣满败。

10.png
11.png

將模塊寫成 webpack 形式肤频,在自執(zhí)行方法里調(diào)用,然后定義全局變量來接收算墨,再將原來的 o, e 換成全局變量即可宵荒,這里還需要注意的一個(gè)地方,那就是 o.doSignature 傳入的 h净嘀,是一個(gè)定值报咳,需要定義一下,不然后面解密是失敗的挖藏。如下圖所示:

12.png
13.png

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

14.png

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

15.png
16.png

到此加密用到的函數(shù)都扣完了,此時(shí)我們可以寫一個(gè)方法练慕,對加密的過程進(jìn)行封裝绰沥,使用時(shí)只需要傳入類似以下參數(shù)即可:

{
    "addr": "", 
    "regnCode": "110000", 
    "medinsName": "", 
    "sprtEcFlag": "", 
    "medinsLvCode": "", 
    "medinsTypeCode": "", 
    "pageNum": 1, 
    "pageSize": 10
}

如下圖所示 getEncryptedData 就是加密方法:

17.png

那么解密方法呢?很明顯返回的數(shù)據(jù)是 encData贺待,直接搜索 encData 就只有三個(gè)結(jié)果,很容易找到就行 function y零截,同樣的麸塞,這里要注意把 e.from 改成我們自定義的 e_.Buffer.from,另外我們也可以將 header 參數(shù)的生成方法也封裝成一個(gè)函數(shù)涧衙,便于調(diào)用哪工。

18.png
19.png

完整代碼

GitHub 關(guān)注 K 哥爬蟲,持續(xù)分享爬蟲相關(guān)代碼弧哎!歡迎 star 雁比!https://github.com/kgepachong/

以下只演示部分關(guān)鍵代碼,不能直接運(yùn)行撤嫩! 完整代碼倉庫地址:https://github.com/kgepachong/crawler/

JavaScript 加密關(guān)鍵代碼架構(gòu)

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 個(gè)模塊
})

// 此處省略 N 個(gè)變量

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 個(gè)方法

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 獲取數(shù)據(jù)關(guān)鍵代碼

# ==================================
# --*-- coding: utf-8 --*--
# @Time    : 2021-11-03
# @Author  : 微信公眾號:K哥爬蟲
# @FileName: nhsa.py
# @Software: PyCharm
# ==================================


import execjs
import requests


regn_code_url = "脫敏處理偎捎,完整代碼關(guān)注 GitHub:https://github.com/kgepachong/crawler"
lv_and_type_url = "脫敏處理,完整代碼關(guān)注 GitHub:https://github.com/kgepachong/crawler"
result_url = "脫敏處理序攘,完整代碼關(guān)注 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 參數(shù)茴她,每次請求改變"""
    headers = nhsa_js.call("getHeaders")
    headers["User-Agent"] = UA
    headers["Content-Type"] = "application/json"
    headers["Host"] = "脫敏處理,完整代碼關(guān)注 GitHub:https://github.com/kgepachong/crawler"
    headers["Origin"] = "脫敏處理程奠,完整代碼關(guān)注 GitHub:https://github.com/kgepachong/crawler"
    headers["Referer"] = "脫敏處理丈牢,完整代碼關(guān)注 GitHub:https://github.com/kgepachong/crawler"
    # print(headers)
    return headers


def get_regn_code():
    """獲取城市代碼,返回結(jié)果無加密"""
    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):
    """獲取醫(yī)療機(jī)構(gòu)等級 (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("請輸入醫(yī)療機(jī)構(gòu)詳細(xì)地址(默認(rèn)無): ") or ""
    medins_lv_code = input("請輸入醫(yī)療機(jī)構(gòu)等級代碼(默認(rèn)無): ") or ""
    medins_name = input("請輸入醫(yī)療機(jī)構(gòu)名稱(默認(rèn)無): ") or ""
    medins_type_code = input("請輸入醫(yī)療機(jī)構(gòu)類型代碼(默認(rèn)無): ") or ""
    regn_code = input("請輸入醫(yī)療機(jī)構(gòu)所在地代碼(默認(rèn)北京市): ") or "110000"
    page_num = input("請輸入要爬取的頁數(shù)(默認(rèn)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()
    # 獲取醫(yī)療機(jī)構(gòu)等級代碼
    # get_medins_lv_or_type_code("LV")
    # 獲取醫(yī)療機(jī)構(gòu)類型代碼
    # get_medins_lv_or_type_code("TYPE")
    # 獲取搜索結(jié)果
    get_result()


if __name__ == "__main__":
    main()
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末瞄沙,一起剝皮案震驚了整個(gè)濱河市己沛,隨后出現(xiàn)的幾起案子慌核,更是在濱河造成了極大的恐慌,老刑警劉巖申尼,帶你破解...
    沈念sama閱讀 219,039評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件垮卓,死亡現(xiàn)場離奇詭異,居然都是意外死亡晶姊,警方通過查閱死者的電腦和手機(jī)扒接,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,426評論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來们衙,“玉大人钾怔,你說我怎么就攤上這事∶商簦” “怎么了宗侦?”我有些...
    開封第一講書人閱讀 165,417評論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長忆蚀。 經(jīng)常有香客問我矾利,道長,這世上最難降的妖魔是什么馋袜? 我笑而不...
    開封第一講書人閱讀 58,868評論 1 295
  • 正文 為了忘掉前任男旗,我火速辦了婚禮,結(jié)果婚禮上欣鳖,老公的妹妹穿的比我還像新娘察皇。我一直安慰自己,他們只是感情好泽台,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,892評論 6 392
  • 文/花漫 我一把揭開白布什荣。 她就那樣靜靜地躺著,像睡著了一般怀酷。 火紅的嫁衣襯著肌膚如雪稻爬。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,692評論 1 305
  • 那天蜕依,我揣著相機(jī)與錄音桅锄,去河邊找鬼。 笑死笔横,一個(gè)胖子當(dāng)著我的面吹牛竞滓,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播吹缔,決...
    沈念sama閱讀 40,416評論 3 419
  • 文/蒼蘭香墨 我猛地睜開眼商佑,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了厢塘?” 一聲冷哼從身側(cè)響起茶没,我...
    開封第一講書人閱讀 39,326評論 0 276
  • 序言:老撾萬榮一對情侶失蹤肌幽,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后抓半,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體喂急,經(jīng)...
    沈念sama閱讀 45,782評論 1 316
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,957評論 3 337
  • 正文 我和宋清朗相戀三年笛求,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了廊移。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,102評論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡探入,死狀恐怖狡孔,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情蜂嗽,我是刑警寧澤苗膝,帶...
    沈念sama閱讀 35,790評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站植旧,受9級特大地震影響辱揭,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜病附,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,442評論 3 331
  • 文/蒙蒙 一问窃、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧完沪,春花似錦泡躯、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,996評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽咕别。三九已至技健,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間惰拱,已是汗流浹背雌贱。 一陣腳步聲響...
    開封第一講書人閱讀 33,113評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留偿短,地道東北人欣孤。 一個(gè)月前我還...
    沈念sama閱讀 48,332評論 3 373
  • 正文 我出身青樓,卻偏偏與公主長得像昔逗,于是被迫代替她去往敵國和親降传。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,044評論 2 355

推薦閱讀更多精彩內(nèi)容