【JS 逆向百例】醫保局 SM2+SM4 國產加密演算法實戰

K哥爬蟲發表於2021-11-16
關注微信公眾號:K哥爬蟲,QQ交流群:808574309,持續分享爬蟲進階、JS/安卓逆向等技術乾貨!

宣告

本文章中所有內容僅供學習交流,抓包內容、敏感網址、資料介面均已做脫敏處理,嚴禁用於商業用途和非法用途,否則由此產生的一切後果均與作者無關,若有侵權,請聯絡我立即刪除!

逆向目標

  • 目標:醫療保障局公共查詢
  • 主頁:aHR0cHM6Ly9mdXd1Lm5oc2EuZ292LmNuL25hdGlvbmFsSGFsbFN0LyMvc2VhcmNoL21lZGljYWw=
  • 介面:aHR0cHM6Ly9mdXd1Lm5oc2EuZ292LmNuL2VidXMvZnV3dS9hcGkvbnRobC9hcGkvZml4ZWQvcXVlcnlGaXhlZEhvc3BpdGFs
  • 逆向引數:Request Payload 的 encDatasignData、Request Headers 的 x-tif-noncex-tif-signature

逆向過程

抓包分析

來到公共查詢頁面,點選翻頁,就可以看到一個 POST 請求,Request Payload 的引數部分是加密的,主要是 appCode、encData 和 signData 引數,同樣返回的資料也有這些引數,其加密解密方法是一樣的,其中 encType 和 signType 分別為 SM4 和 SM2,所以大概率這是國密演算法了,有關國密演算法 K 哥前期文章有介紹:《爬蟲逆向基礎,認識 SM1-SM9、ZUC 國密演算法》,此外請求頭還有 x-tif-nonce 和 x-tif-signature 引數,如下圖所示:

01.png

引數逆向

直接全域性搜尋 encData 或 signData,搜尋結果僅在 app.1634197175801.js 有,非常明顯,上面還有設定 header 的地方,所有引數都在這裡,埋下斷點,可以看到這裡就是加密的地方,如下圖所示:

02.png

這裡的加密函式,主要都傳入了一個 e 引數,我們可以先看一下這個 e,裡面的引數含義如下:

  • addr:醫療機構詳細地址,預設空;
  • medinsLvCode:醫療機構等級程式碼,預設空;
  • medinsName:醫療機構名稱,預設空;
  • medinsTypeCode:醫療機構型別程式碼,預設空;
  • pageNum:頁數,預設 1;
  • pageSize:每頁資料條數,預設 10;
  • regnCode:醫療機構所在地程式碼,預設 110000(北京市);
  • sprtEcFlag:暫時不知其含義,預設空。

等級程式碼、型別程式碼、所在地程式碼,都是通過請求加密介面得到的,他們的加密和解密方法都一樣,在最後的完整程式碼裡有分享,這裡不再贅述。其他引數比如 appCode,是在 JS 裡寫死的。

03.png

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

04.png

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

05.png

06.png

這裡要注意的是,觀察這個函式後面匯出的 sha256 實際上是呼叫了 createMethod 這個方法,那麼我們 copy 下來的方法直接呼叫 createMethod 即可,即 var t = createMethod(),不需要這些 exports 了。

07.png

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

08.png

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

09.png

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

10.png

11.png

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

12.png

13.png

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

14.png

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

15.png

16.png

到此加密用到的函式都扣完了,此時我們可以寫一個方法,對加密的過程進行封裝,使用時只需要傳入類似以下引數即可:

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

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

17.png

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

18.png

19.png

完整程式碼

GitHub 關注 K 哥爬蟲,持續分享爬蟲相關程式碼!歡迎 star !https://github.com/kgepachong/

以下只演示部分關鍵程式碼,不能直接執行! 完整程式碼倉庫地址:https://github.com/kgepachong...

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()

相關文章