先引一段这次要分析的游戏的资料:

引用:
游戏全名:Tom Clancy's H.A.W.X
中文译名:汤姆克兰西之鹰击长空
下载地址:http://www.verycd.com/topics/2736263/
由育碧Bucharest工作室开发,充分利用次世代平台的好处,《鹰击长空》给我们带来了一个紧张和真实的空战体验。故事发生在2012年。当单一民族国家聚集越密的世纪,战争的规则发展得越来越快。越来越多的国家开始增加对私人军队公司(PMCs)的依赖。法律松懈的立场给精英们有利可图。雷克雅末协定更加深地令他们授权军队工作的每一方面合化法,当PMCs这样的利益浮上水面之后,周围增强的利害关系给与他们足够强大的力量开始扩张。

【前言】
需要说明的是育碧官方已经出了这款游戏的中文补丁,所以如果你是玩家的话可以直接用中文版。
这款游戏自问世以来,玩家一直比较多,但是官方始终没出中文版,也没有明确表态不会出中文版。正因为官方态度比较暧昧,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
这几行代码表示游戏把创建的指针保存到了0xe57f18,接着往下看,0x48de13取出了这个指针,然后又按指针找到了接口地址,又在这个地址上加上0x40。查阅微软dx sdk得知,CreateDevice这个函数是IDirect3D9接口中的第17个函数,所以他的偏移为16*4=0x40。由此可以确定,0x48de2e处的call调用的就是CreateDevice。
先看看CreateDevice的原型:
代码:
HRESULT CreateDevice( 
UINT Adapter, 
D3DDEVTYPE DeviceType, 
HWND hFocusWindow, 
DWORD BehaviorFlags, 
D3DPRESENT_PARAMETERS *pPresentationParameters, 
IDirect3DDevice9** ppReturnedDeviceInterface 
);
其中我们最关心的就是最后一个参数IDirect3DDevice9** ppReturnedDeviceInterface,这个就是我们要的设备接口指针。按照stdcall入栈顺序,0x48dde1处push的0xE57F1C就是这个接口的指针,我们只需要在这个call返回时取出0xE57F1C处的指针保存即可。

【寻找显示函数】
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掉这个显示函数然后使用我们前面保存的那个设备接口指针调用D3DXCreateFont创建一个字体,再调用DrawText直接绘制文本。具体如何hook不重要,思路对了就行。下面是我创建宋体然后DrawText直接绘制中文的效果截图

终于有亲切的方块汉字出现了。尽管不太好看,但总算在正确的位置,以正确的大小,正确的颜色,正确的透明度显示出了正确的汉字,作为破解者,算是成功了吧。
因为补丁代码风格比较不堪,这里就不贴了,有了思路,我想写出来不难吧。

【总结】
外挂字幕汉化的步骤大致如下:首先通过hook的方式获得D3D设备接口指针。然后找出显示函数并分析出函数接口。再然后hook该显示函数,按照分析出的接口重新设计。
从上文可以看出,实现起来并不难。不过麻烦的就是找指针,找函数,分析函数接口这些比较琐碎的没有规律的事情。不过破解就是这样,不能光靠运气,但没运气也不行。
这里还有我的一篇关于opengl引擎的汉化文章,
基于OPENGL引擎3D游戏逆向分析及汉化修改实例