超星网页端阅读进度上报请求分析
超星网页端阅读进度上报请求分析
关键字:超星学习通阅读进度分析;超星阅读进度上报接口;addPoint.js 逆向;jsjiami 混淆还原
超星的章节阅读任务做完之后,进度是怎么报上去的?页面安静地往 data-xxt.aichaoxing.com 打了一条带 enc 签名的 GET,里面还塞了一串压缩后的鼠标轨迹。整个过程被一份混淆到妈都不认识的 addPoint.js 包着。
这篇文章把整条链路拆开:抓包看请求长什么样 → 混淆还原看参数怎么组装 → 逐个字段讲清楚含义 → 最后还原出 enc 的签名公式。
说明
- 适用平台:PC 浏览器(Web 端)
- 分析对象:章节任务点阅读页
- 实现功能:阅读进度上报接口的「全参数 + 签名」完全还原
- 底层原理:抓包 + JS 混淆还原 + 断点调试
本文仅记录研究思路,请用于正规学习用途,任何使用造成的一切后果自负。
一、分析对象
章节任务点页面:
https://mooc1.chaoxing.com/coursedata/readjobv2/show?courseid=216769884&knowledgeid=413116808&userid=null&ut=s&clazzId=38291652&jobid=read-200841370&djobid=read-200841370&isphone=false&enc=dc64cdcd2f7d94295ed7b7ea6500caaa&utenc=3dc074a650414c299b24f1ea1b8fa717&cpi=109959280阅读页面:
https://mooc1.chaoxing.com/course/200841370.html?_from_=216769884_38291652_119093614_347fdd80e8ae2e12d186d854654c2857&rtag=打开阅读页,往下翻几页,抓包就能看到更新进度的请求:
GET https://data-xxt.aichaoxing.com/analysis/ac_mark?&f=readPoint&u=119093614&d=%7B...%7D&t=20230504160614987&enc=f0ec6c925db52834cfeaf16bfa0f8362| 参数 | 值 |
|---|---|
| f | readPoint |
| u | 119093614 |
| d | (详情见下文) |
| t | 20230504160614987 |
| enc | f0ec6c925db52834cfeaf16bfa0f8362 |
请求包全文:
f readPoint
u 119093614
d {"r":"200841370,114740787","t":"special","l":1,"f":0,"wc":1226,"ic":1,"v":2,"s":2,"h":918,"e":"H4sIAAAAAAAAA6XZwY4rKQyF4ae5ezAYU9s77/9Ok+Ckq+rwd2mk2UStTwhsAgbS7Y/943O+Put4f45S/rS/7UEP0qOgVlRDbagd1VHH0hAN1Il6gL7/Is3chqihNtSO6qgDNXNz0Yl6kNaCmll00YbaUR01s2iigTpRMwu7q1VUQ22oHTXjraKBOklbzm8RNdSG6qiBumLwQ/Qg7RV1xeBTtKMO1EA9SL2gVtSMTHahD9QcTXbLKKiGmhnLvshq5LJKssK4fPNZS0Qjq8ammVsRddSJepDWioqj5T7edKAGKsZgGEPuzU0xMsN5MIzMMDLDOTOMN3fspvi95T7etMEqiUZr56tNNDPu2FaV2zqs32jjQYdoPGiI5vxOUdrz0XN+D9Gs4DLrPeukzG/WqF9U5rc76kDNmiozmZVL1encDKc7QTjdYSIr15B5WDNbQ+Yh61lIxnm7OiSLrDvHPYaZ95Jjir4js1JEY2kVPZY20F7/mw5UA40V6a6OWklnoDbSoHgjOiqONnA0xywc+10ratNOc5ZrfdOGWTQcreFohlkYjlZxtJrxym6pRlomqqPmrOt5PFE76Wc9yDk/O2qOJreKCNSOWkg/q0RuNqOjNtRC6tivY789vze99w3UFYPJLXNV8E3XKdJN778NtZDm6tu0oxpprslNsd+K/daMTG54JVA7agXNl+Wu1G++eXfNfu/7LV/juzqqkQb2EAO1o2K/A/sd2G/uALvvefcDNVC5h0rasd+O/XZHNdRCuu5Guw5UzKLhaA1HM8zis4dc1FAraiGtE3WgOipGVhsqxlsx3ryB2BCdqIE6UB21o2YW8htT3nc2ragFtH9qiepEDdSB6qiUxVenqKFW1EL6qWeqEzVQM7dD1FE76vpsRdQetIrWBzXRIur/Q9/nDKmTvmYK9LUb7pG9/25d9T1zbWvrd82z5RcdqPGjjppt54NO0bVK1vuyvfb/t1/VbLvWzqru17aq2dYftIn2B+0ymmq2bae+6uC3rWq2NWxbT33V+G9b1kKaJ/pFV7/rXbu3ZY1Tm4k20X7Or7ef0S56aNt3LkedokXUf9f1grrq+FlRu4ZoPOgQPX720K7trnHWkqtmpZV446zKVz3PrKueN5CrZk3VGM579UVzz1eJIStilXmY58v9qvluMdG1dor0m+dFkX7zxFmn8q6SW55v5T5a3s+a3yPzrAxeRdfK7SG6Kll30WOpjLZ+/2uyovL22mSV5LvFRhddv8OMe2T5HjKfoqsHyW2sVWOSW/5Pwr65/QuN7UtUFxwAAA==","ext":"{\"_from_\":\"216769884_38291652_119093614_347fdd80e8ae2e12d186d854654c2857\",\"rtag\":\"\"}"}
t 20230504160614987
enc f0ec6c925db52834cfeaf16bfa0f8362二、加密JS分析
所有逻辑都在这个文件里:
http://data-xxt.aichaoxing.com/res/addPoint.jsjsjiami.com.v5:字符串全部 base64 编码塞进一个超长数组,再用 _0x37aa 之类的解码函数按索引取,变量名全部扁平化。

