上次把腳本文件解了出來,不過文件都被加密了,需要推算出加密算法才能編寫封包程序。實際上也不難,本質上就是個簡單的壓縮算法,這次我們來分析一下這一段解壓縮的彙編程序。
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