某服務器平臺sm系列算法分析
樣品網址:aHR0cHM6Ly9mdXd1Lm5oc2EuZ292LmNuL25hdGlvbmFsSGFsbFN0LyMvc2VhcmNoL21lZGljYWw/Y29kZT05MDAwMCZmbGFnPWZhbHNlJmdiRmxhZz10cnVl
打開網站后,需要的就是中間顯示的數據

分析數據來自于哪個接口就跳過了,因為不重要,這里直接說結果

其中數據來自于【queryFixedHospital】這個接口,本次要分析的就是請求頭中所有【x-tif】開頭的參數,以及請求體中的【encData】和【signData】的生成算法
然后隨便搜索請求體中的【encData】或者【signData】,都可以直接定位到【app.1654997618917.js】這個js文件

這里可以直接找到所有參數的生成的地方,好像比較順利,接著從上往下開始分析
【paasid】是定值,【timestamp】是當前時間戳,【nonce】是8位隨機值,這三個都非常容易看出來,而【signature】就是【s(g)】的結果,g就是時間戳拼接隨機值再拼接時間戳,s就是sha256函數。請求頭的參數非常容易,接下來看看請求體的參數。
在signData函數內部下一個斷點

其中比較重要的是【v(i)】的函數,這里生成了一段字符串來計算簽名

這個函數和查詢參數編碼的功能類似,除了data參數,并且在最后拼接了一個定值字符串,這里用python進行簡單的復現
復制代碼 隱藏代碼
def v(e): t = []
for n in e:
if n == 'data':
data = e[n].copy()
for each in e[n]:
if not data[each]:
del data[each]
else:
data[each] = str(data[each])
t.append(n + '=' + json.dumps(data, separators=(',', ':')))
else:
t.append(n + '=' + str(e[n]))
t.append('key=NMVFVILMKT13GEMD3BKPKCTBOQBPZR2P')
return '&'.join(t)
獲取到這段字符串后繼續往下走,就進入到了【doSignature】函數

這里的第一個參數就是前面的字符串,第二個參數就是私鑰

來到這里有一個判斷,因為前面傳入的hash參數恒為真,所以要先對簽名的內容計算hash,計算hash用到的是【y】函數,網上查看來源

這里可以看到,【y】函數就是sm3算法。繼續進入到【y】函數分析

可以看到這里更新了兩次數據,相當于是計算這兩個數據的hash,首先是r參數,這個是【getZ】函數的返回值。另一個是a參數,就是前面傳入的字符串,這個前面已經分析了,那么繼續進入到【getZ】函數查看

這里又是一個sm3算法,這次更新的參數就比較多了,包括一個【1234567812345678】的一個固定值,以及sm2算法的初始化ecc表,還有傳入的私鑰。這里已經可以發現,所有的參數其實都是定值(私鑰一般不改的情況下),那么這里的返回值也是一個固定的值【fde9a74125ca149ca75f4c2ccdaeed3e7d0b4b8c0f2c9e35530b9fe9a3ba1233】,代碼中可以寫成固定值,這里只是說明【getZ】函數的算法,用python還原getZ函數
復制代碼 隱藏代碼
def getZ(crypto): sign_data = bytes()
n = '1234567812345678'.encode()
sign_data += bytes([0, 8 * len(n)])
sign_data += n
sign_data += bytes.fromhex(crypto.ecc_table['a'])
sign_data += bytes.fromhex(crypto.ecc_table['b'])
sign_data += bytes.fromhex(crypto.ecc_table['g'])
sign_data += bytes.fromhex(crypto.public_key[2:])
return bytes.fromhex(sm3.sm3_hash(list(sign_data)))
# 可以寫成固定值 # return bytes.fromhex('fde9a74125ca149ca75f4c2ccdaeed3e7d0b4b8c0f2c9e35530b9fe9a3ba1233')
那么將【getZ】的返回值和前面的字符串一起計算sm3,就得到了消息hash

接著將消息hash計算sm2簽名,就得到了【signData】了,最后分析【encData】參數。

【encData】這里固定傳入了sm4,那么必定走sm4的分支,進入函數繼續分析

這個函數比較短,主要是一個b函數,一個w函數,其中b函數是用來計算一個密鑰

