一款遊戲資源解包工具的開發始末

來自 <http://www.jybase.net/ruanjianpojie/20120311795_5.html>

時間:2012-03-11 21:59來源:未知 整理:寂涯網絡 點擊:9678

 

最近爲了支持下漢化遊戲,加入了一個漢化組做技術,主要的工作就是解包遊戲資源, 提取其中的資源文本交給翻譯人員來翻譯,因此我就將自己一次分析遊戲資源包格式到寫出解包程序的過程整理成文,與大家共享。

我們今天的目標是一款名爲《夜神任務:同心同靈》的遊戲,下載地址: http://www.verycd.com/topics/2854279/。遊戲並不大,這樣利於我們由淺入深地進行學習。

分析遊戲用到的工具有:

1.查殼工具PEiD,同時還要用它來識別程序中使用的壓縮算法;

2.調試器OD 1.10;

3.靜態反彙編工具IDA;

4.16進制編輯器WinHex

1.最後要用一種編程語言寫出自己的解包工具,爲了鍛鍊自己的彙編語言編程能力,開發語言我選擇了win32彙編,使用了彙編的IDE--Radasm和彙編的sdk——MASM32 v10。大家只要明白了資源文件的結構,可以選用自己所擅長的編程語言來寫解包器。

靜態分析

安裝完遊戲之後,在遊戲的根目錄我們能看到如圖1所示幾個文件和目錄,從大小和名字來看,可以很容易的確定資源文件是放在DATApc文件夾中。

在Datapc文件夾中有三個比較大的pak文件,這三個文件就是遊戲的資源包了,也就是我們要解包的對象。在很多情況下?六<文件文件是可以用winrar、好壓或者72解壓的, 這類PAK文件用WinHex之類的16進制打開後可以發現開頭的兩個字母都是“PK”,而且右 鍵菜單中會有用winrar之類的壓縮軟件解壓的選項。但是這個遊戲的資源文件不屬於這種類型(是的話就不會有這篇文章了)。我們用WinHex打開三個PAK文件,找一下三個文件 的共同特徵,首先會發現文件開頭部分格式比較相似,都是以“KCAP”四個字母開頭,這 應該是這類文件的一個標誌,後面是一個hex值爲00 00 00 01的雙字,用途未知,從偏移0x20開始到0x1f處都是0,只有0x8偏移處的一個雙字有所不同,都在偏移0x20處開始有格式比較相同的數據,如圖2。

在這裏,我們可以假定前面20h個字節的內容爲固定的文件頭大小,繼續往下看,會發 現三個PAK文件都是些以HEX值爲78 9C開頭的數據塊,這些數據塊大多數都是長度不定,並以不定數量個0結尾。到了接近末尾的時候,纔出現了一些不一樣的數據。這些數據中還包括一些可讀字符,看起來應該是文件名,而且每隔0\120字節就是下一個文件名,直到文件結束。文件名的後面是長串的0,應該是填充文件名緩衝區的。在從文件名開頭往後0x100

字節處開始出現其他信息,但是目前還不知道這些信息是做什麼的,最後的16字節也是全部是0。如圖3

根據上面的分析,可以大體的推斷這種PAK文件由三部分組成:第一部分從偏移0到 0x1F處是文件頭;第二部分是包含了大量以HEX值爲78 9C開頭的數據塊的部分則是保存資源壓縮後的數據的部分;最後的部分因該是每一個文件的信息,應該根據這些信息將壓縮後 的數據解壓。當然這些都是猜測,只有實踐後纔會知道猜測的是否正確。

通過這些分析,我們可以先簡單的定義出表示這些部分的結構體,暫時作用未知的部分 就叫做UnKnown:

文件頭部分

HEAD STRUCT  

dwFlag DWORD ?

dwUnKnownl DWORD ?

dwUnKnown2 DWORD ?

dwZero DWORD 5 dup(?)

HEAD ENDS  

文件信息部分

FILEI0F0 STRUCT  

szFileName BYTE  100h dup(?)

dwUnKnownl DWORD ? 

dwUnKnown2 DWORD ? 

dwUnKnown3 DWORD ? 

dwUnKnown4 DWORD ? 

dwZero DWORD 4 dup(?)

FILEI0F0 ENDS  

