老设备之家

找回密码
立即注册
搜索
热搜: iOSIPA 美化
发新帖

264

积分

0

好友

4

主题
发表于 2025-11-14 22:14:16 | 查看: 129| 回复: 0
0x01 抓包分析

首先打开抓包工具,返回网页,点击登录按钮,触发登录请求

截图202511112112228539.png

抓取到如下数据包,其中通过 POST 请求 /auth/sign-in 的数据包便是登录请求,

截图202511112112542980.png

这个数据包的请求头非常多,其中,signature、nonce、time 便是服务器用来校验的。我们只需要在前端找 signature、nonce 这两个的生成算法即可


0x02 前端调试

回到浏览器,在登录页按下 F12 开启开发者模式,设置一个断点拦截登录

截图202511112126122003.png

点击登录后,触发断点,停在了 index.js 上,

截图202511112132037233.png

可以看到,上面就是负责构造请求的 JS 方法,其中 w 调用了 r(时间戳) o(请求路径) l(请求方法),那么很可能,这个 w 就是负责生成校验值的函数,给这一行打一个断点,重新触发一次

截图202511121007567949.png

然后按 F11 跳转到 w 函数内部,来到了 bt

截图202511112143472181.png

根据本地作用域得知,e 是时间戳,t 是请求路径,a 是请求方法,我们再看它的下下行:
  1. o = ft(s + t, e, a)
复制代码

调用 ft 方法传入三个参数,将返回结果赋予变量 o,既然我们知道了 t e a 是什么,那么这个 s + t 也不难理解,就是请求的基础地址和请求路径拼接成一个完整地址

那么这个 o 是什么呢,在下一行可以看到一个 n 定义了一个数组,储存了所有请求头,往下翻即可看到 :
  1. {
  2.         name: "signature",
  3.         value: o
  4.     }];
复制代码
signature 这个键,值是变量 o ,根据 o = ft(s + t, e, a),即可得出 ft 即为计算 signature 的方法,给这一行打一个断点,按 F11 跳转 ft 函数内部

截图202511121047433368.png

这里 ft 定义了 o n 两个常量,重点看 n ,这个常量调用了 mt 传入"b397e2wXZHtgb2RvUBh7bnB+bnt8bEEfZ2xSQUFtY0F4G3h4bWhzeA==" 将 mt 返回结果赋给自己。至于这个字符串,根据结尾的"=="可以看出这肯定(可能)是一个 base64 。那这个 mt 函数,无非就两种可能:
  • 负责解密
  • 直接计算