还原思路:
- 用在线工具解混淆:
https://ctf.bugku.com/tool/sojson5(专门针对 jsjiami v5),解出来的结果虽然不完美,但可读性已经天差地别 - 把解密后的 JS 覆盖替换原文件( Chrome DevTools Override ),方便直接断点调试
- 边断点边把
_0x406caa这种变量改成人话,逐步理清调用链
解出来之后能看到脚本其实是个 browserify 打包产物:内置了 pako 的 deflate/inflate 全套(./lib/deflate、./lib/inflate、./lib/utils/common),对外暴露两个接口:
window._readlogaddpoint = factory(document.getElementsByClassName("reader")[0]);
window.setSetting = factory();_readlogaddpoint:给阅读器实例用(reader容器),自动采集行为并打点setSetting:设置阅读参数(resourceID / passportUID / token 等)
三、参数逐个拆
3.1 f / u / t
var arr = {
'f': fdata.BVpdL, // 常量 "readPoint"
'u': _0x112d77.passportUID, // 用户 ID
'd': fdata.hixtE(encodeURIComponent, JSON.stringify(_0x15df64)), // 核心 JSON
't': (_0x406caa = new Date(), fdata.DnBUr(fdata.qlrER(fdata.pOyZF('' + _0x406caa.getFullYear() + _0x5eb083(fdata.pOyZF(_0x406caa.getMonth(), 0x1)), fdata.UwBCf(_0x5eb083, _0x406caa.getDate())) + fdata.lyaBj(_0x5eb083, _0x406caa.getHours()), fdata.lyaBj(_0x5eb083, _0x406caa.getMinutes())), fdata.llxQK(_0x5eb083, _0x406caa.getSeconds())) + (_0x3d84c9 = _0x406caa.getMilliseconds(), fdata.BHAuN(_0x3d84c9, 0xa) ? fdata.dUmrl('00', _0x3d84c9) : fdata.BHAuN(_0x3d84c9, 0x64) ? fdata.EBWlA('0', _0x3d84c9) : _0x3d84c9)) // 年月日时分秒 + 3 位毫秒
};f:写死的常量readPointu:passportUID,用户 IDt:本地时间,格式yyyyMMddHHmmss+ 3 位毫秒,共 17 位,如20230504160614987pid、s(sessionID)在代码里组装了,但实际请求包里不出现(值为 undefined 会被跳过)
3.2 参数 d
跟踪 _0x15df64,还原出来的结构:
var _0x15df64 = {
'r': _0x112d77.resourceID, // 资源唯一ID(见下文)
't': _0x112d77.resourceType, // 资源类型,如 "special"
'l': _0x112d77.location || 0x1,
'f': _0x112d77.from || 0x0,
'p': _0x112d77.curPage, // 请求中未出现
'tp': _0x112d77.totalPage, // 请求中未出现
'wc': _0x112d77.wordCount, // 文字数目
'ic': _0x112d77.imgCount, // 图片数目
'v': 0x2, // 固定值
's': sval, // 打点原因(见 3.3)
'h': fdata.RDAEI(_0x4c43a6), // 当前 scrollTop
'e': ... // 鼠标轨迹(见 3.4)
};r、t:


