上次把脚本文件解了出来,不过文件都被加密了,需要推算出加密算法才能编写封包程序。实际上也不难,本质上就是个简单的压缩算法,这次我们来分析一下这一段解压缩的汇编程序。

0042C327   .  398424 2C1000>CMP DWORD PTR SS:[ESP+102C],EAX
0042C32E   .  BF EE0F0000   MOV EDI,0FEE
0042C333   .  0F84 CB000000 JE AI5WIN.0042C404
0042C339   .  EB 09         JMP SHORT AI5WIN.0042C344

[ESP+102C]存放解密前的数据长度,eax初值是0。edi是临时缓冲区的指针,初值ffe。下句跳转基本不会执行,所以直接去42C344。

0042C344   >  D1E8          SHR EAX,1
0042C346   .  A9 00010000   TEST EAX,100
0042C34B   .  894424 10     MOV DWORD PTR SS:[ESP+10],EAX
0042C34F   .  75 12         JNZ SHORT AI5WIN.0042C363

检查eax的第8位是否为1,是的话跳转到42C363。eax的初值为0,所以第一次运行到这里的时候会不发生跳转,我们继续往下看。

0042C351   .  8A041E        MOV AL,BYTE PTR DS:[ESI+EBX]
0042C354   .  0FB6C0        MOVZX EAX,AL
0042C357   .  83C6 01       ADD ESI,1
0042C35A   .  0D 00FF0000   OR EAX,0FF00
0042C35F   .  894424 10     MOV DWORD PTR SS:[ESP+10],EAX
0042C363   >  F64424 10 01  TEST BYTE PTR SS:[ESP+10],1
0042C368   .  74 22         JE SHORT AI5WIN.0042C38C

从[ebx+esi]中取一个字节给eax后递增esi,于是我们猜想ebx是密文基址,esi是密文偏移。然后将eax的8-15位都变成1后赋给[esp+10]暂时保存了起来。接着检查eax的第0位是否为1,这一行也是上一段代码跳转到的地址,看来是个比较重要的判断点。如果是0的话往下跳转,第一次运行的时候不是0,所以继续往下看。

0042C36A   .  8A041E        MOV AL,BYTE PTR DS:[ESI+EBX]
0042C36D   .  8B5424 14     MOV EDX,DWORD PTR SS:[ESP+14]
0042C371   .  0FB6C0        MOVZX EAX,AL
0042C374   .  88443C 1C     MOV BYTE PTR SS:[ESP+EDI+1C],AL
0042C378   .  88042A        MOV BYTE PTR DS:[EDX+EBP],AL
0042C37B   .  83C7 01       ADD EDI,1
0042C37E   .  83C6 01       ADD ESI,1
0042C381   .  83C5 01       ADD EBP,1
0042C384   .  81E7 FF0F0000 AND EDI,0FFF
0042C38A   .  EB 69         JMP SHORT AI5WIN.0042C3F5

又从[ebx+esi]中取了一个字节给eax,证明了上一段的判断(ebx是密文基址,esi是密文偏移),然后给edx赋了值,看起来像是另一个缓冲区的地址。然后分别把eax赋给了[esp+edi+1c]和[edx+ebp],第一个在栈上,第二个在堆上,在栈上那个利用了上文说到的那个缓冲区,奇怪的是还加了个1c的偏移,先不管它;在堆上的那个应该就是明文地址了,再次猜测edx是密文基址,ebp是密文偏移。最后递增了一堆变量,于是可以确定ebp是偏移。还有个add edi,1比较怪,看来是限制了栈缓冲区的大小不超过0x1000。最后是一个强制跳转:

0042C3F5   >  3BB424 2C1000>CMP ESI,DWORD PTR SS:[ESP+102C]
0042C3FC   .^ 0F85 3EFFFFFF JNZ AI5WIN.0042C340

这里很简单,是循环的控制语句,看来esi是密文偏移,和密文长度进行比较,于是我们跳回去:

0042C340   >  8B4424 10     MOV EAX,DWORD PTR SS:[ESP+10]
0042C344   >  D1E8          SHR EAX,1
0042C346   .  A9 00010000   TEST EAX,100
……
0042C34F   .  75 12         JNZ SHORT AI5WIN.0042C363

把之前暂存在[esp+10]的变量又赋给了eax,然后继续进行了上文分析过的判断。看来[esp+10]的变量应该起到一个控制符的作用。继续往下分析,这时eax的8-15位已经是ff了,所以跳转发生,跟进:

0042C363   >  F64424 10 01  TEST BYTE PTR SS:[ESP+10],1
0042C368   .  74 22         JE SHORT AI5WIN.0042C38C

又是这行判断。结合or eax,0ff00和test eax,100两行语句,到这里已经可以基本判断出这个小循环会进行8次,正好是一个字节的大小,加上上文对[esp+10]的变量作用的猜测,于是我们可以进一步猜测[esp+10]作为一个控制字节,每一位是1还是0会改变下面的代码流程。之前这里没有跳,我们单步运行几次,看看跳转之后的情况:

