先引一段这次要分析的游戏的资料:
【前言】
需要说明的是育碧官方已经出了这款游戏的中文补丁,所以如果你是玩家的话可以直接用中文版。
这款游戏自问世以来,玩家一直比较多,但是官方始终没出中文版,也没有明确表态不会出中文版。正因为官方态度比较暧昧,3dm和yx各个组也都没敢动手,这状况持续很长时间。当时我正好安装了这款游戏,正有汉化的意向。索性开始调试了,到了调试分析基本完成补丁也写好了的时候,官方突然出了一个中文补丁,据说是为hawx2做售前宣传。汉化工作便停止了,补丁也没有用了。
这些都是题外话,无论补丁有没有用,汉化经验总是有用的。现在将这游戏的分析过程整理出来,想必是有价值的。
【准备工作】
国外公司开发游戏都有个习惯,那就是不使用OS提供的字体,自己制作字符字库,然后设计一个字符输出引擎。这样做有两个好处,一是保持了游戏美工风格的统一,二是规避了使用OS字体可能带来的版权问题。这款游戏就是这样。
知道了这一点,我们的工作便有了方向:首先找到所有的文本资源,翻译之,统计汉字数量,并编码之。第二步找到字符资源,按编码制作相同格式中文字符资源替换之。第三步导入翻译完成的中文文本,汉化结束。
先找资源,其实不用找,就在根目录,data2.pak data3.pak名字起得很明显,再确认一下就行了。到这里没接触过汉化的朋友一定有疑问,pak到底什么文件啊。Pak就是package打包文件,至于怎么打包的怎么解包的只有开发人员知道。打包文件不同于pe,格式自由扩展名也自由,开发人员要是乐意了叫.exe都行。所以无视扩展名是非常有必要的。不过别灰心,用winhex看看文件头两字节为0x50、0x4b,即字符‘PK’,这有点像zip文件。改改后缀名发现还真就是zip文件,不过加了密码。调试得知密码为rF4hfGe1PfrzGe3IbaRtWsIn,然后解压出一大堆dds纹理,找到了少量的未压缩文本资源,以及大部分压缩了的文本资源。
到这里遇到了问题,字符资源始终没找到。看来替换字符资源这条路是行不通了。不过还有一个方法,就是外挂字幕汉化。具体方法就是hook掉游戏的文字显示函数,接管之,重新设计一个文字显示函数。这就是下面的重点。
【游戏分析】
所谓的重新设计一个显示函数,其实就是调用D3DXCreateFont创建一个字体,然后调用DrawText直接绘制文本,这样做便不必考虑字符字库资源问题了。
DirectX9游戏的启动过程大概是这样的:先调用Direct3DCreate9,返回一个IDirect3D9接口指针,然后执行IDirect3D9->CreateDevice,再返回一个D3D设备接口指针。有了这个设备指针我们便可以使用DX9提供的一切绘图手段,包括我们最关心的D3DXCreateFont和DrawText。我们要做的就是在游戏调用完CreateDevice之后,hook之保存这个指针备用。
OD载入游戏,CTRL+N,抛弃掉几个无用的搜索结果以后,确认为0x48dd7e
代码:
0048DD7E |. E8 CD604600 call <jmp.&d3d9.Direct3DCreate9> 0048DD83 |. 3BC3 cmp eax, ebx 0048DD85 |. A3 187FE500 mov dword ptr [E57F18], eax ; 保存创建的接口指针 0048DD8A |. 75 05 jnz short 0048DD91 0048DD8C |. 5F pop edi 0048DD8D |. 32C0 xor al, al 0048DD8F |. 5B pop ebx 0048DD90 |. C3 retn 0048DD91 |> 8B08 mov ecx, dword ptr [eax] 0048DD93 |. 8B51 38 mov edx, dword ptr [ecx+38] 0048DD96 |. 56 push esi 0048DD97 |. 68 F877E500 push 00E577F8 0048DD9C |. 57 push edi 0048DD9D |. 53 push ebx 0048DD9E |. 50 push eax 0048DD9F |. FFD2 call edx 0048DDA1 |. 0FB615 207FE5>movzx edx, byte ptr [E57F20] 0048DDA8 |. 8B4424 10 mov eax, dword ptr [esp+10] 0048DDAC |. 8B4C24 14 mov ecx, dword ptr [esp+14] 0048DDB0 |. 57 push edi 0048DDB1 |. 52 push edx 0048DDB2 |. 893D 28BFDE00 mov dword ptr [DEBF28], edi 0048DDB8 |. 893D 34BFDE00 mov dword ptr [DEBF34], edi 0048DDBE |. 893D 50BFDE00 mov dword ptr [DEBF50], edi 0048DDC4 |. A3 1CBFDE00 mov dword ptr [DEBF1C], eax 0048DDC9 |. 890D 20BFDE00 mov dword ptr [DEBF20], ecx 0048DDCF |. E8 EC38FFFF call 004816C0 0048DDD4 |. 8B7424 28 mov esi, dword ptr [esp+28] 0048DDD8 |. 83C4 08 add esp, 8 0048DDDB |. 33C0 xor eax, eax 0048DDDD |. 385C24 24 cmp byte ptr [esp+24], bl 0048DDE1 |. 68 1C7FE500 push 00E57F1C ; 注意第一个参数 0048DDE6 |. 0F95C0 setne al 0048DDE9 |. 33C9 xor ecx, ecx 0048DDEB |. 385C24 20 cmp byte ptr [esp+20], bl 0048DDEF |. 68 1CBFDE00 push 00DEBF1C 0048DDF4 |. 0F94C1 sete cl 0048DDF7 |. C705 24BFDE00>mov dword ptr [DEBF24], 16 0048DE01 |. 8935 38BFDE00 mov dword ptr [DEBF38], esi 0048DE07 |. 8D0485 470000>lea eax, dword ptr [eax*4+47] 0048DE0E |. A3 44BFDE00 mov dword ptr [DEBF44], eax 0048DE13 |. A1 187FE500 mov eax, dword ptr [E57F18] ; 取出上面保存的IDirect3D9接口指针 0048DE18 |. 890D 3CBFDE00 mov dword ptr [DEBF3C], ecx 0048DE1E |. 8B0D 0475E500 mov ecx, dword ptr [E57504] 0048DE24 |. 8B10 mov edx, dword ptr [eax] ; edx为IDirect3D9对象地址 0048DE26 |. 8B52 40 mov edx, dword ptr [edx+40] ; edx为CreateDevice函数地址 0048DE29 |. 51 push ecx 0048DE2A |. 56 push esi 0048DE2B |. 57 push edi 0048DE2C |. 53 push ebx 0048DE2D |. 50 push eax 0048DE2E |. FFD2 call edx ; 调用CreateDevice
先看看CreateDevice的原型:
代码:
HRESULT CreateDevice( UINT Adapter, D3DDEVTYPE DeviceType, HWND hFocusWindow, DWORD BehaviorFlags, D3DPRESENT_PARAMETERS *pPresentationParameters, IDirect3DDevice9** ppReturnedDeviceInterface );
【寻找显示函数】
OD载入游戏,运行,当运行到有文字显示的时候,切到od然后暂停。ALT+M打开内存窗口,搜索刚才看到的字符串,然后在找到的字符串上下内存访问断点。假设断到了某处,CTRL+F9执行到返回,把这个过程nop掉(注意栈平衡),继续游戏。假如游戏没有报错,正常运行而且没有文字显示了说明你找到的就是文字显示函数。如果报错或者有文字显示,继续下断,继续nop,继续观察。如此反复N次,一定能找到。
不要嫌烦,找不到也别灰心,所谓调试,不就是调整测试调整测试。
我尝试n次终于断在了0x614082
代码:
00614082 . FFD0 call eax ; 显示函数 00614084 . 83C4 18 add esp, 18

