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的程序员真是蛋疼……
转载请注明来源及链接:未来代码研究所 本文链接地址:http://blog.atelier39.org/rev_eng/638.html