JSVMP-某Q音乐sign,data,响应解密

网址:https://y.qq.com/

接口:以https://u6.y.qq.com/cgi-bin/musics.fcg开头的post数据包

输入网址打开devtool

任意点击即可触发网络请求

请求和响应都是加密:

定位请求调用堆栈,点击进入后查找关键代码

设置断点后重新触发请求,可以看到断点被命中:

释放断点继续执行:

then打印出来消息了,感觉就是请求体:

去网络看看:

发现只有sign相同,而请求体不同:

推测可能为随机数或时间戳,后续再深入分析,先重点研究sign参数

请求sign

再次发包然后进到ie函数发现是一个典型的JSVMP:

为便于本地分析(可借助AI辅助),将JSVMP代码折叠后复制到本地,保存为ori_copy.js文件:

在Node.js环境中将ne替换为global,并复制浏览器中的相关值进行验证,发现结果不一致:

决定通过插桩方式分析其检测逻辑,展开JSVMP后发现case分支中的表达式模式规整,可使用AST进行批量插桩:

先新建一个getObjStr.js(同目录),用于保存我们的方法,粘贴以下代码并保存

在此处说明一下,这个代码还在使用观测中,可能还有bug,并且这个代码以后还有可能会用到,需要妥善保存:

复制代码
const me_ = {
    Function_prototype_toString: Function.prototype.toString,
    Symbol_prototype_toString: Symbol.prototype.toString,
    RegExp_prototype_toString: RegExp.prototype.toString,
    // 使用 Map 一次性完成字符串转义,避免多次 split/join
    escapeMap: new Map([
        ['\\', '\\\\'],
        ['"', '\\"'],
        ['\n', '\\n'],
        ['\t', '\\t'],
        ['\r', '\\r'],
        ['\b', '\\b'],
        ['\f', '\\f'],
        ['\v', '\\v'],
        // 可选:处理零宽字符或控制字符
        // 如 '\x00' -> '\\u0000' 等,按需添加
    ]),
    // 控制是否展开 global / Window
    unPack_global: 0,
    unPack_Window: 0,
    // 是否包含不可枚举属性(原代码使用了 getOwnPropertyNames)
    includeNonEnumerable: true,
    // 类数组 / 可迭代对象是否展开为数组(而不是对象)
    expandIterables: true,
    // 缩进(null 表示不缩进,数字表示空格数,字符串表示缩进符)
    indent: null,
};

