首页
论坛
专栏
课程

[调试逆向] [原创]基于DirectX 9引擎3D游戏逆向分析及汉化修改实例

2011-5-21 22:11 27169

[调试逆向] [原创]基于DirectX 9引擎3D游戏逆向分析及汉化修改实例

2011-5-21 22:11
27169
先引一段这次要分析的游戏的资料:
游戏全名: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游戏逆向分析及汉化修改实例

[公告]安全服务和外包项目请将项目需求发到看雪企服平台:https://qifu.kanxue.com

上传的附件:
最新回复 (34)
suhong 2011-5-21 23:10
2
0
受教了,非常感谢
leeone 2011-5-21 23:20
3
0
通俗易懂,感谢分享
msfan 2011-5-21 23:29
4
0
不错学习了,正在找这方面的资料,谢谢
justlovemm 2011-5-22 15:57
5
0
请教楼主,在取得了IDirect3DDevice9指针之后,如何定义自己的drawtext的类函数类型,才能直接做例如lpDirtect3DDevice9->mydrawtext(....)这样的调用?
bitt 5 2011-5-22 19:41
6
0
上面有一部分专门讲分析游戏显示函数接口,指的就是这个
知道了游戏的显示函数接口就可以按照该接口定义自己的函数
justlovemm 2011-5-22 20:46
7
0
多谢。

不过,我问的是在没有定义一个类的情况下,如何定义一个包含this指针的类成员函数的调用,就是说比正常的函数定义多一项把 lpDirect3Device9付给ecx这个过程,这个应该怎么在C里面定义?
bitt 5 2011-5-22 21:28
8
0
不行就内联汇编 多自由
JuanA 2011-5-22 22:49
9
0
一直听说UBISoft的东西比较难汉化,楼主这篇文章可谓深入浅出,很有启发意义
dlmu 1 2011-5-23 20:19
10
0
支持~~~学习了
djxh 2011-5-23 21:52
11
0
跟着楼主继续学汉化~
mingligli 2011-5-23 22:13
12
0
通俗易懂,感谢分享
shuyangzjg 2011-5-23 23:10
13
0
LZ大哥再来个解密资源篇吧。。
tihty 3 2011-5-23 23:57
14
0
mark :)
莫灰灰 8 2011-5-24 09:30
15
0
我是来学习汉化的~
noword_forever 5 2011-5-24 10:37
16
0
强制类型转换成 Direct3Device9
lixupeng 2011-5-24 18:43
17
0
厉害啊学习!!
bitt 5 2011-5-24 19:02
18
0
汉化界的前辈啊
我就是看您的文章启蒙的
qqlinhai 2011-5-25 15:20
19
0
学习一下思路
bbwtjjw 2011-5-25 23:06
20
0
我是来学习汉化的~
kennyl 2011-5-26 09:42
21
0
有水平的游戏汉化方面的文章在网上非常少啊,感谢楼主无私分享的精神!

Added: 读完,请教楼主之所以采用这种hook 游戏原本的drawtext函数的方法,是由于找不到游戏的字库文件(其实就是图片吧?),那么你最终是绕过(不再使用)原本游戏要使用的字库直接调用Direct3D的某些文字输出函数(在下不懂这方面的API)实现的吗?
swordkok 1 2011-5-26 11:22
22
0
谢谢了,正好拿个游戏练练手
zhuangbx 2011-5-26 14:07
23
0
很好,学习中!!!!!!!!!!!!!!!!!!!!
DETY 2011-5-26 17:17
24
0
很强大 膜拜学习了 谢谢共享心得
bitt 5 2011-5-26 18:47
25
0
是的 绕过去
orchid88 2011-5-26 21:44
26
0
好教程,学习了。
justlovemm 2011-5-27 00:02
27
0
再请教一下前辈。
按我的理解,类型可以强制转化,但是类成员函数的定义其本身是隐式的,就是说参数传递的时候把类指针付给ecx这种类成员函数的调用方法,是因为编译器知道它是一个类函数,而不是那个函数的声明部分有什么特殊的可以直接复制的声明。就是说我觉得我无法声明一个函数类型,让指向这个类型的函数指针在被调用的时候能把前面的类指针传给ECX,也无法用类型转化的方式去实现lpDirect3Device9->myOutText(....)这样的call过程。
当然自己写汇编是另一回事,那就没有类型声明的这个苦恼了。
noword_forever 5 2011-5-27 11:54
28
0
看这个
http://bbs.pediy.com/showthread.php?t=113739

还有源码
justlovemm 2011-5-27 14:52
29
0
多谢了。
前辈的源代码都共享出来了,赞一个!
9494 2011-5-30 04:36
30
0
学习了 谢谢楼主.
YwdxY 2011-5-30 14:39
31
0
学习,感谢分享
amour 2011-6-2 15:42
32
0
另辟蹊径,思路不错哦
夕阳无影 2011-6-2 20:47
33
0
都是牛人啊!
zzzzzzx 2011-6-10 16:29
34
0
mark 下
qqj1228 2012-1-3 22:57
35
0
学到了很多,多谢多谢
游客
登录 | 注册 方可回帖
返回