SM系列
1、国家加密算法介绍
事实上从 2010 年开始,我国国家密码管理局就已经开始陆续发布了一系列国产加密算法,其中SM1、SM4、SM7、祖冲之密码(ZUC)是对称算法;SM2、SM9是非对称算法;SM3是哈希算法。目前,这些算法已广泛应用于各个领域中。其中SM2、SM3、SM4
三种加密算法是比较常见的
2、算法分类
算法名称 | 算法类别 | 应用领域 | 特点 |
---|---|---|---|
SM1 | 对称(分组)加密算法 | 芯片 | 分组长度、密钥长度均为 128 比特,算法安全保密强度及相关软硬件实现性能与AES相当 |
SM2 | 非对称(基于椭圆曲线 ECC)加密算法 | 数据加密 | ECC 椭圆曲线密码机制 256 位,相比 RSA 处理速度快,消耗更少,SM2算法在很多方面都优于RSA算法(RSA发展的早应用普遍,SM2领先也很正常) |
SM3 | 散列(hash)函数算法 | 完整性校验 | 安全性及效率与 SHA-256 相当,压缩函数更复杂, 此算法适用于商用密码应用中的数字签名和验证,消息认证的生成与验证以及随机函数的生成,可满足多种密码应用的安全需求。 |
SM4 | 对称(分组)加密算法 | 数据加密和局域网产品 | 该算法的分组长度为128比特,密钥长度为128比特。加密算法与密钥扩展算法都采用32轮非线性迭代结构 |
SM7 | 对称(分组)加密算法 | 非接触式 IC 卡 | 分组长度为128比特,密钥长度为128比特 |
SM9 | 标识加密算法(IBE) | 端对端离线安全通讯 | SM9算法不需要申请数字证书,适用于互联网应用的各种新兴应用的安全保障 |
ZUC | 对称(序列)加密算法 | 移动通信 4G 网络 | 祖冲之序列密码算法是中国自主研究的流密码算法,是运用于移动通信4G网络中的国际标准密码算法,该算法包括祖冲之算法(ZUC)、加密算法(128-EEA3)和完整性算法(128-EIA3)三个部分。目前已有对ZUC算法的优化实现,有专门针对128-EEA3和128-EIA3的硬件实现与优化 |
3、算法实现
JavaScript实现
在 JavaScript 中已有比较成熟的实现库,这里推荐 sm-crypto[4]
,目前支持 SM2、SM3 和 SM4
,需要注意的是,SM2 非对称加密的结果由 C1、C2、C3
三部分组成,其中 C1
是生成随机数的计算出的椭圆曲线点,C2
是密文数据,C3
是 SM3
的摘要值,最开始的国密标准的结果是按 C1C2C3
顺序的,新标准的是按 C1C3C2
顺序存放的,sm-crypto
支持设置 cipherMode
,也就是 C1C2C3
的排列顺序。
// cnpm install sm-crypto --save
const sm2 = require('sm-crypto').sm2
// 1: C1C3C2,0: C1C2C3,默认为1
const cipherMode = 1
// 生成密钥对
let keypair = sm2.generateKeyPairHex()
let publicKey = keypair.publicKey // 公钥
let privateKey = keypair.privateKey // 私钥
let msgString = "待加密数据"
let encryptData = sm2.doEncrypt(msgString, publicKey, cipherMode) // 加密结果
let decryptData = sm2.doDecrypt(encryptData, privateKey, cipherMode) // 解密结果
console.log("encryptData: ", encryptData)
console.log("decryptData: ", decryptData)
Python实现
使用之前先安装三方库:pip install gmssl
from gmssl import sm2
# 16 进制的公钥和私钥
private_key = '2423c43b9d3a5a8aa41c983ef30231986ea38398b8a36f082057478f0ef23a13'
public_key = '045cfaca8c4fc5af189bbe36f831017daa51dd5116c1118affda2185aaec53a8cbc80762f1b7df97c7ac004194f721cb2862e493945f9dc86a53c06fe00fbd6273'
sm2_crypt = sm2.CryptSM2(public_key=public_key, private_key=private_key)
# 待加密数据和加密后数据为 bytes 类型
data = b"I love Python"
enc_data = sm2_crypt.encrypt(data)
dec_data = sm2_crypt.decrypt(enc_data)
print('enc_data: ', enc_data.hex())
print('dec_data: ', dec_data)
关于算法还原,大家尽量能用javascript实现尽量用javascript,当然这只是个人建议,毕竟不同的程序猿对语言的偏好不同
具体案例
1、逆向目标
- 目标:
某某某
- 主页:
https://fuwu.nhsa.gov.cn/nationalHallSt/#/search/medical?code=90000&flag=false&gbFlag=true
- 接口:
https://fuwu.nhsa.gov.cn/ebus/fuwu/api/nthl/api/CommQuery/queryFixedHospital
- 逆向参数:
- 头部加密:
X-Tif-Nonce、X-Tif-Paasid、X-Tif-Signature、X-Tif-Timestamp、X-Tingyun
- pyaload:
encData、signData
- response:
encData、signData
- 头部加密:
2、逆向过程
老规矩,直接翻页观察网络请求,然后比对两次的请求结果,哪些参数是加密的显而易见了
3、逆向分析
3.1 我们先来分析头部参数加密
对这种请求头加密,我们先挑一个比较有特点的加密参数搜索看下,这里我们选择 X-Tif-Signature
进行搜索尝试,发现只有两条结果
这就好办了,每个地方都打一个断点,然后翻页,发现断点断住了,如下
function f(t) {
var r = n("6c27").sha256
, s = Math.ceil((new Date).getTime() / 1e3)
, h = Object(i.a)()
, f = s + h + s;
return t.headers["x-tif-paasid"] = l.paasId,
t.headers["x-tif-signature"] = r(f),
t.headers["x-tif-timestamp"] = s,
t.headers["x-tif-nonce"] = h,
t.headers.Accept = "application/json",
t.headers.contentType = "application/x-www-form-urlencoded",
t.data = {
data: t.data || {}
},
t.data.appCode = l.appCode,
t.data.version = l.version,
t.data.encType = "SM4",
t.data.signType = "SM2",
t.data.timestamp = s,
t.data.signData = function(t) {
try {
var n = m(t.data)
, i = p(n);
i.data = p(i.data);
var r = v(i)
, a = o.doSignature(r, d, {
hash: !0
});
return e.from(a, "hex").toString("base64")
} catch (e) {}
}(t),
t.data.data = {
encData: function(e, t) {
switch (e.toUpperCase()) {
case "SM2":
return function(e) {
try {
var t = o.generateKeyPairHex()
, n = t.publicKey
, i = e;
o.doEncrypt(i, n, 1)
} catch (e) {}
}(t);
case "SM3":
return function(e) {
try {
var t = a(e);
return t
} catch (e) {}
}(t);
case "SM4":
return function(e) {
try {
for (var t = e.data.data && JSON.stringify(e.data.data), n = "", i = 0; i < t.length; i++) {
var r = t.charAt(i)
, o = t.charCodeAt(i);
n += o > 127 ? "\\u" + o.toString(16).padStart(4, "0") : r
}
var a = A(n);
e.data.appCode && e.data.appCode !== u && (u = e.data.appCode);
var s = y(u, c)
, l = b(s, a);
return l.toUpperCase()
} catch (e) {}
}(t)
}
}("SM4", t)
},
t.data = JSON.stringify({
data: t.data
}),
t
}
请求头的参数加密逻辑可以说是一目了然了,如下
var s = Math.ceil((new Date).getTime() / 1e3), h = i(),f = s + h + s;
t.headers["x-tif-timestamp"] = s
t.headers["x-tif-nonce"] = h
t.headers["x-tif-signature"] = r(f)
从上面代码我们看出 r 应该是一个sha256的加密参数,我们直接用CryptoJS模块做下验证
用CryptoJS对 f
的值做 SHA256
加密
> var CryptoJS = require('crypto-js');
undefined
> CryptoJS.SHA256('1705906542B4qU1fDP1705906542').toString();
'43b186cd11580d55ba684f7fdecbf7f52c1c5b18d5df3a23e868aed2d13a8029'
>
最后看下 X-Tingyun
参数,仍然先搜索这个加密参数,发现有两条结果,比对分析后排除了第二条结果
然后我们再在文件里搜索 wo
这个参数,只有这一个地方使用,我们在这打个断点然后继续向下执行,发现在断点处断住了
我们直接根据代码分析加密逻辑
// 前文中比对了翻页后的两次网络请求,判断Gu.key是一个固定值:4Nl_NnGbjwY
Gu.key = '4Nl_NnGbjwY';
var a = Ir(), i = "c=B|" + Gu.key;
function Ir() {
try {
return Zi().substring(0, 16)
} catch (t) {}
}
Zi = (e.location.protocol,
function() {
function t(t) {
return 0 > t ? NaN : 30 >= t ? 0 | Math.random() * (1 << t) : 53 >= t ? (0 | Math.random() * (1 << 30)) + (0 | Math.random() * (1 << t - 30)) * (1 << 30) : NaN
}
function e(t, e) {
for (var n = t.toString(16), r = e - n.length, a = "0"; r > 0; r >>>= 1,
a += a)
1 & r && (n = a + n);
return n
}
return function(n) {
return n || (n = ""),
e(t(32), 8) + n + e(t(16), 4) + n + e(16384 | t(12), 4) + n + e(32768 | t(14), 4) + n + e(t(48), 12)
}
}());
Zi
函数改写如下
function Zi(){
function t(t) {
return 0 > t ? NaN : 30 >= t ? 0 | Math.random() * (1 << t) : 53 >= t ? (0 | Math.random() * (1 << 30)) + (0 | Math.random() * (1 << t - 30)) * (1 << 30) : NaN
}
function e(t, e) {
for (var n = t.toString(16), r = e - n.length, a = "0"; r > 0; r >>>= 1,
a += a)
1 & r && (n = a + n);
return n
}
var ret = function(n) {
return n || (n = ""),
e(t(32), 8) + n + e(t(16), 4) + n + e(16384 | t(12), 4) + n + e(32768 | t(14), 4) + n + e(t(48), 12)
}();
console.log(ret.substring(0, 16));
}
还有一个 appCode
是一个固定字符串
至此,所有的请求头加密参数全部分析完毕
3.2 payload加密参数
先来分析 signData
,可以看到使用的是 sm2
加密算法,
t.data.signData = function(t) {
try {
var n = m(t.data)
, i = p(n);
i.data = p(i.data);
var r = v(i)
, a = o.doSignature(r, d, {
hash: !0
});
return e.from(a, "hex").toString("base64")
} catch (e) {}
}(t)
var i = n("1602")
, r = n("68b2")
, o = r.sm2
, a = r.sm3
, s = r.sm4
, l = (n("94f8"),
{
appCode: "T98HPCGN5ZVVQBS8LZQNOAEXVI9GYHKQ",
version: "1.0.0",
appSecret: "NMVFVILMKT13GEMD3BKPKCTBOQBPZR2P",
publicKey: "BEKaw3Qtc31LG/hTPHFPlriKuAn/nzTWl8LiRxLw4iQiSUIyuglptFxNkdCiNXcXvkqTH79Rh/A2sEFU6hjeK3k=",
privateKey: "AJxKNdmspMaPGj+onJNoQ0cgWk2E3CYFWKBJhpcJrAtC",
publicKeyType: "base64",
privateKeyType: "base64"
}
sm2
加密所涉及的 publicKey、privateKey
,都已在代码中明文给出,所以我们只需要弄清楚用于加密的明文是什么即可了,我们先来看下 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-signature": "43c7aa1773742fc396f5cd85086c0b0c353a63fe6c35138087c0fafb34a7ea0a",
"x-tif-timestamp": 1705908253,
"x-tif-nonce": "J6YWpOnL",
"contentType": "application/x-www-form-urlencoded"
},
"withCredentials": false,
"baseURL": "/ebus/fuwu/api",
"method": "post",
"url": "/nthl/api/CommQuery/queryFixedHospital",
"data": {
"data": {
"addr": "",
"regnCode": "320100",
"medinsName": "",
"medinsLvCode": "",
"medinsTypeCode": "",
"openElec": "",
"pageNum": 4,
"pageSize": 10,
"queryDataSource": "es"
},
"appCode": "T98HPCGN5ZVVQBS8LZQNOAEXVI9GYHKQ",
"version": "1.0.0",
"encType": "SM4",
"signType": "SM2",
"timestamp": 1705908253
}
}
然后按照以下代码逻辑处理后就是最终用于计算加密的值
var n = m(t.data),
i = p(n);
i.data = p(i.data);
var r = v(i),
a = o.doSignature(r, d, {
hash: !0
});
// 可以直接把这几个函数的源码 copy 过来直接用
function m(e) {
var t = {}
, n = ["signData", "encData", "extra"];
for (var i in e)
e.hasOwnProperty(i) && !n.includes(i) && null != e[i] && (t[i] = e[i]);
return t
}
function p(e) {
var t = new Array
, n = 0;
for (var i in e)
t[n] = i,
n++;
var r = [].concat(t).sort()
, o = {};
for (var a in r)
o[r[a]] = e[r[a]];
return o
}
function v(e) {
var t = [];
for (var n in e)
if (e.hasOwnProperty(n) && (e[n] || "".concat(e[n])))
if ("data" === n) {
var i = Object.assign({}, e[n]);
for (var r in i) {
if ("number" != typeof i[r] && "boolean" != typeof i[r] || (i[r] = "" + i[r]),
Array.isArray(i[r]) && !i[r].length && delete i[r],
Array.isArray(i[r]) && i[r].length > 0)
for (var o = 0; o < i[r].length; o++)
i[r][o] = p(i[r][o]);
null != i[r] && i[r] || delete i[r]
}
var a = p(i);
t.push("".concat(n, "=").concat(JSON.stringify(a)))
} else
t.push("".concat(n, "=").concat(e[n]));
return t.push("key=".concat(c)),
t.join("&")
}
加密的逻辑我们已经很清楚了,现在直接做下验证
const sm2 = require('sm-crypto').sm2;
var privatekey = '009c4a35d9aca4c68f1a3fa89c93684347205a4d84dc260558a049869709ac0b42';
var data = 'appCode=T98HPCGN5ZVVQBS8LZQNOAEXVI9GYHKQ&data={"pageNum":"4","pageSize":"10","queryDataSource":"es","regnCode":"320100"}&encType=SM4&signType=SM2×tamp=1705908253&version=1.0.0&key=NMVFVILMKT13GEMD3BKPKCTBOQBPZR2P';
var result = sm2.doSignature(data, privatekey, {hash: 1});
console.log(result);
result: d3fc3a2ef0ca66500ee1988ec8ca366a11c29b3a4853984258dc3e8a1caa27bf7caca8346eac0f4f1288e5016c05bf306b94fa58bc7d6961cb96cc21b16d96cf
# 后面应该还要做个 base64,这里我们就不验证了,大家感兴趣的可以去补全一下
继续看 encData
加密,从前面接口参数分析中可以看到数据加密方式是 SM4
方式
我们简单分析下 encData
的加密逻辑
try {
for (var t = e.data.data && JSON.stringify(e.data.data), n = "", i = 0; i < t.length; i++) {
var r = t.charAt(i)
, o = t.charCodeAt(i);
n += o > 127 ? "\\u" + o.toString(16).padStart(4, "0") : r
}
var a = A(n);
e.data.appCode && e.data.appCode !== u && (u = e.data.appCode);
var s = y(u, c)
, l = b(s, a);
return l.toUpperCase()
} catch (e) {}
function y(e, t) {
return A(b(A(e.substr(0, 16)), A(t)).toUpperCase().substr(0, 16))
}
function b(t, n) {
var i = 16 - parseInt(n.length % 16);
n = n.concat(new Array(i).fill(i));
var r = s.encrypt(n, t);
return e.from(r).toString("hex")
}
function A(e) {
var t, n, i = new Array;
t = e.length;
for (var r = 0; r < t; r++)
(n = e.charCodeAt(r)) >= 65536 && n <= 1114111 ? (i.push(n >> 18 & 7 | 240),
i.push(n >> 12 & 63 | 128),
i.push(n >> 6 & 63 | 128),
i.push(63 & n | 128)) : n >= 2048 && n <= 65535 ? (i.push(n >> 12 & 15 | 224),
i.push(n >> 6 & 63 | 128),
i.push(63 & n | 128)) : n >= 128 && n <= 2047 ? (i.push(n >> 6 & 31 | 192),
i.push(63 & n | 128)) : i.push(255 & n);
return i
}
上述代码中我们只需要关心 var r = s.encrypt(n, t);
中的 s
是哪来的,我们把思维向前放一放就发现了,如下图,很明显是一个 SM4
加密
至此,payload
的所有加密参数我们分析完毕,response
的参数加密分析逻辑和 payload
完全一致,大家感兴趣的可以自行分析
4、逆向总结
回顾整个流程,其实没有什么难度,入口也很好找,参数的加密逻辑也不复杂
原创声明:未经许可,不得转载。
如有侵权,请联系作者删除删除。