好,初步靜態分析過程已經完成,但是還有很多地方沒搞明白,這就需要我們動態跟蹤下游戲程序了。

動態跟蹤

跟蹤前,先用PEID查一下殼,顯示microsoft visual c++,應該沒加殼,前面分析道有很多數據塊,並且開頭都是78 9c,說明文件是被壓縮了,我們可以用peid的插件krypto ANALyzer插件看一下程序中都使用了哪些加密或者壓縮算法,結果找到了這些算法:

ADLER32 ::00233292 ::00633E92 

ADLER32 ::00233446 ::00634046 

CRC32 :: 0028CD58 :: 0068E158 

CRC32b : :0027DCE8 :0067F0E8 

ZLIB deflate [word]  ::0028CC2C : :0068E02C

ADLER32和CRC32應該是用來校驗文件的,我們先不管。ZLIB這個是遊戲資源包壓縮很 常用的壓縮算法,而且壓縮後的文件開頭的標誌就是78 90,所以可以確定這個遊戲是先用zlib算法將文件壓縮,然後放到一個大文件(PAK)中的。

接下來就該od+ida分析文件結構中還沒有搞明白的地方。od載入遊戲的主程序,先 Ctrl+N查看下游戲都調用了哪些跟文件有關的API,經過一番查找發現調用了 CreateDirectoryA、CreateFileA、ReadFile、SetFilePointer、WriteFile 幾個api。先都下上斷點, 然後f9,程序斷在createFile上,從堆桟上看參數如下:

0012F5A4 0061A407 /CALL to CreateFileA from NyxQuest. 0061A405

0012F5A8 0012F7C8 |FileName = 〃DataPC/DataCommon.pak〃

0012F5AC 80000000 |Access = GENERIC_READ

0012F5B0 00000003 |ShareMode = FILE_SHARE_READ|FILE_SHARE_WRITE

0012F5B4 0012F5D0 |pSecurity = 0012F5D0

0012F5B8 00000003 |Mode = OPEN_EXISTING

0012F5BC 00000080 |Attributes = NORMAL

0012F5C0 00000000 \hTemplateFile = NULL

Ctrl+F9返回,到ida中看一下這個地方做了些啥,結果發現這個地方很複雜

查看下函數名,發現這裏是c的庫函數,_tsopen_nolock,既然如此就不在這裏浪 費力氣了,繼續ctrl+F9 N次,並在ida中觀察函數的名字,發現程序其實調用的是fopen, 並且還發現下面有fseek、ftell、rewind,但是並沒有發現fread,所以繼續F9,然後會 在SetFilePointer上中斷三次,分別是fseek、ftell、rewind調用的,繼續F9,就會中斷到ReadFile上,這個就應該是fread調用的了,然後Ctrl+F9返回多次,找到是在004317M 處調用的fread。

  分別在fopen、fseek、ftell、rewind、fread上下斷點,並把在api上的斷點去掉, 重新載入遊戲,F9運行。中斷到了 fopen上,從參數可以看出是以“rb”的方式打開了 “DataPC/DataCommon.pak”。ctrl+F9,發現返回值是006C6C60h,記下這個值,然後繼續 F9,中斷到了 fseek,從堆棧看參數是

0012F700 0043170D RETURN to NyxQuest. 0043170D from NyxQuest. 005F5370

0012F704 006C6C60 NyxQuest. 006C6C60

0012F708 00000000

0012F70C 00000002

還原成 C 就是 fseek(0x006C6C60,0, SEEK_END);

可以看出是將DataPC/DataCommon.pak的指針移動到了文件最後。繼續F9中斷到了 ftell, 參數是:

0012F708 0043171F RETURN to NyxQuest. 0043171F from NyxQuest. 005F5278 0012F70C 006C6C60 NyxQuest. 006C6C60

還原成 C 就是 fteN(0x006C6C60);

這個函數是返回當前文件指針的位置的,ctrl+F9發現返回值是013DD0C0h,用Winhex打開datapc/DataCommon.pak,發現指向的是文件末尾,這裏應該就是確定下前面的fseek,是不是把指針設置到了文件的末尾,沒有什麼用,繼續F9,中斷到了rewind,參數是:

0012F708 0043173A RETURN to NyxQuest. 0043173A from NyxQuest. 005F5015 0012F70C 006C6C60 NyxQuest. 006C6C60

