天天看点

关于.SMP格式音乐加密文件破解方法的一些尝试关于.SMP格式音乐加密文件破解方法的一些尝试

关于.SMP格式音乐加密文件破解方法的一些尝试

最近老妈在淘宝上买了个自带广场舞音乐的音响,她想让我帮她把里面的音乐拷出来放手机上听,却发现里面的音乐格式都是.smp的加密文件,闲着也是闲着,就做了些尝试帮帮老妈。

是新人,可能会有很多错误,还请大家多多包涵,欢迎大家提出建议。

使用语言:C#

—目录—

  1. 什么是.smp
  2. .smp文件的加密原理
  3. 如何破解.smp文件
  4. 加上批量操作的功能

1.什么是.smp

.smp格式是一种现在很少见的音乐加密格式,一般出现在老的游戏或者一些与电子产品捆绑销售的音乐里,比如淘宝上卖的那种带音乐包的小音箱,或者是一些学习机复读机内的音频资料。

这种格式的文件被加密过所以不能在别的设备上正常播放。

它一般长这样:

关于.SMP格式音乐加密文件破解方法的一些尝试关于.SMP格式音乐加密文件破解方法的一些尝试

2. .smp文件的加密原理

像这些小音箱,上古学习机,复习机之类的东西,他们一般都有一个可以进行简单逻辑运算甚至编程的单片机在其中对音乐文件进行解码播放,而要想在播放的时候可以及时解密的同时不卡顿影响体验,所以应该不会是特别复杂的加密方式。

用WinHex打开一个此类文件观察后可发现,在文件头与文件尾有大量的89 6B A5 22。

关于.SMP格式音乐加密文件破解方法的一些尝试关于.SMP格式音乐加密文件破解方法的一些尝试

再打开一个普通的未加密MP3文件对比发现,大多数MP3文件的尾部会有大量的0或A。

关于.SMP格式音乐加密文件破解方法的一些尝试关于.SMP格式音乐加密文件破解方法的一些尝试

到这里,我们就想到了一种简单的加密方法:异或加密。

这个.smp文件实际上是用89 6B A5 22进行了异或加密,而

0 XOR 89 6B A5 22=89 6B A5 22。

为了验证该猜想,我们对所有数据进行异或89 6B A5 22,再用WinHex打开后发现,文件头三位出现了49 44 33即ASCII的ID3,这是MP3文件的标志,说明我们的猜想没有问题。

关于.SMP格式音乐加密文件破解方法的一些尝试关于.SMP格式音乐加密文件破解方法的一些尝试

也就是说这类.SMP格式的MP3文件多半是用了异或加密!

3. 如何破解.smp文件

既然我们已经摸清楚了这类文件的加密方式,那么只需要逆向进行就可以完成该文件的解密。

既:

3.1读取文件与存储文件

可以直接定义一个文件类专门负责文件相关的管理与操作。

代码如下:

