Skip to content

Instantly share code, notes, and snippets.

@ix64
Last active Aug 4, 2022
Embed
What would you like to do?
2021/08/26 MGG/MFLAC研究进展

密钥格式

  • 文件扩展名有 .mgg .mggl .mflac 三种
  • 密钥长度为: 364/704B (Base64-Encoded)
    • 文件末端4字节使用Little-Endian记录
  • Base64解码后: 8B key + 8B cipher header + 256B/512B cipher text

QMC* 的官方解密方法

参见 qmc_offcial.go

  • 此前的解锁方案是我diff加密前后的文件,按照数学规律和直觉,总结出来的解密方案
    • 能够通过观察总结出来,很大程度上是因为他们这套加密方案参数设计上有缺陷
    • 可以看到,以下揭秘方案使用了 256B 的字典,但实际有效的只有其中的 44B

对密钥本身进行解密

  • 密钥本身又进行了加密处理,使用的是QQ魔改过的TEA(似乎自称Coffee?),使用CBC Mode, Rounds参数为16

  • 参见 tc_tea.cpp 或 参考 tars::oi_symmetry_decrypt2

  • 目前已知TEA需要 16B 的Key,而密钥中的时 8B, 需要通过一个算法进行转换得到

    • 完成这一步之后,应该所有256B的MGG/MFLAC都能够成功解锁
    • 毕竟按照我根据经验总结的算法,QMC* 和 256B MGG/MFLAC 算法是一样的,Map不同而已
    • 至于 512B 的,还需要再看看,有可能是RC4
  • 但是QQMusic Mac在这个地方使用了 VMProtect,IDA 的静态分析解决不了问题

    • 我手头没有Mac和Mac版本的IDA,没法搞动态调试
    • 静态调试Windows版本更加困难,很多符号都丢失了
    • 动态调试Widnows版本,使用QM总是闪退,连着IDA一起崩溃,估摸着是碰到反调试或者IDA的Bug了
    • 有空试试x64dbg的动态调试

其他

  • 反编译 QQMusic Mac 7.5.5 时发现 CSongURL::getSongNamePrefixAndSuffix 函数
    • 它的作用是根据 URL的Path中文件名求前缀来判断文件格式
    • 似乎可以推测服务端加密已经在推进了,下载服务器提供加密后的结果,而不是在本地加密
    • 参见 QMMac_ExtByURL.cpp
