- 浏览: 28212 次
- 性别:
- 来自: 广州
最新评论
-
谁解怨妇心:
night_stalker 写道您谦虚了 …… 代码写得相当的 ...
在BMP位图中隐藏数据 -
RednaxelaFX:
话说我想起了Brainloller和Braincopter
在BMP位图中隐藏数据 -
RednaxelaFX:
32位BMP在某些软件(例如Windows自带的图片浏览器)里 ...
在BMP位图中隐藏数据 -
night_stalker:
您谦虚了 …… 代码写得相当的好看,命名非常的标准正统 口牙
在BMP位图中隐藏数据
前一段时间写了篇关于游戏封包格式反汇编的文章,而且分析完了也没写出能提取资源的程序。于是这次我就由解包到翻译到封包到修改程序完整地写一篇教程。这次的游戏选择的是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下断,断下后单步运行到函数返回,回到下面的地方:
0041A0BF |> \6A 04 push 4 ; 读取四个字节
0041A0C1 |. 68 4CA0AF00 push 00AFA04C ; 缓冲区
0041A0C6 |. 56 push esi
0041A0C7 |. E8 64080100 call 0042A930 ; 读取文件
0041A0CC |. 8B15 4CA0AF00 mov edx, dword ptr [AFA04C]
0041A0D2 |. 8D0495 000000>lea eax, dword ptr [edx*4] ; 文件首双字*4
0041A0D9 |. 50 push eax ; 读取字节数
0041A0DA |. 68 2431B000 push 00B03124 ; 缓冲区
0041A0DF |. 56 push esi ; 未知
0041A0E0 |. E8 4B080100 call 0042A930 ; 读取文件
0041A0E5 |. 68 A0860100 push 186A0 ; 读取字节数
0041A0EA |. 68 B8D3AA00 push 00AAD3B8 ; 缓冲区
0041A0EF |. 56 push esi
0041A0F0 |. E8 3B080100 call 0042A930 ; 读取文件
简而言之,脚本文件的首个双字*4就是控制符的大小(大概),紧接着就在文件读取这些数据。这段东西是脚本中对话的位置索引,不过在我跟踪的过程中程序读取出来后就没对这段东西进行任何的操作,所以我们不关心这个,我们要关心的是第三次读取的数据,也就是0x00AAD3B8中的数据。读取完后,在数据窗口查看0x00AAD3B8的内容,对那段奇怪乱码下内存访问断点,然后删除函数里面其他的断点,F9运行,接下来断在下面的地方:
0041C575 |. 892D 30CEB000 |mov dword ptr [B0CE30], ebp
0041C57B |. 0FBEB0 B8D3AA>|movsx esi, byte ptr [eax+AAD3B8]
0041C582 |. 83FE 29 |cmp esi, 29
0041C585 |. 897424 28 |mov dword ptr [esp+28], esi
0041C589 |. 8915 2CCEB000 |mov dword ptr [B0CE2C], edx
0041C58F |. 7D 32 |jge short 0041C5C3
0041C591 |. 8A90 B9D3AA00 |mov dl, byte ptr [eax+AAD3B9]
很抱歉下面的代码我没贴,因为我没做注释,因为比起汇编代码注释,我认为用人类的语言表述出来更容易懂(实际上下面一直到解密过程的代码也不太复杂,但是很长,为了节省篇幅,我只好省去了)。
接下来的一段代码是判断esi中的值进行不同的操作,而esi中的值是由[eax+AAD3B8]这个决定的,可以看出,程序是依靠脚本文件中每个字节的值来确定接下来要完成的操作。而当esi的值大于等于0x4A并且少于0x4E的时候,就会将接下来的乱码字符串复制到堆栈的临时变量区,然后进行解密。解密的代码如下:
0041C711 |. 8A4C24 30 |mov cl, byte ptr [esp+30] ; esp+30=堆栈中待解密字符串的地址
0041C715 |. 84C9 |test cl, cl ; 若字符串第一个字符为0则跳转
0041C717 |.^ 0F84 12FFFFFF |je 0041C62F
0041C71D |> 80C1 20 |/add cl, 20 ; 加0x20
0041C720 |. 8808 ||mov byte ptr [eax], cl ; 更新到临时变量中去
0041C722 |. 8A48 01 ||mov cl, byte ptr [eax+1] ; 解密下一个字节
0041C725 |. 40 ||inc eax ; 指针+1
0041C726 |. 45 ||inc ebp ; 字符串长度+1
0041C727 |. 84C9 ||test cl, cl ; 是否空字符
0041C729 |.^ 75 F2 |\jnz short 0041C71D ; 不是则循环
好了,有了以下信息那就可以写程序来提取文本了。代码如下:
(作者的碎碎念:这段代码是将脚本中的文本解密出来放在一个新的文本里面。然后转换编码就可以用解密出来的文本进行翻译了。好像很多人喜欢python来写解包器跟封包器,好像那很方便的样子。可惜我学艺不精,除了C/C++就只会汇编跟RUBY了,用汇编来写回解包器好像是更麻烦,而RUBY不支持精确到位的运算,因此只好用C++来写了OTL)
提取出来的文本如下:
l 深々と。
l 桜が舞っていた。
l 驚くほどゆったりと。
l 音もなく。
l 見渡す限りに舞い散る桜の花びら。
l それは一面を色づけるように、
l 白で塗りつぶされた世界を彩るように、
l ただゆったりと舞い踊っていた。
l それはとても綺麗で、
l 呆れるくらいにとても綺麗で、
l ひとりぼっちで、
l ただ震えることしかできなくて、
l 寂しくて、
l どうしようもなく途方にくれていたボクでさえ
見惚れてしまうくらい、
l 綺麗な景色だった。
l だから、
l だからこれはきっと夢なんだと思った。
l 真っ白な夢。
l 夢のような夢。
将前面的几句翻译了一下:
好吧,请无视掉翻译的质量问题,毕竟我们的重点是在程序的修改而不是翻译的问题,或者各位可以安慰自己说这几句其实是机器翻译的(当然这会极大地打击偶的自信心OTL)。
三、封包
幸好这个解密算法够简单,所以封包器就很好写(像我上两篇文章的那么复杂的加密算法,真不知道资源拆出来后要怎样封回去了,不过貌似那两个游戏不用封包也可以运行游戏……),封包器代码代码如下:
简单说下算法,前面的解包器是将脚本中字节的值大于等于0x4a并小于0x4e的值后的字符串提取出来然后逐字节加0x20,而封包器就是打开原来的脚本文件后再将翻译好的文字减0x20替换到原来的加密的字符串的位置。虽然前面说过脚本文件中开头的索引好像没什么用,但保险起见,还是要修正后再写入新的文件。
四、程序修改
当我满心欢喜将新的脚本文件替换掉原来的文件之后,打开游戏,却出现了下面的画面:
干!
仔细想一下,由于原来的文本是用日文编码的,现在换成了GBK编码,不乱码就有鬼了,于是我们就要修正程序。
首先,用OD载入程序,右键选“查找当前模块中的所有名称”,看到有CreateFontA这个函数,查一下MSDN,发现这个函数的第九个参数fdwCharSet正是决定字符编码的参数,在程序里面看一下,有几个连续的CreateFontA的调用:
00408D7A |> \68 500C5000 push 00500C50 ; /FaceName = ""82,"l",82,"r ",83,"S",83,"V",83,"b",83,"N"
00408D7F |. 6A 04 push 4 ; |PitchAndFamily = DEFAULT_PITCH|4|FF_DONTCARE
00408D81 |. 6A 00 push 0 ; |Quality = DEFAULT_QUALITY
00408D83 |. 6A 20 push 20 ; |ClipPrecision = CLIP_DEFAULT_PRECIS|CLIP_TT_ALWAYS
00408D85 |. 6A 04 push 4 ; |OutputPrecision = OUT_TT_PRECIS
00408D87 |. 68 86000000 push 80 ; |CharSet = 128.
00408D8C |. 8B35 2C304300 mov esi, dword ptr [<&GDI32.CreateFo>; |GDI32.CreateFontA
00408D92 |. 6A 00 push 0 ; |StrikeOut = FALSE
00408D94 |. 6A 00 push 0 ; |Underline = FALSE
00408D96 |. 6A 00 push 0 ; |Italic = FALSE
00408D98 |. 6A 00 push 0 ; |Weight = FW_DONTCARE
00408D9A |. 6A 00 push 0 ; |Orientation = 0
00408D9C |. 6A 00 push 0 ; |Escapement = 0
00408D9E |. 6A 10 push 10 ; |Width = 10 (16.)
00408DA0 |. 6A 20 push 20 ; |Height = 20 (32.)
00408DA2 |. FFD6 call esi ; \CreateFontA
而0x80则是SHIFTJIS_CHARSET编码的值,在windows.h头文件中我们可以查到GB2312_CHARSET的值为0x86。于是我们可以将那个0x80改成是0x86,用OD保存,然后运行……
还是没有显示出中文来!
现在该怎么办?这时应该看看程序利用哪些函数来进行输出的。用右键选“查看”-》“当前模块的所有参考”可以看到有个叫GetGlyphOutlineA的函数。在MSDN中有明确说明这个函数可以将字符转换成点阵来输出的。往上看,可以看到一段奇怪的代码:
00406765 |. 8A06 mov al, byte ptr [esi]
00406767 |. 3C 80 cmp al, 80
00406769 |. 76 04 jbe short 0040676F
0040676B |. 3C A0 cmp al, 0A0
0040676D |. 72 08 jb short 00406777
0040676F |> 3C E0 cmp al, 0E0
00406771 |. 72 5D jb short 004067D0
00406773 |. 3C FC cmp al, 0FC
00406775 |. 77 59 ja short 004067D0
这段代码的作用是为了检查字符编码的边界而防止产生异常而导致不可预料的效果的。而由于GBK编码比游戏原来的编码的范围要大(CBK编码的范围是0x80xx到0xfexx),所以将比较处的0A0跟0FC替换成0FE。
整个程序中大概有三到四处的边界检查,可以利用WinHex查找3C A0再将AO跟后面的FC替换成FE。而有一处的边界检查是这个样子的:
00406A4A |. 3C 80 cmp al, 80
00406A4C |. 76 21 jbe short 00406A6F
00406A4E |. 3C 98 cmp al, 98
00406A50 |. 77 21 ja short 00406A73
…………
00406A6F |> \3C 98 cmp al, 98
00406A71 |. 76 21 jbe short 00406A94
00406A73 |> 3C A0 cmp al, 0A0
00406A75 |. 73 1D jnb short 00406A94
…………
00406A94 |> \3C E0 cmp al, 0E0
00406A96 |. 72 21 jb short 00406AB9
00406A98 |. 3C FC cmp al, 0FC
00406A9A |. 73 1D jnb short 00406AB9
这个边界判断比较诡异,假如将第一个cmp al,98改成cmp al,0FE的话程序会出错,解决方法是将最后一个判断中的FC改成FE,并且第二组判断中的0A0改成98。
保存修改后运行,效果如下:
(关于编码边界检查的更详细内容可以参考我的另外一篇文章)
就这样,这个游戏的汉化算是大功告成- -
一、资源分析
一般对于汉化游戏而言,脚本是汉化者最重要的部分(大多数时候还要提取出部分图片进行修改,部分有过场动画的游戏还要提取出动画嵌入字幕再封回去,至于声音的话……难道有人打算将日语的配音换成普通话么)。一般脚本文件要么是多个大小不超过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下断,断下后单步运行到函数返回,回到下面的地方:
引用
0041A0BF |> \6A 04 push 4 ; 读取四个字节
0041A0C1 |. 68 4CA0AF00 push 00AFA04C ; 缓冲区
0041A0C6 |. 56 push esi
0041A0C7 |. E8 64080100 call 0042A930 ; 读取文件
0041A0CC |. 8B15 4CA0AF00 mov edx, dword ptr [AFA04C]
0041A0D2 |. 8D0495 000000>lea eax, dword ptr [edx*4] ; 文件首双字*4
0041A0D9 |. 50 push eax ; 读取字节数
0041A0DA |. 68 2431B000 push 00B03124 ; 缓冲区
0041A0DF |. 56 push esi ; 未知
0041A0E0 |. E8 4B080100 call 0042A930 ; 读取文件
0041A0E5 |. 68 A0860100 push 186A0 ; 读取字节数
0041A0EA |. 68 B8D3AA00 push 00AAD3B8 ; 缓冲区
0041A0EF |. 56 push esi
0041A0F0 |. E8 3B080100 call 0042A930 ; 读取文件
简而言之,脚本文件的首个双字*4就是控制符的大小(大概),紧接着就在文件读取这些数据。这段东西是脚本中对话的位置索引,不过在我跟踪的过程中程序读取出来后就没对这段东西进行任何的操作,所以我们不关心这个,我们要关心的是第三次读取的数据,也就是0x00AAD3B8中的数据。读取完后,在数据窗口查看0x00AAD3B8的内容,对那段奇怪乱码下内存访问断点,然后删除函数里面其他的断点,F9运行,接下来断在下面的地方:
引用
0041C575 |. 892D 30CEB000 |mov dword ptr [B0CE30], ebp
0041C57B |. 0FBEB0 B8D3AA>|movsx esi, byte ptr [eax+AAD3B8]
0041C582 |. 83FE 29 |cmp esi, 29
0041C585 |. 897424 28 |mov dword ptr [esp+28], esi
0041C589 |. 8915 2CCEB000 |mov dword ptr [B0CE2C], edx
0041C58F |. 7D 32 |jge short 0041C5C3
0041C591 |. 8A90 B9D3AA00 |mov dl, byte ptr [eax+AAD3B9]
很抱歉下面的代码我没贴,因为我没做注释,因为比起汇编代码注释,我认为用人类的语言表述出来更容易懂(实际上下面一直到解密过程的代码也不太复杂,但是很长,为了节省篇幅,我只好省去了)。
接下来的一段代码是判断esi中的值进行不同的操作,而esi中的值是由[eax+AAD3B8]这个决定的,可以看出,程序是依靠脚本文件中每个字节的值来确定接下来要完成的操作。而当esi的值大于等于0x4A并且少于0x4E的时候,就会将接下来的乱码字符串复制到堆栈的临时变量区,然后进行解密。解密的代码如下:
引用
0041C711 |. 8A4C24 30 |mov cl, byte ptr [esp+30] ; esp+30=堆栈中待解密字符串的地址
0041C715 |. 84C9 |test cl, cl ; 若字符串第一个字符为0则跳转
0041C717 |.^ 0F84 12FFFFFF |je 0041C62F
0041C71D |> 80C1 20 |/add cl, 20 ; 加0x20
0041C720 |. 8808 ||mov byte ptr [eax], cl ; 更新到临时变量中去
0041C722 |. 8A48 01 ||mov cl, byte ptr [eax+1] ; 解密下一个字节
0041C725 |. 40 ||inc eax ; 指针+1
0041C726 |. 45 ||inc ebp ; 字符串长度+1
0041C727 |. 84C9 ||test cl, cl ; 是否空字符
0041C729 |.^ 75 F2 |\jnz short 0041C71D ; 不是则循环
好了,有了以下信息那就可以写程序来提取文本了。代码如下:
#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]>=0x4a&&buff[i]<0x4e) { 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; }
(作者的碎碎念:这段代码是将脚本中的文本解密出来放在一个新的文本里面。然后转换编码就可以用解密出来的文本进行翻译了。好像很多人喜欢python来写解包器跟封包器,好像那很方便的样子。可惜我学艺不精,除了C/C++就只会汇编跟RUBY了,用汇编来写回解包器好像是更麻烦,而RUBY不支持精确到位的运算,因此只好用C++来写了OTL)
提取出来的文本如下:
引用
l 深々と。
l 桜が舞っていた。
l 驚くほどゆったりと。
l 音もなく。
l 見渡す限りに舞い散る桜の花びら。
l それは一面を色づけるように、
l 白で塗りつぶされた世界を彩るように、
l ただゆったりと舞い踊っていた。
l それはとても綺麗で、
l 呆れるくらいにとても綺麗で、
l ひとりぼっちで、
l ただ震えることしかできなくて、
l 寂しくて、
l どうしようもなく途方にくれていたボクでさえ
見惚れてしまうくらい、
l 綺麗な景色だった。
l だから、
l だからこれはきっと夢なんだと思った。
l 真っ白な夢。
l 夢のような夢。
将前面的几句翻译了一下:
好吧,请无视掉翻译的质量问题,毕竟我们的重点是在程序的修改而不是翻译的问题,或者各位可以安慰自己说这几句其实是机器翻译的(当然这会极大地打击偶的自信心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; }
简单说下算法,前面的解包器是将脚本中字节的值大于等于0x4a并小于0x4e的值后的字符串提取出来然后逐字节加0x20,而封包器就是打开原来的脚本文件后再将翻译好的文字减0x20替换到原来的加密的字符串的位置。虽然前面说过脚本文件中开头的索引好像没什么用,但保险起见,还是要修正后再写入新的文件。
四、程序修改
当我满心欢喜将新的脚本文件替换掉原来的文件之后,打开游戏,却出现了下面的画面:
干!
仔细想一下,由于原来的文本是用日文编码的,现在换成了GBK编码,不乱码就有鬼了,于是我们就要修正程序。
首先,用OD载入程序,右键选“查找当前模块中的所有名称”,看到有CreateFontA这个函数,查一下MSDN,发现这个函数的第九个参数fdwCharSet正是决定字符编码的参数,在程序里面看一下,有几个连续的CreateFontA的调用:
引用
00408D7A |> \68 500C5000 push 00500C50 ; /FaceName = ""82,"l",82,"r ",83,"S",83,"V",83,"b",83,"N"
00408D7F |. 6A 04 push 4 ; |PitchAndFamily = DEFAULT_PITCH|4|FF_DONTCARE
00408D81 |. 6A 00 push 0 ; |Quality = DEFAULT_QUALITY
00408D83 |. 6A 20 push 20 ; |ClipPrecision = CLIP_DEFAULT_PRECIS|CLIP_TT_ALWAYS
00408D85 |. 6A 04 push 4 ; |OutputPrecision = OUT_TT_PRECIS
00408D87 |. 68 86000000 push 80 ; |CharSet = 128.
00408D8C |. 8B35 2C304300 mov esi, dword ptr [<&GDI32.CreateFo>; |GDI32.CreateFontA
00408D92 |. 6A 00 push 0 ; |StrikeOut = FALSE
00408D94 |. 6A 00 push 0 ; |Underline = FALSE
00408D96 |. 6A 00 push 0 ; |Italic = FALSE
00408D98 |. 6A 00 push 0 ; |Weight = FW_DONTCARE
00408D9A |. 6A 00 push 0 ; |Orientation = 0
00408D9C |. 6A 00 push 0 ; |Escapement = 0
00408D9E |. 6A 10 push 10 ; |Width = 10 (16.)
00408DA0 |. 6A 20 push 20 ; |Height = 20 (32.)
00408DA2 |. FFD6 call esi ; \CreateFontA
而0x80则是SHIFTJIS_CHARSET编码的值,在windows.h头文件中我们可以查到GB2312_CHARSET的值为0x86。于是我们可以将那个0x80改成是0x86,用OD保存,然后运行……
还是没有显示出中文来!
现在该怎么办?这时应该看看程序利用哪些函数来进行输出的。用右键选“查看”-》“当前模块的所有参考”可以看到有个叫GetGlyphOutlineA的函数。在MSDN中有明确说明这个函数可以将字符转换成点阵来输出的。往上看,可以看到一段奇怪的代码:
引用
00406765 |. 8A06 mov al, byte ptr [esi]
00406767 |. 3C 80 cmp al, 80
00406769 |. 76 04 jbe short 0040676F
0040676B |. 3C A0 cmp al, 0A0
0040676D |. 72 08 jb short 00406777
0040676F |> 3C E0 cmp al, 0E0
00406771 |. 72 5D jb short 004067D0
00406773 |. 3C FC cmp al, 0FC
00406775 |. 77 59 ja short 004067D0
这段代码的作用是为了检查字符编码的边界而防止产生异常而导致不可预料的效果的。而由于GBK编码比游戏原来的编码的范围要大(CBK编码的范围是0x80xx到0xfexx),所以将比较处的0A0跟0FC替换成0FE。
整个程序中大概有三到四处的边界检查,可以利用WinHex查找3C A0再将AO跟后面的FC替换成FE。而有一处的边界检查是这个样子的:
引用
00406A4A |. 3C 80 cmp al, 80
00406A4C |. 76 21 jbe short 00406A6F
00406A4E |. 3C 98 cmp al, 98
00406A50 |. 77 21 ja short 00406A73
…………
00406A6F |> \3C 98 cmp al, 98
00406A71 |. 76 21 jbe short 00406A94
00406A73 |> 3C A0 cmp al, 0A0
00406A75 |. 73 1D jnb short 00406A94
…………
00406A94 |> \3C E0 cmp al, 0E0
00406A96 |. 72 21 jb short 00406AB9
00406A98 |. 3C FC cmp al, 0FC
00406A9A |. 73 1D jnb short 00406AB9
这个边界判断比较诡异,假如将第一个cmp al,98改成cmp al,0FE的话程序会出错,解决方法是将最后一个判断中的FC改成FE,并且第二组判断中的0A0改成98。
保存修改后运行,效果如下:
(关于编码边界检查的更详细内容可以参考我的另外一篇文章)
就这样,这个游戏的汉化算是大功告成- -
发表评论
-
未完的病毒分析func.dll
2009-09-20 17:33 1375这个dll会释放出一个驱动,于是就被这个诡异的驱动迷惑了很久。 ... -
随手捉了个毒来分析
2009-09-11 00:39 1061本来暑假前就以外地感染的这个病毒,不过我的系统装了影子 ... -
《星空のメモリア》脚本简单分析
2009-08-31 20:55 991脚本是编译型的脚本,虽然没加密,但不去调试的话就很难分 ... -
OllyDBG插件OllyGal1.10
2009-07-30 01:48 1907嗯,不知道发汉化技术版还是发多媒体教室。发在这里应该多点人 ... -
OllGal1.10正式版
2009-07-30 01:31 0详细内容待更新 -
丢个OD插件娱乐一下大众
2009-07-23 11:06 1225看到来福丢了个JAVA做的纸牌出来应援,又看到有人丢了个F ... -
Gift汉化相关
2009-07-13 15:53 1421虽然大概没什么人来看这个blog,但还是要稍微更新一下 ... -
《俺たちに翼はない》脚本加密算法分析
2009-06-17 01:03 1913前言:游戏汉化中的一个重要的步骤是对资源的解包以及封包。虽然现 ... -
字符编码与游戏中的字符边界检查
2009-06-17 00:55 5865在汉化以及开发国 ...
相关推荐
《基于MATLAB的DC2DC斩波电路仿真详解》 在电力电子技术领域,DC2DC斩波电路是一种重要的直流电压转换方式,广泛应用于电源管理、电池系统、电动车充电等领域。本文将围绕“DC2DC_Chopper.rar”压缩包中的内容,...
兰宝 LR6.5QE1-DC2金属圆柱形DC2线制电感式传感器 说明书pdf,兰宝 LR6.5QE1-DC2金属圆柱形DC2线制电感式传感器 说明书
常用工具:win10开发常用工具,亲测好用,使用平台为win10的1809 64位系统。其他平台未验证。
根据提供的文件信息,以下是对“兰宝 LR18E2-AC DC2金属圆柱形AC DC2线制电感式传感器”说明书的知识点汇总。 该说明书主要针对的是兰宝公司生产的LR18E2-AC DC2型号金属圆柱形AC DC2线制电感式传感器。该系列...
兰宝 LE30 DC2方形DC2线制电感式传感器 说明书pdf,兰宝 LE30 DC2方形DC2线制电感式传感器 说明书
兰宝 LR6.5Q-DC2金属圆柱形DC2线制电感式传感器 说明书pdf,兰宝 LR6.5Q-DC2金属圆柱形DC2线制电感式传感器 说明书
兰宝 LE17 DC2方形DC2线制电感式传感器是一种由上海兰宝传感器有限公司生产的塑料方形电感式传感器。该系列传感器具有非接触式检测方式,响应快,频率高,并具备逆极性保护和浪涌保护功能。传感器采用PBT材质外壳,...
兰宝LE20 DC2方形DC2线制电感式传感器是一款非接触式的塑料方形传感器,主要用于工业领域中进行物体检测。下面是对该传感器详细知识点的总结: 1. 基本概念:电感式传感器利用电磁感应原理进行非接触式的物体检测。...
DC2分析 该存储库包含DC2模拟数据集的常规分析脚本。 特定于项目的工作可以在单独的存储库中完成,但是一些分析任务通常会很有用,并且可以在此处包括相关的脚本和笔记本以供更一般的使用。 :right_arrow: ! 链接...
1. 传感器类型:该文档介绍的是兰宝LE40系列塑料方形电感式传感器,它属于DC2线制电感式传感器的一种。 2. 直流供电系统:DC2线制表示该传感器采用双线直流供电方式,这有助于简化电源布线和提高系统的可靠性。 ...
The TRDB_DC2 Kit provides everything you need to develop a 1.3Mega Pixel Digital Camera on the Altera DE2/DE1 and Terasic TREX C1 boards (TR1). The kit contains hardware design (in Verilog) and ...
DC2AC converter with SVM done in plecs so it's easy to modify without a PLL for simplicity.
塑料方形电感式传感器LE68系列是兰宝公司生产的一款非接触式检测的传感器,它拥有方形的外观设计,并采用DC2线制,分为常开(NO)和常闭(NC)两种输出方式。传感器以其快速的响应时间、高频率的特点而著称,适用于各种...
本文以Xxx公司的案例为背景,详细介绍了如何将辅助域控DC2强制夺取DC1的5个主要角色,以及角色转移后的后续操作。 DC1作为主域控制器,承载了全局编录(GC)、重命名主机、RID主控(RID Master)和主域控制器(PDC ...
DC2-12001 采购过程控制程序.doc
兰宝LR30E2 DC2线制金属圆柱形电感式传感器是一款用于检测金属物体存在的电子元件。本传感器是兰宝公司LR30系列产品线中的一部分,具有多个型号可供选择,包括NOLR30BF10DLO-E2、LR30BN15DLO-E2、NCLR30BF10DLC-E2、...
根据提供的信息,兰宝LE68-10-60V DC2方形DC2线制电感式传感器是一款非接触式检测的传感器,其说明书详细介绍了该传感器的特点、规格参数、电气接线、安装方式等技术细节。下面将对其中的知识点进行详细介绍。 一、...
传感器型号中,“DC2线NO”和“DC2线NC”分别代表两种不同的输出方式。NO(Normally Open)表示传感器的输出在未检测到目标物体时为开启状态;NC(Normally Closed)则表示传感器的输出在未检测到目标物体时为关闭...
aebfadae68f89e7dc2b19bcc398e1368
DC2-03001 方针目标制定与管理程序.doc