引言

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 代码段:

发起网络请求的JS代码段

可以看到这里有一些密文,都作为函数 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 函数将 ab 两个字符串进行异或操作,并转换为字符串,这就是整个加密过程了。

但仔细阅读代码,可以发现调用 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 属性值。

这次逆向工程不仅让我学到了许多关于前端和后端交互的知识,还为我的群聊机器人项目提供了强大的数学计算支持。未来,我计划将这些接口集成到我的项目中,为用户提供更加便捷的数学解题服务。