function getObjStr(value) {
    // 用于跟踪已访问对象,检测循环引用
    const visited = new Set();

    function getType(v) {
        return Object.prototype.toString.call(v).slice(8, -1);
    }

    // 安全的字符串转义(替换特殊字符)
    function escapeString(s) {
        let result = '';
        for (const ch of s) {
            if (me_.escapeMap.has(ch)) {
                result += me_.escapeMap.get(ch);
            } else {
                result += ch;
            }
        }
        return result;
    }

    // 辅助:生成缩进字符串
    function indent(level) {
        if (me_.indent === null) return '';
        const unit = typeof me_.indent === 'number' ? ' '.repeat(me_.indent) : me_.indent;
        return unit.repeat(level);
    }

    // 主递归序列化
    function toStr(v, depth = 0) {
        // 处理基本类型
        if (v === null) return 'null';
        if (v === undefined) return 'undefined';
        if (typeof v === 'boolean') return v ? 'true' : 'false';
        if (typeof v === 'number') {
            if (isNaN(v)) return 'NaN';
            if (!isFinite(v)) return v > 0 ? 'Infinity' : '-Infinity';
            return String(v);
        }
        if (typeof v === 'string') return `"${escapeString(v)}"`;
        if (typeof v === 'bigint') return `BigInt(${v.toString()})`;
        if (typeof v === 'symbol') {
            return me_.Symbol_prototype_toString.call(v);
        }
        if (typeof v === 'function') {
            return serializeFunction(v);
        }

        // 循环引用检测
        if (visited.has(v)) {
            return `"#circular_${getType(v)}#"`;
        }
        visited.add(v);

        let result;

        // 优先处理 toJSON
        if (typeof v.toJSON === 'function') {
            const jv = v.toJSON();
            result = toStr(jv, depth);   // 递归序列化 toJSON 返回值
            visited.delete(v);
            return result;
        }

        const type = getType(v);

        switch (type) {
            case 'Array':
                result = arrayToStr(v, depth);
                break;
            case 'Object':
                result = objectToStr(v, depth);
                break;
            case 'Map':
                result = mapToStr(v, depth);
                break;
            case 'Set':
                result = setToStr(v, depth);
                break;
            case 'Date':
                result = `Date(${v.toISOString()})`;
                break;
            case 'RegExp':
                result = me_.RegExp_prototype_toString.call(v);
                break;
            case 'Error':
            case 'EvalError':
            case 'RangeError':
            case 'ReferenceError':
            case 'SyntaxError':
            case 'TypeError':
            case 'URIError':
                result = errorToStr(v);
                break;
            case 'ArrayBuffer':
                result = `ArrayBuffer { byteLength: ${v.byteLength} }`;
                break;
            case 'DataView':
                // DataView 可提取 byteLength,但内容不易简洁表示
                result = `DataView { byteLength: ${v.byteLength}, byteOffset: ${v.byteOffset} }`;
                break;
            case 'Promise':
                result = 'Promise { ... }';
                break;
            case 'WeakMap':
            case 'WeakSet':
                result = `${type} { ... }`;
                break;
            case 'Function':
                result = serializeFunction(v);
                break;
            case 'Symbol':
                result = me_.Symbol_prototype_toString.call(v);
                break;
            case 'global':
                if (me_.unPack_global) {
                    result = objectToStr(v, depth);
                } else {
                    result = 'global';
                }
                break;
            case 'Window':
                if (me_.unPack_Window) {
                    result = objectToStr(v, depth);
                } else {
                    result = 'Window';
                }
                break;
            default:
                // 处理 TypedArray
                if (ArrayBuffer.isView(v)) {
                    // TypedArrays 可迭代,展开为数组
                    result = `TypedArray ${type} [${[...v].map(item => toStr(item, depth + 1)).join(', ')}]`;
                }
                // 处理 Node.js Buffer
                else if (typeof globalThis.Buffer !== 'undefined' && globalThis.Buffer.isBuffer(v)) {
                    result = `Buffer [${[...v].map(item => toStr(item, depth + 1)).join(', ')}]`;
                }
                // 处理可迭代对象(如 arguments、NodeList 等)
                else if (me_.expandIterables && typeof v[Symbol.iterator] === 'function' && typeof v !== 'string') {
                    result = `Iterable ${type} [${[...v].map(item => toStr(item, depth + 1)).join(', ')}]`;
                }
                // 类数组对象(有 length 和数字索引)
                else if (typeof v === 'object' && v !== null && typeof v.length === 'number' && v.length >= 0) {
                    result = `ArrayLike ${type} { length: ${v.length} }`;
                    // 也可以尝试展开索引,但索引可能是稀疏的,这里从简
                }
                else {
                    // 后备:作为普通对象尝试序列化
                    const objStr = objectToStr(v, depth);
                    if (objStr === '{}' && getType(v) !== 'Object') {
                        result = type;  // 空无意义的对象,只返回类型名
                    } else {
                        result = objStr;
                    }
                }
        }

        visited.delete(v);
        return result;
    }

    function serializeFunction(func) {
        const funcName = func.name || 'anonymous';
        const funcStr = me_.Function_prototype_toString.call(func);
        if (funcStr.endsWith('() { [native code] }')) {
            return `f ${funcName}`;
        } else if (funcStr.length > 100) {
            return funcName;  // 防止过长
        } else {
            // 返回完整函数体
            return funcStr;
        }
    }

    function errorToStr(err) {
        const parts = [`name: ${JSON.stringify(err.name)}`, `message: ${JSON.stringify(err.message)}`];
        if (err.stack) parts.push(`stack: ${JSON.stringify(err.stack)}`);
        return `${getType(err)} { ${parts.join(', ')} }`;
    }

    function objectToStr(obj, depth) {
        const keys = me_.includeNonEnumerable ?
            Object.getOwnPropertyNames(obj) :
            Object.keys(obj);
        if (keys.length === 0) return '{}';

        const indentStr = indent(depth + 1);
        const endIndent = indent(depth);
        const items = [];

        for (const key of keys) {
            try {
                const val = obj[key];
                const valStr = toStr(val, depth + 1);
                items.push(`${indentStr}"${key}": ${valStr}`);
            } catch (e) {
                items.push(`${indentStr}"${key}": error: ${e.message}`);
            }
        }

        if (me_.indent === null) {
            return `{ ${items.join(', ')} }`;
        } else {
            return `{\n${items.join(',\n')}\n${endIndent}}`;
        }
    }

    function arrayToStr(arr, depth) {
        if (arr.length === 0) return '[]';
        const indentStr = indent(depth + 1);
        const endIndent = indent(depth);
        const items = arr.map(item => `${indentStr}${toStr(item, depth + 1)}`);

        if (me_.indent === null) {
            return `[${items.map(s => s.trimStart()).join(', ')}]`;
        } else {
            return `[\n${items.join(',\n')}\n${endIndent}]`;
        }
    }

    function mapToStr(map, depth) {
        if (map.size === 0) return 'Map {}';
        const indentStr = indent(depth + 1);
        const endIndent = indent(depth);
        const items = [];
        for (const [key, value] of map) {
            const kStr = toStr(key, depth + 1);
            const vStr = toStr(value, depth + 1);
            items.push(`${indentStr}${kStr}: ${vStr}`);
        }
        if (me_.indent === null) {
            return `Map { ${items.join(', ')} }`;
        } else {
            return `Map {\n${items.join(',\n')}\n${endIndent}}`;
        }
    }

    function setToStr(set, depth) {
        if (set.size === 0) return 'Set {}';
        const indentStr = indent(depth + 1);
        const endIndent = indent(depth);
        const items = [...set].map(item => `${indentStr}${toStr(item, depth + 1)}`);
        if (me_.indent === null) {
            return `Set { ${items.join(', ')} }`;
        } else {
            return `Set {\n${items.join(',\n')}\n${endIndent}}`;
        }
    }

    return toStr(value);
}