class TargetFile
    {
        private string name;//文件名
        private byte[] key;//文件密匙
        private byte[] target_file;//文件内容

        public byte[] Key { get => key; set => key = value; }
        public byte[] Target_file { get => target_file; set => target_file = value; }

        public TargetFile(string file_path)//读取文件
        {
            try
            {
                target_file = File.ReadAllBytes(file_path);
                name = file_path.Substring(file_path.LastIndexOf('\\')+1, file_path.LastIndexOf('.')-file_path.LastIndexOf('\\')-1);
            }
            catch (Exception ex) { throw ex; }
            
        }
        
        public void saveFile(string file_path, string save_suffix)//保存文件
        {
            try
            {
                File.WriteAllBytes(file_path + @"\" + name+save_suffix, target_file);
            }
            catch (Exception ex) { throw ex; }
        }
    }
           

3.2寻找密匙

和我们最开始用肉眼寻找密匙的方法一样,利用MP3文件在文件头与尾一般会存在大量的0的特性,我们只需要在文件头与尾寻找最长重复子串就好。关于查找最长重复子串的算法网上已经有很多了,这里就不再赘述,大家可以咨询查阅。

(这个就挺不错:https://blog.csdn.net/qq_34826261/article/details/97319790)

查找最长重复子串我用的是二分法,是先选取尾部128个字符进行查找,如果不符合要求再选取头部的128个字符进行查找。

代码如下:

private static byte[] findKey(bool mod)//两种模式,在文件头查找或在文件尾查找
    {
        byte[] b1 = new byte[128];
        if (mod)//在文件头尾找
        {
            for (int i = 0; i < 128; i++) b1[i] = file.Target_file[file.Target_file.Length - 128 + i];
        }
        else//在文件头查找
        {
            for (int i = 0; i < 128; i++) b1[i] = file.Target_file[i];
        }
        string str = ByteArrayToHexString(b1);//二进制转换为字符串

        for (int len = str.Length / 2; len >= 0; len--)//二分法查找最长重复子串
        {
            for (int i = 0; i < str.Length - len; i++)
            {
                string t1 = str.Substring(i, len);
                string t2 = str.Substring(i + len);
                //密匙长度一般为2位,4位,8位(也有可能是1位的)
                if (t2.IndexOf(t1) != -1 && (t1.Length==8 ||t1.Length==4 ||t1.Length==2) return HexStringToByteArray(t1);
            }
        }
        return null;
    }
           

调用该函数的解密函数:

public static void decode(TargetFile f, string save_path, string save_suffix,string key)
        {
            file = f;
            if(key=="")//如果没有输入已知密码,就寻找密码
            {
                file.Key = findKey(true);//先在文件尾找
                if (file.Key == null) file.Key = findKey(false);//没找到就在文件头找
            }
            else//输入了已知密码就直接解码
            {
                file.Key = HexStringToByteArray(key);//字符串转2进制
            }
            int idx = 0;
            //异或运算
            for (long i = 0; i < file.Target_file.Length; ++i)
                file.Target_file[i] ^= file.Key[idx++ % file.Key.Length];

            file.saveFile(save_path, save_suffix);//解密后文件保存
        }
           

再附上二进制与字符串间的转换代码:

private static byte[] HexStringToByteArray(string hex)
        {
            if (hex.Length % 2 != 0)
                hex = "0" + hex;
            List<byte> bytes = new List<byte>();
            for (int i = 0; i < hex.Length; i += 2)
                bytes.Add(byte.Parse(hex.Substring(i, 2), System.Globalization.NumberStyles.AllowHexSpecifier));
            return bytes.ToArray();
        }
        
        public static string ByteArrayToHexString(byte[] bytes)
        {
            string hexString = string.Empty;
            if (bytes != null)
            {
                StringBuilder strB = new StringBuilder();
                for (int i = 0; i < bytes.Length; i++)
                {
                    strB.Append(bytes[i].ToString("X2"));
                }
                hexString = strB.ToString();
            }
            return hexString;
        }
           

这个方法是有BUG的,MP3格式在音频标签中要求所有的空间必须以空字符(ASCII 0)填充。但是并不是所有的应用程序遵循该规则,比如winamp就用空格(ASCII 32)代替之,因此并不是所有的MP3文件的头尾部都有大量的0,有时也有可能是大量的A或者别的什么,所以这样的算法并不总是能获得正确的密匙。

如果运气不好的话,还是自己打开文件分析一下吧!

3.3异或运算

之后就是将整个文件按位异或运算了,代码上面已经出现过一次了。

for (long i = 0; i < file.Target_file.Length; ++i)
                file.Target_file[i] ^= file.Key[idx++ % file.Key.Length];
           

3.4验证密匙正确性

得到的文件我们可以判断一下其正确性。

MP3 文件大体分为三部分:TAG_V2(ID3V2),音频数据,TAG_V1(ID3V1)

a). ID3V2在文件开始的位置,以ID3开头,包含了作者,作曲,专辑等信息,长度不固定,扩展了ID3V1 的信息量,非必需

b). 一系列的音频数据帧,在文件的中间位置,个数由文件大小和帧长决定;每个帧都以FFF开头,的长度可能不固定,也可能固定,由位率bitrate决定;每个帧又分为帧头和数据实体两部分;帧头记录了mp3 的位率,采样率,版本等信息,每个帧之间相互独立 。

c). ID3V1在文件结尾的位置,以TAG开头,包含了作者,作曲,专辑等信息,长度为128Byte,非必须。

只要解密后的文件中满足以上要求,就可以证明解密正确了!

