182013
 

仙剑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

  发表在 下午 6:38

  有36 条回复 被发表在“【笔记】《仙剑奇侠传4》SMP音乐解密部分研究(附源代码)” 页面

  1. 打开之后就自动关闭了啊。
    查了一下他会自动跳到 if not FileExists(ParamStr(1)) then这一句。
    如果删掉这一部分的话。
    他会只要求一个叫做p01-3的文件。其他文件都没反应的。

    • 需解密的文件名和输出的文件名是以参数形式输入程序的~如果你想批量解密其他的,可以写个bat

      • 我的打算是在读出的时候把新的音乐文件替换进去的。只是不会做,能帮忙写两句么。给个思路也行。。

        • 其实4的音乐资源就是一个TEA加密而已,当时研究时没注意到,TEA的密码在文章里面的源代码里面写清楚了的,直接用就行了

        • 转了之后读不出来。。
          那个原来的代码里长度除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;

  2. 转过之后还是读不了。 不知道什么原因,麻烦看一下吧。加密代码已经找到了。
    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加密了。。解出来是乱码。这个尚无头绪。
        另外可能游戏在其他地方有校对。我把游戏自己的音乐文件互相替换,游戏程序也不会读出。

  3. 不好意思我又看了一下,应该是前六个解密后的索引值有用,根据你这个文章第一个值是crc32校检值,校检的内容是文件名,不过我用网上的校检工具计算文件名得出的值和这个不一样。这个能再看一下吗?
    第二个值和第三个值都是65541,0
    第四个值是非音乐数据的大小
    第五个和第六个是压缩前后的大小。
    之后的数据我分析了数个文件,内容(前50个数据,后面太多没看)基本一致。

  4. 好的谢谢,这段时间多有麻烦,还请包涵~

  5. 看了下,dwSize基本都是0x400,音乐数据的话也一般比这大,所以52除以dwSize再加上6并取整其实就是6,这样可以简化后面的代码内容,也可以推出加密轮数应该是5。

  6. 总算解密成功了,感谢博主。
    贴一下我的发现:XXTEA解密是以4个字节为一块解密的。如果数据大小不是4的倍数的话(如P01-3.smp),需要将最后多余的1~3个垃圾字节去除(估计是作者故意这样做增加解密难度),然后再XXTEA解密。

 发表评论

(required)

(required)

你可以使用下列HTML标签和属性:<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>