这里暂定 mt 函数负责解密,我们继续看 ft 函数的后半部分:
let r = e.replace(o, "") + t + gt() + s + n;
    r = r.toLowerCase();
    return function(e, t) {
        const s = 64
          , o = new TextEncoder;
        let n = o.encode(t);
        if (n.length > s && (n = new Uint8Array(a.arrayBuffer(n))),
        n.length < s) {
            const e = new Uint8Array(s);
            e.set(n),
            n = e
        }
        const r = new Uint8Array(s)
          , i = new Uint8Array(s);
        for (let a = 0; a < s; a++)
            r[a] = 92 ^ n[a],
            i[a] = 54 ^ n[a];
        const l = new Uint8Array([...i, ...o.encode(e)])
          , c = a.arrayBuffer(l)
          , d = new Uint8Array([...r, ...new Uint8Array(c)])
          , m = a.arrayBuffer(d);
        return Array.from(new Uint8Array(m)).map(e => e.toString(16).padStart(2, "0")).join("")
    }(r, ( () => {
        try {
            return mt("aGh+G0dwfHpGUGRmYGxrGUFsZmRyGUMZa19kfUxfRxMfXGAaGxNBbmBhZRpMQUFma20Bbn58YElIYGQTbGdsQkxrfEd8X3xueBocH1JQf2RpSG9B")
        } catch (e) {
            return ""
        }
    }

这里将 e(请求路径)、t(时间戳)、gt(Nonce值)、s(请求方法)n(解密base64)全部拼接并转换为小写后赋给 r ,然后定义了一个匿名函数,接收 r 和调用 mt 的返回结果,这里也传给 mt 一个长字符串,那么 mt 是一个负责解密的函数无疑了。该匿名函数首先将解密出的 KEY 填充、派生出内层和外层密钥,然后对数据进行两次 SHA256 哈希运算。最终,函数将计算出的哈希值转换为 十六进制字符串 并返回,得出最终的 signature 值,具体步骤和计算公式:
  1. 原始字符串r = 请求路径e + 时间戳t + Nonce值gt +请求方法s + 密钥n
复制代码
  1. 内部密钥 = 密钥n XOR 54 (将密钥的每个字节,与 54 这个固定数值进行 XOR 运算,得出内部密钥)
复制代码
  1. 内部哈希 = SHA256(内部密钥 XOR 原始字符串r)
复制代码
  1. 外部密钥 = 密钥n XOR 92
复制代码
  1. signature = SHA256(外部密钥 XOR 内部哈希)
复制代码
光知道算法不行,还不知道 mt 函数到底是什么,给 mt 函数打一个断点,F11 跳转过去

截图202511121152373517.png

mt 函数接收 e 作为参数,e 就是我们看到的 base64 ,首先 mt 函数调用 atob() 对字符串进行 base64 解密,将解密结果和 "PicaWeb2025" 传给 dt 函数 ,然后将 dt 返回结果赋给 a ,最后将 a 的每个字符的 ASCII 码与固定值 42 进行 XOR 运算,运算结果赋给 s ,然后再调用 atob() 解密 s ,最后将解密出来的明文返回。

那么问题又来了,dt 又是负责什么的,继续断点 F11 跳转 (结果发现 dt 函数就在 mt 函数上面……)

截图202511142212249573.png

根据图片可以看出来, dt 函数也负责加密,这里的 e 就是 mt 函数传入的 base64 解密结果 ,而这个 t 就是传入的 "PicaWeb2025" 。

这里 dt 定义了一个变量 s 和一个数组 a,然后遍历出 t 每一个字符,每遍历一个字符,就会将当前字符的 ASCII 值,累加到变量 s 中,最后得出种子值。得到种子后,开始利用种子进行伪随机生成,具体代码为:
  1. s = (9301 * s + 49297) % 233280;
复制代码
然后新的 s 值通过 线性同余法公式 持续生成新的 伪随机数 s每次循环,利用新的 s 值计算出交换索引e ,最后执行洗牌算法 [a[o],a[e]] = [a[e], a[o]] ,交换数组 a 中第 o 个元素和第 e 个元素的位置。最后用数组 a 来还原 e 的字符顺序,最终返回明文。
至此,我们已经完整地掌握了请求头中 signature 字段的生成算法和全部依赖常量,仅剩下 Nonce 的生成算法没有找出来,下一步开始寻找 Nonce 生成算法

0x03 Nonce 算法

要找 Nonce 这个键,我们回到 mt 函数,来看它下面的代码:
  1. const ut = () => Math.floor(Date.now() / 1e3).toString()
  2.   , ht = (e=32) => {
  3.     const t = "ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz2345678";
  4.     let a = "";
  5.     for (let s = 0; s < e; s++)
  6.         a += t.charAt(Math.floor(48 * Math.random()));
  7.     return a
  8. }
  9.   , gt = () => {
  10.     if ("undefined" != typeof window) {
  11.         let e = localStorage.getItem("nonce");
  12.         return e || (e = ht(32).toLowerCase(),
  13.         localStorage.setItem("nonce", e)),
  14.         e
  15.     }
  16.     return ht(32).toLowerCase()
  17. }
复制代码
其中 ut 负责生成时间戳,ht 负责生成 32 位随机字符串,gt 则是负责判断是否使用本地储存的 Nonce 值还是调用 ht 生成一个新的 Nonce 值。得出结论:Nonce 就是个随机字符串……


至此,我们已经通过逆向找到了请求头中 signature、nonce 的生成算法,写一个 Python 脚本测试一下

截图202511121258568914.png

接口成功返回正常响应

0x04 结语

回顾整个 PicACG API 签名逆向过程,虽然代码经过了混淆,乍一看非常复杂,但其核心逻辑清晰、路径明确,是一个非常适合练手的项目
相比于那些使用 WebSocket、动态VM 校验的反爬网站,这网站的加密手段算是其中最简单的了。

至此,本次 PicACG 签名逆向工作圆满结束。所有核心算法和密钥均已掌握。各位可以跟着我的步骤去尝试和复现。
——逆向的终点,即为工程的起点。




您需要登录后才可以回帖 登录 | 立即注册

Archiver|手机版|小黑屋|老设备之家

GMT+8, 2025-11-28 07:41 , Processed in 0.052396 second(s), 23 queries .

Powered by Discuz! X3.4

© 2001-2023 Discuz! Team.

快速回复 返回顶部 返回列表