大家好,好久不见,我是某昨。
复习之余摸鱼「さくら、もゆ。」,结果遇到了这样的问题:
之前还没在意,打开一看果然如此:
字体列表空绝对很奇怪吧!但是咕咕噜上没有任何类似的问题反馈。
ToC
初期调查
字符串
既然是字体相关的问题,那首先想到的就是去找字体。我上来先是在 Strings Window
里搜索了 Font
:
其实这里最合我胃口的是 .?AVFontList@@
,但用 X
找不到引用。又看了看其他的那几个,都没什么有价值的东西。
Imports
字符串无果之后,我又开始寻找新的切入点。下一个切入点是 Imports
列表,同样是在这里搜索 Font
:
可以看到有两个 GDI32
的函数,而最引人注意的就是第二个。Enum
看上去就很遍历,那是不是这个呢?
EnumFontFamiliesExA
首先是看这个函数的调用。通过 X
,我们找到了 sub_443BC0
,再用 Tab
:
在了解了传入参数之后,接下来就是查文档的时光了。在 MicroSoft Docs
上,我找到了这个函数的文档[1]:
可以看到,第三个参数是 lpProc
,对应的是 EnumFontFamExProc
类型。而去看 EnumFontFamExProc
的文档,我们发现这其实是一个回调,对应这个回调的就是上面第 13 行的 Proc
。
EnumFontFamExProc(Proc)
首先还是查文档[2]:
同时进入 Proc
,我们来看伪代码:
先看这几个参数吧。a1
是 LOGFONTA
类型的指针,对应的是字体的信息;a3
是字体类型。
接下来就是这个 if
的判断了。if
判断有三个条件,我们逐个来看。
a3 & 4
我们已经知道,a3
对应的是字体类型,而从文档中,我们找到了更加详细的内容:
从一般角度来考虑,DEVICE_FONTTYPE
应该是 1
,RASTER_FONTTYPE
应该是 2
,TRUETYPE_FONTTYPE
应该是 4
,这样才方便通过 AND
进行比对。于是这个判断条件的含义就是:字体类型为 TrueType
。
a1->lfFaceName[0] != 64
这个就比较简单了。ASCII
码的 64
对应的是符号 @
。其实看到这个符号的时候就已经能够明白了(笑),这是防止列表中加入竖排字体的选项。
!strcmp(&a1[2].lfFaceName[8], (const char*)&unk_45E560)
这是最后也是最难判断的条件了。首先我们发现,他使用的是 !strcmp
,也就是期望两个字符串相等:
第一个字符串是从下标 8 开始到结束,而第二个字符串看上去像是一串没有意义的数据:
鉴于这是字符串,我们先把它导出:
然后尝试用 VSCode
打开:
干脆利落地乱码了。这时候我们选择用 ShiftJIS
重新打开文件:
破案了。
综上
综上,我们明白了这三个判断条件的意义。用一条注释来概括吧:
解决方案 A
在知道了问题的原因之后,解决起来就简单多了。这里用了最粗暴的方式,直接把这个 !
删掉了。修改的部分在这里:
对应的 IDA View B
是这样的(把 jnz
替换成了 jz
):
对应到伪代码就是少了个 !
:
这样就可以正常显示字体列表了:
解决方案 B
上面这种方案虽然直接运行没什么问题,但是还是略显粗暴了。而且还有一个关键的问题:会导致 Locale Emulator
无法正常工作,没法启动的同时在游戏目录下面给你塞一个 core dump
。于是这次借着这个不完美的机会,我决定来探究一下这个问题的本质。
我们知道,这个问题源于不同语言的 Windows 对某些内容的处理不尽相同。对于 Windows 而言,编码显然虽然是大多数问题的根源,但却不是导致所有问题的罪魁祸首。导致问题的核心是语言本身的差别。
当然了,在研究之前我们是不知道这一点的。我只是单纯地将问题怪罪于 ShiftJIS
ShiftJIS
确实不行
调试方式
在正式介绍之前,迫于 IDA 7.0
调试的严重 bug,我不得不在这里提一句解决方案。
对 IDA 7.0
,直接 F9
是无法正常动态分析的,会出现 internal error 1491
,进而导致 IDA
崩溃。
唯一的解决方案是通过远程调试,即通过 127.0.0.1
下的 Remote Debug
来曲线救国。进入 IDA
安装目录,打开 dbgsrv/win32_remote.exe
文件,然后再 IDA
里设置远程调试的 IP
为 127.0.0.1
:
然后就可以正常调试了。
比较失败?
既然问题出在 strcmp
上,那也就自然而然地有了第二种解决问题的灵感:把 ShiftJIS
出问题的「日本語」替换掉。于是,我们希望知道和「日本語」(ShiftJIS
版)作了比较的原文究竟是什么。
要探究这个问题,我们就必须要回到汇编,而不能单纯依靠 HexRay
了。unk_45E560
附近的代码是这样的:
可以看到,在 loc_443B43
下第二行就是 cmp
,比较的是存有 unk_45E560
地址的 ecx
的内容和 [eax]
。eax
是通过计算得出的,值相当于 arg_0+9Ch
,对应的就是伪代码中 [2].lfFaceName[8]
的部分。
于是我们尝试在 cmp
这行打上断点,看看 [eax]
里究竟存了什么:
F9
运行:
切换到 Hex View
:
解决
解决这个问题的方式是修改 unk_45E560
的值。鉴于新的值比旧的要短,我们只需要在多出来的地方补 \0
就可以了:
至此,不优雅的方案 A 就可以抛弃了。
解决方案 C
上面这种方案使用的思想从根本上解决问题,但对于 Locale Emulator
而言,仍然是不可用的。LE
本身存在兼容性问题不说,也不能完全解决所遇到的问题,导致对于各个不同游戏实现而言,存在着大量的兼容性问题。
解决方案 C 不是对解决方案 B 的修正,它只是另一种工作环境下的 Polyfill
罢了。可以说,方案 C 是方案 B 在 LocaleEmulator
环境下的兼容版本。那究竟该如何兼容呢?
调试方式
这里还是要插播一条调试新闻,不过这次不是 IDA
的问题了,而是 LE
的。
首先我们知道,我们需要通过 LE
启动游戏才能达到转区的效果,但这个“启动”的过程对于 IDA
而言就不是那么好办了。我们希望的是游戏启动时的状态是挂起,这样我们才能把调试器 Attach
上去。不过好在 LE
提供了这个功能:
通过这个功能我们终于可以附加到进程了。不过这还不够,无论是 IDA
还是 OD
,都没办法正常调试,我们来看解决方案。
首先是 Attach
上去之后的效果:
按 F9
继续运行,等到 Threads
只有一个时(大约要等一到两分钟),我们会发现 Sakura.exe
进程不知道为什么又被挂起了。这时使用系统自带的资源管理器恢复进程:
就可以正常调试了。
分析
我们跟方案 B 时候一样,把断点打在 cmp dl, [ecx]
处:
这时候来看 [eax]
的内容:
我们发现,[eax]
的内容是 93FA3F
,对应的东西不知所云。但其实,我们只需要通过 VSCode
就可以明白这串文本的来历了。
复现
打开 VSCode
新建文件,将编码更改为 GB2312
,然后输入“日语”二字。
保存文件,然后将编码更换为 ShiftJIS
:
最后通过 hexdump
插件打开文件:
我想,我们已经找到答案了。
解决
这个解决方案也很简单,只需要将解决方案 B 中的 C8 D5 D3 EF
修改为 93 FA 3F
就可以了。
思考
不难想到,这里是 LE
把对应的 GB2312
转成了 ShiftJIS
。但 ShiftJIS
里并没有简体中文的“语”,于是导致这串内容不仅无法通过匹配,还变成了只有一半内容正确的乱码。也正是由于这个原因,方案 B 才无法对 LE
起效。
写在最后
嘛,总之问题是解决了,太好了(
文件存了一份在 GDrive
,链接是这个w