0042C38C   >  8A141E        MOV DL,BYTE PTR DS:[ESI+EBX]
0042C38F   .  8A441E 01     MOV AL,BYTE PTR DS:[ESI+EBX+1]
0042C393   .  83C6 01       ADD ESI,1
0042C396   .  0FB6C8        MOVZX ECX,AL
0042C399   .  8BC1          MOV EAX,ECX
0042C39B   .  25 F0000000   AND EAX,0F0
0042C3A0   .  0FB6D2        MOVZX EDX,DL
0042C3A3   .  C1E0 04       SHL EAX,4
0042C3A6   .  0BC2          OR EAX,EDX
0042C3A8   .  83E1 0F       AND ECX,0F
0042C3AB   .  83C6 01       ADD ESI,1
0042C3AE   .  83C1 02       ADD ECX,2
0042C3B1   .  894C24 18     MOV DWORD PTR SS:[ESP+18],ECX
0042C3B5   .  BA 00000000   MOV EDX,0

遇见了新的代码,看起来比较复杂,所以分段分析。这一段首先连续取出了两个字节,进行一番运算后放到了eax,运算规则是第二个字节的高4位左移4位后加上第一个字节,组成一个新的word。接着将第二个字节的低4位加上2之后存放到[esp+18]作为下面小循环的最大循环次数。往下看小循环的代码:

0042C3BA   .  78 39         JS SHORT AI5WIN.0042C3F5
0042C3BC   .  8D6424 00     LEA ESP,DWORD PTR SS:[ESP]
0042C3C0   >  8B5C24 14     MOV EBX,DWORD PTR SS:[ESP+14]
0042C3C4   .  8D0C02        LEA ECX,DWORD PTR DS:[EDX+EAX]
0042C3C7   .  81E1 FF0F0000 AND ECX,0FFF
0042C3CD   .  0FB64C0C 1C   MOVZX ECX,BYTE PTR SS:[ESP+ECX+1C]
0042C3D2   .  884C3C 1C     MOV BYTE PTR SS:[ESP+EDI+1C],CL
0042C3D6   .  83C7 01       ADD EDI,1
0042C3D9   .  880C2B        MOV BYTE PTR DS:[EBX+EBP],CL
0042C3DC   .  83C2 01       ADD EDX,1
0042C3DF   .  83C5 01       ADD EBP,1
0042C3E2   .  81E7 FF0F0000 AND EDI,0FFF
0042C3E8   .  3B5424 18     CMP EDX,DWORD PTR SS:[ESP+18]
0042C3EC   .^ 7E D2         JLE SHORT AI5WIN.0042C3C0
0042C3EE   .  8B9C24 241000>MOV EBX,DWORD PTR SS:[ESP+1024]
0042C3F5   >  3BB424 2C1000>CMP ESI,DWORD PTR SS:[ESP+102C]
0042C3FC   .^ 0F85 3EFFFFFF JNZ AI5WIN.0042C340

第一行的js基本没用,第二行的lea也意义不明,往下看。首先改变了ebx的值,变成了明文基址,因为edx被挪用作为循环计数器和栈缓冲区偏移值了。计算出eax(栈缓冲区偏移1)+edx(栈缓冲区偏移2)赋给ecx,然后从[esp+ecx+1c]中取出一个字节赋给ecx,然后继续赋给[esp+edi+1c],看来栈缓冲区是循环利用的,里面保存着已经解出来的最大0x1000字节的明文数据。接着累加一堆变量,并把明文字节cl赋给明文缓冲区[ebx+ebp]。最后还原ebx,并检查大循环的计数器。

到此所有的解密代码分析完毕,总结一下就是:

1、读取下一个字节作为控制字节,控制接下来的8次操作。

2、从二进制低位向高位检查控制字节的值,如果为1则直接取出下个字节,进行步骤4;如果为0则取出下2个字节,进行步骤3。

3、第二个字节的高4位左移4位后加上第一个字节,作为从栈缓冲区中读数据的偏移值;接着将第二个字节的低4位加上2之后存放到[esp+18]作为读取字节的个数。

4、如果操作次数未到8次则进行步骤2,否则进行步骤5。

5、如果读取的总字节数小于密文长度则进行步骤1,否则解密结束。

有了解密算法之后很容易就能逆推出加密算法了,为了加快封包速度,可以不进行压缩,直接将所有的控制字符变成ff,然后8个字节一组写进去就行。原始的mes.arc大小是2066KB,不压缩封包后的大小是6694KB,还可以接受,关键是这样可以避免很多潜在的由于压缩算法导致的问题。代码如下:

DWORD Encode(void* srt, DWORD len, void* dst)
{
    DWORD offset_by_srt = 0;
    BYTE* pCon = (BYTE*)dst;
    BYTE* pOutput = pCon + 1;
    BYTE bitmask = 0x00000001;

    while (offset_by_srt < len)
    {
        BYTE bCon = 0x00000000;
        for (unsigned char i = 0; i < 8 && offset_by_srt < len; i++)
        {
            *pOutput++ = *((char*)srt + offset_by_srt);
            bCon = bCon | (bitmask << i);
            offset_by_srt++;
        }
        *pCon = bCon;
        pCon = pOutput;
        pOutput = pCon + 1;
    }

    return pCon - dst;
}

到这里,游戏脚本文件的解包和封包工作均进行完毕。有时间的话,我会再讨论一下CG文件的解包和封包,因为CG文件又多进行了一次加密,elf的程序员真是蛋疼……

» 转载请注明来源及链接:未来代码研究所

Related Posts:

Leave a Reply

World Line
Time Machine
Friendly Links
Online Tools