喜马拉雅音频链接逆向实战

// 实战案例:喜马拉雅音频链接逆向实战

// 网址:https://www.ximalaya.com/album/40514512

概述

实现了一个喜马拉雅音频链接的解密功能,能够将加密的音频链接转换为可用的真实链接。代码主要使用了Base64解码和自定义的XOR异或操作进行解密。

逆向网页分析

复制代码
const r = new Uint8Array([188, 174, 178, 234, 171, 147, 70, 82, 76, 72, 192, 132, 60, 17, 30, 127, 184, 233, 48, 105, 38, 232, 240, 21, 47, 252, 41, 229, 209, 213, 71, 40, 63, 152, 156, 88, 51, 141, 139, 145, 133, 2, 160, 191, 11, 100, 10, 78, 253, 151, 42, 166, 92, 22, 185, 140, 164, 91, 194, 175, 239, 217, 177, 75, 19, 225, 94, 107, 125, 138, 242, 31, 182, 150, 15, 24, 226, 29, 80, 116, 168, 118, 28, 1, 186, 220, 158, 79, 59, 244, 119, 9, 189, 161, 74, 130, 221, 56, 216, 241, 212, 26, 218, 170, 85, 165, 153, 69, 238, 93, 255, 142, 3, 159, 215, 67, 33, 249, 53, 176, 77, 254, 222, 25, 115, 101, 148, 16, 13, 237, 197, 5, 58, 157, 135, 248, 223, 61, 198, 211, 110, 44, 54, 111, 52, 227, 4, 46, 205, 7, 219, 136, 14, 87, 114, 64, 104, 50, 39, 203, 81, 196, 43, 163, 173, 109, 108, 187, 102, 195, 37, 235, 65, 190, 113, 149, 143, 8, 27, 155, 207, 134, 123, 224, 129, 245, 62, 66, 172, 122, 126, 12, 162, 214, 90, 247, 251, 124, 201, 236, 117, 183, 73, 95, 89, 246, 181, 179, 83, 228, 193, 99, 6, 45, 112, 32, 154, 128, 230, 131, 206, 243, 57, 84, 146, 0, 35, 96, 250, 137, 36, 208, 103, 34, 68, 204, 231, 144, 120, 98, 202, 49, 210, 23, 200, 18, 86, 55, 121, 20, 199, 97, 167, 180, 169, 106])
  , n = new Uint8Array([20, 234, 159, 167, 230, 233, 58, 255, 158, 36, 210, 254, 133, 166, 59, 63, 209, 177, 184, 155, 85, 235, 94, 1, 242, 87, 228, 232, 191, 3, 69, 178])
  , o = new Uint8Array([183, 174, 108, 16, 131, 159, 250, 5, 239, 110, 193, 202, 153, 137, 251, 176, 119, 150, 47, 204, 97, 237, 1, 71, 177, 42, 88, 218, 166, 82, 87, 94, 14, 195, 69, 127, 215, 240, 225, 197, 238, 142, 123, 44, 219, 50, 190, 29, 181, 186, 169, 98, 139, 185, 152, 13, 141, 76, 6, 157, 200, 132, 182, 49, 20, 116, 136, 43, 155, 194, 101, 231, 162, 242, 151, 213, 53, 60, 26, 134, 211, 56, 28, 223, 107, 161, 199, 15, 229, 61, 96, 41, 66, 158, 254, 21, 165, 253, 103, 89, 3, 168, 40, 246, 81, 95, 58, 31, 172, 78, 99, 45, 148, 187, 222, 124, 55, 203, 235, 64, 68, 149, 180, 35, 113, 207, 118, 111, 91, 38, 247, 214, 7, 212, 209, 189, 241, 18, 115, 173, 25, 236, 121, 249, 75, 57, 216, 10, 175, 112, 234, 164, 70, 206, 198, 255, 140, 230, 12, 32, 83, 46, 245, 0, 62, 227, 72, 191, 156, 138, 248, 114, 220, 90, 84, 170, 128, 19, 24, 122, 146, 80, 39, 37, 8, 34, 22, 11, 93, 130, 63, 154, 244, 160, 144, 79, 23, 133, 92, 54, 102, 210, 65, 67, 27, 196, 201, 106, 143, 52, 74, 100, 217, 179, 48, 233, 126, 117, 184, 226, 85, 171, 167, 86, 2, 147, 17, 135, 228, 252, 105, 30, 192, 129, 178, 120, 36, 145, 51, 163, 77, 205, 73, 4, 188, 125, 232, 33, 243, 109, 224, 104, 208, 221, 59, 9])
  , a = new Uint8Array([204, 53, 135, 197, 39, 73, 58, 160, 79, 24, 12, 83, 180, 250, 101, 60, 206, 30, 10, 227, 36, 95, 161, 16, 135, 150, 235, 116, 242, 116, 165, 171])
  , i = "function" == typeof atob
