
在开发彩票自动投注脚本中的一个关键技术发现。初始抓包数据显示请求主体包含大段不明字符串,初步判断可能采用某种加密算法。然而,经过系统的逆向工程分析,我们确认该数据并未使用任何加密技术,其本质是通过数据压缩配合Base64编码实现的标准化处理。
本文将通过一个彩票脚本的案例,完整展示一次JS压缩加密逆向的典型流程,其核心关键在于精准判断数据处理的本质是‘加密’还是‘编码’。
1. 数据抓包:发现神秘字符串
首先打开目标彩票网页,使用F12打开浏览器抓包工具,切换到网络面板。点击投注按钮后,通过网页抓包观察请求结果。
在请求列表中,我们发现了一个关键接口:
目标接口:/tools/_ajax/OG15FT/betSingle
请求方法:POST
仔细观察请求主体,发现了一长串神秘的字符串:
H4sIAAAAAAAAA02PQWvDMAyF/4vOYUi2lTg5rrBRBtthZZemBzcRWyBORuMcQtv/PsdkUKHDQ+8TvHcF1zTjPIR9C5XRBTMZxRk0fSdDOHReoKIiV6Vmzk2pbAbfzstKw8cr8csBMuimaY4cKFRMRCpnvV6DeKiOcK3BS/gZ266toarh+XOHaBApbg1ZDcPsp+hQlBc5uyAJwyfEZIcY4t/34yBL1CrpVrZzCvA+pr+HEOm9idiUnNs2aG7J+e3dsl8zHU93OGUgQ3NZ3mSJTR4rl9qi0siaDeVsFWzkl+tTayzZliQOrSY6tyTGFOZcFFaLcC4a7n9RrEEnZQEAAA==
这串字符串没有参数名,就是单纯的一大段编码内容,看起来像是某种加密结果。