package main
import (
"errors"
"io"
"log"
"os"
)
// Encrypt
// Ref: QQ Music Car(Android) libencrypt.so
// Java Path: com.tencent.mediaplayer.crypto.MediaCrypto::encrypt
// C++ Path: Cencrypt::Encrypt
func Encrypt(offset int, buf []byte) ([]byte, error) {
if offset < 0 {
return nil, errors.New("bad offset")
}
dst := make([]byte, len(buf))
for i := 0; i < len(buf); i++ {
v8 := offset + i
if v8 >= 0 && v8 >= 0x8000 {
v8 %= 0x7FFF
}
dst[i] = buf[i] ^ cEncryptMap[(v8*v8+27)&0xff]
}
return dst, nil
}
var cEncryptMap = [...]byte{
0x77, 0x48, 0x32, 0x73, 0xDE, 0xF2, 0xC0, 0xC8, //0x00
0x95, 0xEC, 0x30, 0xB2, 0x51, 0xC3, 0xE1, 0xA0, //0x08
0x9E, 0xE6, 0x9D, 0xCF, 0xFA, 0x7F, 0x14, 0xD1, //0x10
0xCE, 0xB8, 0xDC, 0xC3, 0x4A, 0x67, 0x93, 0xD6, //0x18
0x28, 0xC2, 0x91, 0x70, 0xCA, 0x8D, 0xA2, 0xA4, //0x20
0xF0, 0x08, 0x61, 0x90, 0x7E, 0x6F, 0xA2, 0xE0, //0x28
0xEB, 0xAE, 0x3E, 0xB6, 0x67, 0xC7, 0x92, 0xF4, //0x30
0x91, 0xB5, 0xF6, 0x6C, 0x5E, 0x84, 0x40, 0xF7, //0x38
0xF3, 0x1B, 0x02, 0x7F, 0xD5, 0xAB, 0x41, 0x89, //0x40
0x28, 0xF4, 0x25, 0xCC, 0x52, 0x11, 0xAD, 0x43, //0x48
0x68, 0xA6, 0x41, 0x8B, 0x84, 0xB5, 0xFF, 0x2C, //0x50
0x92, 0x4A, 0x26, 0xD8, 0x47, 0x6A, 0x7C, 0x95, //0x58
0x61, 0xCC, 0xE6, 0xCB, 0xBB, 0x3F, 0x47, 0x58, //0x60
0x89, 0x75, 0xC3, 0x75, 0xA1, 0xD9, 0xAF, 0xCC, //0x68
0x08, 0x73, 0x17, 0xDC, 0xAA, 0x9A, 0xA2, 0x16, //0x70
0x41, 0xD8, 0xA2, 0x06, 0xC6, 0x8B, 0xFC, 0x66, //0x78
0x34, 0x9F, 0xCF, 0x18, 0x23, 0xA0, 0x0A, 0x74, //0x80
0xE7, 0x2B, 0x27, 0x70, 0x92, 0xE9, 0xAF, 0x37, //0x88
0xE6, 0x8C, 0xA7, 0xBC, 0x62, 0x65, 0x9C, 0xC2, //0x90
0x08, 0xC9, 0x88, 0xB3, 0xF3, 0x43, 0xAC, 0x74, //0x98
0x2C, 0x0F, 0xD4, 0xAF, 0xA1, 0xC3, 0x01, 0x64, //0xA0
0x95, 0x4E, 0x48, 0x9F, 0xF4, 0x35, 0x78, 0x95, //0xA8
0x7A, 0x39, 0xD6, 0x6A, 0xA0, 0x6D, 0x40, 0xE8, //0xB0
0x4F, 0xA8, 0xEF, 0x11, 0x1D, 0xF3, 0x1B, 0x3F, //0xB8
0x3F, 0x07, 0xDD, 0x6F, 0x5B, 0x19, 0x30, 0x19, //0xC0
0xFB, 0xEF, 0x0E, 0x37, 0xF0, 0x0E, 0xCD, 0x16, //0xC8
0x49, 0xFE, 0x53, 0x47, 0x13, 0x1A, 0xBD, 0xA4, //0xD0
0xF1, 0x40, 0x19, 0x60, 0x0E, 0xED, 0x68, 0x09, //0xD8
0x06, 0x5F, 0x4D, 0xCF, 0x3D, 0x1A, 0xFE, 0x20, //0xE0
0x77, 0xE4, 0xD9, 0xDA, 0xF9, 0xA4, 0x2B, 0x76, //0xE8
0x1C, 0x71, 0xDB, 0x00, 0xBC, 0xFD, 0x0C, 0x6C, //0xF0
0xA5, 0x47, 0xF7, 0xF6, 0x00, 0x79, 0x4A, 0x11, //0xF8
}
__int64 __fastcall CSongURL::getSongNamePrefixAndSuffix(
__int64 a1,
unsigned int a2,
__int64 a3,
char a4,
__int64 a5,
__int64 a6) {
// ......
if (a3 == 112) {
// ......
} else {
result = a2;
switch (a2) {
// ......
case 7u:
if ((a4 & 1) != 0) {
std::string::operator=(a5, "F0M0", a2);
result = std::string::operator=(a6, "mflac", v18);
} else {
std::string::operator=(a5, "F000", a2);
result = std::string::operator=(a6, "flac", v19);
}
break;
// ......
case 0xBu:
if ((a4 & 1) != 0) {
LABEL_14:
std::string::operator=(a5, "O4M0", a2);
result = std::string::operator=(a6, "mgg", v14);
} else {
std::string::operator=(a5, "O400", a2);
result = std::string::operator=(a6, "ogg", v23);
}
break;
default:
return result;
}
}
return result;
}
#include <cstdlib>
#include <ws2dnet.h>
#ifndef WORD32
typedef unsigned int WORD32;
#endif
const WORD32 DELTA = 0x9e3779b9;
#define ROUNDS 16
#define LOG_ROUNDS 4
#define SALT_LEN 2
#define ZERO_LEN 7
/*pOutBuffer、pInBuffer均为8byte, pKey为16byte*/
void TeaDecryptECB(const char *pInBuf, const char *pKey, char *pOutBuf) {
WORD32 y, z, sum;
WORD32 k[4];
int i;
/*now encrypted buf is TCP/IP-endian;*/
/*TCP/IP network byte order (which is big-endian).*/
y = ntohl(*((WORD32 *) pInBuf));
z = ntohl(*((WORD32 *) (pInBuf + 4)));
for (i = 0; i < 4; i++) {
/*key is TCP/IP-endian;*/
k[i] = ntohl(*((WORD32 *) (pKey + i * 4)));
}
sum = DELTA << LOG_ROUNDS;
for (i = 0; i < ROUNDS; i++) {
z -= ((y << 4) + k[2]) ^ (y + sum) ^ ((y >> 5) + k[3]);
y -= ((z << 4) + k[0]) ^ (z + sum) ^ ((z >> 5) + k[1]);
sum -= DELTA;
}
*((WORD32 *) pOutBuf) = htonl(y);
*((WORD32 *) (pOutBuf + 4)) = htonl(z);
/*now plain-text is TCP/IP-endian;*/
}
/*pKey为16byte*/
/*
输入:pInBuf为密文格式,nInBufLen为pInBuf的长度是8byte的倍数; *pOutBufLen为接收缓冲区的长度
特别注意*pOutBufLen应预置接收缓冲区的长度!
输出:pOutBuf为明文(Body),pOutBufLen为pOutBuf的长度,至少应预留nInBufLen-10;
返回值:如果格式正确返回true;
*/
/*TEA解密算法,CBC模式*/
/*密文格式:PadLen(1byte)+Padding(var,0-7byte)+Salt(2byte)+Body(var byte)+Zero(7byte)*/
bool oi_symmetry_decrypt2(const char *pInBuf, int nInBufLen, const char *pKey, char *pOutBuf, size_t *pOutBufLen) {
int nPadLen, nPlainLen;
char dest_buf[8], zero_buf[8];
const char *iv_pre_crypt, *iv_cur_crypt;
int dest_i, i, j;
// const char *pInBufBoundary;
int nBufPos;
nBufPos = 0;
if ((nInBufLen % 8) || (nInBufLen < 16)) return false;
TeaDecryptECB(pInBuf, pKey, dest_buf);
nPadLen = dest_buf[0] & 0x7/*只要最低三位*/;
/*密文格式:PadLen(1byte)+Padding(var,0-7byte)+Salt(2byte)+Body(var byte)+Zero(7byte)*/
i = nInBufLen - 1/*PadLen(1byte)*/ - nPadLen - SALT_LEN - ZERO_LEN; /*明文长度*/
if ((*pOutBufLen < (size_t) i) || (i < 0)) return false;
*pOutBufLen = i;
// pInBufBoundary = pInBuf + nInBufLen; /*输入缓冲区的边界,下面不能pInBuf>=pInBufBoundary*/
for (i = 0; i < 8; i++) zero_buf[i] = 0;
iv_pre_crypt = zero_buf;
iv_cur_crypt = pInBuf; /*init iv*/
pInBuf += 8;
nBufPos += 8;
dest_i = 1; /*dest_i指向dest_buf下一个位置*/
/*把Padding滤掉*/
dest_i += nPadLen;
/*dest_i must <=8*/
/*把Salt滤掉*/
for (i = 1; i <= SALT_LEN;) {
if (dest_i < 8) {
dest_i++;
i++;
} else if (dest_i == 8) {
/*解开一个新的加密块*/
/*改变前一个加密块的指针*/
iv_pre_crypt = iv_cur_crypt;
iv_cur_crypt = pInBuf;
/*异或前一块明文(在dest_buf[]中)*/
for (j = 0; j < 8; j++) {
if ((nBufPos + j) >= nInBufLen) return false;
dest_buf[j] ^= pInBuf[j];
}
/*dest_i==8*/
TeaDecryptECB(dest_buf, pKey, dest_buf);
/*在取出的时候才异或前一块密文(iv_pre_crypt)*/
pInBuf += 8;
nBufPos += 8;
dest_i = 0; /*dest_i指向dest_buf下一个位置*/
}
}
/*还原明文*/
nPlainLen = *pOutBufLen;
while (nPlainLen) {
if (dest_i < 8) {
*(pOutBuf++) = dest_buf[dest_i] ^ iv_pre_crypt[dest_i];
dest_i++;
nPlainLen--;
} else if (dest_i == 8) {
/*dest_i==8*/
/*改变前一个加密块的指针*/
iv_pre_crypt = iv_cur_crypt;
iv_cur_crypt = pInBuf;
/*解开一个新的加密块*/
/*异或前一块明文(在dest_buf[]中)*/
for (j = 0; j < 8; j++) {
if ((nBufPos + j) >= nInBufLen) return false;
dest_buf[j] ^= pInBuf[j];
}
TeaDecryptECB(dest_buf, pKey, dest_buf);
/*在取出的时候才异或前一块密文(iv_pre_crypt)*/
pInBuf += 8;
nBufPos += 8;
dest_i = 0; /*dest_i指向dest_buf下一个位置*/
}
}
/*校验Zero*/
for (i = 1; i <= ZERO_LEN;) {
if (dest_i < 8) {
if (dest_buf[dest_i] ^ iv_pre_crypt[dest_i]) return false;
dest_i++;
i++;
} else if (dest_i == 8) {
/*改变前一个加密块的指针*/
iv_pre_crypt = iv_cur_crypt;
iv_cur_crypt = pInBuf;
/*解开一个新的加密块*/
/*异或前一块明文(在dest_buf[]中)*/
for (j = 0; j < 8; j++) {
if ((nBufPos + j) >= nInBufLen) return false;
dest_buf[j] ^= pInBuf[j];
}
TeaDecryptECB(dest_buf, pKey, dest_buf);
/*在取出的时候才异或前一块密文(iv_pre_crypt)*/
pInBuf += 8;
nBufPos += 8;
dest_i = 0; /*dest_i指向dest_buf下一个位置*/
}
}
return true;
}
@ishowshao
Copy link

