引言
2024年10月18日,刚步入大一的我正被一道求极限的题所烦恼,百般思索之下,我想到了多年前曾使用过的一个很全面的网页工具 MathDF。我打开网页,输入了困扰了我许久的题目,不一会,网页中就弹出了分步骤的详细解法。
诶?这是个好东西啊,正好最近在做群聊机器人项目,如果能够部署到机器人上,或者结合提示词工程来与大语言模型联动使用,效果应该很惊艳吧!
说干就干!
Anti-F12?
爬取后端接口的想法一出来,我就按下F12来打开了浏览器的调试工具,但我却发现,网页上方多出了一栏 已在调试程序中暂停
,而右侧的开发工具则始终卡在 源
标签页上,即使我尝试继续运行代码,也始终无法跳出这个标签页。
我暂时放弃了这个想法,为了快速开发,我首先查看了 Microsoft Math Solver 页面和它是否有开放接口,又查看了市面上类似 MathDF 的网页工具,却始终不如 MathDF 做得优秀。我又在 GitHub 上搜索了 MathDF API 的相关项目,却仍然一无所获。
最后,我还是选择了继续尝试逆向 MathDF,却意外在开发工具中点击了 停用断点
按钮,没想到竟然就此解决了这个困扰我许久的问题。
监听网络
打开 网络
标签页,先清除掉无用的其他请求,点击 Fetch/XHR
标签,然后点击 监听
按钮,此时,所有请求都会被记录下来。
回到网页中,我随便输入了一道示例题目,点击 =
按钮,此时页面已经显示出了解法的详细步骤,而右侧的开发工具中也显示出了一个可疑的网络请求:calculate.php
,点开它可以看到一些详细的请求内容。
点击 负载
标签,可以查看到请求数据:
- p:
SkZWTkgQG1...hVAVAFVRJK
- f:
false
这里的 p
参数显然已经过加密,而 f
参数是一个布尔值,暂时不知道它的具体作用。
解密参数
我首先尝试了使用 Base64 解密,但解密结果让人两眼一黑:
- JFVNHXAEL……UUPUJ
我又尝试了比较常用的各种算法,但均无法解密出可读的内容。
这可咋办?
正当我一筹莫展之际,我突然想到了一个最基本的方法:查询这段密文的实际来源。在上面的网络监听中,位于 calculate.php
请求之前的其他请求,貌似与 p
参数无关。这可以证明,该参数的加密位置是在前端实现的,只要找到前端中的请求函数,并从中逆推到 p
参数所在的变量和加密规则,即可得到 p
参数的具体解释了。
点击 发起程序
标签,可以定位到发送网络请求的具体 JavaScript 代码段:
可以看到这里有一些密文,都作为函数 l
的参数传递进去了,我们把整段代码复制出来到 VSCode 中,可以看到 l
函数的具体实现:
const l = window.atob
可以看到 l
实际上是 window.atob
这个函数,通过查看 MDN 文档,可以知道 window.atob
是一个将 Base64 编码的字符串解码为原始数据的函数。
我们先把使用了 l
函数进行加密的关键代码给解密后使用字符串明文表示:
const l = window.atob
, gc = eval("String")
, hc = "fromCharCode"
, nb = "charCodeAt"
, mb = "length"
, Mc = token
, Lc = eval("JSON.stringify");
这下明白了,这段代码中的大部分混淆都是使用了 Base64 编码了某些函数或者工具类的名称。
接下来,我们从网络请求的发起程序的那行代码,依次往回查看各个变量的含义和各个变量之间的关系(此处仅展示了部分关键代码):
var p = new XMLHttpRequest;
var h = {};
h["expr"] = a;
h["arg"] = k;
h["to"] = b;
h["params"] = l("dXNlaG9waXRhbD0=") + Ua;
h["token"] = token;
h = Lc(h);
h = "p=" + encodeURIComponent(btoa(fc(h, Mc + "a"))) + "&f=" + encodeURIComponent("false");
p.addEventListener("timeout", a);
p.addEventListener("error", a);
p.addEventListener("abort", a);
p.send(h);
- p:
new XMLHttpRequest
,这个对象用于发起网络请求 - h:
{}
,用于存储请求参数
其中 Lc
函数的实现为 JSON.stringify
,故可认定此处 h
从一个JS对象被转换为JSON字符串,最后被加密后拼接为了一个完整的请求参数字符串。加密函数即为 fc
,我们可以继续查看其实现:
function fc(a, b) {
let f = "";
for (let k = 0; k < a[mb]; k++)
f += gc[hc](a[nb](k) ^ b[nb](k % b[mb]));
return f
}
将上述函数中的部分代码由混淆代码还原后,可以得到如下代码:
function fc(a, b) {
let f = "";
for (let k = 0; k < a.length; k++)
f += String.fromCharCode(a.charCodeAt(k) ^ b.charCodeAt(k % b.length));
return f
}
从中可以得知,fc
函数将 a
和 b
两个字符串进行异或操作,并转换为字符串,这就是整个加密过程了。
但仔细阅读代码,可以发现调用 fc
的时候,b
的值是 Mc + "a"
,而阅读代码可知 Mc
的值是 token
,但到这里时,就无法继续往前走了,因为整段代码中并没有出现 token
的定义。但通过文本搜索,我们找到了一处 token
的赋值操作和对应的一个函数 r
的定义:
null != r('meta[name="csrf-token"]') && (token = r('meta[name="csrf-token"]').getAttribute("content"));
function r(a, b=!1) {
return b ? document.querySelectorAll(a) : document.querySelector(a)
}
阅读代码可知,token
的值是 r(meta[name="csrf-token"])
元素的 content
属性的值,而 meta[name="csrf-token"]
元素的 content
属性值正是页面HTML中一个名为 csrf-token
的 <meta>
标签的 content
属性值。
最后回到浏览器前端对页面进行搜索,最终找到了该 <meta>
标签:
<meta name="csrf-token" content="1d368b9b3c078266">
至此,我们就完成了 p
参数的所有解读:
- 从HTML页面中获取到名为
csrf-token
的<meta>
标签的值,记为token
- 构造请求参数,包含字段
expr
arg
to
params
token
,记为h
- 将
h
转为JSON字符串,并与token + "a"
进行异或运算加密,记为p
- 此处的
p
即为前端向后端请求参数时所携带的已加密参数
总结
最终经过不懈努力,我们不仅成功地逆向了 MathDF 网页工具的后端接口,还深入理解了其加密机制。具体来说:
- 遇到了网页的 Anti-F12 保护机制,但通过点击
停用断点
按钮解决了这一问题。 - 通过监听网络请求,发现了关键的
calculate.php
接口及其加密的请求参数p
。 - 解密过程中,逐步分析前端代码,最终确定了
p
参数的加密逻辑,包括使用token
进行异或运算。 - 最终找到了
token
的来源,即页面中<meta>
标签的content
属性值。
这次逆向工程不仅让我学到了许多关于前端和后端交互的知识,还为我的群聊机器人项目提供了强大的数学计算支持。未来,我计划将这些接口集成到我的项目中,为用户提供更加便捷的数学解题服务。