国密(SM)系列逆向学习及实战
简介
例如SM4:我国SM4分组密码算法正式成为ISO/IEC国际标准
国密算法,即国家商用密码算法。是由国家密码管理局认定和公布的密码算法标准及其应用规范,其中部分密码算法已经成为国际标准。如SM系列密码,SM代表商密,即商业密码,是指用于商业的、不涉及国家秘密的密码技术。
——国密算法介绍
![](https://pic3.zhimg.com/v2-58c351513a73515fea7ccd48894f3196_r.jpg)
这里常用的为SM2
和SM4
算法
SM2
简介
它是基于椭圆曲线密码的公钥密码算法标准,其秘钥长度256bit,包含数字签名、密钥交换和公钥加密,用于替换RSA/DH/ECDSA/ECDH等国际算法。可以满足电子认证服务系统等应用需求,由国家密码管理局于2010年12月17号发布。
SM2采用的是ECC 256位的一种,其安全强度比RSA 2048位高,且运算速度快于RSA。
——国密算法介绍
JS实现
JS已有较为成熟的实现库,这里推荐sm-crypto
,可实现SM2
、SM3
和SM4
上面链接的NPM库间接已给出示例,这里直接照搬:
密钥对
const sm2 = require('sm-crypto').sm2
let keypair = sm2.generateKeyPairHex()
publicKey = keypair.publicKey // 公钥
privateKey = keypair.privateKey // 私钥
// 默认生成公钥 130 位太长,可以压缩公钥到 66 位
const compressedPublicKey = sm2.compressPublicKeyHex(publicKey) // compressedPublicKey 和 publicKey 等价
sm2.comparePublicKeyHex(publicKey, compressedPublicKey) // 判断公钥是否等价
// 自定义随机数,参数会直接透传给 jsbn 库的 BigInteger 构造器
// 注意:开发者使用自定义随机数,需要自行确保传入的随机数符合密码学安全
let keypair2 = sm2.generateKeyPairHex('123123123123123')
let keypair3 = sm2.generateKeyPairHex(256, SecureRandom)
let verifyResult = sm2.verifyPublicKey(publicKey) // 验证公钥
verifyResult = sm2.verifyPublicKey(compressedPublicKey) // 验证公钥
加解密
const cipherMode = 1 // 1 - C1C3C2,0 - C1C2C3,默认为1
let encryptData = sm2.doEncrypt(msgString, publicKey, cipherMode) // 加密结果
let decryptData = sm2.doDecrypt(encryptData, privateKey, cipherMode) // 解密结果
encryptData = sm2.doEncrypt(msgArray, publicKey, cipherMode) // 加密结果,输入数组
decryptData = sm2.doDecrypt(encryptData, privateKey, cipherMode, {output: 'array'}) // 解密结果,输出数组
来测试一下:
const sm2 = require('sm-crypto').sm2
let keypair = sm2.generateKeyPairHex()
publicKey = keypair.publicKey // 公钥
privateKey = keypair.privateKey // 私钥
// 默认生成公钥 130 位太长,可以压缩公钥到 66 位
const compressedPublicKey = sm2.compressPublicKeyHex(publicKey) // compressedPublicKey 和 publicKey 等价
sm2.comparePublicKeyHex(publicKey, compressedPublicKey) // 判断公钥是否等价
// 自定义随机数,参数会直接透传给 jsbn 库的 BigInteger 构造器
// 注意:开发者使用自定义随机数,需要自行确保传入的随机数符合密码学安全
//let keypair2 = sm2.generateKeyPairHex('123123123123123')
//let keypair3 = sm2.generateKeyPairHex(256, SecureRandom)
let verifyResult = sm2.verifyPublicKey(publicKey) // 验证公钥
verifyResult = sm2.verifyPublicKey(compressedPublicKey) // 验证公钥
console.log(verifyResult)
const cipherMode = 1 // 1 - C1C3C2,0 - C1C2C3,默认为1
msgString = 'HELLO WORLD'
let encryptData = sm2.doEncrypt(msgString, publicKey, cipherMode) // 加密结果
let decryptData = sm2.doDecrypt(encryptData, privateKey, cipherMode) // 解密结果
console.log(encryptData)
console.log(decryptData)
//encryptData = sm2.doEncrypt(msgArray, publicKey, cipherMode) // 加密结果,输入数组
//decryptData = sm2.doDecrypt(encryptData, privateKey, cipherMode, {output: 'array'}) // 解密结果,输出数组
输出结果:
true
f378b1ea6ee7d8d1729ee38fc5bbe8ccc2c2813f7fc07bcad877169d57fa5e2212d994cbf4c4dff78bd58acc82304373013e5d2e793c0ad2aafc90ab2c65083af6762f53b00324f091f67df9cac9d851a31baee07dfc19114bc0188dcb7aa37c9c9f9bfb43dec6a0712225
HELLO WORLD
SM4
是我国自主设计的分组对称密码算法,用于替代DES/AES等国际算法。SM4算法与AES算法具有相同的密钥长度、分组长度,都是128bit。于2012年3月21日发布,适用于密码应用中使用分组密码的需求。
——国密算法介绍
SM4 NPM库中也给出了示例,同样照搬:
加密
const sm4 = require('sm-crypto').sm4
const msg = 'hello world! 我是 juneandgreen.' // 可以为 utf8 串或字节数组
const key = '0123456789abcdeffedcba9876543210' // 可以为 16 进制串或字节数组,要求为 128 比特
let encryptData = sm4.encrypt(msg, key) // 加密,默认输出 16 进制字符串,默认使用 pkcs#7 填充(传 pkcs#5 也会走 pkcs#7 填充)
let encryptData = sm4.encrypt(msg, key, {padding: 'none'}) // 加密,不使用 padding
let encryptData = sm4.encrypt(msg, key, {padding: 'none', output: 'array'}) // 加密,不使用 padding,输出为字节数组
let encryptData = sm4.encrypt(msg, key, {mode: 'cbc', iv: 'fedcba98765432100123456789abcdef'}) // 加密,cbc 模式
解密
const encryptData = '0e395deb10f6e8a17e17823e1fd9bd98a1bff1df508b5b8a1efb79ec633d1bb129432ac1b74972dbe97bab04f024e89c' // 可以为 16 进制串或字节数组
const key = '0123456789abcdeffedcba9876543210' // 可以为 16 进制串或字节数组,要求为 128 比特
let decryptData = sm4.decrypt(encryptData, key) // 解密,默认输出 utf8 字符串,默认使用 pkcs#7 填充(传 pkcs#5 也会走 pkcs#7 填充)
let decryptData = sm4.decrypt(encryptData, key, {padding: 'none'}) // 解密,不使用 padding
let decryptData = sm4.decrypt(encryptData, key, {padding: 'none', output: 'array'}) // 解密,不使用 padding,输出为字节数组
let decryptData = sm4.decrypt(encryptData, key, {mode: 'cbc', iv: 'fedcba98765432100123456789abcdef'}) // 解密,cbc 模式
实战
逆向
定点医疗机构查询-国家医保服务平台
注意:只做技术研究、交流!
可以看到需要逆向的参数有很多:
请求头:
表单:
响应结果:
一顿找(推荐搜索关键字):
本来想半扣的,毕竟这里面比如那个r
就是个标准的SHA256加密,可以直接用三方库,SM不就说了,当时在半扣的过程中,越扣越多,索性全扣得了
同时需要一个参数t
,直接在控制台输出t
看看:
vat t ={
"transformRequest": {},
"transformResponse": {},
"timeout": 30000,
"xsrfCookieName": "XSRF-TOKEN",
"xsrfHeaderName": "X-XSRF-TOKEN",
"maxContentLength": -1,
"headers": {
"common": {
"Accept": "application/json, text/plain, */*"
},
"delete": {},
"get": {},
"head": {},
"post": {
"Content-Type": "application/x-www-form-urlencoded"
},
"put": {
"Content-Type": "application/x-www-form-urlencoded"
},
"patch": {
"Content-Type": "application/x-www-form-urlencoded"
},
"Accept": "application/json",
"Content-Type": "application/json",
"channel": "web"
},
"withCredentials": false,
"baseURL": "/ebus/fuwu/api",
"method": "post",
"url": "/nthl/api/CommQuery/queryFixedHospital",
"data": {
"addr": "",
"regnCode": "340600",
"medinsName": "",
"medinsLvCode": "",
"medinsTypeCode": "",
"openElec": "",
"pageNum": 5,
"pageSize": 10
}
}
t
明显可直接写死,其中一些参数猜测可能为:
- regnCode:地区代码(打开此网站会检测你IP位置并在控制台输出)
- pageNum:翻页
- pageSize:一页内容量
如下所示,打开网站在开发人员工具开启下重新刷新页面,会看到地区检测结果:
如果想要获取到其它位置,自行研究API了
将其封装进代码,最终Webpack代码6W行,如果想将生成相关的代码自己扣下来的话(因为这段代码被包含在了Webpack中)同时还要注意去掉(仅自己封装用于生成表单和请求头的代码)try-catch
语句,不然少了参数都不知道。
因为生成加密参数的代码被包含在Webpack中"7d92"
,所以可以试试不自己扣(因为上面所有的Webpack代码已经全扣了),那么要如何调用呢?
可以知道代码在"7d92"
,那么先输出一下webpack("7d92")
(这里是自行导出了加载器,详见Webpack文章)
console.log(webpack("7d92"))
结果:
{ a: [Getter], b: [Getter] }
选择a
再输出:
console.log(webpack("7d92").a)
结果:
[Function: f]
可以看到a
为一个函数,并且其名字似乎预示选对了
再调用一下函数:
console.log(webpack("7d92").a())
结果(只截取片段):
return t.headers["x-tif-paasid"] = l.paasId,
发现报错,很明显,这里便是我们要找的加密方法,把上面的找到的t
塞进去看看结果:
console.log(webpack("7d92").a(t))
结果:
{
transformRequest: {},
transformResponse: {},
timeout: 30000,
xsrfCookieName: 'XSRF-TOKEN',
xsrfHeaderName: 'X-XSRF-TOKEN',
maxContentLength: -1,
headers: {
common: { Accept: 'application/json, text/plain, */*' },
delete: {},
get: {},
head: {},
post: { 'Content-Type': 'application/x-www-form-urlencoded' },
put: { 'Content-Type': 'application/x-www-form-urlencoded' },
patch: { 'Content-Type': 'application/x-www-form-urlencoded' },
Accept: 'application/json',
'Content-Type': 'application/json',
channel: 'web',
'x-tif-paasid': undefined,
'x-tif-signature': '092eeb43442f3633e5d22790695b39264f0f0834e14f60dfe99d514e95a2cf93',
'x-tif-timestamp': 1672929556,
'x-tif-nonce': 'Y4aUfjoK',
contentType: 'application/x-www-form-urlencoded'
},
withCredentials: false,
baseURL: '/ebus/fuwu/api',
method: 'post',
url: '/nthl/api/CommQuery/queryFixedHospital',
data: '{"data":{"data":{"encData":"3DFBCA4667B978F639BB23B95DCE4CC73F6F89D76EF932292FE8BA14BC87DB17CCD20943B4DAE96380B41164D761DE9742C84A985FE3BABC31CB352556BB87C9C1495DB24A29AB6BC3A85AB7FCA00F338EE714ACFC4C924F01CF575098AEF167B7A135B72DAC6C2F3CD510E411A3C63A7D334AF16D66C5F4C5E72412B915CB27"},"appCode":"T98HPCGN5ZVVQBS8LZQNOAEXVI9GYHKQ","version":"1.0.0","encType":"SM4","signType":"SM2","timestamp":1672929556,"signData":"2+IZoksuP1rgU4LhVryfmBs1icvANuGmjR3FNk95bJyTjyqm3TK66ts3oV7L1d3Aot5RXvB7GRCK9QjnypILxw=="}}'
}
成功拿到需要的数据!
暂时放下,来看看结果如何处理。
最终找到此处:
此时e.data
还是原始响应结果,并未解密:
{
"code": 0,
"data": {
"signData": "+EJhLYxiMpbwjGCsEvK+wyXNoAyMKpkGnjb7tGZ3OoFNWqKzDjIeDfqE9ZSO0anEt3z4rNoFJ5lgHxeDuddVdg==",
"encType": "SM4",
"data": {
"encData": "2DC8C08C5C251B04B9CCBA9436917607527A4CC8F48CC3C8D7C226EB9483D8FA03CECA79ABA4AC1CE5 ....篇幅有限,这里省略.....FCF4D05388AA6A2E4E171B6486547C2E9DB06B68A81A61409372E7E00A0D1691A2F222D7698DCAC"
},
"signType": "SM2",
"appCode": "T98HPCGN5ZVVQBS8LZQNOAEXVI9GYHKQ",
"version": "1.0.0",
"timestamp": "1672919504404"
},
"message": "成功",
"timestamp": "1672919504",
"type": "success"
}
复制到本地备后测试使用。
同时这段代码也被包含在Webpack中,可以选择不扣,不过因为实际涉及解密的代码都集中在了一行上,这里选择半扣:
function get_data(data){
const e = {data:data},
c = webpack("7d92");
return e.data.data.appCode && (e.data.data = Object(c.b)("SM4", e.data)),e.data
}
看到这里可以猜测,webpack("7d92")
里a
负责加密,b
负责解密
将备好的数据放进去看运行结果:
成功解密
简单封装下代码,准备模拟请求
模拟请求
这里继续使用Python来模拟请求,只爬取一页数据
同时要注意,从js代码获取的值需要进行一些处理才能让requests
得以正确传递,
同时如果遇到UnicodeEncodeError: 'gbk' codec...
问题请参照此文章:使用PyExecJS为Python爬虫提供加密参数
完整代码请见仓库 国家医疗保障服务平台-SM/gjyb
代码仅作学习交流之用,请勿用于非法用途!