ishowshao commented Dec 15, 2021

@jixunmoe 问个C语言的问题
364长度base64 key的调通了,704的执行结果不对,我猜是这段的执行结果JS和C不一致
这里面的计算中间值是不是都按int32来存,代码里有auto类型,我这块不太懂

void StreamCencrypt::GetHashBase()
{
	this->key_hash = 1;
	for (size_t i = 0; i < this->N; i++) {
		int32_t value = int32_t{ this->rc4_key[i] };

		// ignore if key char is '\x00'
		if (!value) continue;

		auto next_hash = this->key_hash * value;
		if (next_hash == 0 || next_hash <= this->key_hash)
			break;

		this->key_hash = next_hash;
	}
}

@jixunmoe
Copy link

jixunmoe commented Dec 15, 2021

next_hash 的类型是 uint32_t。JS 里应该没有问题,但是你要检查这个结果是否超过了 0xFFFFFFFF,并取 (next_hash % (0xFFFFFFFF+1)) 的值(JS 里 & 会把变量改成 int32)。

(next_hash& 0xFFFFFFFF) < 0 检测也许也能用,因为后面的 next_hash <= this->key_hash 就是检测溢出的,但我不知道这个东西对不对。

@ishowshao
Copy link

ishowshao commented Dec 15, 2021

