-
-
[原创]解决进退魔兽争霸3中文名乱码问题
-
发表于:
2009-1-6 02:28
29908
-
前半部分是之前给某网站写的稿,面向一般人,没有什么技术含量,把文字复制过来这里一份,大概了解一下便可。
------------------------------------------------------------------------------------
曾几何时,在魔兽里取一个中文名是一件很拉风的事情,因为知道方法的人少。但任何事情传开了之后,也就变得草根了,比如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退出时就不写入此项了。
[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课