新建一个ast_work.js(同目录)用于写插桩代码,ast_output.js(同目录)用于ast输出,log_output.txt(同目录)用于日志输出

在ast_work.js粘贴以下代码,注意babel的依赖库需要安装哈,不会安的问问ai或者网上找找

复制代码
const parser = require('@babel/parser');
const traverse = require('@babel/traverse').default;
const generator = require('@babel/generator').default;
const te = require('@babel/types');
const fs = require('fs');
const path = require('path');

function read_file(file_path) {
    return fs.readFileSync(file_path, 'utf8');
}
function write_file(file_path, content) {
    try {
        fs.writeFileSync(file_path, content, 'utf8');
        console.log('写入成功!');
    } catch (e) {
        console.log(e);
    }
}


const func_path = path.join(__dirname, './getObjStr.js');
const func_str = read_file(func_path);
const input_path = path.join(__dirname, './ori_copy.js');
const input_code = read_file(input_path);
var ast = parser.parse(input_code);
global.generator = generator, global.parser = parser;


traverse(ast, {
    SwitchCase(path) {
        let { consequent,test } = path.node;
        let test_value=test.value;
        if (consequent.length !== 2) return;
        if (!(te.isExpressionStatement(consequent[0]) && te.isBreakStatement(consequent[1]))) return;
        let expr = consequent[0].expression;
        let break_stmt = consequent[1];
        let exprs = [];
        if (te.isAssignmentExpression(expr)) {
            exprs.push(expr)
        } else if (te.isSequenceExpression(expr)) {
            exprs.push(...expr.expressions);
        } else {
            console.log(expr.type, generator(expr).code);
        }
        let new_exprs = [];
        exprs.forEach(item => {
            let item_code = generator(item).code;
            switch (item_code) {
                case 'h[n[++p]] = h[n[++p]][h[n[++p]]]':
                    var new_code = `log(p, ${test_value}, "对象取值", \`object->h[\${n[p+2]}]->\`, getObjStr(h[n[p+2]]), \`key->h[\${n[p+3]}]->\`, getObjStr(h[n[p+3]]), "value->", getObjStr(h[n[p+2]][h[n[p+3]]]),"place_index->", \`h[\${n[p+1]}]\`)`;
                    new_exprs.push(parser.parse(new_code).program.body[0]);
                    break;
                case 'h[n[++p]] = h[n[++p]][n[++p]]':
                    new_code = `log(p, ${test_value}, "对象取值", \`object->h[\${n[p+2]}]->\`, getObjStr(h[n[p+2]]), "key->", getObjStr(n[p+3]), "value->", getObjStr(h[n[p+2]][n[p+3]]),"place_index->", \`h[\${n[p+1]}]\`)`;
                    new_exprs.push(parser.parse(new_code).program.body[0]);
                    break;
                case 'h[n[++p]][n[++p]] = h[n[++p]]':
                    new_code = `log(p, ${test_value}, "对象赋值", \`object->h[\${n[p+1]}]->\`, getObjStr(h[n[p+1]]), "key->", getObjStr(n[p+2]), \`value->h[\${n[p+3]}]->\`, getObjStr(h[n[p+3]]))`;
                    new_exprs.push(parser.parse(new_code).program.body[0]);
                    break;
                case 'h[n[++p]] = n[++p]':
                    new_code = `log(p, ${test_value}, "堆栈放值", "value->", n[p+2], "place_index->", n[p+1])`;
                    new_exprs.push(parser.parse(new_code).program.body[0]);
                    break;
                case 'h[n[++p]] = h[n[++p]]':
                    new_code = `log(p, ${test_value}, "堆栈放值", \`value->h[\${n[p+2]}]->\`, getObjStr(h[n[p+2]]), "place_index->", \`h[\${n[p+1]}]\`)`;
                    new_exprs.push(parser.parse(new_code).program.body[0]);
                    break;
                case 'h[n[++p]][h[n[++p]]] = h[n[++p]]':
                    new_code = `log(p, ${test_value}, "对象赋值", \`object->h[\${n[p+1]}]->\`, getObjStr(h[n[p+1]]), \`key->h[\${n[p+2]}]->\`, getObjStr(h[n[p+2]]), \`value->h[\${n[p+3]}]->\`, getObjStr(h[n[p+3]]))`;
                    new_exprs.push(parser.parse(new_code).program.body[0]);
                    break;
                case 'h[n[++p]] = false':
                case 'h[n[++p]] = true':
                case 'h[n[++p]] = ""':
                    new_code = `log(p, ${test_value}, "堆栈放值", "value->${item.right.value}", "place_index->", n[p+1])`;
                    new_exprs.push(parser.parse(new_code).program.body[0]);
                    break;
                case 'h[n[++p]] = !h[n[++p]]':
                case 'h[n[++p]] = -h[n[++p]]':
                case 'h[n[++p]] = ~h[n[++p]]':
                    new_code = `log(p, ${test_value}, "一元计算", "${item.right.operator}", getObjStr(h[n[p+2]]), "=",  ${item.right.operator}h[n[p+2]], "place_index->", n[p+1])`;
                    new_exprs.push(parser.parse(new_code).program.body[0]);
                    break;
                case 'h[n[++p]] = h[n[++p]].call(d)':
                    break
                    // case 'h[n[++p]] += String.fromCharCode(n[++p])':
                    //     new_code = 'log(p, "添加字符", `h[${n[p+1]}]->`, getObjStr(h[n[p+1]]), `+=`,`String.fromCharCode(${n[p+2]})->`, getObjStr(String.fromCharCode(n[p+2])))';
                    //     new_exprs.push(parser.parse(new_code).program.body[0]);
                    break;
                case 'h[n[++p]] = h[n[++p]] + h[n[++p]]':
                case 'h[n[++p]] = h[n[++p]] % h[n[++p]]':
                case 'h[n[++p]] = h[n[++p]] | h[n[++p]]':
                case 'h[n[++p]] = h[n[++p]] ^ h[n[++p]]':
                case 'h[n[++p]] = h[n[++p]] === h[n[++p]]':
                case 'h[n[++p]] = h[n[++p]] > h[n[++p]]':
                case 'h[n[++p]] = h[n[++p]] < h[n[++p]]':
                case 'h[n[++p]] = h[n[++p]] << h[n[++p]]':
                case 'h[n[++p]] = h[n[++p]] & h[n[++p]]':
                case 'h[n[++p]] = h[n[++p]] - h[n[++p]]':
                case 'h[n[++p]] = h[n[++p]] * h[n[++p]]':
                case 'h[n[++p]] = h[n[++p]] / h[n[++p]]':
                    var op = item.right.operator;
                    new_code = `log(p, ${test_value}, "二元运算",  getObjStr(h[n[p+2]]),"${op}", getObjStr(h[n[p+3]]), "=", getObjStr(h[n[p+2]]${op}h[n[p+3]]), "place_index->", n[p+1])`;
                    new_exprs.push(parser.parse(new_code).program.body[0]);
                    break;
                case 'h[n[++p]] = h[n[++p]] + n[++p]':
                case 'h[n[++p]] = h[n[++p]] & n[++p]':
                case 'h[n[++p]] = h[n[++p]] >> n[++p]':
                case 'h[n[++p]] = h[n[++p]] << n[++p]':
                case 'h[n[++p]] = h[n[++p]] === n[++p]':
                case 'h[n[++p]] = h[n[++p]] - n[++p]':
                case 'h[n[++p]] = h[n[++p]] >>> n[++p]':
                case 'h[n[++p]] = h[n[++p]] | n[++p]':
                case 'h[n[++p]] = h[n[++p]] < n[++p]':
                case 'h[n[++p]] = h[n[++p]] >= n[++p]':
                    op = item.right.operator;
                    new_code = `log(p, ${test_value}, "二元运算",  getObjStr(h[n[p+2]]),"${op}", getObjStr(n[p+3]), "=", getObjStr(h[n[p+2]]${op}n[p+3]), "place_index->", n[p+1])`;
                    new_exprs.push(parser.parse(new_code).program.body[0]);
                    break;
                // debugger
            }
            new_exprs.push(te.expressionStatement(te.cloneNode(item)));
        });
        new_exprs.push(break_stmt);
        path.node.consequent = new_exprs;
        update = true;
    }
});