我按照int32来存试了不对,我改成uint32_t...

@ishowshao
Copy link

ishowshao commented Dec 15, 2021

确实是uint32_t,刚才int32导出的文件直接是无序的binary,现在导出的音频有显示时长了,但还是不能正常播放,估计哪里没对,应该距离正确不远了,看到了fLaC的头,但是后面都还不对

@Akarinnnnn
Copy link

Akarinnnnn commented Dec 15, 2021

问号部分原本是数字,不清楚是什么内容。第一个逗号之前的内容为 ekey,中间的是不知名的数字(歌曲 id 或用户 id?),后面跟着 ',2'、大小(ekey 到 '2' 位置处的大小)、'QTag' 字样。

这部分可不可以当csv处理

@ishowshao
Copy link

ishowshao commented Dec 15, 2021

uint64_t StreamCencrypt::GetSegmentKey(uint64_t id, uint64_t seed)
{
	return uint64_t((double)this->key_hash / double((id + 1) * seed) * 100.0);
}

这里面uint64_t不太好处理

@jixunmoe
Copy link

jixunmoe commented Dec 15, 2021

这部分可不可以当csv处理

没有必要吧,除了开头的 key 没有什么有用的数据

这里面uint64_t不太好处理

的确,JS 的整数位数不太够 🤔

@jixunmoe
Copy link

jixunmoe commented Dec 15, 2021

我看了下用到这个的地方,是拿来求索引的。

    uint64_t key = uint64_t{this->rc4_key[offset % this->N]};
    buf[i] ^= this->rc4_key[GetSegmentKey(offset, key) % this->N];

直接截断应该问题不大。offset 是当前文件位置,key 是 0~255。

@ishowshao
Copy link

ishowshao commented Dec 15, 2021

是,我用BigInt处理了一下还是不对,BigInt是signed,感觉应该不至于装不下
可能其他地方没翻译对,我再仔细看看...

@Akarinnnnn
Copy link

Akarinnnnn commented Dec 15, 2021

@ix64
Copy link
Author

ix64 commented Dec 15, 2021

@jixunmoe 有没有fork 能看到代码的话会好交流一点

@ix64
Copy link
Author

ix64 commented Dec 15, 2021

@jixunmoe js里面 uint 的右移操作要用 >>> 忽略符号位 是不是这个原因

@ishowshao
Copy link

ishowshao commented Dec 15, 2021

>>>我再tc_tea里面用了,他这段代码里没有这样的操作,我觉得我前128位解出来是对的,头上是fLaC,他代码的EncFirstSegment和EncASegment实现是有区别的,可能我哪里没搞对

@jixunmoe
Copy link

jixunmoe commented Dec 15, 2021

image

unlock-music/unlock-music#207

@ix64 整了个实验性分支,能下载,不能预览播放。 发现是可以的,要手动点列表里的播放…

@ishowshao
Copy link

ishowshao commented Dec 16, 2021

我跑出来了512B的

@ishowshao
Copy link

ishowshao commented Dec 16, 2021

有个疑问,有没有遇到base64那部分不是长度364/704的?

@Akarinnnnn
Copy link

Akarinnnnn commented Dec 16, 2021

有个疑问,有没有遇到base64那部分不是长度364/704的?
暂时没看见

@ishowshao
Copy link

ishowshao commented Dec 16, 2021

tc_tea.js

const ROUNDS = 16;
const LOG_ROUNDS = 4;
const SALT_LEN = 2;
const ZERO_LEN = 7;

const DELTA = 0x9e3779b9;

/**
 * @param {Buffer} pInBuf
 * @param {Buffer} pKey
 * @param {Buffer} pOutBuf
 */
const TeaDecryptECB = (pInBuf, pKey, pOutBuf) => {
    //   let y, z, sum;
    let yzsum = new Uint32Array(3);
    //   let k = Buffer.alloc(16);

    //   y = pInBuf.readUInt32BE(0);
    //   z = pInBuf.readUInt32BE(4);
    yzsum[0] = pInBuf.readUInt32BE(0);
    yzsum[1] = pInBuf.readUInt32BE(4);

    let k0 = pKey.readUInt32BE(0);
    let k1 = pKey.readUInt32BE(4);
    let k2 = pKey.readUInt32BE(8);
    let k3 = pKey.readUInt32BE(12);

    //   sum = DELTA << LOG_ROUNDS;
    yzsum[2] = DELTA << LOG_ROUNDS;
    for (let i = 0; i < ROUNDS; i++) {
        yzsum[1] -= ((yzsum[0] << 4) + k2) ^ (yzsum[0] + yzsum[2]) ^ ((yzsum[0] >>> 5) + k3);
        yzsum[0] -= ((yzsum[1] << 4) + k0) ^ (yzsum[1] + yzsum[2]) ^ ((yzsum[1] >>> 5) + k1);
        // sum -= DELTA;
        yzsum[2] -= DELTA;
    }

    pOutBuf.writeUInt32BE(yzsum[0], 0);
    pOutBuf.writeUInt32BE(yzsum[1], 4);
};

