保姆式解析某高质量听书网站音频地址全过程(适合小白练手)

查看 38|回复 7
作者:lenvy1   
免责声明:
本贴中所有内容仅供学习交流使用,不用于其他任何目的,不提供完整代码,严禁用于商业用途和非法用途,否则由此产生的一切后果均与作者无关。
一、前言
要获取网络上免费站的音视频真实地址的方法其实有很多,比如直接用IDman插件嗅探,或者抓包缓存等。
但是!!!这只是单个音频地址的获取方法,而且不一定有效,比如音频地址使用异步动态加载,甚至是加密包装过的,使用嗅探工具不一定能嗅探到。
而且!!!如果想要批量获取,就必须了解这个音频真实地址的产生过程,这个过程就是解析(逆向)其算法过程。
本次解析目标就是一个解析算法的过程。
我也是小白,对JS一窍不通,有错漏之处望海涵。
大佬见笑了。
二、目标地址
aHR0cDovL3d3dy55dWV0aW5nYmEuY24vYm9vay9kZXRhaWwvM2ExODAxNzQtN2I4Yi05ZWVhLWUwNmYtMzk0NmQ5Y2E5NmYwLzA=
三、解析思路
1、通过HTML源码 -> 直接搜索关键词

  • 通过Ctrl + U查看HTML源码 Ctrl + F搜索关键词 .mp3 或.m4a 找到明文地址,如以下这种:



    m4a明文.jpg (49.57 KB, 下载次数: 0)
    下载附件
    m4a明文
    2025-5-29 15:54 上传


  • 如果搜索关键词
    .mp3

    .m4a
    没能找到,则试着搜索
    var now

    var next
    ,找到以下这种,一般都是被加密或Base64转码了。



    星号.jpg (42.82 KB, 下载次数: 0)
    下载附件
    星号
    2025-5-29 15:55 上传



    如果是这种加“
    *
    ”号的,可以使用这个方法解析获得明文地址:
    // 可自行转成Python的方法;
    function FonHen_JieMa(u) {
        var a = u.split("*");
        var b = '';
        for (var i = 1, n = a.length; i
    2、找到对应API接口分析
    如果在HTML源码并没有找到疑似加密或明文的音频真实地址,大概率就是通过API接口请求了。

  • 有些通过API接口可以直接返回明文真实地址,比如某听书网:



    明文api.jpg (36.25 KB, 下载次数: 0)
    下载附件
    明文api
    2025-5-29 15:54 上传


  • 还有一些API接口返回的是经过加密的地址,需要二次解密,比如这一次的yue听巴网:



    yuetingba api.jpg (45.77 KB, 下载次数: 0)
    下载附件
    yuetingba api
    2025-5-29 16:01 上传



    四、开始对目标地址解析全过程
    1、发现有防调试机制 -> 可利用脚本破除控制台检测
  • 首先,经过关键词搜索尝试之后,发现这个站点的音频真实地址并没有加载在静态的HTML源码中
  • 然后通过F12 或手动打开浏览器开发者工具,发现被秒关闭调试网页,说明该站点有防御控制台打开的反调试机制。
  • 猜测可能是检测控制台被打开然后触发反调试,这个简单,我们可以通过开启“控制台防检测”脚本或工具,以绕过这种反调试机制。

    比如这个油猴脚本:aHR0cHM6Ly9ncmVhc3lmb3JrLm9yZy96aC1DTi9zY3JpcHRzLzUyMzc5Mi3mtY/op4jlmajmjqfliLblj7DpmLLmo4DmtYs=(需自行将目标站点添加进包含规则@match


    控制台.jpg (63.43 KB, 下载次数: 0)
    下载附件
    控制台
    2025-5-29 16:39 上传



    2、打开开发者工具 -> 查找API接口
  • 对目标站点开启“控制台防检测”脚本后,接下来就是分析API了。
  • 因为F12被禁止,我们可以手动打开浏览器开发者工具。

  • 切换到“网络”标签选项卡 ->重载刷新(F5)当前网页 -> 随意点播放一个音频章节 -> 找到可疑的api接口地址“
    ting-with-efi
    ”:



    找到API.jpg (171.63 KB, 下载次数: 0)
    下载附件
    ting-with-efi
    2025-5-29 17:00 上传


    这个API接口地址:3a1801e1-74ec-871b-6d8b-68bce50caf01/ting-with-efi" target="_blank" rel="noopener noreferrer nofollow">http://www.yue听巴.cn/api/app/docs-listen/
    3a1801e1-74ec-871b-6d8b-68bce50caf01
    /ting-with-efi 并没有负载,而且请求头也没有签名Token。
    不需要分析,可以肯定这是根据请求地址上
    3a1801e1-74ec-871b-6d8b-68bce50caf01
    这个ID来确定区分章节的,直接在HTML源代码搜索验证了猜想:


    id章节来源.jpg (26.9 KB, 下载次数: 0)
    下载附件
    id章节来源
    2025-5-29 17:09 上传

    而且通过替换这个ID,API返回的是不同的
    efi
    字段(疑似加密的真实音频地址)。
    3、解密真实地址 -> 查找解密函数逻辑链
    接下来也就是分析api返回的efi字段的值具体是什么了,看看到底是不是音频地址。
  • 在调试工具窗口试着Ctrl + Shift + F 全局搜索“
    efi
    ”这个字段,看看它生成逻辑。



    大海捞针.jpg (222.76 KB, 下载次数: 0)
    下载附件
    大海捞针
    2025-5-29 17:37 上传

  • 发现搜索结果太多干扰项了,换成跟踪ting-with-efi的启动器调用栈,发现都是源自同一个js解析,这说明这个js启动器文件才是关键。



    启动器.jpg (46.28 KB, 下载次数: 0)
    下载附件
    启动器
    2025-5-29 17:32 上传

  • 很明显,”
    initGetData
    “这个调用栈名称是关于初始化数据包的,我们直接从这一个点进去(以后分析推荐直接从启动器入手)。



    data.jpg (51.02 KB, 下载次数: 0)
    下载附件
    data
    2025-5-29 17:33 上传

  • 太明显了,一眼就看到了相关初始化方法,特别是
    initAudio()
    ,一看就是关于音频初始化的。



    get方法.jpg (96.6 KB, 下载次数: 0)
    下载附件
    get
    2025-5-29 17:54 上传

  • 从initAudio()入手,发现【e.playUrl = o,】这一行,右键加断点然后刷新查看,果然是真实的音频地址,看生成逻辑链,很明显,我们下一步就是要找到vs函数:



    需要找到vs方法.jpg (143.86 KB, 下载次数: 0)
    下载附件
    需要找到vs
    2025-5-29 18:06 上传



    4、解析函数算法-> 找到关键算法逻辑方法
    上一步发现vs函数才是解码地址的关键,完整的明文地址实际上就是服务器地址 'http://36.5.86.202:50010‘ 与 vs.d(e.currentPlayInfo.ef, n, i.padEnd(20, "0")) 返回的结果进行拼接。
    js关键代码片段:
    const n = e.currentPlayInfo.tingId.replaceAll("-", "")
       , i = e.currentPlayInfo.creationTime.replaceAll("-", "").replaceAll(":", "").replaceAll("T", "").replaceAll(".", "").replaceAll(" ", "")
      , r = vs.d(e.currentPlayInfo.ef, n, i.padEnd(20, "0"))
      , o = e.tingDefaultData.audioServer.url + r;
    api返回的响应数据:
    {
        "id": "3a1801e1-74ec-871b-6d8b-68bce50caf01",
        "bookId": "3a180174-7b8b-9eea-e06f-3946d9ca96f0",
        "tingNo": 2,
        "title": "0002_避难所",
        "efi": "30Bf6+rGEZSFx+73P01z67iEDCBtOZzjkC4E2ENX2g7VwllOb/MS4GO5SMb7ug8rdcpKiGpARuRzHYPuqp+mHROrNemNftGuVT7QW6G3Nyjjc2MRRg+NLyRACPORkhoCbTl3WiHAWrVCyYIev2UZ1g==",
        "creationTime": "2025-02-10T19:52:58.873983"
    }
    先别急,结合代码片段,需要分析vs.d(e.currentPlayInfo.ef, n, i.padEnd(20, "0"))中各传递参数:
  • 参数e.currentPlayInfo.ef ,很容易就猜到是当前API请求响应中efi字段加密的Base64字符串值。
  • 参数n:根据n = e.currentPlayInfo.tingId.replaceAll("-", ""),就是API响应的章节id字段的值经过去除”-“号的结果(不是bookId)。
  • 参数:i.padEnd(20, "0"),就是i = e.currentPlayInfo.creationTime.replaceAll("-", "").replaceAll(":", "").replaceAll("T", "").replaceAll(".", "").replaceAll(" ", ""),翻译过来就是根据API响应的creationTime,去掉”-“连接号,去掉”:"号,去掉"T",去掉”.“号,去掉空格 -> 即"20250210195258873983",然后经过padEnd(20, "0"),我没学过js,但可以问AI助手啊:

    str.padEnd(targetLength [, padString]) 即根据"20250210195258873983"填充后的长度为20,不够则右侧补0,最终结果还是i.padEnd(20, "0") -> "20250210195258873983"


    所以,vs.d(e.currentPlayInfo.ef, n, i.padEnd(20, "0"))实际上就是 -> vs.d(efi, id, creationTime)) // id、creationTime都需要按照上面提到的逻辑处理
    5、定位并重现关键函数方法 -> 最终得到明文解密
    5.1理清传递参数之后,全局搜索找到vs这个函数或声明方法
    发现是vs = new class{}声明,而且在下方还看到了定义的Base64解密字符集、AES,以及d的方法。
    我是js小白,看不懂怎么办?
    没关系,使用复制-粘贴大法,写好自然语言指令,把涉及到的相关代码一股脑丢给AI助手:
    已知API返回响应数据:
    {
        "id": "3a1801e1-74ec-871b-6d8b-68bce50caf01",
        "bookId": "3a180174-7b8b-9eea-e06f-3946d9ca96f0",
        "tingNo": 2,
        "title": "0002_避难所",
        "efi": "30Bf6+rGEZSFx+73P01z67iEDCBtOZzjkC4E2ENX2g7VwllOb/MS4GO5SMb7ug8rdcpKiGpARuRzHYPuqp+mHROrNemNftGuVT7QW6G3Nyjjc2MRRg+NLyRACPORkhoCbTl3WiHAWrVCyYIev2UZ1g==",
        "creationTime": "2025-02-10T19:52:58.873983"
    }
    efi的解密方法为vs.d(efi, id.tingId.replaceAll("-", ""), creationTime.padEnd(20, "0")) // 注意其中"id": "3a1801e1-74ec-871b-6d8b-68bce50caf01",非bookid
    其中vs.d相关代码如下,请写出整个解密efi值的过程(步骤),最后转为python的方法:
    , vs = new class {
        constructor() {
            this.k = pi.enc.Base64.parse("le95G3hnFDJsBE+1/v9eYw=="),
            this.i = pi.enc.Base64.parse("IvswQFEUdKYf+d1wKpYLTg=="),
            this.as = 43,
            this.ae = 116
        }
        e(e, t, n) {
            const i = this.gk(t, n)
              , r = this.gi(t, n);
            this.k = pi.enc.Base64.parse(i),
            this.i = pi.enc.Base64.parse(r);
            const o = pi.enc.Utf8.parse(e);
            return pi.AES.encrypt(o, this.k, {
                iv: this.i,
                mode: pi.mode.CBC,
                padding: pi.pad.Pkcs7
            }).ciphertext.toString(pi.enc.Base64)
        }
        d(e, t, n) {
            const i = this.gk(t, n)
              , r = this.gi(t, n);
            this.k = pi.enc.Base64.parse(btoa(i)),
            this.i = pi.enc.Base64.parse(btoa(r)),
            e = (e + "").replace(/\n*$/g, "").replace(/\n/g, "");
            const o = pi.enc.Base64.parse(e)
              , a = pi.enc.Base64.stringify(o);
            return pi.AES.decrypt(a, this.k, {
                iv: this.i,
                mode: pi.mode.CBC,
                padding: pi.pad.Pkcs7
            }).toString(pi.enc.Utf8).toString()
        }
        gk(e, t) {
            let n = "";
            for (let i = 0; i  4; i--) {
                const r = e.charCodeAt(0) + Number(t[i - 1]);
                n += String.fromCharCode(r)
            }
            return n
        }
    }
    某AI助手分析过程:


    Snipaste_2025-05-29_20-18-55.jpg (80.6 KB, 下载次数: 0)
    下载附件
    2025-5-29 20:20 上传



    Snipaste_2025-05-29_20-27-30.jpg (57.84 KB, 下载次数: 0)
    下载附件
    2025-5-29 20:31 上传

    5.2最终AI助手转成Python的等价方法:
    import base64
    from Crypto.Cipher import AES
    from Crypto.Util.Padding import unpad
    class VS:
        def __init__(self):
            self.k = base64.b64decode("le95G3hnFDJsBE+1/v9eYw==")
            self.i = base64.b64decode("IvswQFEUdKYf+d1wKpYLTg==")
        def gk(self, e: str, t: str) -> str:
            n = ''
            for i in range(20):
                r = ord(e) + int(t)
                n += chr(r)
            for i in range(20, len(e)):
                r = ord(e) + int(t[i - 20])
                n += chr(r)
            return n
        def gi(self, e: str, t: str) -> str:
            n = ''
            for i in range(20, 4, -1):
                r = ord(e) + int(t[i - 1])
                n += chr(r)
            return n
        def d(self, efi: str, ting_id: str, creation_time: str) -> str:
            e = ting_id.replace('-', '')
            t = creation_time.replace("-", "").replace(":", "").replace("T", "").replace(".", "").replace(" ", "")
            t = t.ljust(20, '0')
            key_str = self.gk(e, t)
            iv_str = self.gi(e, t)
            key = base64.b64encode(key_str.encode())
            iv = base64.b64encode(iv_str.encode())
            key_bytes = base64.b64decode(key)
            iv_bytes = base64.b64decode(iv)
            # clean and decode efi
            efi_clean = efi.replace('\n', '').rstrip()
            encrypted_data = base64.b64decode(efi_clean)
            cipher = AES.new(key_bytes, AES.MODE_CBC, iv_bytes)
            decrypted = cipher.decrypt(encrypted_data)
            try:
                return unpad(decrypted, AES.block_size).decode('utf-8')
            except ValueError:
                return "[解密失败] Padding error 或 key/iv 错误"
    # 示例用法
    vs = VS()
    efi = "30Bf6+rGEZSFx+73P01z67iEDCBtOZzjkC4E2ENX2g7VwllOb/MS4GO5SMb7ug8rdcpKiGpARuRzHYPuqp+mHROrNemNftGuVT7QW6G3Nyjjc2MRRg+NLyRACPORkhoCbTl3WiHAWrVCyYIev2UZ1g=="
    ting_id = "3a1801e1-74ec-871b-6d8b-68bce50caf01"
    creation_time = "2025-02-10T19:52:58.873983"
    result = vs.d(efi, ting_id, creation_time)
    print("解码结果:", result)
    5.3验证
    打印结果:
    解码结果: /myfiles/host/listen/听书目录/黄金召唤师~醉虎~紫襟剧社/e804fa07a1bb47d8835da153c2643c2c.m4a
    与上述提到的服务器地址 'http://36.5.86.202:50010‘ 拼接,就是http://36.5.86.202:50010/myfiles/host/listen/听书目录/黄金召唤师~醉虎~紫襟剧社/e804fa07a1bb47d8835da153c2643c2c.m4a
    验证地址真实有效


    2025-05-29_204456.jpg (227.93 KB, 下载次数: 0)
    下载附件
    2025-5-29 20:49 上传

    地址, 下载次数

  • eastmarquis   

    这个嗅探到地址能直接下载缓存不能
    FYL11162022   

    前来学习
    lenvy1
    OP
      


    eastmarquis 发表于 2025-5-29 21:04
    这个嗅探到地址能直接下载缓存不能

    IDman等工具能直接嗅探到播放地址下载
    zsr849408332   

    学习学习,感谢大佬的精彩讲解
    amwquhwqas128   

    多谢大佬的讲解文章
    killjd   

    收藏学习进步
    crazyxsl   

    学习了 感谢
    您需要登录后才可以回帖 登录 | 立即注册