const output_path = path.join(__dirname, './ast_output.js');
var output_code = generator(ast, {
    compact: false,
    retainLines: false,     // 不保留原始行,让生成器重新排列
    concise: false,         // 不简洁,保留空格和缩进
    jsescOption: { "minimal": true }
}).code;

output_code = String.raw`const fs=require('fs');
${func_str}
const path=require('path');
function log(...args) {
    log_str = args.join(' ')+'\n';
    fs.appendFileSync(path.join(__dirname, './log_output.txt'), log_str);
}`  + '\n' + output_code;
write_file(output_path, output_code);

然后运行ast_work.js,会在ast_output.js输出:

然后运行ast_output.js会输出日志(在log_output.txt)中:

运行后发现结果不同:

接下来分析日志内容,可借助AI辅助分析检测了哪些环境因素导致结果差异。AI分析指出检测了__qmfe_sign_check属性,需要将其设为1,随后在日志中定位到相关代码:

在ori_copy.js中添加该属性赋值语句即可:

然后再次一次运行ast_work.js,ast_output.js,发现这次相同:

接下来借助AI根据日志还原算法逻辑:

还原结果(因为用到了SHA1加密,还原算法直接从加密结果开始):

复制代码
const CryptoJS=require('crypto-js');  // 需要安装
function encrypt(enc_res) {
    const str_map = { "0": 0, "1": 1, "2": 2, "3": 3, "4": 4, "5": 5, "6": 6, "7": 7, "8": 8, "9": 9, "A": 10, "B": 11, "C": 12, "D": 13, "E": 14, "F": 15 };
    const middle_arr = [89, 39, 179, 150, 218, 82, 58, 252, 177, 52, 186, 123, 120, 64, 242, 133, 143, 161, 121, 179];
    const str_t_ = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";

    // Phase 1: Hex解析 + XOR
    let bytes = [];
    for (let i = 0; i < 20; i++) {
        let byte = str_map[enc_res[i * 2]] * 16 + str_map[enc_res[i * 2 + 1]];
        bytes.push(byte ^ middle_arr[i]);
    }

    // Phase 2: 自定义Base64编码
    let encoded = "";
    for (let i = 0; i < bytes.length; i += 3) {
        let a = bytes[i], b = bytes[i + 1] || 0, c = bytes[i + 2] || 0;
        encoded += str_t_[a >> 2];
        encoded += str_t_[(a & 3) << 4 | (b >> 4)];
        if (i + 1 < bytes.length) encoded += str_t_[(b & 15) << 2 | (c >> 6)];
        if (i + 2 < bytes.length) encoded += str_t_[c & 63];
    }

    return encoded.replace(/[\/+]/g, '');
}
function getSign(shaEncStr) {
    const middleChars = encrypt(shaEncStr);
    const head_arr = [23, 14, 6, 36, 16, 40, 7, 19], tail_arr = [16, 1, 32, 12, 19, 27, 8, 5];
    const prefixChars = head_arr.map((idx) => shaEncStr[idx]).join('');
    const suffixChars = tail_arr.map((idx) => shaEncStr[idx]).join('');
    return ('zzc' + prefixChars + middleChars + suffixChars).toLowerCase();
}
var data_str = '{"comm":{"cv":4747474,"ct":24,"format":"json","inCharset":"utf-8","outCharset":"utf-8","notice":0,"platform":"yqq.json","needNewCode":1,"uin":0,"g_tk_new_20200303":5381,"g_tk":5381},"req_1":{"method":"get_playlist_by_category","module":"playlist.PlayListPlazaServer","param":{"id":3056,"curPage":1,"size":40,"order":5,"titleid":3056}}}'
data_str = CryptoJS.SHA1(data_str).toString().toUpperCase();
var result=getSign(data_str);
console.log(result)
console.log(result == 'zzc266d271b6us8foe9zsbvbxzrl4vybehqyg269c1e21');