/*pKey为16byte*/
/*
    输入:pInBuf为密文格式,nInBufLen为pInBuf的长度是8byte的倍数; *pOutBufLen为接收缓冲区的长度
        特别注意*pOutBufLen应预置接收缓冲区的长度!
    输出:pOutBuf为明文(Body),pOutBufLen为pOutBuf的长度,至少应预留nInBufLen-10;
    返回值:如果格式正确返回true;
*/
/*TEA解密算法,CBC模式*/
/*密文格式:PadLen(1byte)+Padding(var,0-7byte)+Salt(2byte)+Body(var byte)+Zero(7byte)*/
// bool oi_symmetry_decrypt2(const char *pInBuf, int nInBufLen, const char *pKey, char *pOutBuf, size_t *pOutBufLen)
const oi_symmetry_decrypt2 = (pInBuf, nInBufLen, pKey, pOutBuf, pOutBufLen) => {
    // int nPadLen, nPlainLen;
    let nPadLen, nPlainLen;
    // char dest_buf[8], zero_buf[8];
    let dest_buf = Buffer.alloc(8);
    let zero_buf = Buffer.alloc(8);

    // const char *iv_pre_crypt, *iv_cur_crypt;
    let iv_pre_crypt, iv_cur_crypt;

    // int dest_i, i, j;
    let dest_i, i, j;

    // int nBufPos;
    let nBufPos;

    nBufPos = 0;

    if (nInBufLen % 8 || nInBufLen < 16) return false;

    TeaDecryptECB(pInBuf, pKey, dest_buf);

    nPadLen = dest_buf[0] & 0x7 /*只要最低三位*/;

    /*密文格式:PadLen(1byte)+Padding(var,0-7byte)+Salt(2byte)+Body(var byte)+Zero(7byte)*/
    i = nInBufLen - 1 /*PadLen(1byte)*/ - nPadLen - SALT_LEN - ZERO_LEN; /*明文长度*/
    if (pOutBufLen.value < i || i < 0) return false;
    pOutBufLen.value = i;

    // for (i = 0; i < 8; i++) zero_buf[i] = 0;

    iv_pre_crypt = zero_buf;
    iv_cur_crypt = pInBuf; /*init iv*/

    // pInBuf += 8;
    pInBuf = pInBuf.slice(8);
    nBufPos += 8;

    dest_i = 1; /*dest_i指向dest_buf下一个位置*/

    /*把Padding滤掉*/
    dest_i += nPadLen;

    /*dest_i must <=8*/

    /*把Salt滤掉*/
    for (i = 1; i <= SALT_LEN; ) {
        if (dest_i < 8) {
            dest_i++;
            i++;
        } else if (dest_i == 8) {
            /*解开一个新的加密块*/

            /*改变前一个加密块的指针*/
            iv_pre_crypt = iv_cur_crypt;
            iv_cur_crypt = pInBuf;

            /*异或前一块明文(在dest_buf[]中)*/
            for (j = 0; j < 8; j++) {
                if (nBufPos + j >= nInBufLen) return false;
                dest_buf[j] ^= pInBuf[j];
            }

            /*dest_i==8*/
            TeaDecryptECB(dest_buf, pKey, dest_buf);

            /*在取出的时候才异或前一块密文(iv_pre_crypt)*/

            // pInBuf += 8;
            pInBuf = pInBuf.slice(8);
            nBufPos += 8;

            dest_i = 0; /*dest_i指向dest_buf下一个位置*/
        }
    }

    /*还原明文*/

    nPlainLen = pOutBufLen.value;
    let pOutBufPointer = 0;
    while (nPlainLen) {
        if (dest_i < 8) {
            // *(pOutBuf++) = dest_buf[dest_i] ^ iv_pre_crypt[dest_i];
            pOutBuf[pOutBufPointer++] = dest_buf[dest_i] ^ iv_pre_crypt[dest_i];
            dest_i++;
            nPlainLen--;
        } else if (dest_i == 8) {
            /*dest_i==8*/

            /*改变前一个加密块的指针*/
            iv_pre_crypt = iv_cur_crypt;
            iv_cur_crypt = pInBuf;

            /*解开一个新的加密块*/

            /*异或前一块明文(在dest_buf[]中)*/
            for (j = 0; j < 8; j++) {
                if (nBufPos + j >= nInBufLen) return false;
                dest_buf[j] ^= pInBuf[j];
            }

            TeaDecryptECB(dest_buf, pKey, dest_buf);

            /*在取出的时候才异或前一块密文(iv_pre_crypt)*/

            // pInBuf += 8;
            pInBuf = pInBuf.slice(8);
            nBufPos += 8;

            dest_i = 0; /*dest_i指向dest_buf下一个位置*/
        }
    }

    /*校验Zero*/
    for (i = 1; i <= ZERO_LEN; ) {
        if (dest_i < 8) {
            if (dest_buf[dest_i] ^ iv_pre_crypt[dest_i]) return false;
            dest_i++;
            i++;
        } else if (dest_i == 8) {
            /*改变前一个加密块的指针*/
            iv_pre_crypt = iv_cur_crypt;
            iv_cur_crypt = pInBuf;

            /*解开一个新的加密块*/

            /*异或前一块明文(在dest_buf[]中)*/
            for (j = 0; j < 8; j++) {
                if (nBufPos + j >= nInBufLen) return false;
                dest_buf[j] ^= pInBuf[j];
            }

            TeaDecryptECB(dest_buf, pKey, dest_buf);

            /*在取出的时候才异或前一块密文(iv_pre_crypt)*/

            // pInBuf += 8;
            pInBuf = pInBuf.slice(8);
            nBufPos += 8;

            dest_i = 0; /*dest_i指向dest_buf下一个位置*/
        }
    }

    return true;
};

