Skip to content

Instantly share code, notes, and snippets.

@ix64
Last active December 15, 2023 20:09
Show Gist options
  • Star 10 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save ix64/bcd72c151f21e1b050c9cc52d6ff27d5 to your computer and use it in GitHub Desktop.
Save ix64/bcd72c151f21e1b050c9cc52d6ff27d5 to your computer and use it in GitHub Desktop.
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;
}
@nullptr-0
Copy link

nullptr-0 commented Dec 20, 2021

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

@ishowshao
Copy link

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

@ishowshao
Copy link

没人关注了吗 @ix64

@ix64
Copy link
Author

ix64 commented Mar 14, 2022

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

@ishowshao
Copy link

@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

@ix64 @hyoldman please 邀请我,感谢

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