首页
论坛
课程
招聘
[原创]解决进退魔兽争霸3中文名乱码问题
2009-1-6 02:28 26383

[原创]解决进退魔兽争霸3中文名乱码问题

2009-1-6 02:28
26383
前半部分是之前给某网站写的稿,面向一般人,没有什么技术含量,把文字复制过来这里一份,大概了解一下便可。

------------------------------------------------------------------------------------

曾几何时,在魔兽里取一个中文名是一件很拉风的事情,因为知道方法的人少。但任何事情传开了之后,也就变得草根了,比如DOTA里的小鸡臂章……

首先是取中文名的方法,知道的略过。进游戏里,自己建个主,在聊天里面打出中文,然后按住Shift,按下Home键(或者用鼠标拖动全选),放开Shift,按下Ctrl+C复制,退出来,把名字删光,Ctrl+V,就行了。

有的人会说,咋我换了几次名之后,房间里名字就变成...了,或者我取了五个字的名字,退出War3再进,怎么少了一个字?我们先从原理开始研究。比如我要取个中文名“波导终结者”,War3里它是如何储存的?
War3的名字是保存在注册表里,HKEY_CURRENT_USER\Software\Blizzard Entertainment\Warcraft III\String底下的userlocal项,而且是以UTF8编码格式保存的。
简单的来说,英文、数字等字符的UTF8编码不变,而汉字等双字节字符的编码,从2个字节变成了3个字节。让我们新建一个记事本,输入“波导终结者”,保存,用WinHEX等十六进制工具打开,可以看到汉字的ANSI编码与十六进制的对应情况:
波       导      终       结       者
B2A8     B5BC     D6D5     BDE1     D5DF

此时使用记事本的另存为功能,底下的编码设置为UTF-8,保存,再用WinHEX打开(最前头的EFBBBF三个字节是UTF-8的文件头标识),可以看到汉字的UTF-8编码
波        导         终        结         者
E6B3A2     E5AFBC     E7BB88     E7BB93     E88085

本来War3使用UTF-8并没有什么问题,要命的是,userlocal项是字符串格式,系统默认使用ANSI编码读写和储存,所以写进注册表中就会变成
娉       ㈠              缁      堢      粨       鑰      ?
E6B3     A2E5     AFBC     E7BB     88E7     BB93     E880     85

最后的十六进制0x85落单了,由于它不是个可见字符,读写过程中它就被抛弃或者变成问号(0x3F)!(取决于落单的这个字节的十六进制)那么你关了War3,再开,读出来的字符串就少或者变了一个字节,此时War3将字符串转换回汉字,就有以下结果:
波        导        终        结        者!!
E6B3A2     E5AFBC     E7BB88     E7BB93     E8803F

可以看到,最后这个“者”字的UTF-8编码错了一字节,所以在大厅里这个字当然就显示不出来了,只有“波导终结”四个字,但是0xE8803F还保留在War3的内存当中,无法转换成正常显示的字符,所以进游戏之后名字会变成...  

此时再来回答最早那个问题,换了名字之后为什么也会变成...?其实只是没删干净,一个汉字3个字节,而你按一次退格或者Del,War3只删了一个字节,删完大厅里看字是没了,却还有错误的字符存在内存当中,所以进游戏也会变成...。解决的办法也很简单,先按退格或者Del把名字清空(看起来),然后再依次长按退格和Del,把光标前后的余党剿灭干净,这样再改名字就不会有...了。

至于解决办法,暂时没有。只要War3储存名字的项继续用字符串值保存UTF-8编码,这个烦恼就会一直在,落单的编码始终会在读写注册表项的过程中被抛弃。变通的办法倒是有,如下:

进入注册表HKEY_CURRENT_USER\Software\Blizzard Entertainment\Warcraft III\String,把userlocal项(此时它是字符串项)删除,手动新建一个二进制项,也叫userlocal,填入 E6B3A2E5AFBCE7BB88E7BB93E88085,保存。此时打开War3,“波导终结者”五个字全都显示出来了。遗憾的是,关闭War3 之后,又被它以字符串项覆盖掉了。至于各大游戏平台的中文名,是直接写入内存的方式,所以和注册表无关也不会丢字符,或许可以修改War3主程序或者通过 HOOK的方式,把退出时候的字符串项保存改成二进制方式保存,可行性应该是不错的,有待高人研究。

-------------------------------------------------------------------------------------

后半部分是调试的部分,有段时间没搞了,有些手生。

打开OllyDbg,载入War3.exe,由于全屏不便调试,还要加上参数-window窗口化