// bool TC_Tea::decrypt(const char *key, const char *sIn, size_t iLength, vector<char> &buffer)
const decrypt = (key, sIn, iLength, buffer) => {
    let outlen = { value: 0 };
    outlen.value = iLength;

    // if (buffer.capacity() < outlen * 2)
    if (buffer.value.length < outlen.value * 2) {
        // buffer.resize(outlen * 2);
        const newBuf = Buffer.alloc(outlen.value * 2);
        buffer.value.copy(newBuf);
        buffer.value = newBuf;
    }

    // buffer.resize(outlen * 2);

    if (!oi_symmetry_decrypt2(sIn, iLength, key, buffer.value, outlen)) {
        return false;
    }

    // buffer.resize(outlen);
    const newBuf = Buffer.alloc(outlen.value);
    buffer.value.copy(newBuf);
    buffer.value = newBuf;

    return true;
};

module.exports = decrypt;

@ishowshao
Copy link

ishowshao commented Dec 16, 2021

翻译+略微改造了一下 @jixunmoe 的qmc2 C++版本
main.js

const decrypt = require('./tc_tea');

const footer_detection_size = 0x40;
const encrypted_key_size_v1 = 364;
const encrypted_key_size_v2 = 704;

const SimpleMakeKey = (seed, len, buf) => {
    for (let i = 0; len > i; ++i) {
        // buf[i] = (uint8_t)(fabs(tan((float)seed + (double)i * 0.1)) * 100.0);
        buf[i] = Math.floor(Math.abs(Math.tan(seed + i * 0.1) * 100.0));
    }
};

class KeyDec {
    constructor() {
        this.key = null;
    }

    SetKey(ekey) {
        let simple_key_buf = Buffer.alloc(8);
        SimpleMakeKey(106, 8, simple_key_buf);

        let ekey_decoded = Buffer.from(ekey.toString('ascii'), 'base64');
        let decode_len = ekey_decoded.length;

        if (decode_len < 8) {
            throw new Error('ERROR: decoded key size is too small');
        }

        let tea_key = Buffer.alloc(16);
        for (let i = 0; i < 16; i += 2) {
            tea_key[i + 0] = simple_key_buf[Math.floor(i / 2)];
            tea_key[i + 1] = ekey_decoded[Math.floor(i / 2)];
        }

        const decrypted_buf = { value: Buffer.alloc(0) };
        if (!decrypt(tea_key, ekey_decoded.slice(8), decode_len - 8, decrypted_buf)) {
            throw new Error('decrypt fail');
        }
        // console.log(decrypted_buf);
        this.key_len = decrypted_buf.value.length + 8;

        this.key = Buffer.alloc(8 + decrypted_buf.value.length);
        ekey_decoded.copy(this.key, 0, 0, 8);
        decrypted_buf.value.copy(this.key, 8, 0);
    }

    GetKey(key_out) {
        const key = Buffer.alloc(this.key.length);
        this.key.copy(key);
        return key;
    }
}

const FIRST_SEGMENT_SIZE = 0x80;
const SEGMENT_SIZE = 0x1400;

const rotate = (value, bits) => {
    let rotate = (bits + 4) % 8;
    let left = value << rotate;
    let right = value >> rotate;
    return (left | right) & 255;
};

class StreamCencrypt {
    constructor() {
        // this.key_hash = 0;
        this.key_hash = new Uint32Array(1);
        // RC4 vars
        // uint8_t* rc4_key = nullptr;
        // uint8_t* S = nullptr;
        // uint8_t* S2 = nullptr;
        // size_t N = 0;
        this.rc4_key = null;
        this.S = null;
        this.S2 = null;
        this.N = 0;
    }

    StreamEncrypt(offset, buf, len) {
        if (this.N > 300) {
            this.ProcessByRC4(offset, buf, len);
        } else {
            for (let i = 0; i < len; i++) {
                buf[i] ^= this.mapL(offset + i);
            }
        }
    }

    StreamDecrypt(offset, buf, len) {
        return this.StreamEncrypt(offset, buf, len);
    }

