仙剑5前传发售已有一段时间了,该做的工具差不多都做了,因为这学期课程较松的缘故,平时我有较多的自由支配时间,除去考研复习,最近也研究了一下仙剑4音乐SMP的解密,贴上来就当是学习笔记给大家分享了。另外,SMP解密工具网上已经有很多了,所以只想要解密音乐的同学去其他网站上下载别人做的现呈工具吧,这里目的主要是分析一下SMP文件解密原理。
进入正题,用WinHEX查看smp文件,发现文件头标识RST与CPK文件的一致,往下看几乎没有明文,果断上OD。老规矩用CreateFileW断点,在游戏程序创建p01-1.smp文件句柄时断下
返回程序领空到790C8F处,F8单步一路跟下去,790CD8-790CEE这段读取了文件头,并验证文件标识是否为0x1A545352(即RST):
00790CD5 |> 8D5E 08 lea ebx, dword ptr [esi+8] 00790CD8 |. 68 80000000 push 80 00790CDD |. 53 push ebx 00790CDE |. 50 push eax 00790CDF |. E8 FC090000 call <ReadFile> 00790CE4 |. 8B03 mov eax, dword ptr [ebx] 00790CE6 |. 83C4 0C add esp, 0C 00790CE9 |. 3D 5253541A cmp eax, 1A545352 00790CEE |. 74 38 je short 00790D28
再往下790D28处取了文件0x10偏移的一个DWORD值,并左移5位,算得一个值,并将这个值作为buffer大小继续读取文件
00790D28 |> 8B56 18 mov edx, dword ptr [esi+18] 00790D2B |. 8B86 AC001000 mov eax, dword ptr [esi+1000AC] 00790D31 |. C1E2 05 shl edx, 5 00790D34 |. 8D9E 88000000 lea ebx, dword ptr [esi+88] 00790D3A |. 52 push edx ; dwSize: [esi + 8] << 5 00790D3B |. 53 push ebx ; pBuffer 00790D3C |. 50 push eax ; hFile 00790D3D |. E8 9E090000 call <ReadFile> 00790D42 |. 83C4 0C add esp, 0C 00790D45 |. 84C0 test al, al 00790D47 |. 75 38 jnz short 00790D81
接下来就是关键了,因为刚才读取的内容的指针被压栈调用CALL 0x7919D0。
00790D81 |> 68 00001000 push 100000 00790D86 |. 53 push ebx 00790D87 |. E8 440C0000 call 007919D0
F7跟进CALL,791B8F-791C5D这个循环实际作用是memcpy,拷贝了两组大小为0x1000的数据,之后又是两个CALL调用,数据窗口跟随并对比后发现这个CALL是解密数据的
00791C63 |. 8D8C24 800000>lea ecx, dword ptr [esp+80] 00791C6A |. 8D9424 800100>lea edx, dword ptr [esp+180] 00791C71 |. 51 push ecx ; pPasswd 00791C72 |. 68 00040000 push 400 ; dwSize 00791C77 |. 52 push edx ; pData 00791C78 |. E8 23FCFFFF call <CPK_Decode> 00791C7D |. 8D8424 8C0000>lea eax, dword ptr [esp+8C] 00791C84 |. 8D8C24 901100>lea ecx, dword ptr [esp+1190] 00791C8B |. 50 push eax ; pPasswd 00791C8C |. 68 00040000 push 400 ; dwSize 00791C91 |. 51 push ecx ; pData 00791C92 |. E8 09FCFFFF call <CPK_Decode>
跟进这个解密CALL并进行代码分析,另外插一句,在开始之前我已经用IDA分析了脱壳后的pal4.exe,已经识别出了部分库函数。在开头:
007918C1 |. DF6C24 04 fild qword ptr [esp+4] ; dwSize 007918C5 |. 56 push esi 007918C6 |. 57 push edi 007918C7 |. 8B7C24 18 mov edi, dword ptr [esp+18] 007918CB |. 83EC 08 sub esp, 8 007918CE |. D83D 6C298400 fdivr dword ptr [84296C] 007918D4 |. 8B37 mov esi, dword ptr [edi] 007918D6 |. D805 68298400 fadd dword ptr [842968] 007918DC |. DD1C24 fstp qword ptr [esp] 007918DF |. E8 4930EEFF call <floor> 007918E4 |. 83C4 08 add esp, 8 007918E7 |. E8 302BEEFF call <_ftol> 007918EC |. 69C0 B979379E imul eax, eax, 9E3779B9 007918F2 |. 85C0 test eax, eax 007918F4 |. 894424 0C mov dword ptr [esp+C], eax 007918F8 |. 0F84 BA000000 je 007919B8
这个运算很简单,用52除以压栈的dwSize再加上6并取整,再乘以0x9E3779B9看是否等于0,如果等于0,则跳过下面那个循环解密过程。
那个循环解密过程的算法就有一点复杂了,分析纯粹是体力活了,这里就不贴具体分析方法了,贴一下我已经根据这段代码写好的Pascal解密代码:
(注:实际上就是XXTEA加密的,密钥见下面源码)
type arrIndex = array of DWORD; pIndex = ^arrIndex; //----------------------------------------------------------- // Original: // Pal4.exe_0x7918A0 | arg_1: pBuffer, arg_2: dwSize, arg_3: pPasswd // New: // arrIndex : 待解密数据数组指针 // dwSize : 解密数据数组长度 // arrPwd : 解密密码(已定义为常量) //----------------------------------------------------------- procedure CPK_Decode(dwSize: DWORD; arrIndex: pIndex); const arrPwd : array [0..13] of DWORD = ($706D6156, $2E657269, $204A2E43, $53207461, $7374666F, $20726174, $68636554, $6F6C6F6E, $28207967, $6E616853, $69614867, $6F432029, $4C202C2E, $00006474 );//str: "Vampire.C.J at Softstar Technology (ShangHai) Co., Ltd" var tmpNum_A, tmpNum_B, tmpNum_C, tmpNum_D : DWORD; tmpNum_E : DWORD; dwLoop : DWORD; begin tmpNum_A := Floor(52 / dwSize + 6) * $9E3779B9; tmpNum_D := arrIndex^[0]; while tmpNum_A <> 0 do begin tmpNum_B := (tmpNum_A shr 2) and 3; dwLoop := dwSize - 1; while (dwLoop > 0) do begin tmpNum_C := arrIndex^[dwLoop-1]; tmpNum_E := (tmpNum_D * 4) xor (tmpNum_C shr 5); tmpNum_E := tmpNum_E + ((tmpNum_D shr $3) xor (tmpNum_C shl $4)); tmpNum_D := arrIndex^[dwLoop] - (tmpNum_E xor ((tmpNum_A xor tmpNum_D) + (arrPwd[(dwLoop and $3) xor tmpNum_B] xor tmpNum_C))); arrIndex^[dwLoop] := tmpNum_D; dwLoop := dwLoop - 1; end; tmpNum_D := arrIndex^[0] - ((((tmpNum_D * 4) xor (arrIndex^[dwSize-1] shr $5)) + ((tmpNum_D shr $3) xor (arrIndex^[dwSize-1] shl $4))) xor (((arrPwd[(dwLoop and $3) xor tmpNum_B]) xor arrIndex^[dwSize - 1]) + (tmpNum_A xor tmpNum_D))); arrIndex^[0] := tmpNum_D; tmpNum_A := tmpNum_A + $61C88647; end; end;
解密完成后观察数据,发现这一段解密出来的是索引信息,并非是音乐数据,继续往下分析。791DAA-791EA0这一段代码的作用是将解密后的索引信息拷回去。对拷回去的索引信息下内存访问断点,F9运行,断在了7915A3,很容易看出来这里是拿第一个DWORD作循环比较。
从子程序返回后进入上面那个790A80的call进行分析,发现这里是CRC32效验,效验结果被作为参数压栈调用下一个也就是刚才断下来的那个CALL,因此可以看出仙剑4的CPK索引信息查找是通过查找crc32效验值进行的,当然,索引信息中第一DWORD就是CRC32效验值无误了。进一步分析可以知道这个CRC32效验值是文件名的效验值。
从子程序返回,继续往下分析,在791217处调用了SetFilePointer,Offset是索引信息第四个DWORD值。
既然已经调用了SetFilePointer,接下来很快应该就会ReadFile,直接bp ReadFile,F9运行,断下后观察堆栈,BytesToRead一项刚好是索引信息第五个和第六个DWORD值。
这里有两个一样的值?后续分析可以得知实际上CPK里面部分文件用LZO压缩过了,第五个DWORD实际上是压缩后的大小,第六个是压缩前的大小。
返回程序领空,往下看,发现7909EE处再次调用了上面所提到的解密CALL。F4过去看一下参数,发现dwSize是BytesToRead4。数据窗口跟随,F8直接步过CALL观察,可以发现解密后的数据就是我们想要的音乐数据了。
由于本文主要是探讨SMP音乐文件解密的,因此索引信息方面不再详细描述。根据刚才的分析,现在解密程序的编写思路已经很明了了,因为smp文件中只有一个文件,因此只需解密索引信息后读取第一组索引信息第四个到第六个DWORD,再据此读入数据并解密即可。
附SMP文件解密Delphi7工程源代码:Pal4_SMP.rar
打开之后就自动关闭了啊。
查了一下他会自动跳到 if not FileExists(ParamStr(1)) then这一句。
如果删掉这一部分的话。
他会只要求一个叫做p01-3的文件。其他文件都没反应的。
需解密的文件名和输出的文件名是以参数形式输入程序的~如果你想批量解密其他的,可以写个bat
我的打算是在读出的时候把新的音乐文件替换进去的。只是不会做,能帮忙写两句么。给个思路也行。。
其实4的音乐资源就是一个TEA加密而已,当时研究时没注意到,TEA的密码在文章里面的源代码里面写清楚了的,直接用就行了
我看了好久 还是没思路。能帮忙做一个吗?
是k0=$706D6156………………一直到k13=$00006474么?
另外解密次数是多少 是32还是64?
迭代次数就那那份源代码调一下就知道了,KEY就是那个字符串
我最近在忙考研复习和轩六MOD,实在是抽不出更多时间来做其他东西了,抱歉~
是这个吗?
Vampire.C.J at Softstar Technology (ShangHai) Co., Ltd
我看tea的密钥是有固定长度要求的,是要转换一次吗?
恩,就是这个,其实转不转换无所谓,先把固定长度的内存空间分配出来,再把这个字符串用memcpy拷进去就行了,这样更简单
转了之后读不出来。。
那个原来的代码里长度除4之后再decode里好像不是全部都乘回去的吧。。
另外,左右移动的字节数也没问题吗。
长度除4是因为这样做起来更方便而已,毕竟TEA加解密是以四字节为一组进行加解密。乘回去是什么意思?没看懂~TEA加密跟解密的左右移位数我记得是不一样的,我这里这份源代码是解密不是加密的,如果你要加密的话看我这份源代码是肯定不行的
还是不行 我把转换过的文件加上rst头部之后,无法读取。
我看了下每个原文件在实际内容前有1M左右的内容,大概是索引的样子。可能还和那个有关。
我这直接导出解码中arrindex的内容还是乱码。。
不解码的结果是在索引里读出了相当的信息(还有部分信息还是乱码)。。 不过都没什么意义。。
我对加密代码不是很确定,能帮忙看一下吗
begin
result := (((z shr 5) xor y ) + ((y shr 3) xor (z shl 4))) xor ((sum xor y) + (arrpwd[(p and 3) xor e] xor z) );
end;
begin
n := Length(data);
q := 6 + 52 div n;
z := data[n-1];
y := data[0];
sum := 0;
repeat
inc(sum,Delta);
e := (sum shr 2) and 3;
for p := 0 to n-2 do
begin
y := data[p+1];
x := data[p];
inc(x,mx);
data[p] := x;
z := x;
end;
y := data[0];
x := data[n-1];
inc(x,mx);
data[n-1] := x;
z := x;
dec(q);
until q = 0;
end
单就音乐SMP而言,前面的索引只有12个字节有用,后面全是垃圾数据,不用管。我看了下你的代码,应该是网上的一份很常见的pascal的TEA算法代码的XXTeaEncrypt函数部分,不过我记得那个函数是128位的,而SMP加密至少是32*14=448位,你应该换一份代码
不会写啊。。。 那个14是怎么得出来的?
麻烦看一下这个吧,新人求指点。
begin
tmpNum_A := Floor(52 / dwSize + 6) div $9E3779B9;
tmpNum_D := arrIndex^[0]; ;
while tmpNum_A 0 do
begin
tmpNum_B := (tmpNum_A shr 2) and 3;
dwLoop := 0;
while (dwLoop < dwsize -1 ) do
begin
tmpNum_C := arrIndex^[dwLoop+1];
tmpNum_E := (tmpNum_D * 4) xor (tmpNum_C shr 5);
tmpNum_E := tmpNum_E + ((tmpNum_D shr $3) xor (tmpNum_C shl $4));
tmpNum_D := arrIndex^[dwLoop] – (tmpNum_E xor ((tmpNum_A xor tmpNum_D) + (arrPwd[(dwLoop and $3) xor tmpNum_B] xor tmpNum_C)));
arrIndex^[dwLoop] := tmpNum_D;
dwLoop := dwLoop + 1;
end;
tmpNum_D := arrIndex^[0] – ((((tmpNum_D * 4) xor (arrIndex^[dwSize-1] shr $5)) + ((tmpNum_D shr $3) xor (arrIndex^[dwSize-1] shl $4))) xor (((arrPwd[(dwLoop and $3) xor tmpNum_B]) xor arrIndex^[dwSize – 1]) + (tmpNum_A xor tmpNum_D)));
arrIndex^[0] := tmpNum_D;
tmpNum_A := tmpNum_A – $61C88647
end;
end;
之前说过了,你要做加密的话我这份源代码是肯定不能用的~加密算法中的sum(我那份源代码中定义的tmpNum_A)的初始值跟解密算法都不一样,如果你想加密的话,另外找一份代码吧,网上很多的,4的TEA加密又没有变形什么的,就是加密位数多了点而已(加密位数=32*密码数组长度)
这次把sum改成f1了,可是还是读不了,调了一天了还是不对啊。
找了半天代码都找不到。。
我已经按照加密的格式写了啊。(保留A1是因为长度够肯定是循环6次)
我看xxblocktea的解密和加密的位移是一样的。不过你这个D1*4的部分没有左移。
帮忙看一下吧。上网真的不知道去哪找。找到的代码全都一样。
begin
tmpNum_A1 := Floor(52 / dwSize + 6) * $9E3779B9;
tmpNum_f1 := 0 ;
tmpNum_c1 := arrIndex^[dwsize -1];
while tmpNum_a1 0 do
begin
tmpnum_F1:= tmpNum_f1 + $9E3779B9;
tmpNum_B1 := (tmpNum_f1 shr 2) and 3;
dwLoop := 0;
while (dwLoop < dwsize – 1 ) do
begin
tmpNum_d1 := arrIndex^[dwLoop+1];
tmpNum_E1 := (tmpNum_c1 * 4) xor (tmpNum_d1 shr 5);
tmpNum_E1 := tmpNum_E1 + ((tmpNum_c1 shr $3) xor (tmpNum_d1 shl $4));
tmpNum_c1 := arrIndex^[dwLoop] + (tmpNum_E1 xor ((tmpNum_f1 xor tmpNum_c1) + (arrPwd[(dwLoop and $3) xor tmpNum_B1] xor tmpNum_d1)));
arrIndex^[dwLoop] := tmpNum_c1;
dwLoop := dwLoop + 1;
end;
tmpNum_c1 := arrIndex^[dwsize -1] + ((((tmpNum_c1 * 4) xor (arrIndex^[dwsize-1] shr $5)) + ((tmpNum_c1 shr $3) xor (arrIndex^[dwsize-1] shl $4))) xor (((arrPwd[(dwLoop and $3) xor tmpNum_B1]) xor arrIndex^[dwsize -1]) + (tmpNum_f1 xor tmpNum_c1)));
arrIndex^[dwsize -1] := tmpNum_c1;
tmpNum_A1 := tmpNum_A1 + $61C88647;
end;
end;
转过之后还是读不了。 不知道什么原因,麻烦看一下吧。加密代码已经找到了。
var
tmpNum_A, tmpNum_B, tmpNum_C, tmpNum_D : DWORD;
dwLoop ,tmpnum_F : DWORD;
function tmpnum_e : dword;
begin
result:= ((tmpNum_c * 4) xor (tmpNum_d shr 5)) + ((tmpNum_c shr $3) xor (tmpNum_d shl $4)) xor ((tmpNum_A xor tmpNum_c) + (arrPwd[(dwLoop and $3) xor tmpNum_B] xor tmpNum_d)) ;
end;
begin
tmpNum_A := 0;
tmpnum_f := Floor(52 / dwSize + 6);
tmpNum_D := arrIndex^[dwsize-1];
while tmpNum_f 0 do
begin
tmpnum_a:= tmpnum_a+ $9E3779B9 ;
tmpNum_B := (tmpNum_A shr 2) and 3;
dwLoop := 0;
while (dwLoop < dwsize -1 ) do
begin
tmpNum_c := arrIndex^[dwLoop+1];
tmpNum_d := arrIndex^[dwLoop] + tmpnum_e;
arrIndex^[dwLoop] := tmpNum_D;
dwLoop := dwLoop + 1;
end;
tmpnum_c:=arrindex^[0];
tmpNum_D := arrIndex^[dwsize -1] + tmpnum_e;
arrIndex^[dwsize-1] := tmpNum_D;
tmpNum_f := tmpNum_f – 1 ;
end;
end;
这份加密代码我已经完全看不懂了= =首先还是要确定一个问题,加密后的数据用我这里这份代码能解不?如果不能解,那么是加密代码的问题,如果能解,则是索引信息的问题
能解。我调了一下解密代码之后就比较明朗了:
procedure CPK_Decode(dwSize: DWORD; arrIndex: pIndex);
const
var
tmpNum_A, tmpNum_B, tmpNum_C, tmpNum_D : DWORD;
dwLoop : DWORD;
function tmpnum_e : dword;
begin
result:= ((tmpNum_D * 4) xor (tmpNum_C shr 5)) + ((tmpNum_D shr $3) xor (tmpNum_C shl $4)) xor ((tmpNum_A xor tmpNum_D) + (arrPwd[(dwLoop and $3) xor tmpNum_B] xor tmpNum_C)) ;
end;
begin
tmpNum_A := Floor(52 / dwSize + 6) * $9E3779B9;
tmpNum_D := arrIndex^[0];
while tmpNum_A 0 do
begin
tmpNum_B := (tmpNum_A shr 2) and 3;
dwLoop := dwSize – 1;
while (dwLoop > 0) do
begin
tmpNum_C := arrIndex^[dwLoop-1];
tmpNum_D := arrIndex^[dwLoop] – tmpnum_e;
arrIndex^[dwLoop] := tmpNum_D;
dwLoop := dwLoop – 1;
end;
tmpnum_c:=arrindex^[dwsize-1];
tmpNum_D := arrIndex^[0] – tmpnum_e;
arrIndex^[0] := tmpNum_D;
tmpNum_A := tmpNum_A -$9E3779B9 ;
end;
end;
已经可以拿大小与原曲完全一样的音乐文件做替换了。
现在就是如果大小不一样还是完全读不出来。
索引信息就不是tea加密了。。解出来是乱码。这个尚无头绪。
另外可能游戏在其他地方有校对。我把游戏自己的音乐文件互相替换,游戏程序也不会读出。
手头现在没有游戏也没法看,不过我确定索引信息是TEA加密,只是只有开头几个dword有用,后面全部都是乱码而已;你有没有修改前面索引信息中那个文件大小?
没有,那个大小要如何修改?另外索引开头的那一段信息要怎么确认?每个的长度是从80h到22dc0h。
文章中提到的“BytesToRead一项刚好是索引信息第五个和第六个DWORD值”,因此你需要把这两个DWORD值改成加密后的音乐数据大小才行
不好意思我又看了一下,应该是前六个解密后的索引值有用,根据你这个文章第一个值是crc32校检值,校检的内容是文件名,不过我用网上的校检工具计算文件名得出的值和这个不一样。这个能再看一下吗?
第二个值和第三个值都是65541,0
第四个值是非音乐数据的大小
第五个和第六个是压缩前后的大小。
之后的数据我分析了数个文件,内容(前50个数据,后面太多没看)基本一致。
文件名那个HASH不需要管,游戏程序中所使用的crc32算法是变形非标准算法,你只需要改那第五个和第六个数据大小即可
好的谢谢,这段时间多有麻烦,还请包涵~
看了下,dwSize基本都是0x400,音乐数据的话也一般比这大,所以52除以dwSize再加上6并取整其实就是6,这样可以简化后面的代码内容,也可以推出加密轮数应该是5。
另外key=”Vampire.C.J at Softstar Technology (ShangHai) Co., Ltd”其实只用到了前面4个整数(因为涉及到此值都有and 3作为下标)
另外第二次读取的0x400数据似乎没有用到?我下内存断点是没跟到的。
搜了下,毫无疑问是XXTEA: http://blog.sina.com.cn/s/blog_6d193c030100zdvc.html
的确
加密/解密代码(C语言): https://www.zybuluo.com/Aqua-Dream/note/563670
总算解密成功了,感谢博主。
贴一下我的发现:XXTEA解密是以4个字节为一块解密的。如果数据大小不是4的倍数的话(如P01-3.smp),需要将最后多余的1~3个垃圾字节去除(估计是作者故意这样做增加解密难度),然后再XXTEA解密。
我想替换部分音乐体验仙剑4
可是将替换的音乐文件后缀名改成仙剑4音频文件smp格式、上网搜解密工具都没用。希望大神能提供获取被加密过的音频文件密钥的工具和一些使用步骤,谢谢
工具的话这个还真没有,直接替换肯定不行的,必须将MP3文件加密回去并CPK打包