用python還原也比較簡單
復制代碼 隱藏代碼 def b(e, t): crypto = sm4.CryptSM4() crypto.set_key(e[:16].encode(), sm4.SM4_ENCRYPT) return crypto.crypt_ecb(t.encode()).hex().upper()[:16]
拿到密鑰后,直接使用sm4算法加密就可以得到【encData】了,現在所有參數都已經能夠獲取了,就可以發送請求了。

不過請求的響應也是加密的,幸好的是解密就一個sm4算法,key和前面的是一樣的,那么直接解密就可以了,完整代碼
復制代碼 隱藏代碼
import requests_htmlimport randomimport timeimport jsonimport base64from Crypto.Hash import SHA256from gmssl import sm2, sm3, sm4, func
publicKey = base64.b64decode("BEKaw3Qtc31LG/hTPHFPlriKuAn/nzTWl8LiRxLw4iQiSUIyuglptFxNkdCiNXcXvkqTH79Rh/A2sEFU6hjeK3k=".encode()).hex()
privateKey = base64.b64decode("AJxKNdmspMaPGj+onJNoQ0cgWk2E3CYFWKBJhpcJrAtC".encode()).hex()
appSecret = 'NMVFVILMKT13GEMD3BKPKCTBOQBPZR2P'appCode = 'T98HPCGN5ZVVQBS8LZQNOAEXVI9GYHKQ'def main(): requests = requests_html.HTMLSession()
key = b(appCode, appSecret).encode()
s = str(int(time.time()))
c = ''.join(random.choices("ABCDEFGHIJKLMNOPQRSTUVWXYZzbcdefghijklmnopqrstuvwxyz0123456789", k=8))
headers = {
'x-tif-paasid': 'undefined',
'"x-tif-timestamp': s,
'x-tif-nonce': c,
'"x-tif-signature': SHA256.new((s + c + s).encode()).hexdigest(),
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36' }
data = {
'appCode': appCode,
'data': {
'addr': '',
'medinsLvCode': '',
'medinsName': '',
'medinsTypeCode': '',
'openElec': '',
'pageNum': 1,
'pageSize': 10,
'regnCode': '110000' },
'encType': 'SM4',
'signType': 'SM2',
'timestamp': int(s),
'version': '1.0.0' }
crypto = sm2.CryptSM2(privateKey, publicKey)
data['signData'] = base64.b64encode(bytes.fromhex(crypto.sign(bytes.fromhex(sm3.sm3_hash(list(getZ(crypto) + v(data).encode()))), func.random_hex(crypto.para_len)))).decode()
crypto = sm4.CryptSM4()
crypto.set_key(key, sm4.SM4_ENCRYPT)
data['data'] = {
'encData': crypto.crypt_ecb(json.dumps(data['data']).encode()).hex().upper()
}
data = {
'data': data
}
response = requests.post('https://fuwu.nhsa.gov.cn/ebus/fuwu/api/nthl/api/CommQuery/queryFixedHospital', json=data, headers=headers).json()
crypto = sm4.CryptSM4()
crypto.set_key(key, sm4.SM4_DECRYPT)
data = json.loads(crypto.crypt_ecb(bytes.fromhex(response['data']['data']['encData'])).decode())
print(data)def getZ(crypto): sign_data = bytes()
n = '1234567812345678'.encode()
sign_data += bytes([0, 8 * len(n)])
sign_data += n
sign_data += bytes.fromhex(crypto.ecc_table['a'])
sign_data += bytes.fromhex(crypto.ecc_table['b'])
sign_data += bytes.fromhex(crypto.ecc_table['g'])
sign_data += bytes.fromhex(crypto.public_key[2:])
return bytes.fromhex(sm3.sm3_hash(list(sign_data)))
# 可以寫成固定值 # return bytes.fromhex('fde9a74125ca149ca75f4c2ccdaeed3e7d0b4b8c0f2c9e35530b9fe9a3ba1233')def v(e): t = []
for n in e:
if n == 'data':
data = e[n].copy()
for each in e[n]:
if not data[each]:
del data[each]
else:
data[each] = str(data[each])
t.append(n + '=' + json.dumps(data, separators=(',', ':')))
else:
t.append(n + '=' + str(e[n]))
t.append('key=' + appSecret)
return '&'.join(t)def b(e, t): crypto = sm4.CryptSM4()
crypto.set_key(e[:16].encode(), sm4.SM4_ENCRYPT)
return crypto.crypt_ecb(t.encode()).hex().upper()[:16]if __name__ == '__main__':
main()

成功獲取到結果,完成!!!