const c = (e => {
  let t = {};
  return e.forEach(( (e, r) => t[e] = r)),
    t
}
          )(Array.prototype.slice.call("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="))
  , s = /^(?:[A-Za-z\d+\/]{4})*?(?:[A-Za-z\d+\/]{2}(?:==)?|[A-Za-z\d+\/]{3}=?)?$/
  , l = String.fromCharCode.bind(String);
"function" == typeof Uint8Array.from && Uint8Array.from.bind(Uint8Array);
const f = i ? e => atob(e.replace(/[^A-Za-z0-9\+\/]/g, "")) : u ? t => e.from(t, "base64").toString("binary") : e => {
    if (e = e.replace(/\s+/g, ""),
    !s.test(e))
        throw new TypeError("malformed base64.");
    e += "==".slice(2 - (3 & e.length));
    let t, r, n, o = "";
    for (let a = 0; a < e.length; )
        t = c[e.charAt(a++)] << 18 | c[e.charAt(a++)] << 12 | (r = c[e.charAt(a++)]) << 6 | (n = c[e.charAt(a++)]),
        o += 64 === r ? l(t >> 16 & 255) : 64 === n ? l(t >> 16 & 255, t >> 8 & 255) : l(t >> 16 & 255, t >> 8 & 255, 255 & t);
    return o
}
;
function p(e, t, r) {
    let n = Math.min(e.length - t, r.length);
    for (let o = 0; o < n; o++)
        e[o + t] = e[o + t] ^ r[o]
}

function decrypt(e) {
                const {link: t="", deviceType: i="www2"} = e;
                let u = o
                  , c = a;
                ["www2", "mweb2"].includes(i) || (u = r,
                c = n);
                try {
                    let e = f(t.replace(/_/g, "/").replace(/-/g, "+"));
                    if (null === e || e.length < 16)
                        return t;
                    let r = new Uint8Array(e.length - 16);
                    for (let t = 0; t < e.length - 16; t++)
                        r[t] = e.charCodeAt(t);
                    let n = new Uint8Array(16);
                    for (let t = 0; t < 16; t++)
                        n[t] = e.charCodeAt(e.length - 16 + t);
                    for (let e = 0; e < r.length; e++)
                        r[e] = u[r[e]];
                    for (let e = 0; e < r.length; e += 16)
                        p(r, e, n);
                    for (let e = 0; e < r.length; e += 32)
                        p(r, e, c);
                    return function(e) {
                        var t, r, n, o, a, i;
                        for (t = "",
                        n = e.length,
                        r = 0; r < n; )
                            switch ((o = e[r++]) >> 4) {
                            case 0:
                            case 1:
                            case 2:
                            case 3:
                            case 4:
                            case 5:
                            case 6:
                            case 7:
                                t += String.fromCharCode(o);
                                break;
                            case 12:
                            case 13:
                                a = e[r++],
                                t += String.fromCharCode((31 & o) << 6 | 63 & a);
                                break;
                            case 14:
                                a = e[r++],
                                i = e[r++],
                                t += String.fromCharCode((15 & o) << 12 | (63 & a) << 6 | (63 & i) << 0)
                            }
                        return t
                    }(r)
                } catch (e) {
                    return console.warn(e, "secret failed"),
                    ""
                }
            }

const e = "Wy_eXUyeNkOhjn9AMAdt2p9v7ke5HcsKgiZFfP1TyaF8b-WLiozow2Yzab4oVwOICeL-i6uQ-YIhXSdADuNK0kBo-htUHoNikUZq8CgZIA4QexRxKxdcDdUVRciIKUtaqx_kqbmgyU0Zf3zyANyy-J_Zz6qBX8P8VXHFtn09xetz55cMQeYpWY5_2W2l8zeXinzIYXMDXykAmz10Yanh2Z_z71AQFr106xT3dyYase8Qqwmqbq7LU9sA8VQcLIa2t1JV0C1pks-rRvc5jVxUK2F_jaQATxsqYptFqg"
params = {
    deviceType: "www2",
    link: e
}

console.log(decrypt(params))

主要变量说明