- resourceID 形如
200841370,114740787(阅读页中的 courseId + knowledgeId),resourceType 为special。 wc/ic:页面文字数、图片数,setSetting之后脚本自动统计p/tp:当前页/总页数,这版抓包里没出现,忽略
3.3 s
原代码里 s 有一大段看着很唬人的东西:
'TgCEM': function _0x5c5f01(a, b) { return a ^ b; },
sval = _0x15df64;
for (var i = 0x0; i < 0x8; i++)
sval = 0x1 & sval ? fdata.TgCEM(0xedb88320, sval >>> 0x1) : fdata.SBTVv(sval, 0x1);
_0x112d77[_0x15df64] = sval;0xedb88320 是 CRC32 表生成用的多项式常数,看着像是给某个值算 CRC,但这段压根没跟到真实取值——是混淆里的干扰代码(那会儿还问了 GPT,没看懂,多次抓包发现 s 一直是 2)。

后来使用https://jsnice.org/解混淆才真相大白,load(scale) 里是这么写的:
function load(scale) {
...
var q = {
a: parseQuery("refaid") || parseQuery("deptid"),
r: data.resourceID,
...
s: scale, // 就是传入的 scale!
h: parse(),
...
};
}
...
load(1);
if (_0x1f6f41) {
aux = (new Date).getTime();
end = parse();
setInterval(function () {
if (document && (!document.hidden && fragment)) {
_0xc28a29++;
}
if (_0xc28a29 % 5 == 0) {
var start = parse();
if (end !== start) {
load(2);
end = start;
aux = (new Date).getTime();
} else {
var max = (new Date).getTime();
if (max - aux >= 3E4) {
load(2);
aux = max;
}
}
}
}, 1E3);
window.onbeforeunload = function (e) {
load(0);
};s 就是「打点原因」,三个值:
| 值 | 触发时机 |
|---|---|
| 0 | 页面离开(onbeforeunload) |
| 1 | 初始化(setSetting 后第一发) |
| 2 | 滚动变化 / 定时轮询(每 1s 检查一次,滚动位置变了或者 30s 没动也会发) |
当初抓包都是滚动才触发的,所以 s 恒为 2,就把结论猜成了「定值」。混淆代码里的干扰项 + 没跟对的变量,是逆向里最浪费时间的东西。
3.4 e:鼠标轨迹
H4sIAAAAAAAAA6XZwY4... 解 base64 → gzip 解压 → urldecode,得到一串:
3,588,168,600;3,588,168,600;3,589,168,600;3,590,168,600;3,591,168,600;3,592,168,600;...动态调试,第一个参数


第二个数是鼠标X值,第三个数是鼠标Y值

第四个数,一路跟进,是scrollTop值

生成了第一组鼠标轨迹数据

总结格式:
[鼠标事件代码],[X],[Y],[H];
[X]:鼠标在可视页面中的X坐标值
[Y]:鼠标在可视页面中的Y坐标值
[H]:document.body["scrollTop"]
事件代码枚举:
DOMMouseScroll: 6
click: 0
contextmenu: 9
keydown: 4
mousedown: 2
mousemove: 3
mouseup: 1
mousewheel: 5
touchcancel: 7
touchmove: 8要点:
- 坐标取
pageX/pageY,取不到就用clientX + scrollLeft/scrollTop换算;触摸类事件用changedTouches[0] - 发送时最多取前 1000 组,超出截断(数组本身继续累积,发送成功后清空)
- 轨迹记录数组会在每次打点成功后清空(
guesses.length = 0) - 页面不可见(document.hidden)时不发送打点(
load()只在可见时执行)
所以这接口上报的不只是「我看了多少字」,还有一整段真实鼠标轨迹——这就是防挂机浏览的护城河。 你模拟学习,但鼠标轨迹是假的,轨迹和历史记录对不上,风控一眼就能看出来。
3.5 ext:来源追踪
{"_from_":"216769884_38291652_119093614_347fdd80e8ae2e12d186d854654c2857","rtag":""}_from_:带过来的来源参数(courseId_clazzId_uid_签名)rtag:另一个路由参数,通常为空

四、enc:请求签名公式
签名代码:
for (var arrdata in arr) fdata.KfDHm(void 0x0, arr[arrdata]) && (arr1.push(arrdata), url += fdata.Fbwfg(fdata.Fbwfg(fdata.lYeoG('&', arrdata), '='), arr[arrdata]));
arr1.sort();
for (var str1 = '', str2 = 0x0; fdata.BHAuN(str2, arr1.length); str2++) str1 += arr[arr1[str2]];
if (url += fdata.ODKzq("&enc=", fdata.llxQK(_0x596fa4, fdata.ODKzq(str1, 'NrRzLDpWB2JkeodIVAn4'))), 直接断点看str1

%7B%22r%22%3A%22200841370%2C114740787%22%2C%22t%22%3A%22special%22%2C%22l%22%3A1%2C%22f%22%3A0%2C%22wc%22%3A1226%2C%22ic%22%3A1%2C%22v%22%3A2%2C%22s%22%3A2%2C%22h%22%3A918%2C%22e%22%3A%22H4sIAAAAAAAAA6XZwY4rKQyF4ae5ezAYU9s77%2F9Ok%2BCkq%2Brwd2mk2UStTwhsAgbS7Y%2F943O%2BPut4f45S%2FrS%2F7UEP0qOgVlRDbagd1VHH0hAN1Il6gL7%2FIs3chqihNtSO6qgDNXNz0Yl6kNaCmll00YbaUR01s2iigTpRMwu7q1VUQ22oHTXjraKBOklbzm8RNdSG6qiBumLwQ%2FQg7RV1xeBTtKMO1EA9SL2gVtSMTHahD9QcTXbLKKiGmhnLvshq5LJKssK4fPNZS0Qjq8ammVsRddSJepDWioqj5T7edKAGKsZgGEPuzU0xMsN5MIzMMDLDOTOMN3fspvi95T7etMEqiUZr56tNNDPu2FaV2zqs32jjQYdoPGiI5vxOUdrz0XN%2BD9Gs4DLrPeukzG%2FWqF9U5rc76kDNmiozmZVL1encDKc7QTjdYSIr15B5WDNbQ%2BYh61lIxnm7OiSLrDvHPYaZ95Jjir4js1JEY2kVPZY20F7%2Fmw5UA40V6a6OWklnoDbSoHgjOiqONnA0xywc%2B10ratNOc5ZrfdOGWTQcreFohlkYjlZxtJrxym6pRlomqqPmrOt5PFE76Wc9yDk%2FO2qOJreKCNSOWkg%2Fq0RuNqOjNtRC6tivY789vze99w3UFYPJLXNV8E3XKdJN778NtZDm6tu0oxpprslNsd%2BK%2FdaMTG54JVA7agXNl%2BWu1G%2B%2BeXfNfu%2F7LV%2FjuzqqkQb2EAO1o2K%2FA%2Fsd2G%2FuALvvefcDNVC5h0rasd%2BO%2FXZHNdRCuu5Guw5UzKLhaA1HM8zis4dc1FAraiGtE3WgOipGVhsqxlsx3ryB2BCdqIE6UB21o2YW8htT3nc2ragFtH9qiepEDdSB6qiUxVenqKFW1EL6qWeqEzVQM7dD1FE76vpsRdQetIrWBzXRIur%2FQ9%2FnDKmTvmYK9LUb7pG9%2F25d9T1zbWvrd82z5RcdqPGjjppt54NO0bVK1vuyvfb%2Ft1%2FVbLvWzqru17aq2dYftIn2B%2B0ymmq2bae%2B6uC3rWq2NWxbT33V%2BG9b1kKaJ%2FpFV7%2FrXbu3ZY1Tm4k20X7Or7ef0S56aNt3LkedokXUf9f1grrq%2BFlRu4ZoPOgQPX720K7trnHWkqtmpZV446zKVz3PrKueN5CrZk3VGM579UVzz1eJIStilXmY58v9qvluMdG1dor0m%2BdFkX7zxFmn8q6SW55v5T5a3s%2Ba3yPzrAxeRdfK7SG6Kll30WOpjLZ%2B%2F2uyovL22mSV5LvFRhddv8OMe2T5HjKfoqsHyW2sVWOSW%2F5Pwr65%2FQuN7UtUFxwAAA%3D%3D%22%2C%22ext%22%3A%22%7B%5C%22_from_%5C%22%3A%5C%22216769884_38291652_119093614_347fdd80e8ae2e12d186d854654c2857%5C%22%2C%5C%22rtag%5C%22%3A%5C%22%5C%22%7D%22%7DreadPoint20230504160614987119093614规律很清楚:对请求参数按 key 字典序排序,然后把对应的 value 顺序拼接:
str1 = rawurlencode(d) + "readPoint" + t + u则
enc = md5( rawurlencode(d) + "readPoint" + t + u + "NrRzLDpWB2JkeodIVAn4" )用抓到的包验证一下,f0ec6c925db52834cfeaf16bfa0f8362 ✓ 完全吻合。
五、总结
| 字段 | 说明 | 默认值 |
|---|---|---|
| f | readPoint | |
| u | uid | |
| d | json | |
| -r | 阅读页面中的courseId,knowledgeId | |
| -t | special | |
| -l | 1 | |
| -f | 0 | |
| -wc | 文字数目 | |
| -ic | 图片数目 | |
| -v | 2 | |
| -s | 0 :页面离开,1 : 初始化,2 :滚动变化 / 定时轮询 | |
| -e | gzip(urlencode(鼠标事件和轨迹)) | |
| -ext | json | |
| --\_from\_ | 阅读HTML页面中的_from | |
| --rtag | [空] | |
| t | 时间,年月日时分秒毫秒 | |
| enc | md5(rawurlencode(d)+"readPoint"+t+u+"NrRzLDpWB2JkeodIVAn4") |
写在最后
这篇分析写于 2023 年。那时朋友来找我玩,晚上一起去喝酒,但我喝不了,于是干脆去了趟网吧,随手找了一个加密样本,花两个小时做了这篇分析。现在属于冷饭热炒(?)还可能是馊了的那种。
回头看,2023 年也恰好处在这一轮 AI 浪潮刚刚起步的阶段。彼时的大模型对复杂语义的理解和分析能力还远没有今天这么成熟,更谈不上通过中间件,将自然语言的输出进一步转化为对计算机程序和工具的直接控制。
而到了今天,借助 MCP 等协议和工具生态,LLM 已经开始真正连接外部程序、数据与服务,算是打通了语言模型与计算机世界之间的第一堵墙。
那么下一堵墙,会不会是现实世界?
如今,各大厂商都在持续押注具身智能与人形机器人,希望让 AI 不再只是理解和操作数字世界,而是能够感知、行动,并真正参与物理世界。
从这个角度来看,那一天或许并没有想象中那么遥远。
参考
- jsjiami.com.v5 混淆样本:http://data-xxt.aichaoxing.com/res/addPoint.js
- 解密工具:https://ctf.bugku.com/tool/sojson5
附录
用到的工具
- 浏览器 DevTools(Network / Sources / 断点)
- fiddler抓包(阅读进度上报接口抓包)
- sojson5 在线解密
- GPT