這個函數又將文件的指針設置回了文件開頭(有意思麼? ?)。 繼續F9,在Fread上斷下來了,參數如下:

0012F8F4 004317AF RETURN to NyxQuest. 004317AF from NyxQuest. 005F4FBD 

0012F8F8 0012FAA4 ;將讀取的東西放到這個緩衝區

0012F8FC 00000001 ;sizeSl,以字節爲單位讀取的

0012F900 00000020 ;讀取的大小爲20h字節

0012F904 006C6C60 NyxQuest. 006C6C60 ;讀取的是前面fopen打開

的 DataCommon. oak  

Ctrl+F9,然後在00的內存窗口看下0012FAA4位置到底讀了些什麼,結果發現就是我們定義爲HEAD結構體的部分。繼續F9,又中斷到了fseek,參數如下:

0012F8F8 00431828 RETURN to NyxQuest. 00431828 from NyxQuest. 005F5370 0012F8FC 006C6C60 NyxQuest. 006C6C60 0012F900 013D4F60 0012F904 00000000

還原成 C 就是 fseek(0x006C6C60, 0x013D4F60, SEEK_SET);

在winhex裏面看看這個0x013d4f60位置是什麼,結果發現是第一個文件名的開頭。那麼這個值是如何來的呢?我們在IDA中逆着向上看,發現這個值有可能從兩個地方來,用 OD在0x00430621這個跳轉處下斷點,重新載入,看一下到底是執行了哪個分支,結果發 5見跳轉並沒有執行,所以這個值是00430627 . E8 C40A0000 CALL 004310F0這條指令調用的函數的返回值,然後在下面經過多次傳遞後又乘以12011所得到的。這個函數是有 一個參數,從od中看這個參數是一個指針,指向讀取到內存中的pak文件的偏移018處的一個DWORD,也就是head中的第三個dword,而這個函數的作用就是將這個值每個字節從後往前排,並把得到的值返回(感覺好亂啊)。舉個例子,比如DataCommon.pak的HEAD中的第三個雙字在WinHex中看是00 00 00 73,經過轉換之後就是73 00 00 00 (這個轉換是 在01 00430627處調用一個函數轉換的),在程序中讀取後就是73h乘以12011就是801611 而DataExt.pak中的這個位置是00 00 03 D7,轉換後就是D7 03 00 00,在程序中讀取後 就是3D7h,乘以120h就是451E0ho

繼續F9,中斷到了fread上,參數如下:

0012F8F4 004317AF RETURN to NyxQuest. 004317AF from NyxQuest. 005F4FBD  

0012F8F8 013055D8 ASCII "

0012F8FC 00000001 

0012F900 00008160 

0012F904 006C6C60 NyxQuest. 006C6C60

可以看出來這裏把所有的文件信息部分都讀取到了內存。

分析到了這裏,我們已經可以不用在分析了,已經完全可以憑猜測加測試搞定。根據分析結 果HEDA結構體可以沖洗定義成這樣:

HEAD STRUCT 

dwFlag DWORD ?

dwUnKnownl DWORD ?

dwFilelnfoSize DWORD ?

dwZero DWORD 5 dup(?)

HEAD ENDS 

再看DataPC.PAK文件的文件信息部分的第一個FILEIOFO結構體,最開始的的100h字節 是文件名,往後的有數據的雙字則是重點,它們應該包含文件名所指的這個文件在這個pak包中的大小,偏移等信息,經過試驗發現第二個雙字經過反轉(同上面方法一樣)之後(不用乘以120h)是這個文件壓縮後的數據的大小,因爲zlib解壓的時候要求輸入解壓完的文 件大小,這個大小一般都是在壓縮之前保存好的,而正好第一個雙字的數值大小經過反轉(不乘以120h)後大小比較合適,就假設他是文件的大小。第三個雙字反轉後則是這個文件pak中的偏移,但是這個偏移沒有什麼太大用處,因爲每個文件之間都是沒有空隙的,讀取 完了一個直接讀取下一個就可以了。由於剩下的部分都是一樣的,可以不用在乎。這樣我麼 可以把FILEIORD重新修改成這樣

FILEIOFO STRUCT  

szFileName BYTE 100h dup(?)

