一款游戏资源解包工具的开发始末

来自 <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 

限于篇幅就不将其它代码贴出来了。 希望本文能为想自己修改汉化游戏的朋友带来一点帮助,这方面的技术我也是初学,有所疏漏在所难免,希望大家不吝赐教。

  • 10
    点赞
  • 35
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值