验证效果:

请求体及响应体

同样都是JSVMP:

采用相同方法进行还原,注意这两个函数同样检测了特定属性,需将其设为1:

还原后的算法:

复制代码
const crypto = require('crypto');

async function __cgiEncrypt(plaintext) {
    const AES_KEY = new Uint8Array([
        189, 48, 95, 16, 208, 255, 116, 182, 239, 84, 218, 184, 53, 181, 225, 207
    ]);

    let iv = new Uint8Array(12);
    crypto.getRandomValues(iv);
    // let iv=new Uint8Array([81, 81, 161, 145, 177, 245, 70, 65, 240, 203, 121, 5]);

    let cryptoKey = await crypto.subtle.importKey(
        'raw',
        AES_KEY,
        { name: 'AES-GCM' },
        false,
        ['encrypt']
    );

    let encoded = new TextEncoder().encode(plaintext);

    let cipherBuffer = await crypto.subtle.encrypt(
        { name: 'AES-GCM', iv },
        cryptoKey,
        encoded
    );

    let combined = new Uint8Array(iv.length + cipherBuffer.byteLength);
    combined.set(iv);
    combined.set(new Uint8Array(cipherBuffer), iv.length);

    return Buffer.from(combined).toString('base64');
}


function __cgiDecrypt(b64Str) {
    const bufferStr = atob(b64Str);
    const bufferLen = bufferStr.length;
    const data = new Uint8Array(bufferLen);
    for (let i = 0; i < bufferLen; i++) {
        data[i] = bufferStr.charCodeAt(i);
    }

    const key = new Uint8Array([
        122, 63, 140, 29, 94, 155, 47, 10, 108, 77, 126, 139, 31, 58, 92, 157, 14, 43, 111, 74, 129
    ]);

    let keyLen = key.length;
    for (let i = 0; i < data.length; i++) {
        data[i] = data[i] ^ key[i % keyLen];
    }

    let decoder = new TextDecoder();
    let result = decoder.decode(data);
    return result;
}

