Skip to content

「さくら、もゆ。」的空白字体列表——一次逆向问题定位过程实录

Published: at 21:04

大家好,好久不见,我是某昨。

复习之余摸鱼「さくら、もゆ。」,结果遇到了这样的问题:

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

字体列表空绝对很奇怪吧!但是咕咕噜上没有任何类似的问题反馈。于是借着这个机会,把 Notion 最小化,我开始摸鱼了(

ToC

初期调查

字符串

既然是字体相关的问题,那首先想到的就是去找字体。我上来先是在 Strings Window 里搜索了 Font

其实这里最合我胃口的是 .?AVFontList@@,但用 X 找不到引用。又看了看其他的那几个,都没什么有价值的东西。

Imports

字符串无果之后,我又开始寻找新的切入点。下一个切入点是 Imports 列表,同样是在这里搜索 Font

可以看到有两个 GDI32 的函数,而最引人注意的就是第二个。Enum 看上去就很遍历,那是不是这个呢?

EnumFontFamiliesExA

首先是看这个函数的调用。通过 X,我们找到了 sub_443BC0,再用 Tab

在了解了传入参数之后,接下来就是查文档的时光了。在 MicroSoft Docs 上,我找到了这个函数的文档[1]

1
int EnumFontFamiliesExA(
2
HDC hdc,
3
LPLOGFONTA lpLogfont,
4
FONTENUMPROCA lpProc,
5
LPARAM lParam,
6
DWORD dwFlags
7
);

可以看到,第三个参数是 lpProc,对应的是 EnumFontFamExProc 类型。而去看 EnumFontFamExProc 的文档,我们发现这其实是一个回调,对应这个回调的就是上面第 13 行的 Proc

EnumFontFamExProc(Proc)

首先还是查文档[2]

1
int CALLBACK EnumFontFamExProc(
2
const LOGFONT *lpelfe,
3
const TEXTMETRIC *lpntme,
4
DWORD FontType,
5
LPARAM lParam
6
);

同时进入 Proc,我们来看伪代码:

先看这几个参数吧。a1LOGFONTA 类型的指针,对应的是字体的信息;a3 是字体类型。

接下来就是这个 if 的判断了。if 判断有三个条件,我们逐个来看。

a3 & 4

我们已经知道,a3 对应的是字体类型,而从文档中,我们找到了更加详细的内容:

从一般角度来考虑,DEVICE_FONTTYPE 应该是 1RASTER_FONTTYPE 应该是 2TRUETYPE_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。虽然 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 里设置远程调试的 IP127.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 就可以了。

于是我们又多了一个 Sakura.exe

思考

不难想到,这里是 LE 把对应的 GB2312 转成了 ShiftJIS。但 ShiftJIS 里并没有简体中文的“语”,于是导致这串内容不仅无法通过匹配,还变成了只有一半内容正确的乱码。也正是由于这个原因,方案 B 才无法对 LE 起效。

写在最后

我明明只是想摸一会鱼,为什么会这样呢

嘛,总之问题是解决了,太好了(

文件存了一份在 GDrive,链接是这个w