前一段时间写了两篇关于游戏封包格式反汇编的文章(虽然有一篇并没有分析出结果OTL),而且分析完了也没写出能提取资源的程序。于是这次我就由解包到翻译到封包到修改程序完整地写一篇教程。这次的游戏选择的是DC2PC,大小是7.62GB(- -|||)
一、资源分析
一般对于汉化游戏而言,脚本是汉化者最重要的部分(大多数时候还要提取出部分图片进行修改,部分有过场动画的游戏还要提取出动画嵌入字幕再封回去,至于声音的话……难道有人打算将日语的配音换成普通话么)。一般脚本文件要么是多个大小不超过1MB的文件放在特定的文件夹,要么就是单个大小不超过20M的单个文件,还有种情况就是多种资源封装在一起组成一个很大的资源文件。对于本例的游戏而言,脚本文件放在\DC2PC\Advdata\MES\目录下面,有很多个大小在20KB以内的文件。
至于图片等资源文件,为求简洁,这里就不分析了。
进入\DC2PC\Advdata\MES\下面,用WinHex随便打开一个文件,看到如下图所示:
可以看出脚本文件里面能看到明文的要载入的资源位置,而后面有一段乱码,在日文编码中,一般日文的高位是0x80,可见,这段乱码是经过加密的。由此猜测,这段就是游戏中的对白。至于具体的加密方式……好吧,CIRCUS的游戏加密的方式一般都是每字节减0x20,这个也不例外。但本着学术研究的精神,为严谨起见,下面我还是会用OD进行调试的。
在这个文件夹发现有个start.mes的文件,打开看看发现没有像前面那样的乱码。如无意外,这个应该是控制游戏启动的脚本。既然这样,我们就应该在开始游戏的时候来进行分析了。
二、提取文本
到了这一步,就轮到od登场了,首先载入游戏,然后F9运行,然后在游戏界面点下开始游戏的用时在od的命令行输入bp CreateFileA,接着od就将打开文件的操作断了下来。一直按F9到打开的是mes后缀名的文件为止。而目前要打开的是fst_1216_a1_cmn.mes这个文件。接着对ReadFile下断,断下后单步运行到函数返回,回到下面的地方:
的地方:
接下来的一段代码是判断esi中的值进行不同的操作,而esi中的值是由[eax+AAD3B8]这个决定的,可以看出,程序是依靠脚本文件中每个字节的值来确定接下来要完成的操作。而当esi的值大于等于0x4A并且少于0x4E的时候,就会将接下来的乱码字符串复制到堆栈的临时变量区,然后进行解密。解密的代码如下:
#include<windows.h> #include<iostream> #include<string.h> using namespace std; int main() { HANDLE hfile=CreateFile ("c:\\fst_1216_a1_cmn.mes",GENERIC_READ,FILE_SHARE_READ,NULL,OPEN_EXISTING,FILE_ATTRIBUTE_ARCHIVE,NULL); if(hfile==INVALID_HANDLE_VALUE) { cout<<"Can not open file!"<<endl; return 0; } DWORD size=GetFileSize(hfile,NULL); DWORD len; char *buff=new char [size]; if(!ReadFile(hfile,(LPVOID)buff,size,&len,NULL)) { cout<<"Can not read file!"<<endl; CloseHandle(hfile); return 0; } CloseHandle(hfile); char temp[128]={0}; int j=0; hfile=CreateFile("d:\\a1_cmn.txt",GENERIC_WRITE,FILE_SHARE_READ,NULL,OPEN_ALWAYS,FILE_ATTRIBUTE_ARCHIVE,NULL); if(hfile==INVALID_HANDLE_VALUE) { cout<<"Can not create file!"<<endl; return 0; } for(int i=0;i<size;i++) { j=0; if(buff[i]==0x4c) { while(buff[i+j])buff[i+j++]+=0x20; memcpy(temp,&buff[i],j); i+=j; temp[j]='\r'; temp[++j]='\n'; WriteFile(hfile,(LPVOID)temp,j,&len,NULL); } } delete [] buff; delete [] temp; CloseHandle(hfile); return 0; }
提取出来的文本如下:
好吧,请无视掉翻译的质量问题,毕竟我们的重点是在程序的修改而不是翻译的问题,或者各位可以安慰自己说这几句其实是机器翻译的(当然这会极大地打击偶的自信心OTL)。
三、封包
幸好这个解密算法够简单,所以封包器就很好写(像我上两篇文章的那么复杂的加密算法,真不知道资源拆出来后要怎样封回去了,不过貌似那两个游戏不用封包也可以运行游戏……),封包器代码代码如下:
#include<windows.h> #include<iostream> #include<string.h> using namespace std; int main() { HANDLE hfile=CreateFile("c:\\fst_1216_a1_cmn.mes",GENERIC_READ,FILE_SHARE_READ,NULL,OPEN_EXISTING,FILE_ATTRIBUTE_ARCHIVE,NULL); if(hfile==INVALID_HANDLE_VALUE) { cout<<"Can not open file!"<<endl; return 0; } DWORD size=GetFileSize(hfile,NULL); DWORD len; BYTE *buff=new BYTE [size]; if(!ReadFile(hfile,(LPVOID)buff,size,&len,NULL)) { cout<<"Can not read file!"<<endl; CloseHandle(hfile); return 0; } CloseHandle(hfile); int j=0,nu=0; hfile=CreateFile("d:\\a1_cmn.txt",GENERIC_READ,FILE_SHARE_READ,NULL,OPEN_ALWAYS,FILE_ATTRIBUTE_ARCHIVE,NULL); if(hfile==INVALID_HANDLE_VALUE) { cout<<"Can not create file!"<<endl; return 0; } DWORD textsize=GetFileSize(hfile,NULL); BYTE *temp=new BYTE [textsize]; if(!ReadFile(hfile,(LPVOID)temp,textsize,&len,NULL)) { cout<<"Can not read file!"<<endl; CloseHandle(hfile); return 0; } CloseHandle(hfile); hfile=CreateFile("d:\\fst_1216_a1_cmn.mes",GENERIC_WRITE|GENERIC_READ,FILE_SHARE_READ,NULL,OPEN_ALWAYS,FILE_ATTRIBUTE_ARCHIVE,NULL); if(hfile==INVALID_HANDLE_VALUE) { cout<<"Can not create new script file!"<<endl; return 0; } for(int i=0;i<size;i++) { if(buff[i]>=0x4a&&buff[i]<0x4e) { while(buff[i++]){}; while(temp[j]!=0x0d) { temp[j]-=0x20; WriteFile(hfile,(LPVOID)&temp[j++],1,&len,NULL); } WriteFile(hfile,(LPVOID)&nu,1,&len,NULL); WriteFile(hfile,(LPVOID)&buff[i],1,&len,NULL); j++; } else { WriteFile(hfile,(LPVOID)&buff[i],1,&len,NULL); } } delete [] temp; delete [] buff; SetFilePointer(hfile,0,NULL,FILE_BEGIN); size=GetFileSize(hfile,NULL); buff=new BYTE [size]; if(!ReadFile(hfile,(LPVOID)buff,size,&len,NULL)) { cout<<"Can not read file!"<<endl; CloseHandle(hfile); return 0; } DWORD *offsetarray=new DWORD [*(DWORD*)&buff[0]]; DWORD sizeofheader=(*(DWORD*)&buff[0])*4+4; for(j=0,i=0;i<size;i++) { if(buff[i]>=0x4a&&buff[i]<0x4e) { offsetarray[j]=i-sizeofheader; while(buff[i++]); j++; } } SetFilePointer(hfile,4,NULL,FILE_BEGIN); WriteFile(hfile,(LPVOID)offsetarray,sizeofheader-4,&len,NULL); delete [] offsetarray; CloseHandle(hfile); return 0; }
四、程序修改
当我满心欢喜将新的脚本文件替换掉原来的文件之后,打开游戏,却出现了下面的画面:
干!
仔细想一下,由于原来的文本是用日文编码的,现在换成了GBK编码,不乱码就有鬼了,于是我们就要修正程序。
首先,用OD载入程序,右键选“查找当前模块中的所有名称”,看到有CreateFontA这个函数,查一下MSDN,发现这个函数的第
九个参数fdwCharSet正是决定字符编码的参数,在程序里面看一下,有几个连续的CreateFontA的调用:
还是没有显示出中文来!
现在该怎么办?这时应该看看程序利用哪些函数来进行输出的。用右键选“查看”-》“当前模块的所有参考”可以看到有个叫GetGlyphOutlineA的函数。在MSDN中有明确说明这个函数可以将字符转换成点阵来输出的。往上看,可以看到一段奇怪的代码:
(关于编码边界检查的更详细内容可以参考这个链接http://bbs.sumisora.com/read.php?tid=211852)
就这样,这个游戏的汉化算是大功告成- -
PS.为什么只能算是大功告成,因为我写的封包器似乎有点问题,程序的修改方面也出现一些问题(到后面几句翻译的对白会出现错别字- -|||)最重要的一点是,整个游戏的文本有这么多,我就翻了开头的几句,这怎么能算是汉化完成啊
PS的PS.经某大牛指点,mes文件第一个双字是脚本里面文本的个数,接下来的一个数组是文本的偏移位置。虽然据我观察程序并不对这段进行读取……现在将封包程序改一下。另外在调用GetGlyphOutlineA前有两次边界检查,如无意外,第一次应该也要改的orz