其中一个参数为游戏的文本Loading Data,有点像,nop掉这个call之后不显示文字,如图

而游戏正常运行,看来就是它了。
【分析函数接口】
找到显示函数之后,下一步要做的就是分析这个函数的接口,这个至关重要。
从上面的截图来看,我们猜测倒数第六个push的dword很可能就是字符串地址参数。可是光知道这个不够。要绘制出文字,还需要知道字符串指针、位置、大小、颜色、透明度等。这时也没有什么太好的办法,只有一个一个修改运行观察,看看游戏有什么变化,然后猜测再修改再确认。当然,也可以观察汇编代码比如借助浮点数指令可以确定浮点数,或者使用ida的F5来辅助分析(推荐),这里不再赘述,大家各显神通。
我所分析出的显示函数接口如下:
代码:
void drawtext( color * a, //color是我自己定义的一个struct,四字节,分别为alpha,r,g,b float x, //浮点数,x坐标 float y, //浮点数,y坐标 int len, //要显示的字符串长度 DWORD type,//恒为’%s’,用不到,可能表示要显示的是一个字符串 const unsigned short * s//字符串指针 );

终于有亲切的方块汉字出现了。尽管不太好看,但总算在正确的位置,以正确的大小,正确的颜色,正确的透明度显示出了正确的汉字,作为破解者,算是成功了吧。
因为补丁代码风格比较不堪,这里就不贴了,有了思路,我想写出来不难吧。
【总结】
外挂字幕汉化的步骤大致如下:首先通过hook的方式获得D3D设备接口指针。然后找出显示函数并分析出函数接口。再然后hook该显示函数,按照分析出的接口重新设计。
从上文可以看出,实现起来并不难。不过麻烦的就是找指针,找函数,分析函数接口这些比较琐碎的没有规律的事情。不过破解就是这样,不能光靠运气,但没运气也不行。
这里还有我的一篇关于opengl引擎的汉化文章,
基于OPENGL引擎3D游戏逆向分析及汉化修改实例