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