dwExpFileSize DWORD ?

dwCompFileSize DWORD ? 

dwOffset DWORD ?

dwUnKnown DWORD 5 dup(?)

FILEIOFO ENDS  

文件數據結構的分析就到這裏了,對於完整了解這個pak,還需要很多工作要做,不過對於編寫一個解包器,以上的分析就足夠了。

編程實現

根據分析所得的這些信息,寫一個解包程序就不難了,大體流程如下:

1.讀取文件的HEAD,檢查HEAD.dwFlag是否爲5041434Bh,不是則說明不是我們分析的格式的pak文件,不在往下執行。

2.讀取HEAD.dwFilelnfoSize,然後按照前面說的方法計算出文件信息部分的大小。 相關的代碼在WM_COMMAND消息處理邏輯中

invoke SendDlgItemMessage, hWin, LST_INF0, LB_RESETCONTENT, 0, 0 invoke RtlZeroMemory, addr @stOF, sizeof @stOF mov @stOF. lStructSize, sizeof @stOF push hWin pop @stOF. hwndOwner mov @stOF. lpstrFilter, offset szFilter

mov @stOF. lpstrFile, offset szFileName 

mov @stOF. nMaxFile, MAX_PATH 

mov @stOF. Flags, OFN_FILEMUSTEXIST or OFN_PATHMUSTEXIST or

0FN_HIDEREAD0NLY 

invoke GetOpenFileName, addr @stOF 

.if eax 

invoke 

SendDlgItemMessage, hWin, EDT_PAKPATH, WM_SETTEXT, 0, offset szFileName 

invoke CreateFile, offset szFileName, GENERIC_READ, \ 

FILE_SHARE_DELETE, NULL, OPEN_EXISTING, NULL, NULL 

.if eax==-l 

invoke MessageBox, hWin, offset 0penError, NULL, MB_0K 

invoke ExitProcess, 0 

.endif 

mov hFile, eax 

invoke GetFileSize, eax, NULL 

mov dwFileSize, eax 

invoke CreateFileMapping, hFile, NULL, PAGE_READONLY, 0, 0, NULL 

.if !eax 

invoke MessageBox, hWin, offset 0penError, NULL, MB_0K 

invoke CloseHandle, hFile 

invoke ExitProcess, 0 

.endif 

mov hFileMap, eax 

invoke MapViewOfFile, eax, FILE_MAP_READ, 0,0, 0 

.if !eax 

invoke MessageBox, hWin, offset OpenError, NULL, MB_OK

invoke CloseHandle, hFileMap 

invoke CloseHandle, hFile 

invoke ExitProcess, 0 

.endif 

mov dwBaseAddress, eax 

assume eax:ptr HEAD 

.if [eax]. dwF1ag!=PACK 