2.1 置换表

  • r: 一个256字节的Uint8Array,用于www1设备类型的置换表
  • n: 一个32字节的Uint8Array,用于www1设备类型的密钥
  • o: 一个256字节的Uint8Array,用于www2/mweb2设备类型的置换表
  • a: 一个32字节的Uint8Array,用于www2/mweb2设备类型的密钥

2.2 辅助函数

  • c: Base64字符到索引的映射表
  • s: Base64字符串验证正则表达式
  • f: Base64解码函数,兼容不同环境(atob/Node.js Buffer/纯JS实现)

解密流程分析

3.1 输入参数

解密函数decrypt接收一个对象参数,包含:

  • link: 加密的链接字符串
  • deviceType: 设备类型(默认为"www2")

3.2 解密步骤

  1. 选择置换表和密钥:
    • 根据deviceType选择使用www1www2/mweb2的置换表和密钥
  1. Base64解码:
    • 替换链接中的特殊字符(_/, -+)
    • 使用f函数进行Base64解码
  1. 数据拆分:
    • 将解码后的数据分为:
      • 主体数据(前n-16字节)
      • IV向量(最后16字节)
  1. 置换操作:
    • 对主体数据的每个字节,使用置换表进行替换
  1. 第一轮XOR:
    • 将主体数据按16字节分块,每块与IV向量进行异或操作
  1. 第二轮XOR:
    • 将主体数据按32字节分块,每块与密钥进行异或操作
  1. UTF-8解码:
    • 将最终结果转换为UTF-8字符串

关键函数说明

4.1 p(e, t, r) - 异或操作函数

  • 对数组e从位置t开始,与数组r进行逐字节异或操作
  • 只处理r.length长度的数据

4.2 decrypt(e) - 主解密函数

  • 完整实现上述解密流程
  • 包含错误处理,解密失败时返回空字符串

示例使用

复制代码
const encryptedLink = "Wy_eXUyeNkOhjn9AMAdt2p9v7ke5HcsKgiZFfP1TyaF8b-WLiozow2Yzab4oVwOICeL-i6uQ-YIhXSdADuNK0kBo-htUHoNikUZq8CgZIA4QexRxKxdcDdUVRciIKUtaqx_kqbmgyU0Zf3zyANyy-J_Zz6qBX8P8VXHFtn09xetz55cMQeYpWY5_2W2l8zeXinzIYXMDXykAmz10Yanh2Z_z71AQFr106xT3dyYase8Qqwmqbq7LU9sA8VQcLIa2t1JV0C1pks-rRvc5jVxUK2F_jaQATxsqYptFqg";

const params = {
    deviceType: "www2",
    link: encryptedLink
};

console.log(decrypt(params));

逆向要点

  1. Base64变种:
    • 喜马拉雅使用了修改后的Base64,需要先将**_-**替换回标准字符
  1. 双重加密:
    • 使用了置换表+两次异或操作(IV向量和密钥)的加密方式
  1. 设备类型区分:
    • 不同设备类型使用不同的置换表和密钥
  1. 错误处理:
    • 解密过程有完善的错误捕获,失败时返回空字符串

代码实现

复制代码
import execjs
import requests

e = "Wy_eXUyeNkOhjn9AMAdt2p9v7ke5HcsKgiZFfP1TyaF8b-WLiozow2Yzab4oVwOICeL-i6uQ-YIhXSdADuNK0kBo-htUHoNikUZq8CgZIA4QexRxKxdcDdUVRciIKUtaqx_kqbmgyU0Zf3zyANyy-J_Zz6qBX8P8VXHFtn09xetz55cMQeYpWY5_2W2l8zeXinzIYXMDXykAmz10Yanh2Z_z71AQFr106xT3dyYase8Qqwmqbq7LU9sA8VQcLIa2t1JV0C1pks-rRvc5jVxUK2F_jaQATxsqYptFqg"
params = {
    "deviceType": "www2",
    "link": e
}
url = execjs.compile(open("demo.js", "r", encoding="utf-8").read()).call("decrypt", params)

print(url)

