逆向目标
抓包分析
进入网站后,点击加载更多,会发送一个xzxkfr数据包。
其中请求参数中的queryContent和sign需要逆向分析,nonceStr只是一个随机字符串。

1.png (38.67 KB, 下载次数: 0)
下载附件
2025-3-15 18:41 上传
响应头为application/octet-stream,因此响应数据的解密也需要逆向分析。

2.png (37.56 KB, 下载次数: 0)
下载附件
2025-3-15 18:41 上传

3.png (32.9 KB, 下载次数: 0)
下载附件
2025-3-15 18:41 上传
逆向分析
话不多说,直接开始逆向。
加密
刷新网站,尝试搜索关键词queryContent,在某个JS文件中搜索到。

4.png (75.23 KB, 下载次数: 0)
下载附件
2025-3-15 18:41 上传
跳转到文件对应的地方下断,点击加载更多,成功断住,此时,参数queryContent已经生成,为i。

5.png (28.84 KB, 下载次数: 0)
下载附件
2025-3-15 18:41 上传
我们往前看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。

6.png (10.67 KB, 下载次数: 0)
下载附件
2025-3-15 18:41 上传
我们直接跟进去encrypt函数,其中t是明文,e是key。

7.png (10 KB, 下载次数: 0)
下载附件
2025-3-15 18:41 上传
我们可以通过代码的一些特征去网上搜索或者问AI,看有什么库能够实现这种方式的加密(这里是SM4)。
我就直接贴代码了(因为我已经搜索过了)。
const sm4 = require('sm-crypto').sm4
const msg = '' // 可以为 utf8 串或字节数组
const key = '' // 可以为 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 模式
那我们直接尝试通过nodejs模拟一下。
const sm4 = require('sm-crypto').sm4
const msg = '' // 明文
const key = '' // key
console.log(sm4.encrypt(msg, key, {output: 'string'}))
对比一下网页上和本地的执行结果,发现是一致的,那queryContent加密的模拟就完成了。

8.png (13.06 KB, 下载次数: 0)
下载附件
2025-3-15 18:41 上传

9.png (18.04 KB, 下载次数: 0)
下载附件
2025-3-15 18:41 上传
有的时候并不是那么顺利,可能还需要尝试padding、mode之类的,如果是魔改的,那就另当别论了。
我们继续跟sign的加密逻辑,n.sign = o,且signType为SM2,那应该是需要跟o = Zi(o = ls.sm2.signature(a, e.appSignPrivateKey, e.appSignPublicKey, e.appId))的逻辑。
其中a是包括queryContent在内的一系列参数组成的字符串,代码可以直接扣。

10.png (16.97 KB, 下载次数: 0)
下载附件
2025-3-15 18:41 上传
我们继续跟signature这个函数,可以发现应该是个签名算法。

11.png (9.6 KB, 下载次数: 0)
下载附件
2025-3-15 18:42 上传
还是老规矩,网上搜
const sm2 = require('sm-crypto').sm2
const msgString = ''
const privateKey = ''
const userId = ''
let sigValueHex6 = sm2.doSignature(msgString, privateKey, {
hash: true,
publicKey,
userId, // 默认 userId 为 1234567812345678
})
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)
}
参数模拟完成,我们直接请求接口看能不能返回数据。
可以看到,成功返回数据,说明参数模拟没有问题。

12.png (117.01 KB, 下载次数: 0)
下载附件
2025-3-15 18:42 上传
解密
下面我们再看响应数据的解密。
既然接口是xhr请求,那我们就尝试搜索响应拦截器interceptors.response.use,可以在某个JS文件中搜索到。

13.png (71.27 KB, 下载次数: 0)
下载附件
2025-3-15 18:42 上传
我们直接跳去源文件,看起来像是个异步,具体就不跟了,解密的逻辑就在下图所指的oo函数,其中r就是响应,e对象包含了解密所需key等信息。

14.png (31.01 KB, 下载次数: 0)
下载附件
2025-3-15 18:42 上传
我们直接跟进去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("验签失败"));
// 数据解密(SM4)
var c = "{}";
c = kr.sm4.decrypt(function(t) {
for (var e = "", r = 0; r
我们扣代码的时候,并不知道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
验证签名时的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
最后,我们直接模拟请求接口,通过导库实现各种加解密算法,进行最终数据的获取。
我们先对响应进行base64,然后在JS中将base64字符串转为arraybuffer
// 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
模拟请求:

15.png (190.47 KB, 下载次数: 0)
下载附件
2025-3-15 18:42 上传
成功!!!