    SetKeyDec(key_dec) {
        this.Uninit();
        this.rc4_key = null;
        if (key_dec) {
            const key = key_dec.GetKey();
            this.rc4_key = key;
            this.N = key.length;

            if (this.N > 300) {
                this.InitRC4KSA();
            }
        }
    }

    Uninit() {
        this.rc4_key = null;
        this.N = 0;
        this.S = null;
    }

    // size_t offset, uint8_t* buf, size_t size
    // void StreamCencrypt::ProcessByRC4(size_t offset, uint8_t* buf, size_t size)
    ProcessByRC4(offset, buf, size) {
        // uint8_t* orig_buf = buf;
        let orig_buf = buf;

        // uint8_t* last_addr = orig_buf + size;

        // auto len = size;
        let len = size;

        // Initial segment
        if (offset < FIRST_SEGMENT_SIZE) {
            let len_segment = Math.min(size, FIRST_SEGMENT_SIZE - offset);
            this.EncFirstSegment(offset, buf, len_segment);
            len -= len_segment;
            // buf += len_segment;
            buf = buf.slice(len_segment);
            offset += len_segment;
        }

        // FIXME: Move this as a private member?
        // uint8_t* S = new uint8_t[N]();
        const S = Buffer.alloc(this.N);

        // Align segment
        if (offset % SEGMENT_SIZE != 0) {
            let len_segment = Math.min(SEGMENT_SIZE - (offset % SEGMENT_SIZE), len);
            this.EncASegment(S, offset, buf, len_segment);
            len -= len_segment;
            // buf += len_segment;
            buf = buf.slice(len_segment);
            offset += len_segment;
        }

        // Batch process segments
        while (len > SEGMENT_SIZE) {
            let len_segment = Math.min(SEGMENT_SIZE, len);
            this.EncASegment(S, offset, buf, len_segment);
            len -= len_segment;
            // buf += len_segment;
            buf = buf.slice(len_segment);
            offset += len_segment;
        }

        // Last segment (incomplete segment)
        if (len > 0) {
            this.EncASegment(S, offset, buf, len);
        }

        // assert(last_addr == buf + len);

        // delete[] S;
    }

    // uint64_t StreamCencrypt::GetSegmentKey(uint64_t id, uint64_t seed)
    GetSegmentKey(id, seed) {
        // return uint64_t((double)this->key_hash / double((id + 1) * seed) * 100.0);
        return BigInt(Math.floor((Number(this.key_hash[0]) / Number((id + 1n) * seed)) * 100.0));
    }

    EncFirstSegment(offset, buf, len) {
        for (let i = 0; i < len; i++) {
            let key = this.rc4_key[offset % this.N];
            buf[i] ^= this.rc4_key[Number(this.GetSegmentKey(BigInt(offset), BigInt(key))) % this.N];
            offset++;
        }
    }

    EncASegment(S, offset, buf, len) {
        if (!this.rc4_key) {
            // We need to initialise RC4 key first!
            return;
        }

        const N = this.N;

        // Initialise a new seedbox
        // memcpy(S, this->S, N);
        S = Buffer.alloc(N);
        this.S.copy(S);

        // Calculate segment id
        let segment_id = (offset / SEGMENT_SIZE) & 0x1ff;

        // Calculate the number of bytes to skip.
        // The initial "key" derived from segment id, plus the current offset.
        let skip_len = Number(this.GetSegmentKey(BigInt(Math.floor(offset / SEGMENT_SIZE)), BigInt(this.rc4_key[segment_id]))) & 0x1ff;
        skip_len += offset % SEGMENT_SIZE;

        let j = 0;
        let k = 0;
        for (let i = 0; i < skip_len; i++) {
            j = (j + 1) % N;
            k = (S[j] + k) % N;
            // std::swap(S[j], S[k]);
            const tmp = S[k];
            S[k] = S[j];
            S[j] = tmp;
        }

        // Now we also manipulate the buffer:
        for (let i = 0; i < len; i++) {
            j = (j + 1) % N;
            k = (S[j] + k) % N;
            // std::swap(S[j], S[k]);
            const tmp = S[k];
            S[k] = S[j];
            S[j] = tmp;

            buf[i] ^= S[(S[j] + S[k]) % N];
        }
    }

    InitRC4KSA() {
        if (!this.S) {
            this.S = Buffer.alloc(this.N);
        }

        for (let i = 0; i < this.N; ++i) {
            this.S[i] = i & 0xff;
        }

        let j = 0;
        for (let i = 0; i < this.N; ++i) {
            j = (this.S[i] + j + this.rc4_key[i % this.N]) % this.N;
            const tmp = this.S[j];
            this.S[j] = this.S[i];
            this.S[i] = tmp;
        }

        this.GetHashBase();
    }

    GetHashBase() {
        let next_hash = new Uint32Array(1);
        this.key_hash[0] = 1;
        for (let i = 0; i < this.N; i++) {
            let value = this.rc4_key[i];

            // ignore if key char is '\x00'
            if (!value) continue;

            next_hash[0] = this.key_hash[0] * value;
            if (next_hash[0] == 0 || next_hash[0] <= this.key_hash[0]) break;

            this.key_hash[0] = next_hash[0];
        }
    }