headers = {
    "Accept": "*/*",
    "Accept-Language": "zh-CN,zh;q=0.9",
    "Cache-Control": "no-cache",
    "Connection": "keep-alive",
    "Pragma": "no-cache",
    "Referer": "https://www.ximalaya.com/album/40514512",
    "Sec-Fetch-Dest": "empty",
    "Sec-Fetch-Mode": "cors",
    "Sec-Fetch-Site": "same-origin",
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36",
    "sec-ch-ua": "\"Not)A;Brand\";v=\"8\", \"Chromium\";v=\"138\", \"Google Chrome\";v=\"138\"",
    "sec-ch-ua-mobile": "?0",
    "sec-ch-ua-platform": "\"Windows\"",
    "xm-sign": "D2R52wQ49oDg43E+X/k5O0dhy05NanWYCPPFYzv/j/lWUXa6&&h1t4Shl4YXoCm6zn8icedDAOBU7XQ3wXO9P7bj5PG44_1"
}
cookies = {
    "HWWAFSESID": "b7149193cbfa6f10d36",
    "HWWAFSESTIME": "1753766956877",
    "_xmLog": "h5&1da2cf22-6f8c-49d7-a5ed-d0b8669efa75&process.env.sdkVersion",
    "xm-page-viewid": "ximalaya-web",
    "DATE": "1753766960369",
    "wfp": "ACNiOTQwOGZhZDZiYWI3NmIxsN-ky99DAMx4bXdlYl93d3c",
    "Hm_lvt_4a7d8ec50cfd6af753c4f8aee3425070": "1753766961",
    "HMACCOUNT": "6C6C5462E977E59A",
    "impl": "www.ximalaya.com.login",
    "crystal": "U2FsdGVkX19aUPEMu+91bfk1TvDcoEUjahBOGCjdes6aX4Hwr2rTKtj+0LTeGlNMPvvswA7N4gFKGQ9rjSvxTSAHnwFReM36BnJJwCy+GzGxdfaxXYD1kx1+dcPF+NK6egx9B52UiflLrqlBsdDMiajMvYkTYMdjgFulR7O6Kn4EEJmSFlcBKkek1d1tV/1Q/xqWK3WnclVA73Fg4Pl2tIJx/5+c07dZPEPHS/WrJl2te2YEa/DmlszeYTrOSuQL",
    "Hm_lpvt_4a7d8ec50cfd6af753c4f8aee3425070": "1753766970",
    "cmci9xde": "U2FsdGVkX1/EKUC+0g+upx4ulaWEH94LqC5A5SrE1Opx0VnLAkjwpqhA3oTjD3xch9AhEeyYNJ/9LfeCO24m6w==",
    "pmck9xge": "U2FsdGVkX19q/L/MPwnKSOrd4TCj0LeNsQ1MAdHXvS0=",
    "assva6": "U2FsdGVkX19H+YdXwbrOpIoUuUUdH8uoVatJyMw9e14=",
    "assva5": "U2FsdGVkX19HDzYCsHUzF5c+e85dUCQs9SlP9bVcu3zW6UweHXfBKDhY6LdG6BW8isLCh8Msc2lSHEbVJcAH5Q==",
    "vmce9xdq": "U2FsdGVkX19k4H9hsxiBellV342Jnroqv0Cq3+LhzkvF8EcRbrBdgaeHTgvA98WmkYWlOS/aoHjp4wwuNWamyVwKJdhzGk8owBzFrcM6S+uM/cOGPJVj61VG+CpDIvaLkA4OEpNsAU9GrCtU2SLxuZNifnXrCrr/aRVvK9hZayM="
}
response = requests.get(url, headers=headers, cookies=cookies)
with open("demo.mp4", "wb") as f:
    f.write(response.content)

总结

这段代码展示了喜马拉雅音频链接的解密过程,结合了Base64编码、字节置换和异或操作等多种技术。理解这个过程有助于分析喜马拉雅的音频获取机制,并可用于开发相关工具或进行进一步的安全研究。

相关推荐
一枚前端小能手7 小时前
「周更第6期」实用JS库推荐:InversifyJS
前端·javascript
叉歪7 小时前
纯前端函数,一个拖拽移动、调整大小、旋转、缩放的工具库
javascript
Hilaku7 小时前
"事件委托"这个老古董,在现代React/Vue里还有用武之地吗?
前端·javascript·vue.js
前端缘梦7 小时前
Webpack 5 核心升级指南:从配置优化到性能提升的完整实践
前端·面试·webpack
wuxuanok7 小时前
苍穹外卖 —— 公共字段填充
java·开发语言·spring boot·spring·mybatis
汤姆Tom7 小时前
现代 CSS 架构与组件化:构建可扩展的样式系统
前端·css
偷光7 小时前
浏览器中的隐藏IDE: Console (控制台) 面板
开发语言·前端·ide·php
时间的情敌7 小时前
对Webpack的深度解析
前端·webpack·node.js
拜无忧8 小时前
【案例】可视化模板,驾驶舱模板,3x3,兼容移动
前端·echarts·数据可视化