關于.SMP格式音樂加密檔案破解方法的一些嘗試
最近老媽在淘寶上買了個自帶廣場舞音樂的音響,她想讓我幫她把裡面的音樂拷出來放手機上聽,卻發現裡面的音樂格式都是.smp的加密檔案,閑着也是閑着,就做了些嘗試幫幫老媽。
是新人,可能會有很多錯誤,還請大家多多包涵,歡迎大家提出建議。
使用語言:C#
—目錄—
- 什麼是.smp
- .smp檔案的加密原理
- 如何破解.smp檔案
- 加上批量操作的功能
1.什麼是.smp
.smp格式是一種現在很少見的音樂加密格式,一般出現在老的遊戲或者一些與電子産品捆綁銷售的音樂裡,比如淘寶上賣的那種帶音樂包的小音箱,或者是一些學習機複讀機内的音頻資料。
這種格式的檔案被加密過是以不能在别的裝置上正常播放。
它一般長這樣:
2. .smp檔案的加密原理
像這些小音箱,上古學習機,複習機之類的東西,他們一般都有一個可以進行簡單邏輯運算甚至程式設計的單片機在其中對音樂檔案進行解碼播放,而要想在播放的時候可以及時解密的同時不卡頓影響體驗,是以應該不會是特别複雜的加密方式。
用WinHex打開一個此類檔案觀察後可發現,在檔案頭與檔案尾有大量的89 6B A5 22。
再打開一個普通的未加密MP3檔案對比發現,大多數MP3檔案的尾部會有大量的0或A。
到這裡,我們就想到了一種簡單的加密方法:異或加密。
這個.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格式的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);
}
}
可以看到黃色箭頭的記憶體回收部分,隻要檔案讀取的線程和檔案破解的線程達到動态平衡,記憶體的使用率就可以壓下來了。
完整的工程我放到GitHub上吧!
https://github.com/MycroftCooper/.SmpFileDecryptor
肯定還有更好的解決方案~大家也可以自己動手試試的!