技术0
经验6089
魅力0
人气17
分享8
原创0
注册时间2024-7-18
最后登录2025-11-21
阅读权限40
在线时间88 小时
主题13
回帖10

青出于蓝
 
- 积分
- 3277
- 人气
- 17
- 分享
- 8
|
本帖最后由 streliztia 于 2025-10-6 18:25 编辑
QQ音乐mflac解密/转换教程
我尝试了很多的转换工具,要不就是需要花钱才能转换;那种解密网站目前也不能解密QQ音乐的加密了。
所以我就独自和迪克深入探讨了一下,一共27组对话,才达到我想要的效果。
下面的代码有任何问题一定要找迪克三千呀,如果有技术上的问题就不要找我了,因为这些代码都是迪克三千编写制作。
2025/10/06能顺利转换
————————————
文件结构
[HTML] 纯文本查看 复制代码 QQMusicDecryptor/
├── main_gui.py # 主程序(图形界面)
├── hook_qq_music.js # 解密脚本
├── requirements.txt # 依赖包
├── run.bat # Windows运行脚本
└── README.md # 说明文档
文件1: main_gui.py (主程序 - 图形界面)
[Python] 纯文本查看 复制代码 import tkinter as tk
from tkinter import ttk, filedialog, messagebox, scrolledtext
import frida
import os
import hashlib
import threading
import logging
import sys
from datetime import datetime
class QQMusicDecryptorGUI:
def __init__(self, root):
self.root = root
self.root.title("QQ音乐解密工具 v1.0")
self.root.geometry("800x600")
self.root.resizable(True, True)
# 设置图标(如果有的话)
try:
self.root.iconbitmap("icon.ico")
except:
pass
self.setup_ui()
self.setup_logging()
# 状态变量
self.is_processing = False
self.session = None
self.script = None
def setup_ui(self):
# 主框架
main_frame = ttk.Frame(self.root, padding="10")
main_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
# 标题
title_label = ttk.Label(main_frame, text="QQ音乐解密工具", font=("Arial", 16, "bold"))
title_label.grid(row=0, column=0, columnspan=3, pady=(0, 20))
# 输入目录选择
ttk.Label(main_frame, text="输入目录:").grid(row=1, column=0, sticky=tk.W, pady=5)
self.input_path = tk.StringVar()
ttk.Entry(main_frame, textvariable=self.input_path, width=50).grid(row=1, column=1, padx=5, pady=5, sticky=(tk.W, tk.E))
ttk.Button(main_frame, text="浏览", command=self.browse_input).grid(row=1, column=2, pady=5)
# 输出目录选择
ttk.Label(main_frame, text="输出目录:").grid(row=2, column=0, sticky=tk.W, pady=5)
self.output_path = tk.StringVar()
ttk.Entry(main_frame, textvariable=self.output_path, width=50).grid(row=2, column=1, padx=5, pady=5, sticky=(tk.W, tk.E))
ttk.Button(main_frame, text="浏览", command=self.browse_output).grid(row=2, column=2, pady=5)
# 控制按钮框架
button_frame = ttk.Frame(main_frame)
button_frame.grid(row=3, column=0, columnspan=3, pady=20)
self.start_button = ttk.Button(button_frame, text="开始解密", command=self.start_decryption)
self.start_button.pack(side=tk.LEFT, padx=5)
self.stop_button = ttk.Button(button_frame, text="停止", command=self.stop_decryption, state=tk.DISABLED)
self.stop_button.pack(side=tk.LEFT, padx=5)
ttk.Button(button_frame, text="清空日志", command=self.clear_log).pack(side=tk.LEFT, padx=5)
# 进度条
self.progress = ttk.Progressbar(main_frame, mode='determinate')
self.progress.grid(row=4, column=0, columnspan=3, sticky=(tk.W, tk.E), pady=10)
# 状态标签
self.status_label = ttk.Label(main_frame, text="准备就绪")
self.status_label.grid(row=5, column=0, columnspan=3, pady=5)
# 统计信息
stats_frame = ttk.Frame(main_frame)
stats_frame.grid(row=6, column=0, columnspan=3, pady=10, sticky=(tk.W, tk.E))
ttk.Label(stats_frame, text="统计信息:").grid(row=0, column=0, sticky=tk.W)
self.stats_text = tk.StringVar(value="总文件: 0, 成功: 0, 失败: 0, 跳过: 0")
ttk.Label(stats_frame, textvariable=self.stats_text).grid(row=0, column=1, sticky=tk.W, padx=10)
# 日志区域
ttk.Label(main_frame, text="操作日志:").grid(row=7, column=0, sticky=tk.W, pady=(10, 0))
self.log_area = scrolledtext.ScrolledText(main_frame, width=80, height=20)
self.log_area.grid(row=8, column=0, columnspan=3, pady=5, sticky=(tk.W, tk.E, tk.N, tk.S))
# 署名
ttk.Label(main_frame, text="工具由 Strelitzia 开发", font=("Arial", 8), foreground="gray").grid(
row=9, column=2, sticky=tk.E, pady=(10, 0)
)
# 配置网格权重
main_frame.columnconfigure(1, weight=1)
main_frame.rowconfigure(8, weight=1)
self.root.columnconfigure(0, weight=1)
self.root.rowconfigure(0, weight=1)
# 设置默认路径
self.input_path.set("D:\\Music\\VipSongsDownload")
self.output_path.set("D:\\DecryptedMusic")
def setup_logging(self):
# 创建自定义日志处理器
class TextHandler(logging.Handler):
def __init__(self, text_widget):
super().__init__()
self.text_widget = text_widget
def emit(self, record):
msg = self.format(record)
self.text_widget.insert(tk.END, msg + '\n')
self.text_widget.see(tk.END)
# 配置日志
self.log_handler = TextHandler(self.log_area)
self.log_handler.setFormatter(logging.Formatter('%(asctime)s - %(levelname)s - %(message)s'))
self.logger = logging.getLogger()
self.logger.setLevel(logging.INFO)
self.logger.addHandler(self.log_handler)
def browse_input(self):
path = filedialog.askdirectory(title="选择加密文件目录")
if path:
self.input_path.set(path)
def browse_output(self):
path = filedialog.askdirectory(title="选择输出目录")
if path:
self.output_path.set(path)
def log(self, message, level=logging.INFO):
self.logger.log(level, message)
def clear_log(self):
self.log_area.delete(1.0, tk.END)
def update_status(self, message):
self.status_label.config(text=message)
self.root.update_idletasks()
def update_stats(self, total, success, failed, skipped):
self.stats_text.set(f"总文件: {total}, 成功: {success}, 失败: {failed}, 跳过: {skipped}")
def start_decryption(self):
if not self.input_path.get() or not self.output_path.get():
messagebox.showerror("错误", "请选择输入和输出目录")
return
if not os.path.exists(self.input_path.get()):
messagebox.showerror("错误", "输入目录不存在")
return
# 创建输出目录
os.makedirs(self.output_path.get(), exist_ok=True)
# 更新UI状态
self.is_processing = True
self.start_button.config(state=tk.DISABLED)
self.stop_button.config(state=tk.NORMAL)
self.progress['value'] = 0
# 在新线程中运行解密
thread = threading.Thread(target=self.run_decryption)
thread.daemon = True
thread.start()
def stop_decryption(self):
self.is_processing = False
self.start_button.config(state=tk.NORMAL)
self.stop_button.config(state=tk.DISABLED)
self.update_status("解密已停止")
# 断开Frida连接
if self.session:
try:
self.session.detach()
except:
pass
def run_decryption(self):
try:
input_dir = self.input_path.get()
output_dir = self.output_path.get()
self.log("正在连接到QQ音乐进程...")
self.update_status("正在连接到QQ音乐...")
# 连接到QQ音乐进程
try:
self.session = frida.attach("QQMusic.exe")
self.log("✓ 成功连接到QQ音乐进程")
except Exception as e:
self.log(f"✗ 连接QQ音乐进程失败: {e}", logging.ERROR)
self.log("请确保: 1) QQ音乐正在运行 2) frida-server已启动", logging.ERROR)
self.stop_decryption()
return
# 加载解密脚本
try:
with open("hook_qq_music.js", "r", encoding="utf-8") as f:
script_code = f.read()
self.script = self.session.create_script(script_code)
self.script.load()
self.log("✓ 解密脚本加载成功")
except Exception as e:
self.log(f"✗ 加载解密脚本失败: {e}", logging.ERROR)
self.stop_decryption()
return
# 查找加密文件
self.log("正在扫描加密文件...")
self.update_status("正在扫描文件...")
encrypted_files = []
for root, dirs, files in os.walk(input_dir):
for file in files:
file_ext = os.path.splitext(file)[-1].lower()
if file_ext in [".mflac", ".mgg"]:
encrypted_files.append(os.path.join(root, file))
if not encrypted_files:
self.log("未找到任何.mflac或.mgg文件", logging.WARNING)
self.stop_decryption()
return
self.log(f"找到 {len(encrypted_files)} 个加密文件")
self.update_status(f"开始解密 {len(encrypted_files)} 个文件...")
# 统计变量
total_files = len(encrypted_files)
success_files = 0
failed_files = 0
skipped_files = 0
# 处理每个文件
for i, encrypted_file in enumerate(encrypted_files):
if not self.is_processing:
break
# 更新进度
progress = (i / total_files) * 100
self.progress['value'] = progress
self.update_stats(total_files, success_files, failed_files, skipped_files)
file_name = os.path.basename(encrypted_file)
self.log(f"处理文件 {i+1}/{total_files}: {file_name}")
# 构建输出文件名
file_ext = os.path.splitext(file_name)[-1].lower()
if file_ext == ".mflac":
output_ext = ".flac"
else: # .mgg
output_ext = ".ogg"
output_file = os.path.splitext(file_name)[0] + output_ext
output_file_path = os.path.join(output_dir, output_file)
# 检查文件是否已存在
if os.path.exists(output_file_path):
self.log(f"文件已存在,跳过: {output_file}")
skipped_files += 1
continue
# 创建临时文件名
temp_file_name = hashlib.md5(file_name.encode()).hexdigest() + output_ext
temp_file_path = os.path.join(output_dir, temp_file_name)
try:
# 调用解密函数
self.log(f"开始解密: {file_name}")
result = self.script.exports_sync.decrypt(encrypted_file, temp_file_path)
if "Success" in result:
# 重命名临时文件
os.rename(temp_file_path, output_file_path)
success_files += 1
self.log(f"✓ 解密成功: {output_file}")
else:
failed_files += 1
self.log(f"✗ 解密失败: {file_name} - {result}", logging.ERROR)
# 清理临时文件
if os.path.exists(temp_file_path):
os.remove(temp_file_path)
except Exception as e:
failed_files += 1
self.log(f"✗ 处理文件时出错: {file_name} - {e}", logging.ERROR)
# 清理临时文件
if os.path.exists(temp_file_path):
os.remove(temp_file_path)
# 完成处理
self.progress['value'] = 100
self.update_stats(total_files, success_files, failed_files, skipped_files)
if self.is_processing:
self.log("=" * 50)
self.log("批量解密完成!")
self.log(f"总文件数: {total_files}")
self.log(f"成功: {success_files}")
self.log(f"失败: {failed_files}")
self.log(f"跳过: {skipped_files}")
self.log(f"输出目录: {output_dir}")
self.update_status("解密完成")
if failed_files == 0:
messagebox.showinfo("完成", f"解密完成!成功处理 {success_files} 个文件。")
else:
messagebox.showwarning("完成",
f"解密完成!\n成功: {success_files}\n失败: {failed_files}\n跳过: {skipped_files}")
except Exception as e:
self.log(f"解密过程发生错误: {e}", logging.ERROR)
messagebox.showerror("错误", f"解密过程发生错误: {e}")
finally:
# 断开连接
if self.session:
try:
self.session.detach()
except:
pass
# 恢复UI状态
self.is_processing = False
self.start_button.config(state=tk.NORMAL)
self.stop_button.config(state=tk.DISABLED)
if __name__ == "__main__":
# 检查frida是否可用
try:
import frida
except ImportError:
print("错误: 未安装frida,请先运行: pip install -r requirements.txt")
input("按回车键退出...")
sys.exit(1)
root = tk.Tk()
app = QQMusicDecryptorGUI(root)
root.mainloop()
文件2: hook_qq_music.js (解密脚本)
[JavaScript] 纯文本查看 复制代码 const TARGET_DLL = "QQMusicCommon.dll";
console.log("[INFO] Loading QQ Music Decryption Script");
// Get all function addresses
var EncAndDesMediaFileConstructorAddr = Module.findExportByName(TARGET_DLL, "??0EncAndDesMediaFile@@QAE@XZ");
var EncAndDesMediaFileDestructorAddr = Module.findExportByName(TARGET_DLL, "??1EncAndDesMediaFile@@QAE@XZ");
var EncAndDesMediaFileOpenAddr = Module.findExportByName(TARGET_DLL, "?Open@EncAndDesMediaFile@@QAE_NPB_W_N1@Z");
var EncAndDesMediaFileGetSizeAddr = Module.findExportByName(TARGET_DLL, "?GetSize@EncAndDesMediaFile@@QAEKXZ");
var EncAndDesMediaFileReadAddr = Module.findExportByName(TARGET_DLL, "?Read@EncAndDesMediaFile@@QAEKPAEK_J@Z");
console.log("[INFO] Function addresses retrieved");
// Create NativeFunction
var EncAndDesMediaFileConstructor = new NativeFunction(EncAndDesMediaFileConstructorAddr, "pointer", ["pointer"], "thiscall");
var EncAndDesMediaFileDestructor = new NativeFunction(EncAndDesMediaFileDestructorAddr, "void", ["pointer"], "thiscall");
var EncAndDesMediaFileOpen = new NativeFunction(EncAndDesMediaFileOpenAddr, "bool", ["pointer", "pointer", "bool", "bool"], "thiscall");
var EncAndDesMediaFileGetSize = new NativeFunction(EncAndDesMediaFileGetSizeAddr, "uint32", ["pointer"], "thiscall");
var EncAndDesMediaFileRead = new NativeFunction(EncAndDesMediaFileReadAddr, "uint", ["pointer", "pointer", "uint32", "uint64"], "thiscall");
console.log("[INFO] NativeFunction wrappers created");
rpc.exports = {
decrypt: function (srcFileName, tmpFileName) {
console.log("[START] Decrypting: " + srcFileName);
try {
// Allocate object memory
var EncAndDesMediaFileObject = Memory.alloc(0x28);
console.log("[STEP 1] Object memory allocated");
// Call constructor
console.log("[STEP 2] Calling constructor...");
EncAndDesMediaFileConstructor(EncAndDesMediaFileObject);
console.log("[STEP 2] Constructor called");
// Prepare filename
var fileNameUtf16 = Memory.allocUtf16String(srcFileName);
console.log("[STEP 3] Filename allocated");
// Call Open method
console.log("[STEP 4] Calling Open method...");
var openResult = EncAndDesMediaFileOpen(EncAndDesMediaFileObject, fileNameUtf16, 1, 0);
console.log("[STEP 4] Open method returned: " + openResult);
if (!openResult) {
console.log("[ERROR] Open method failed!");
EncAndDesMediaFileDestructor(EncAndDesMediaFileObject);
return "Open failed";
}
// Get file size
console.log("[STEP 5] Calling GetSize method...");
var fileSize = EncAndDesMediaFileGetSize(EncAndDesMediaFileObject);
console.log("[STEP 5] File size: " + fileSize);
if (fileSize == 0 || fileSize > 100 * 1024 * 1024) {
console.log("[ERROR] Invalid file size: " + fileSize);
EncAndDesMediaFileDestructor(EncAndDesMediaFileObject);
return "Invalid file size: " + fileSize;
}
// Create output file
console.log("[STEP 6] Creating output file...");
var tmpFile = new File(tmpFileName, "wb");
// Read file content in chunks
console.log("[STEP 7] Starting chunked file reading...");
var chunkSize = 0x10000; // Read 64KB at a time
var bytesRead = 0;
var buffer = Memory.alloc(chunkSize);
while (bytesRead < fileSize) {
var remaining = fileSize - bytesRead;
var currentChunk = chunkSize < remaining ? chunkSize : remaining;
// Read current chunk
var readResult = EncAndDesMediaFileRead(EncAndDesMediaFileObject, buffer, currentChunk, bytesRead);
if (readResult <= 0) {
console.log("[ERROR] Read failed at offset: " + bytesRead);
break;
}
// Write current chunk to file
var chunkData = buffer.readByteArray(readResult);
tmpFile.write(chunkData);
bytesRead += readResult;
// Output progress every 5MB
if (bytesRead % (5 * 1024 * 1024) == 0 || bytesRead == fileSize) {
var progress = (bytesRead / fileSize * 100).toFixed(1);
console.log("[PROGRESS] " + progress + "% (" + bytesRead + "/" + fileSize + ")");
}
}
tmpFile.close();
if (bytesRead != fileSize) {
console.log("[WARNING] Bytes read mismatch. Expected: " + fileSize + ", Actual: " + bytesRead);
} else {
console.log("[SUCCESS] File reading completed. Total bytes: " + bytesRead);
}
// Call destructor
console.log("[STEP 8] Calling destructor...");
EncAndDesMediaFileDestructor(EncAndDesMediaFileObject);
console.log("[FINISHED] Decryption successful!");
return "Success - Read " + bytesRead + " bytes";
} catch (e) {
console.log("[EXCEPTION] " + e.toString());
return "Exception: " + e.toString();
}
}
};
console.log("[INFO] Decryption script loaded successfully");
文件3: requirements.txt (依赖包)
[HTML] 纯文本查看 复制代码 frida==16.7.10
文件4: run.bat (Windows运行脚本)
[HTML] 纯文本查看 复制代码 @echo off
chcp 65001 >nul
title QQ音乐解密工具
echo ========================================
echo QQ音乐解密工具 v1.0
echo ========================================
echo.
echo 检查Python环境...
python --version >nul 2>&1
if errorlevel 1 (
echo 错误: 未找到Python,请先安装Python 3.6+
pause
exit /b 1
)
echo 检查依赖包...
pip show frida >nul 2>&1
if errorlevel 1 (
echo 安装依赖包...
pip install -r requirements.txt
)
echo 启动图形界面...
python main_gui.py
pause
文件5: README.md (说明文档)
[HTML] 纯文本查看 复制代码 # QQ音乐解密工具
一个用于批量解密QQ音乐VIP下载歌曲的图形化工具。
## 功能特点
- 批量解密QQ音乐下载的 `.mflac` 和 `.mgg` 文件
- 用户友好的图形界面
- 实时显示解密进度和日志
- 支持暂停和继续操作
- 自动跳过已解密的文件
## 支持格式
- 输入: `.mflac`, `.mgg`
- 输出: `.flac`, `.ogg`
## 系统要求
- Windows 10/11
- Python 3.6+
- QQ音乐客户端(最新版本)
- 管理员权限(用于运行frida-server)
## 使用方法
### 1. 环境准备
1. 安装Python 3.6或更高版本
2. 运行 `run.bat` 或手动安装依赖:
```bash
pip install -r requirements.txt
2. 启动服务以管理员身份运行命令提示符 启动frida-server:
[Shell] 纯文本查看 复制代码 frida-server.exe
3. 运行解密工具
1.启动QQ音乐并登录VIP账号
2.运行 run.bat 或直接运行:
[Shell] 纯文本查看 复制代码 python main_gui.py
3.在界面中选择输入和输出目录
4.点击"开始解密"
注意事项请确保QQ音乐正在运行 请确保frida-server已以管理员权限启动 本工具仅用于个人学习和研究 请确保您拥有歌曲的合法使用权
常见问题
连接失败检查QQ音乐是否正在运行 检查frida-server是否已启动 尝试以管理员权限运行所有程序
解密失败确保文件是通过VIP账号下载的 尝试重新下载有问题的歌曲 检查QQ音乐版本是否兼容
开发者工具由 Strelitzia 开发 免责声明本工具仅用于技术学习和研究目的,请勿用于商业用途。使用本工具产生的任何后果由使用者自行承担。 [HTML] 纯文本查看 复制代码
[b][size=4]## 使用说明
[/size][/b][color=rgb(15, 17, 21)][font=quote-cjk-patch, Inter, system-ui, -apple-system, BlinkMacSystemFont, "]
[/font][/color]1. **下载所有文件**到同一个文件夹
2. **双击运行 `run.bat`** 或手动执行:
```bash
pip install -r requirements.txt
python main_gui.py 确保已启动:
QQ音乐客户端(登录VIP账号) frida-server(以管理员权限运行)
在图形界面中选择输入输出目录,点击开始解密
功能特点直观的图形界面:无需命令行操作 实时进度显示:进度条和详细日志 批量处理:自动处理目录下所有加密文件 错误处理:单个文件失败不影响其他文件 智能跳过:已解密的文件自动跳过 可中断操作:支持中途停止解密过程
————————————
|
评分
-
查看全部评分
|