调试,运行,忽略了若干错误和异常之后,War3顺利启动。对War3来说,比较重要的文件有War3.exe,Storm.dll,Game.dll等,查看,可执行模块里下断点,写注册表的API一般用RegSetValueExA,War3退出时写入注册表,最后在Storm.dll里唯一的一处RegSetValueExA断下。

15036126    FF15 04700415   CALL DWORD PTR DS:[<&ADVAPI32.RegCreateK>; ADVAPI32.RegCreateKeyExA
1503612C    8BF0            MOV ESI,EAX
1503612E    85F6            TEST ESI,ESI
15036130    75 59           JNZ SHORT storm.1503618B
15036132    8B4D 14         MOV ECX,DWORD PTR SS:[EBP+14]
15036135    8B55 10         MOV EDX,DWORD PTR SS:[EBP+10]
15036138    8B45 0C         MOV EAX,DWORD PTR SS:[EBP+C]
1503613B    51              PUSH ECX
1503613C    8B4D 08         MOV ECX,DWORD PTR SS:[EBP+8]
1503613F    52              PUSH EDX                                 ; 把参数依次压进堆栈
15036140    50              PUSH EAX
15036141    56              PUSH ESI
15036142    57              PUSH EDI
15036143    51              PUSH ECX
15036144    FF15 08700415   CALL DWORD PTR DS:[<&ADVAPI32.RegSetValu>; ADVAPI32.RegSetValueExA
1503614A    8B3D 20700415   MOV EDI,DWORD PTR DS:[<&ADVAPI32.RegClos>; ADVAPI32.RegCloseKey

此时堆栈里可以看到
0012D918   0000258C  |hKey = 258C
0012D91C   6F7E70F8  |ValueName = "reswidth"
0012D920   00000000  |Reserved = 0
0012D924   00000004  |ValueType = REG_DWORD
0012D928   0012DA74  |Buffer = 0012DA74
0012D92C   00000004  \BufSize = 4
0012D930   00000000
0012D934   6F7E70F8  ASCII "reswidth"

跟到写入userlocal的时候,把ValueType由REG_SZ改成REG_BINARY,发现BufSize大了1,写入的数据后面多了0x00一个字节,应该是C\C++字符串结束符的问题。
ValueType是用寄存器压进去的,而且RegSetValueExA只有一处调用,所以初步断定War3应该是用表或数组存储各个需要写入的注册表项,然后调用同一过程。往前走,一直到返回Game.dll的领空。

6F003170    56              PUSH ESI
6F003171    8BCB            MOV ECX,EBX
6F003173    E8 C8FFFFFF     CALL Game.6F003140                       ; Call到Storm.dll里的写入过程
6F003178    46              INC ESI
6F003179    83FE 4C         CMP ESI,4C                               ; 一共有0x4C个项需要写入
6F00317C  ^ 72 F2           JB SHORT Game.6F003170

用户名是保存在HKEY_CURRENT_USER\Software\Blizzard Entertainment\Warcraft III\String里,F7跟进上面的这个Call,来到如下地方

6F00318C    68 04010000     PUSH 104
6F003191    68 046C7E6F     PUSH Game.6F7E6C04                       ; ASCII "Warcraft III"
6F003196    8D85 FCFEFFFF   LEA EAX,DWORD PTR SS:[EBP-104]           ; 之前跳过了一段代码,是取注册表项前缀
6F00319C    50              PUSH EAX                                 ; 此处取完Warcraft III
6F00319D    E8 1E073900     CALL <JMP.&Storm.#501>                   ; 这两个CALL是取其他的参数
6F0031A2    8BF7            MOV ESI,EDI
6F0031A4    C1E6 04         SHL ESI,4
6F0031A7    8B8E 5C5D706F   MOV ECX,DWORD PTR DS:[ESI+6F705D5C]      ; 第二次进入循环,查看这里的内存地址
6F0031AD    8B148D 405D706F MOV EDX,DWORD PTR DS:[ECX*4+6F705D40]    ; 发现其实是储存在Game.dll文件里的一个表
6F0031B4    68 04010000     PUSH 104
6F0031B9    52              PUSH EDX
6F0031BA    8D85 FCFEFFFF   LEA EAX,DWORD PTR SS:[EBP-104]
6F0031C0    50              PUSH EAX
6F0031C1    E8 06073900     CALL <JMP.&Storm.#503>
6F0031C6    8B86 605D706F   MOV EAX,DWORD PTR DS:[ESI+6F705D60]
6F0031CC    83E8 00         SUB EAX,0                                ; 其实就是判断标志是否为0
6F0031CF    74 54           JE SHORT Game.6F003225                   ; 这里跳则写入的是REG_DWORD
6F0031D1    48              DEC EAX                                  ; 不为零则为1(表里只存了0和1)
6F0031D2    74 0B           JE SHORT Game.6F0031DF                   ; 跳这里,写入的是REG_SZ
6F0031D4    5F              POP EDI
6F0031D5    5E              POP ESI
6F0031D6    33C0            XOR EAX,EAX
6F0031D8    5B              POP EBX
6F0031D9    8BE5            MOV ESP,EBP
6F0031DB    5D              POP EBP
6F0031DC    C2 0400         RETN 4

上面的两个JE跳转,第一个跳则写入REG_DWORD项,第二个跳则写入REG_SZ项,没有写入REG_BINARY的跳转,跟着跳转向前走,一路小跑来到这里。

6F003245    8B45 08         MOV EAX,DWORD PTR SS:[EBP+8]             ; RegSetValueExA的参数五:数据体
6F003248    8B8E 585D706F   MOV ECX,DWORD PTR DS:[ESI+6F705D58]      ; ECX="resheight",参数二:子键名
6F00324E    50              PUSH EAX
6F00324F    6A 00           PUSH 0                                   ; 参数三:0
6F003251    51              PUSH ECX
6F003252    8D95 FCFEFFFF   LEA EDX,DWORD PTR SS:[EBP-104]           ; EDX="Warcraft III\Video"
6F003258    52              PUSH EDX
6F003259    E8 74063900     CALL <JMP.&Storm.#426>                   ; 还有一个CALL,进去
6F00325E    5F              POP EDI
6F00325F    5E              POP ESI
6F003260    5B              POP EBX
6F003261    8BE5            MOV ESP,EBP
6F003263    5D              POP EBP

下面又回到storm.dll,其中PUSH 4是关键,决定着写入的数据类型,可惜的是,只有4(REG_DWORD)和1(REG_SZ)两个过程,而且是写死并且共用的

15036228    8B55 10         MOV EDX,DWORD PTR SS:[EBP+10]
1503622B    6A 04           PUSH 4
1503622D    8D45 14         LEA EAX,DWORD PTR SS:[EBP+14]
15036230    50              PUSH EAX                                 ; 参数五:数据体
15036231    6A 04           PUSH 4                                   ; 参数四:REG_DWORD = 4
15036233    52              PUSH EDX                                 ; 参数三:0
15036234    8BD6            MOV EDX,ESI                              ; 参数二:子项名
15036236    E8 65FEFFFF     CALL storm.150360A0                      ; RegCreateKeyExA,并写入注册表项
1503623B    5E              POP ESI
1503623C    5D              POP EBP
1503623D    C2 1000         RETN 10

走完一遍,War3写注册表项的流程差不多清楚了,有个标志决定着此项是REG_DWORD或REG_SZ,然后跳转到相应的过程写项,由于标志只决定跳向哪个跳转,而非直接给RegSetValueExA的参数四ValueType传值,所以简单修改标志实现其中一项单独写入REG_BINARY类型应该是不可行的,只有把PUSH 1改成PUSH 3,使所有原来的REG_SZ项都变成REG_BINARY。再进入一次循环,找到此处

150361E2    56              PUSH ESI
150361E3    6A 03           PUSH 3                                   ; 把原来的PUSH 1改成PUSH 3(这个是改后的)
150361E5    50              PUSH EAX
150361E6    8BD3            MOV EDX,EBX
150361E8    8BCF            MOV ECX,EDI
150361EA    E8 B1FEFFFF     CALL storm.150360A0
150361EF    5F              POP EDI
150361F0    5E              POP ESI
150361F1    5B              POP EBX
150361F2    5D              POP EBP
150361F3    C2 1000         RETN 10

修改后,原来所有的REG_SZ项都变成REG_BINARY写入,后面多了一个字节0x00也就是字符串结束符,War3用的时候直接指针传参地址。修改完,中文名无论怎么取,退完魔兽再进都不会乱码了。

暴雪的代码写得够紧凑,导致无法单独修改其中一项的键值类型,程序是怎么存储这些信息的呢?

6F0031A7    8B8E 5C5D706F   MOV ECX,DWORD PTR DS:[ESI+6F705D5C]      ; 第二次进入循环,查看这里的内存地址,ESI应该就是数组(表)下标
6F0031AD    8B148D 405D706F MOV EDX,DWORD PTR DS:[ECX*4+6F705D40]    ; 发现其实是储存在Game.dll文件里的一个表

这里查看地址6F7060E0,可以看到Game.dll里的一个表,下面复制一小段

6F706110  00 00 00 00 C8 70 7E 6F D0 6D 7E 6F 02 00 00 00  ....萷~o衜~o...
6F706120  00 00 00 00 F8 6F 7E 6F C4 6D 7E 6F 03 00 00 00  ....鴒~o膍~o...
6F706130  01 00 00 00 F4 B4 84 6F B8 6D 7E 6F 03 00 00 00  ...舸刼竚~o...
6F706140  01 00 00 00 F4 B4 84 6F AC 6D 7E 6F 05 00 00 00  ...舸刼琺~o...

前四个字节是标志,为0则写入REG_DWORD为1写入REG_SZ,第二个四字节指向的地址存的是默认值(读注册表时用到),第三个四字节指向的地址就是子项名,第四个四字节标志着写入HKEY_CURRENT_USER\Software\Blizzard Entertainment\Warcraft III\下的哪个子分支,Video或者是String等。若要修改,右击,查看可执行文件,知道了静态地址就可以改了,可以修改某一项的数据类型,子项名,存放分支等,也可以把标志改成2以上的数字,这样War3退出时就不写入此项了。

[2022冬季班]《安卓高级研修班(网课)》月薪三万班招生中~

收藏
点赞0
打赏
分享
最新回复 (14)
雪    币: 371
活跃值: 活跃值 (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
hyp 活跃值 2009-1-6 08:32
2
0
...小鸡臂章
雪    币: 4656
活跃值: 活跃值 (11835)
能力值: (RANK:350 )
在线值:
发帖
回帖
粉丝
kanxue 活跃值 8 2009-1-6 09:36
3
0
欢迎波导终结者回来
雪    币: 386
活跃值: 活跃值 (17)
能力值: ( LV13,RANK:280 )
在线值:
发帖
回帖
粉丝
火翼[CCG] 活跃值 6 2009-1-6 09:44
4
0
确实好久不见, 终于复活了
雪    币: 435
活跃值: 活跃值 (43)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
ucantseeme 活跃值 2009-1-6 09:49
5
0
你这个是强文啊
雪    币: 212
活跃值: 活跃值 (10)
能力值: ( LV7,RANK:100 )
在线值:
发帖
回帖
粉丝
安摧 活跃值 2 2009-1-6 09:52
6
0
见识了,,,,,
雪    币: 424
活跃值: 活跃值 (145)
能力值: ( LV9,RANK:680 )
在线值:
发帖
回帖
粉丝
playboysen 活跃值 16 2009-1-6 10:21
7
0
波导终结者
隐居江湖数年之后复出

看来武林一场腥风血雨是少不了了
雪    币: 140
活跃值: 活跃值 (10)
能力值: ( LV9,RANK:180 )
在线值:
发帖
回帖
粉丝
波导终结者 活跃值 4 2009-1-6 12:48
8
0
很久没破解,现在几乎啥也不会,让各位老大见笑了,真是少壮不努力,更壮就徒伤悲啊
雪    币: 25609
活跃值: 活跃值 (3475)
能力值: ( LV15,RANK:3306 )
在线值:
发帖
回帖
粉丝
风间仁 活跃值 19 2009-1-6 15:04
9
0
学习一下,有空瞅瞅zmr的是咋回事,老让我3个字的中文ID变成...,不爽它很久了.
雪    币: 33
活跃值: 活跃值 (11)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
PEBOSS 活跃值 2009-1-8 08:10
10
0
怎么就这么牛 X 呢?
雪    币: 340
活跃值: 活跃值 (26)
能力值: ( LV6,RANK:90 )
在线值:
发帖
回帖
粉丝
lichsword 活跃值 2 2009-2-16 22:32
11
0
我从没想过对魔兽用OD,见世面了。。。
雪    币: 200
活跃值: 活跃值 (10)
能力值: ( LV4,RANK:50 )
在线值:
发帖
回帖
粉丝
123112 活跃值 1 2009-2-16 23:53
12
0
欢迎回来 :)
雪    币: 1901
活跃值: 活跃值 (1068)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
kagayaki 活跃值 2009-2-17 03:57
13
0
顶一下, 玩这个有时不知按了什么键, 就会自动退出游戏。。。。
雪    币: 12
活跃值: 活跃值 (10)
能力值: (RANK:210 )
在线值:
发帖
回帖
粉丝
Nooby 活跃值 5 2009-2-17 04:52
14
0
那个表应该是几个enum的联合结构吧。
jmptable是switch的产物,应该有一个注册表类处理相关的操作。
雪    币: 215
活跃值: 活跃值 (14)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
veninson 活跃值 2009-2-21 02:17
15
0
如果是没删干净 为什么我把注册表删了 还是...  万恶的WAR3
游客
登录 | 注册 方可回帖
返回