(更多的MP3文件原理可以看这篇文章https://blog.csdn.net/zhenglie110/article/details/78654410)。

具体的代码CSDN上有Python版的,大家可以去看看,这里就不多写啦:

https://www.cnblogs.com/BigFeng/p/6212853.html

最后将破解完成的文件的后缀名改为.mp3,再通过文件操作存储到外存,就算破解完成了!就可以在任何设备上正常解码播放了。

4. 加上批量操作的功能

有的时候会不止一个这样的文件需要破解,而是成千上万个,那就需要再加些代码进行批量操作了。

在批量操作的时候需要考虑这几个问题:

1- 内存管理:

需要破解的文件特别多,一次性内存装不下咋办?

2- 时间成本:

如何在大量文件的情况下破解的更快些?

3- 错误处理:

在破解的过程中如果有几个文件出错无法破解,怎么跳过这些文件并记录下错误继续正常运行?

其中问题1与2使用多线程编程就可以解决,我们用一个线程来读取文件,另一个线程来破解和存储文件,而读取会比破解快一些,所以破解的线程总有文件可以破解,破解完存储到外存后就会释放回收内存空间,这样就可以把内存占用压到一个可接受的范围内。而且在程序运行的同时就不断有已经破解完成的可用文件产生。

如果想要读取速度更快些,可以用线程池多线程读取文件。

而问题3则加个错误处理机制和错误文件队列就好,最终程序跑完后把这个队列打印出来,就可以知道哪些文件没有成功破解了。

代码如下:

static private void readFile()//读取文件
        {
            while (files.Count != 0)
            {
                FileInfo j = files.Dequeue();
                if (j.Extension == read_suffix)
                    try
                    {
                        file_list.Enqueue(new TargetFile(j.FullName));
                    }
                    catch
                    {
                        erroFiles.Enqueue(j);//文件打开失败就加入错误队列
                        continue;
                    }
                if (file_list.Count == 10) Thread.Sleep(0);//读到十个文件就阻塞,到破解线程上去
            }
            Console.WriteLine("文件读取完成,文件数量:" + file_list.Count);
        }
        static private void decoder()
        {
            while (file_list.Count != 0 || files.Count != 0)
            {
                if (file_list.Count == 0) readFiles.Join(100);如果所有已读取完的文件都破解完成,就阻塞线程,等待文件读取的线程
                else
                {
                    Decryptor.decode(file_list.Dequeue(), save_path, save_suffix,key);
                    counter++;
                    Console.Clear();
                   Console.WriteLine("进度:" + $"{((double)counter / (double)num):P}");
                }
            }
                Console.WriteLine("破解完成,文件已保存至" + save_path);
        }
        static void Main()
        {
            read_suffix = ".smp";
            save_suffix = ".mp3";
            counter = 0;
            menu();
            erroFiles = new Queue<FileInfo>();
            if (pattern)//批量文件破解
            {
                files = new Queue<FileInfo>();
                root = new DirectoryInfo(read_path);
                FileInfo[] f = root.GetFiles();
                foreach (FileInfo i in f)
                {
                    files.Enqueue(i);
                }
                num = files.Count;

                readFiles = new Thread(readFile);
                decodeFiles = new Thread(decoder);
                try
                {
                    file_list = new Queue<TargetFile>();
                    readFiles.Start();
                    decodeFiles.Start();
                }
                catch (Exception ex)
                {
                    Console.WriteLine($"错误: {ex}");
                }
                if(erroFiles.Count!=0)
                {
                    Console.WriteLine("文件损坏数量:" + erroFiles.Count);
                    while(erroFiles.Count!=0)
                    {
                        Console.WriteLine(erroFiles.Dequeue().Name);
                    }
                }
            }
            else//单文件破解
            {
                TargetFile file =new TargetFile(read_path);
                Decryptor.decode(file,save_path,save_suffix,key);
            }

        }
           

可以看到黄色箭头的内存回收部分,只要文件读取的线程和文件破解的线程达到动态平衡,内存的使用率就可以压下来了。

关于.SMP格式音乐加密文件破解方法的一些尝试关于.SMP格式音乐加密文件破解方法的一些尝试

完整的工程我放到GitHub上吧!

https://github.com/MycroftCooper/.SmpFileDecryptor

肯定还有更好的解决方案~大家也可以自己动手试试的!