注意点:

__cgiEncrypt方法返回的值会改变是因为crypto.getRandomValues,如果hook掉就相同了

__cgiDecrypt方法为了方便,我把传入的参数设置为了base64字符串

复制代码
// crypto.getRandomValues hook代码
// 预设的固定返回值(长度 12)
const FIXED_BYTES = new Uint8Array([
  81, 81, 161, 145, 177, 245, 70, 65, 240, 203, 121, 5
]);

// 备份原始方法(可选,用于恢复)
const originalGetRandomValues = crypto.getRandomValues.bind(crypto);

crypto.getRandomValues = function (array) {
  if (array.length === 12) {
    for (let i = 0; i < 12; i++) {
      array[i] = FIXED_BYTES[i];
    }
    return array;
  }
  return originalGetRandomValues(array);
};

完整python源码

do_one_js_code方法已经在 https://www.cnblogs.com/NightShadow-Blog/p/19891504 里面发过了

复制代码
import requests, time, hashlib, json, base64


class Crypto:
    def __init__(self):
        self.js_code = r"""const crypto=require("crypto");function encrypt(t){var r={0:0,1:1,2:2,3:3,4:4,5:5,6:6,7:7,8:8,9:9,A:10,B:11,C:12,D:13,E:14,F:15},n=[89,39,179,150,218,82,58,252,177,52,186,123,120,64,242,133,143,161,121,179],a="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=",o=[];for(let e=0;e<20;e++){var c=16*r[t[2*e]]+r[t[2*e+1]];o.push(c^n[e])}let i="";for(let e=0;e<o.length;e+=3){var y=o[e],l=o[e+1]||0,g=o[e+2]||0;i=(i+=a[y>>2])+a[(3&y)<<4|l>>4],e+1<o.length&&(i+=a[(15&l)<<2|g>>6]),e+2<o.length&&(i+=a[63&g])}return i.replace(/[\/+]/g,"")}function getSign(t){var e=encrypt(t);return("zzc"+[23,14,6,36,16,40,7,19].map(e=>t[e]).join("")+e+[16,1,32,12,19,27,8,5].map(e=>t[e]).join("")).toLowerCase()}async function __cgiEncrypt(e){var t=new Uint8Array([189,48,95,16,208,255,116,182,239,84,218,184,53,181,225,207]),r=new Uint8Array(12),t=(crypto.getRandomValues(r),await crypto.subtle.importKey("raw",t,{name:"AES-GCM"},!1,["encrypt"])),e=(new TextEncoder).encode(e),t=await crypto.subtle.encrypt({name:"AES-GCM",iv:r},t,e),e=new Uint8Array(r.length+t.byteLength);return e.set(r),e.set(new Uint8Array(t),r.length),Buffer.from(e).toString("base64")}function __cgiDecrypt(e){var t=atob(e),r=t.length,n=new Uint8Array(r);for(let e=0;e<r;e++)n[e]=t.charCodeAt(e);var a=new Uint8Array([122,63,140,29,94,155,47,10,108,77,126,139,31,58,92,157,14,43,111,74,129]),o=a.length;for(let e=0;e<n.length;e++)n[e]=n[e]^a[e%o];return(new TextDecoder).decode(n)}"""

    def get_sign(self, param_str: str):
        sha_result = hashlib.sha1(param_str.encode()).hexdigest().upper()
        js_code_ = self.js_code + f'console.log(getSign(\'{sha_result}\'))'
        return do_one_js_code(js_code_)['stdout']

    def enc_params(self, param_str: str):
        js_code_ = self.js_code + f"__cgiEncrypt('{param_str}').then(_=>{{console.log(_)}})"
        return do_one_js_code(js_code_)['stdout']

    def dec_response(self, base64_str: str):
        js_code_ = self.js_code + f"console.log(__cgiDecrypt('{base64_str}'))"
        return do_one_js_code(js_code_)['stdout']


