文章主要分享的内容是本人在逆向过程中遇到标准加解密算法时使用各种库来模拟还原的一些经验,因本人并不擅长密码学,请路过的各位大佬多多指教。
逆向目标
- 网址:
aHR0cHM6Ly9jcmVkaXQuaGQuZ292LmNuL3h5eHhncy8=
- 目标:接口参数加密,响应数据解密
抓包分析
进入网站后,点击加载更多,会发送一个xzxkfr
数据包。
其中请求参数中的queryContent
和sign
需要逆向分析,nonceStr
只是一个随机字符串。
响应头为application/octet-stream
,因此响应数据的解密也需要逆向分析。
逆向分析
话不多说,直接开始逆向。
加密
刷新网站,尝试搜索关键词queryContent
,在某个JS
文件中搜索到。
跳转到文件对应的地方下断,点击加载更多,成功断住,此时,参数queryContent
已经生成,为i
。
我们往前看i
如何生成。可以知道i && null != r.headers && "1" === r.headers["C-GATEWAY-QUERY-ENCRYPT"] && n.encryptType === P.ENCRYPT_TYPE.SM4
这个条件是成立的,因此会走i = ls.sm4.encrypt(i, e.encryptKey)
的逻辑。其实也可以在i = ls.sm4.encrypt(i, e.encryptKey)
和i = us(i, e.encryptKey)
处分别下断后重新请求,然后单步跟,看走哪个函数。
可以看到传入了请求参数的字符串以及SM4
加密的key
,然后生成了queryContent
。
我们直接跟进去encrypt
函数,其中t
是明文,e
是key
。
我们可以通过代码的一些特征去网上搜索或者问AI,看有什么库能够实现这种方式的加密(这里是SM4
)。
我就直接贴代码了(因为我已经搜索过了)。
const sm4 = require('sm-crypto').sm4
const msg = ''
const key = ''
let encryptData = sm4.encrypt(msg, key)
let encryptData = sm4.encrypt(msg, key, {padding: 'none'})
let encryptData = sm4.encrypt(msg, key, {padding: 'none', output: 'array'})
let encryptData = sm4.encrypt(msg, key, {mode: 'cbc', iv: 'fedcba98765432100123456789abcdef'})
那我们直接尝试通过nodejs
模拟一下。
const sm4 = require('sm-crypto').sm4
const msg = ''
const key = ''
console.log(sm4.encrypt(msg, key, {output: 'string'}))
对比一下网页上和本地的执行结果,发现是一致的,那queryContent
加密的模拟就完成了。
有的时候并不是那么顺利,可能还需要尝试padding
、mode
之类的,如果是魔改的,那就另当别论了。
我们继续跟sign
的加密逻辑,n.sign = o
,且signType
为SM2
,那应该是需要跟o = Zi(o = ls.sm2.signature(a, e.appSignPrivateKey, e.appSignPublicKey, e.appId))
的逻辑。
其中a
是包括queryContent
在内的一系列参数组成的字符串,代码可以直接扣。
我们继续跟signature
这个函数,可以发现应该是个签名算法。
还是老规矩,网上搜
const sm2 = require('sm-crypto').sm2
const msgString = ''
const privateKey = ''
const userId = ''
let sigValueHex6 = sm2.doSignature(msgString, privateKey, {
hash: true,
publicKey,
userId,
})
SM2
是一种基于椭圆曲线公钥密码算法的标准,由中国国家密码管理局发布,每次加密时会生成一个新的随机数,且参与密文的生成过程。因此,我们无法确定本地模拟的参数是否正确,那我们可以通过请求接口,如果返回数据了,就表示参数没问题。
SM2
的结果有了,那我们还需要跟一下Zi
这个函数,才能拿到最终的sign
值从而模拟请求接口。
Zi
可以直接扣代码就行,需要注意的是,扣代码过程中遇到的Xi
和Yi
函数内的BigInteger
可以通过导库实现,具体还是网上搜索,直接贴代码了。
const BigInteger = require('jsbn').BigInteger;
function Xi(t) {
return new BigInteger(t,10).toString(16)
}
function Yi(t) {
return new BigInteger(t, 16).toString(10)
}
参数模拟完成,我们直接请求接口看能不能返回数据。
可以看到,成功返回数据,说明参数模拟没有问题。
解密
下面我们再看响应数据的解密。
既然接口是xhr
请求,那我们就尝试搜索响应拦截器interceptors.response.use
,可以在某个JS
文件中搜索到。
我们直接跳去源文件,看起来像是个异步,具体就不跟了,解密的逻辑就在下图所指的oo
函数,其中r
就是响应,e
对象包含了解密所需key
等信息。
我们直接跟进去oo
函数,把一些条件判断去掉后的逻辑代码如下,之后缺什么扣什么就行,t.data
是响应的arraybuffer
,解密还是用的SM4
。
var r = t.data
, n = new DataView(r)
, i = new Uint8Array(r)
, s = {}
, a = 40;
D.forEach((function(t, e) {
var r = n.getInt32(4 * e);
s[t] = i.subarray(a, a + r),
a += r
}
));
var o = ao(s, e)
, u = o[0]
, h = o[1];
if (!u)
return Promise.reject(new Error("验签失败"));
var c = "{}";
c = kr.sm4.decrypt(function(t) {
for (var e = "", r = 0; r < t.length; r++) {
var n = t[r].toString(16);
1 === n.length && (n = "0" + n),
e += n
}
return e
}(s.body), e.encryptKey, {
output: "string"
});
我们扣代码的时候,并不知道var o = ao(s, e)
这里是在验证响应数据的签名,所以会跟着扣。
其中遇到一些需要注意的点是:
Qi
函数可以导库实现
function Qi(t) {
var e = Ai.lib.WordArray.create(t);
return Ai.MD5(e).toString(Ai.enc.Hex)
}
const CryptoJS = require('crypto-js')
function Qi(t) {
var e = CryptoJS.lib.WordArray.create(t);
return CryptoJS.MD5(e).toString(CryptoJS.enc.Hex)
}
ji
函数在nodejs
可以直接模拟
var Pi = "function" == typeof Buffer
var ji = Pi ? function(t) {
return Buffer.from(t).toString("base64")
}
: function(t) {
for (var e = [], r = 0, n = t.length; r < n; r += 4096)
e.push(Ri.apply(null, t.subarray(r, r + 4096)));
return Hi(e.join(""))
}
var ji = function(){
return Buffer.from(t).toString("base64")
}
验证签名时的SM2
算法
Vr.doVerifySignature(a, Zi(i), e.platformPublicKey, {
hash: !0,
userId: e.appId
})
const sm2 = require('sm-crypto').sm2
sm2.doVerifySignature(a, Zi(i), e.platformPublicKey, {
hash: !0,
userId: e.appId
})
然后就是最终的SM4
解密了,既然前面的加密能够在网上搜索到,那解密也不在话下了。
c = kr.sm4.decrypt(function(t) {
if (!(t instanceof Uint8Array))
throw new Error("Invalid Uint8Array");
for (var e = "", r = 0; r < t.length; r++) {
var n = t[r].toString(16);
1 === n.length && (n = "0" + n),
e += n
}
return e
}(s.body), e.encryptKey, {
output: "string"
})
const sm4 = require('sm-crypto').sm4
sm4.decrypt(function (t) {
for (var e = "", r = 0; r < t.length; r++) {
var n = t[r].toString(16);
1 === n.length && (n = "0" + n),
e += n
}
return e
}(s.body), e.encryptKey, {
output: "string"
})
最后,我们直接模拟请求接口,通过导库实现各种加解密算法,进行最终数据的获取。
我们先对响应进行base64
,然后在JS
中将base64
字符串转为arraybuffer
function base64ToArrayBuffer(base64) {
var binary_string = atob(base64);
var len = binary_string.length;
var bytes = new Uint8Array(len);
for (var i = 0; i < len; i++) {
bytes[i] = binary_string.charCodeAt(i);
}
return bytes.buffer;
}
模拟请求:
成功!!!