2. XHR断点:定位加密入口
要追踪这个无特征字符串的来历,我们需要借助XHR断点。打开“来源”面板,在“XHR断点”处点击+号,设置URL包含betSingle的断点规则。
点击投注按钮触发断点后,在调用堆栈中分析代码,发现关键线索:
var B = A ? t.Requirement : k(t, I);fetch(c, {
credentials: "same-origin",
method: "POST",
cache: "no-store",
headers: s()({}, A ? {} : {
"Content-Type": "application/json"
}, P && P.nFrontKey, {
"User-UUID": window.__uuid
}),
body: B // 请求主体由B参数赋值})
从代码中可以看到,请求主体body的值来自变量B,而B又是由k(t, I)函数生成的。这就是我们的突破口!
3. 函数追踪:分析k函数逻辑
接下来我们在k函数处设置断点,跟进分析其内部逻辑:
function k(t, e) {
return !0 === t.noAes || (i = t.Action) && a.i(w.c)(i) && S.some(function(t) {
return t.test(i)
}) || y.a.state.__no_aes ? u()(e) : function(t) {
var e = t;
return void 0 !== t && null !== t && (e =a.i(b.b)(u()(t))),
e
}(e);
var i
}
分析这段代码,我们发现最重要的部分是:
e = a.i(b.b)(u()(t))
通过控制台打印分析,我们得出:
u()(t)实际上就是JSON.stringify(t),将对象转为JSON字符串a.i(b.b)返回的是b.b函数本身- 所以整个表达式等价于:
b.b(JSON.stringify(t))
原始的参数t是一个投注信息对象:
{
"accountId": 437551425,
"clientTime": 1762941491259,
"gameId": "OG15FT",
"issue": "20251112719",
"item": [
"{"methodid":"BSC004001001","nums":1,"rebate":"0.00","times":1,"money":2,"mode":1,"issueNo":"20251112719","codes":"||||||||04|","playId":[]}"
],
"encryKey": "176294149126085458567108297",
"encryValue": "0ec063797926b4050330e283a53ab041"
}
4. 核心分析:揭开压缩编码面纱
现在进入最关键的b.b函数:
e.b = function(t) {
try {
var e = (new TextEncoder).encode(t) // 第一步:Uint8Array编码
, s = a.i(i.a)(e); // 第二步:压缩处理
return function(t) { // 第三步:Base64编码
for (var e = "", a = 0; a < t.length; a++)
e += String.fromCharCode(t[a]);
return btoa(e)
}(s)
} catch (t) {
throw new Error("Compression failed: " + t.message + ", ",t)
}
}
分析这个函数,我们发现了完整的处理流程:
- Uint8Array编码:使用
TextEncoder将字符串转为字节数组 - 数据压缩:通过
a.i(i.a)(e)进行压缩处理 - Base64编码:将压缩后的字节数组转为Base64字符串
前两步和后一步都有原生JavaScript函数支持,关键在于第二步的压缩处理。
5. 关键发现:定位gzip压缩函数
跟进i.a函数,我们发现它指向的是gzip函数:
gzip: function(t, e) {
return (e = e || {}).gzip = !0,
ee(t, e)
},
gzip函数又调用了ee函数:
function ee(t, e) {
const a = new te(e); // 创建压缩器实例 if (a.push(t, !0), // 压入数据
a.err) // 错误处理
throw a.msg || G[a.err];
return a.result // 返回压缩结果}
到这里我们终于明白了:整个”加密”过程实际上是gzip压缩 + Base64编码,并没有使用真正的加密算法!

6. 代码扣取:获取压缩模块
现在我们需要获取gzip压缩的完整代码。由于这个文件有12万行,而且是webpack模块化加载的,我们采用巧妙的方法来扣取:
- 在Sources面板找到gzip所在的JS文件
- 全部拷贝到Notepad++中
- 点击”视图” → “折叠所有层次”
- 搜索”gzip”找到对应模块
- 将整个模块代码拷贝出来

关键步骤:清理Webpack模块包装
拷贝下来的代码是被Webpack包裹的,我们需要手动清理。请按照以下说明操作:
// ==== 删除从这里开始的内容 ====
function(t, e, a) {
"use strict";
a.d(e, "a", function() {
return ta
}),
a.d(e, "b", function() {
return ea
});
// ==== 删除到上面结束 ====
function i(t) {
let e = t.length;
for (; --e >= 0; )
t[e] = 0
}
var te = function(t) {};
function ee(t, e) {}
function xxxxx(t, e) {}
xxxxxs省略一千行
var ta = Qe
, ea = Ze
// ==== 删除结尾的包装括号 ====
} // 删除这个花括号
7. 本地实现:重构加密函数
现在我们可以重构本地的加密函数:
function gzip(t) {
try {
const encoder = new TextEncoder();
var e = encoder.encode(JSON.stringify(t)) // JSON转Uint8Array
, s = ee(e, {"gzip": true}); // gzip压缩
return function(t) { // Base64编码
for (var e = "", a = 0; a < t.length; a++)
e += String.fromCharCode(t[a]);
return btoa(e)
}(s)
} catch (t) {
throw new Error("Compression failed: " + t.message + ", ", t)
}
}
8. 测试验证:对比加密结果
创建测试代码验证我们的实现:
const t = {
"accountId": 437551425,
"clientTime": 1762941491259,
"gameId": "OG15FT",
"issue": "20251112719",
"item": [
"{"methodid":"BSC004001001","nums":1,"rebate":"0.00","times":1,"money":2,"mode":1,"issueNo":"20251112719","codes":"||||||||04|","playId":[]}"
],
"encryKey": "176294149126085458567108297",
"encryValue": "0ec063797926b4050330e283a53ab041"
};
console.log("生成的加密字符串:", gzip(t));
如果一切正确,输出的结果应该与抓包得到的字符串完全一致。

9. 常见问题与解决方案
- Q1: 控制台报错
TextEncoder is not defined- 解决方案:在Node.js环境中,需要安装
util模块:const { TextEncoder, TextDecoder } = require('util');
- 解决方案:在Node.js环境中,需要安装
- Q2: gzip压缩结果与浏览器不一致
- 解决方案:检查压缩级别参数,确保与浏览器使用的参数一致,通常需要设置
{"gzip": true}
- 解决方案:检查压缩级别参数,确保与浏览器使用的参数一致,通常需要设置
- Q3: Base64编码格式不正确
- 解决方案:确保使用标准的
btoa函数,注意字符编码要统一为UTF-8
- 解决方案:确保使用标准的
- Q4: 扣取的压缩模块代码太大
- 解决方案:可以使用在线的gzip压缩库替代,如
pako库,使用方法类似
- 解决方案:可以使用在线的gzip压缩库替代,如