invoke MessageBox, hWin, CTXT (〃不支持的 PAK 格式' 0,0,MB_OK

invoke EndDialog, hWin, 0 

.endif 

mov ebx, [eax]. dwFilelnfoSize 

mov byte ptr dwFilelnfoSize[3], bl

mov byte ptr dwFileInfoSize[2], bh 

shr ebx, 16 

mov byte ptr dwFileInfoSize[l], bl 

mov byte ptr dwFilelnfoSize, bh ;pop dwFilelnfoSize assume eax:nothing

mov ebx, dwBaseAddress

add ebx, dwFileSize

mov @dwMaxOffset, ebx

imul eax, dwFilelnfoSize, 120h

sub ebx, eax

mov @dwFilePoint, ebx

assume ebx:ptr FILEINFO .while ebx<@dwMaxOffset invoke

SendDlgItemMessage, hWin, LST_INF0, LB_ADDSTRING, 0, addr [ebx]. szFileName add ebx, sizeof FILEINFO .endw assume ebx:nothing

3.用一個大的循環讀取文件的信息,根據信息用zlib的解壓函數解壓數據,並將數據寫到文件中。 y^-

ExportProc proc uses ebx esi edi, lParam

LOCAL @dwMaxOffset, @Buffer[110]:BYTE, @szFileName[MAX_PATH]:BYTE LOCAL @dwNumberOfBytesWritten, @szPath[MAX_PATH]:BYTE LOCAL @szCreatePath[MAX_PATH]:BYTE LOCAL @dwErro, @lpDest, @dwSuccess, @dwFilePoint mov @dwErro, 0 mov @dwSuccess, 0

invoke _BrowseFolder, lParam, addr @szPath .if eax

push dwBaseAddress pop @dwFilePoint add @dwFilePoint, 20h

mov ebx, dwBaseAddress add ebx, dwFileSize

mov @dwMaxOffset, ebx ;設置循環結束的位置 imul eax, dwFilelnfoSize, 120h sub ebx, eax

assume ebx:ptr FILEINFO .while ebx<@dwMaxOffset

invoke wsprintf, addr @szFileName, CTXT(〃%s\%s〃),addr @szPath, addr [ebx]. szFileName ;將文件名追加到路徑名後面

invoke Replace, addr @szFileName ;將路徑中的

/轉換爲\

invoke CreateFile, addr

@szFileName, GENERIC_WRITE, FILE_SHARE_READ, NULL, \

CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL ;創建出出文件 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;第一次創建文件失敗可能是因爲文件夾不存在, 則再創建所需要的文件夾再創建一遍該文件* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * i f i

.if eax==-l

invoke InitOutputPath, addr @szFileName, addr

@szCreatePath

invoke SHCreateDirectoryEx, lParam, addr

@szCreatePath, NULL

invoke CreateFile, addr @szFileName, GENERIC_ffRITE, FILE_SHARE_READ, \

NULL, CREATE_ALffAYS, FILE_ATTRIBUTE_NORMAL, NULL

.if eax==-l

inc @dwErro invoke

SetDlgItemInt, lParam, STC_ERROR, @dwErro, FALSE

jmp CONTINUE .endif .endif

mov hCreateFile, eax ;保存創建的文件的句柄 ;;;;;;;;計算導出後文件(實際文件)的大小 * * • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • •

mov eax, [ebx]. dwRealSize

mov byte ptr dwExpFileSize[3], al

mov byte ptr dwExpFileSize[2], ah

shr eax, 16

mov byte ptr dwExpFileSize[l], al mov byte ptr dwExpFileSize, ah

invoke GlobalAlloc, GPTR, dwExpFileSize ;根據要導出 的實際文件大小申請一塊內存,提供21化解壓函數使用

.if eax

mov @lpDest, eax

;;;;;;;;;;;;;;;;; 計算未解壓縮前的數據的大小 * * • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • * * * mov eax, [ebx]. dwFileSize

mov byte ptr dwSrcFileSize[3], al mov byte ptr dwSrcFileSize[2], ah shr eax, 16 mov byte ptr dwSrcFileSize[l], al

mov byte ptr dwSrcFileSize, ah 

;;;;;;;;; 調 用 zlib 的 解 壓 函 數 解 壓 >ur_. 數

^^ • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • * * * * 

invoke uncompress, @lpDest, offset 

dwExpFileSize, @dwFilePoint, dwSrcFileSize 

• • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • * * * * 

invoke 

WriteFile, hCreateFile, @lpDest, dwExpFileSize, \ 

addr @dwNumberOfBytesWrit ten, NULL 

.if eax 

inc @dwSuccess 

invoke 

SetDlgItemInt, lParam, STC_SUCCESS, @dwSuccess, FALSE 

jmp CONTINUE 

.else 

inc @dwErro 

invoke 

SetDlgItemInt, lParam, STC_ERROR, @dwSuccess, FALSE 

.endif 

invoke CloseHandle, hCreateFile 

invoke GlobalFree, @lpDest 

.else 

inc @dwErro 

invoke 

SetDlgItemInt, lParam, STC_ERROR, @dwErro, FALSE 

.endif 

CONTINUE: mov eax, dwSrcFileSize 

add @dwFilePoint, eax 

add ebx,sizeof FILEINFO ;指向下一個FILEINFO 

.endw 

assume ebx:nothing 

invoke MessageBox, lParam, CTXT (〃 導出完成! ! 〃),CTXT (〃 完成! 

"),MB_OK 

.endif 

ret 

ExportProc endp 

限於篇幅就不將其它代碼貼出來了。 希望本文能爲想自己修改漢化遊戲的朋友帶來一點幫助,這方面的技術我也是初學,有所疏漏在所難免,希望大家不吝賜教。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章