class QQMusic:
    def __init__(self):
        self.cookies = {
            # 需要填上cookies
        }
        self.headers = {
            'accept': 'application/octet-stream',
            'accept-language': 'zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6',
            'cache-control': 'no-cache',
            'content-type': 'text/plain',
            'origin': 'https://y.qq.com',
            'pragma': 'no-cache',
            'priority': 'u=1, i',
            'referer': 'https://y.qq.com/',
            'sec-ch-ua': '"Microsoft Edge";v="147", "Not.A/Brand";v="8", "Chromium";v="147"',
            'sec-ch-ua-mobile': '?0',
            'sec-ch-ua-platform': '"Windows"',
            'sec-fetch-dest': 'empty',
            'sec-fetch-mode': 'cors',
            'sec-fetch-site': 'same-site',
            'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/147.0.0.0 Safari/537.36 Edg/147.0.0.0',
        }
        self.cip = Crypto()

    def request_music_fcg(self):
        # 处理逻辑可自己决定,这里只做案例
        param_str = '{"comm":{"cv":4747474,"ct":24,"format":"json","inCharset":"utf-8","outCharset":"utf-8","notice":0,"platform":"yqq.json","needNewCode":1,"uin":0,"g_tk_new_20200303":5381,"g_tk":5381},"req_1":{"module":"newsong.NewSongServer","method":"get_new_song_info","param":{"type":2}}}'
        sign = self.cip.get_sign(param_str)
        params = {
            '_': f'{int(time.time() * 1000)}',
            'encoding': 'ag-1',
            'sign': sign,
        }
        data = self.cip.enc_params(param_str)
        print(f'sign: {sign}, data: {data}')
        response = requests.post('https://u6.y.qq.com/cgi-bin/musics.fcg', params=params, cookies=self.cookies, headers=self.headers, data=data)
        response_base64 = base64.b64encode(response.content).decode()
        print(json.loads(self.cip.dec_response(response_base64)))

免责声明
本文仅供技术学习与研究目的,请勿用于任何商业用途或非法行为。本文所涉及的技术分析基于公开的前端代码,旨在帮助开发者理解Web安全与逆向工程技术。使用本文所述技术时,请确保遵守相关法律法规和目标网站的服务条款。作者不对因使用本文内容而产生的任何后果承担责任。
本文为技术研究分享,如有侵权请联系作者删除。