    // Untested, this might be wrong.
    mapL(offset) {
        if (offset > 0x7fff) offset %= 0x7fff;

        let key = (offset * offset + 71214) % this.N;

        let value = this.rc4_key[key];
        return rotate(value, key & 0b0111);
    }
}

const createInstWidthEKey = (ekey_b64) => {
    const stream = new StreamCencrypt();
    const key_dec = new KeyDec();
    key_dec.SetKey(ekey_b64, ekey_b64.length);
    stream.SetKeyDec(key_dec);
    return stream;
};

const read_buf_len = 1 * 1024 * 1024;

const divide = (buffer) => {
    const result = {
        content: null,
        base64: null,
        base64Length: 0,
    };
    const last4Byte = buffer.slice(buffer.length - 4);
    if (last4Byte.toString('ascii') === 'QTag') {
        // 这段还没拿到真实文件测试
        const base64Length = 704;
        const length = buffer.length;
        let count = 0;
        let i = 8 + 1;
        for (; i < 64; i++) {
            if (buffer[length - i] === 0x2c) {
                count++;
            }
            if (count === 2) {
                break;
            }
        }
        if (count < 2) {
            throw new Error('File format error');
        }
        result.content = buffer.slice(0, length - base64Length - i);
        result.base64 = buffer.slice(length - base64Length - i, length - i);
        result.base64Length = base64Length;
    } else {
        const base64Length = last4Byte.readInt32LE(0);
        result.content = buffer.slice(0, buffer.length - base64Length - 4);
        result.base64 = buffer.slice(buffer.length - base64Length - 4, buffer.length - 4);
        result.base64Length = base64Length;
    }
    return result;
};

const decode = (buffer) => {
    let stream_input = buffer;
    let buf = Buffer.alloc(read_buf_len);

    const result = divide(stream_input);

    const keySize = result.base64Length;
    const base64 = Buffer.alloc(keySize);
    result.base64.copy(base64);

    // console.log('key size', keySize);
    // console.log('base64 size', base64.length);

    const decrypted_file_size = result.content.length;
    // console.log('decrypted_file_size', decrypted_file_size);

    const stream = createInstWidthEKey(base64);

    let offset = 0;
    let to_decrypt_len = decrypted_file_size;

    let stream_out = Buffer.alloc(decrypted_file_size);
    // // Begin decryption
    while (to_decrypt_len > 0) {
        let block_size = Math.min(read_buf_len, to_decrypt_len);
        let bytes_read = stream_input.copy(buf, 0, offset, offset + block_size);

        stream.StreamDecrypt(offset, buf, bytes_read);
        buf.copy(stream_out, offset, 0, bytes_read);

        offset += bytes_read;
        to_decrypt_len -= bytes_read;
    }

    return stream_out;
};

module.exports = decode;

@ix64
Copy link
Author

ix64 commented Dec 16, 2021

@ishowshao Great! 有兴趣PR吗?还是我测试合并

@ishowshao
Copy link

ishowshao commented Dec 17, 2021

@ix64 接口特别简单,输入Buffer,输出也是Buffer,在浏览器里面用npm上的Buffer polyfill一下就能用

import { Buffer } from 'buffer/';
buffer = Buffer.from(buffer);

@ishowshao
Copy link

ishowshao commented Dec 17, 2021

太忙,我就不PR了

@ix64
Copy link
Author

ix64 commented Dec 17, 2021

@ishowshao 嗯嗯 也谢谢你 unlock-music/unlock-music#211 我处理好了,做了一些简化处理

@xhacker-zzz
Copy link

xhacker-zzz commented Dec 20, 2021

https://gist.github.com/ix64/bcd72c151f21e1b050c9cc52d6ff27d5#gistcomment-3993639
@ix64 其实服务端加密也没关系,因为客户端要能离线播放,就肯定有解密方法,到时候再反编译了分析就应该可以了

@ishowshao
Copy link

ishowshao commented Mar 10, 2022

QQ音乐18.59版本的mflac好像又改了,但是大体结构没变,尾部最后几个字节应该有变化

@ishowshao
Copy link

ishowshao commented Mar 14, 2022

没人关注了吗 @ix64

@ix64
Copy link
Author

ix64 commented Mar 14, 2022

我们在大约10天前已经发现这一点 https://t.me/unlock_music_chat/45716
但是由于Windows的dll使用了VMprotect,反编译比较困难;因此暂时不作处理

@ishowshao
Copy link

ishowshao commented Mar 14, 2022

@ix64 tg那个group 我加不进去,我有一些发现可以交流一下

@ix64
Copy link
Author

ix64 commented Mar 14, 2022

@ix64 tg那个group 我加不进去,我有一些发现可以交流一下

应该是未通过机器人的自动验证导致被封禁
尝试一下使用此链接 https://t.me/+VSOrQvK_iX2_aZ_g
或者提供下 Telegram ID

@ishowshao
Copy link

ishowshao commented Mar 14, 2022

@ix64 @hyoldman please 邀请我,感谢

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment