大家好,好久不见,我是某昨。
复习之余摸鱼「さくら、もゆ。」,结果遇到了这样的问题:

之前还没在意,打开一看果然如此:

字体列表空绝对很奇怪吧!但是咕咕噜上没有任何类似的问题反馈。
ToC
初期调查
字符串
既然是字体相关的问题,那首先想到的就是去找字体。我上来先是在 Strings Window 里搜索了 Font:

其实这里最合我胃口的是 .?AVFontList@@,但用 X 找不到引用。又看了看其他的那几个,都没什么有价值的东西。
Imports
字符串无果之后,我又开始寻找新的切入点。下一个切入点是 Imports 列表,同样是在这里搜索 Font:

可以看到有两个 GDI32 的函数,而最引人注意的就是第二个。Enum 看上去就很遍历,那是不是这个呢?
EnumFontFamiliesExA
首先是看这个函数的调用。通过 X,我们找到了 sub_443BC0,再用 Tab:

在了解了传入参数之后,接下来就是查文档的时光了。在 MicroSoft Docs 上,我找到了这个函数的文档[1]:
int EnumFontFamiliesExA( HDC hdc, LPLOGFONTA lpLogfont, FONTENUMPROCA lpProc, LPARAM lParam, DWORD dwFlags);可以看到,第三个参数是 lpProc,对应的是 EnumFontFamExProc 类型。而去看 EnumFontFamExProc 的文档,我们发现这其实是一个回调,对应这个回调的就是上面第 13 行的 Proc。
EnumFontFamExProc(Proc)
首先还是查文档[2]:
int CALLBACK EnumFontFamExProc( const LOGFONT *lpelfe, const TEXTMETRIC *lpntme, DWORD FontType, LPARAM lParam);同时进入 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 而言,编码显然虽然是大多数问题的根源,但却不是导致所有问题的罪魁祸首。导致问题的核心是语言本身的差别。
当然了,在研究之前我们是不知道这一点的。我只是单纯地将问题怪罪于 ShiftJISShiftJIS 确实不行
调试方式
在正式介绍之前,迫于 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