文章来源:吾爱破解论坛
作者:lenvy1
免责声明:
本贴中所有内容仅供学习交流使用,不用于其他任何目的,不提供完整代码,严禁用于商业用途和非法用途,否则由此产生的一切后果均与作者无关。
一、前言
要获取网络上免费站的音视频真实地址的方法其实有很多,比如直接用IDman插件嗅探,或者抓包缓存等。
但是!!!这只是单个音频地址的获取方法,而且不一定有效,比如音频地址使用异步动态加载,甚至是加密包装过的,使用嗅探工具不一定能嗅探到。
而且!!!如果想要批量获取,就必须了解这个音频真实地址的产生过程,这个过程就是解析(逆向)其算法过程。
本次解析目标就是一个解析算法的过程。
我也是小白,对JS一窍不通,有错漏之处望海涵。
大佬见笑了。
二、目标地址
aHR0cDovL3d3dy55dWV0aW5nYmEuY24vYm9vay9kZXRhaWwvM2ExODAxNzQtN2I4Yi05ZWVhLWUwNmYtMzk0NmQ5Y2E5NmYwLzA=三、解析思路
1、通过HTML源码 -> 直接搜索关键词
通过Ctrl + U查看HTML源码 Ctrl + F搜索关键词 .mp3 或.m4a 找到明文地址,如以下这种:
![]()
如果搜索关键词 .mp3 或 .m4a 没能找到,则试着搜索 var now 或 var next,找到以下这种,一般都是被加密或Base64转码了。
如果是这种加“*”号的,可以使用这个方法解析获得明文地址: // 可自行转成Python的方法;function FonHen_JieMa(u) { var a = u.split("*"); var b = ''; for (var i = 1, n = a.length; i < n; i++) { b += String.fromCharCode(a[i]) } return b}
2、找到对应API接口分析如果在HTML源码并没有找到疑似加密或明文的音频真实地址,大概率就是通过API接口请求了。
有些通过API接口可以直接返回明文真实地址,比如某听书网:
![]()
还有一些API接口返回的是经过加密的地址,需要二次解密,比如这一次的yue听巴网:
![]()
四、开始对目标地址解析全过程
1、发现有防调试机制 -> 可利用脚本破除控制台检测
首先,经过关键词搜索尝试之后,发现这个站点的音频真实地址并没有加载在静态的HTML源码中然后通过F12 或手动打开浏览器开发者工具,发现被秒关闭调试网页,说明该站点有防御控制台打开的反调试机制。猜测可能是检测控制台被打开然后触发反调试,这个简单,我们可以通过开启“控制台防检测”脚本或工具,以绕过这种反调试机制。比如这个油猴脚本:aHR0cHM6Ly9ncmVhc3lmb3JrLm9yZy96aC1DTi9zY3JpcHRzLzUyMzc5Mi3mtY/op4jlmajmjqfliLblj7DpmLLmo4DmtYs=(需自行将目标站点添加进包含规则@match)
2、打开开发者工具 -> 查找API接口对目标站点开启“控制台防检测”脚本后,接下来就是分析API了。因为F12被禁止,我们可以手动打开浏览器开发者工具。切换到“网络”标签选项卡 ->重载刷新(F5)当前网页 -> 随意点播放一个音频章节 -> 找到可疑的api接口地址“ting-with-efi”:
![]()
而且通过替换这个ID,API返回的是不同的efi字段(疑似加密的真实音频地址)。
3、解密真实地址 -> 查找解密函数逻辑链
接下来也就是分析api返回的efi字段的值具体是什么了,看看到底是不是音频地址。
在调试工具窗口试着Ctrl + Shift + F 全局搜索“efi”这个字段,看看它生成逻辑。
![]()
发现搜索结果太多干扰项了,换成跟踪ting-with-efi的启动器调用栈,发现都是源自同一个js解析,这说明这个js启动器文件才是关键。
![]()
很明显,”initGetData“这个调用栈名称是关于初始化数据包的,我们直接从这一个点进去(以后分析推荐直接从启动器入手)。
![]()
太明显了,一眼就看到了相关初始化方法,特别是initAudio(),一看就是关于音频初始化的。
![]()
从initAudio()入手,发现【e.playUrl = o,】这一行,右键加断点然后刷新查看,果然是真实的音频地址,看生成逻辑链,很明显,我们下一步就是要找到vs函数:
![]()
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 < 20; i++) {
- const r = e[i].charCodeAt(0) + Number(t[i]);
- n += String.fromCharCode(r)
- }
- for (let i = 20; i < e.length; i++) {
- const r = e[i].charCodeAt(0) + Number(t[i - 20]);
- n += String.fromCharCode(r)
- }
- return n
- }
- gi(e, t) {
- let n = "";
- for (let i = 20; i > 4; i--) {
- const r = e[i].charCodeAt(0) + Number(t[i - 1]);
- n += String.fromCharCode(r)
- }
- return n
- }
- }
某AI助手分析过程:
![]()
![]()
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[i]) + int(t[i])
- n += chr(r)
- for i in range(20, len(e)):
- r = ord(e[i]) + 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[i]) + 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
验证地址真实有效
赞(25)