查看: 86|回复: 1

mflac解密(Windows、Python、frida)

[复制链接]

0

技术

0

魅力

0

原创

青出于蓝

Rank: 5Rank: 5

积分
3277
人气
17
分享
8
发表于 2025-10-6 18:07:35 | 显示全部楼层 |阅读模式
本帖最后由 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, &quot;]
[/font][/color]1. **下载所有文件**到同一个文件夹
2. **双击运行 `run.bat`** 或手动执行:
   ```bash
   pip install -r requirements.txt
   python main_gui.py
  • 确保已启动:

    • QQ音乐客户端(登录VIP账号)
    • frida-server(以管理员权限运行)

  • 在图形界面中选择输入输出目录,点击开始解密

功能特点
  • 直观的图形界面:无需命令行操作
  • 实时进度显示:进度条和详细日志
  • 批量处理:自动处理目录下所有加密文件
  • 错误处理:单个文件失败不影响其他文件
  • 智能跳过:已解密的文件自动跳过
  • 可中断操作:支持中途停止解密过程


————————————






评分

参与人数 2经验 +15 人气 +4 分享 +2 收起 理由
alovelydoge + 3 很给力!
faryou + 15 + 1 + 2 赞一个!

查看全部评分

0

技术

0

魅力

0

原创

略知一二

Rank: 3Rank: 3

积分
666
人气
0
分享
0
发表于 2025-10-9 09:09:07 | 显示全部楼层
太专业了 ,不懂。。。还是支持楼主
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

快速回复 返回顶部 返回列表