记录一次用 Olly Debug 解除 Strawberry Feels 的光盘加载检测(即制作 NoDVD 破解)的过程。
提示:养成良好习惯,做破解前先备份原始文件!
1、引言
在搜索“片雾烈火”(源头忘记是哪里了,这是推荐项)的简历的时候,发现她除了负责过 Strawberry Nauts 的插入曲(实际上就是 ED 啦),还负责过一个名为 Strawberry Feels 的 gal game 的 OP。看简介,SF 也是一个纯爱系作品。不过这个在国内太小众,连百度的自动搜索推荐都只有 SN,而且搜索结果第一页中居然没有多个下载地址或者评测什么的。看来我大 HOOKSOFT 简直登上了纯爱白糖系的顶峰啊。我准备不下载的时候,橙光上的一个帖子宣称:
这游戏的立绘真是不咋地……
不过亮点是超清BG,基本每张BG都有1280×720和2560×1440两个分辨率的版本。
另外还包含少量看着还不错的战斗效果图。
嗯,看来挺有意思的。冲着超清 BG,看一看。旅程就这么开始了。
2、获取 Strawberry Feels
前面说过,这游戏比较小众,所以找到一个下载源还是不是一下就行的。百度搜索的结果中,最正常的就是贴吧里的资源贴了——不过早就失效了。
OK,我们换一个思路。请问哪里还有比较靠谱的发布站?除了各 PT 站,最好的恐怕就是苍雪(绯月)和 2DJ 了吧。由于我并不是 KFB 大富豪,就选择去 2DJ 碰运气。好了,关键点讲完了,这一步剩下的内容请同学们自己尝试一下,就当做一下锻炼吧。
什么,找不到?再给出一个提示:2DJ 的搜索功能其实并不能满足我们的需求(Discuz 自带的帖子搜索表现一向很糟糕)。再换一种思路,2DJ 的游戏发布有两个区,新作区和历史区,但是不管哪一个都是基本上按照时间排序的。同时,纯爱系的更容易受到关注,出现在新作区的概率也更大。已知 SF 的发行时间是2012年3月30日。
什么,还是找不到?好吧,这里是链接。页面中的磁力链接用115离线下载下来就行了。
好了,虽然那帖子标题是“初回限定版“、“NoDVD添加”,不过估计放到了“更多资源”里了。那可是会员的世界,我这个渣渣连注册都没办法(“請找認識的現任會員朋友在站務區置頂帖推薦”,我哪里认识谁啊),只下载了公开的磁力链接的内容。内容就是一个压缩包,初回特典都没有呢……
2DJ 上的吐槽:
看到标题我还以为是Strawberry Nauts的FD兴奋了一下……
我才不说我也是呢……
3、运行测试
下载安装,安装的时候不要用转区工具,直接安装。路径会出现乱码不要管,因为卸载的时候并不会通过转区工具运行。
安装完毕后我们来运行一下。

我怎么知道它在说什么!不过根据在 gal game 上积累的经验(咳咳),肯定是编码错误啦。
先下载 Locale Emulator。(在此澄清一下,在以前的博文中我曾经说那位是 LE 的作者,其实内核是 Amano 写的。)SF 好像不吃 AppLocale 的那一套,运行后只创建了进程,没有出现窗口。安装了 LE 后,用 LE 启动 StrawberyFeels.exe。
好的,能正常加载了,映入眼帘的是另一幅景象:

(屏幕提示是“请将产品光盘放入光驱中”的意思。)
嗯,看来它有一个光盘插入验证。不觉得很麻烦吗,每次玩之前都要用虚拟光驱软件加载光盘。
终于进入正题了:我们要将光盘验证去掉。
4、调试分析
在开始调试之前,请想清楚:如果你是程序设计者,你会用什么方式做光盘插入验证?要是我的话,肯定会对驱动器进行遍历,然后检查特定目录有没有特定的文件——这是最土、实现最简单的方法。(如果用其他的,例如调用底层函数直接读驱动器进行散列的话,就麻烦了。)
当然,已知以上思路的时候,就知道了关键函数:CreateFile()/CreateFileEx();不过有时候直接使用 fopen() 的话,就得下到 NtCreateFile()、ZwCreateFile() 和 IoCreateFile() 了,以上的调用是在 CRT 内进行的,跟踪起来很困难。辅助函数自然是 GetDriveTypeA()/GetDriveTypeW(),要不你怎么知道用户将光盘放入了光盘驱动器中呢?检查函数调用,正常的话只有一个(安装目录已经有了资源的拷贝,为什么还要去读光驱),这就是要 crack 的地方。
我在操作的时候没想过要用上面的函数,而是从是验证哪个文件的存在入手的。因此,我在 Olly Debug 中使用 Search for/Binary string(Ctrl+B)功能找了几个我认为关键的文件,不过都没找到匹配的结果。(后来证明,文件名是从资源中读取,然后 sprintf() 上去的。)
我突然想到,在发现没有光驱的时候,会弹出一个消息框。于是,现在的焦点放在弹出消息框的函数上了。问题又来了:请问是哪一个呢?
MessageBoxA();MessageBoxW();MessageBoxExA();MessageBoxExW();MessageBoxIndirectA();MessageBoxIndirectW()。
熟悉 Windows 编程的同学也知道,MessageBox()、MessageBoxEx() 和 MessageBoxIndirect() 是宏定义,在 UNICODE 这个宏定义了和没定义的时候分别被映射到 ANSI 和 UNICODE 的版本。
首先,5和6概率很小,显示一个简单的警告用不着自定义消息框吧。再结合之前提到的乱码问题,如果用了 UNICODE 的版本的话,字符串被处理之前都要转换成统一的 UNICODE 编码,是不会出现乱码的(见 KiriKiri 2/KAG3,内部使用的就是 UNICODE,不需要担心编码问题,不过文件编码是限定的)。考虑到程序员偷懒的天性,不用 MessageBoxEx() 的时候就不用(因为有附加参数啊),那么答案就是:显示消息框采用的很可能是 MessageBoxA()。
正是因为采用的都是 ANSI 版本的 API,所以字符串在解读时都会用本地字符集翻译——在日文平台上是 Shift-JIS,在中文平台上是 GBK。自然,二者在高码段不兼容,就会出现乱码了。所以我猜刚才看到的乱码大概是想说“本游戏不能在日本以外的国家启动”——可惜弄巧成拙,其他国家的用户一般也不用日文平台,看到的就会是乱码,永远都不会知道那是什么意思。
我们继续。打开名称表窗口(Search for/Names,Ctrl+N)。可以看到,果然导入的是 MessageBoxA()(类型为 Import):

找到这一项了,就查找调用点(Find references,Ctrl+R):

好了,这么多项,请问哪一个是我们需要的呢?
不知道,只好一个一个试了。_(:з」∠)_
值得注意的是,所有的调用都间接的,通过 apphelp.dll 的一个未公开的函数。看接受的参数,恐怕是 assert() 一类的,某个函数调用失败时能显示一个错误。
结果是,位于 00403AFD 的那一项是我们需要的。展开看看有什么:
1 | CPU Disasm |
在 Olly Debug 中,这一段是当前函数的第二部分,很明显是一个整体。之所以说它重要,是因为这里:

在 00403B2C 处,有一个对 PostQuitMessage() 的调用——这个调用说明要发送一个 WM_QUIT 消息,准备终止消息泵了。什么时候要终止消息泵呢?在无法在脚本中处理(意味着脚本引擎没有初始化)的时候,或者正常退出的时候。再看 00403A85,一个对 GetDriveTypeA() 的调用!00403A9A 的参数 "rb" 肯定是传递给 fopen() 或者类似物的,而且被硬编码了(一般就是这么写的嘛)。看来这里就是要找的了。
根据这个循环(00403A62 到 00403AD2)和接下来的函数,我们可以知道这里的逻辑:循环检测各个驱动器是否是光盘驱动器,然后检查是否有需要的文件;如果26个可能的位置都没有,那就说明没插入光盘,显示提示,用户选择“取消”的话就准备退出。
有兴趣的同学可以看看这往前一点点的内容,看看 sprintf() 的调用参数,就在不到20行之前。
于是我们现在要看看跳到哪里。这里写成伪代码大概是这种形式(为什么是这种别扭的形式……汇编的代码啊):
1 | char root_path[4] = "a:\\"; |
可以看到,关键跳转的地方是找到了光驱和验证完成时的跳转。可以在 00403AC0 找到一句 JNE SHORT 00403B39,这是往后跳出了整个循环的,对应的就是那个 break;。
看看前后,可以确认 00403B39 的位置是那个 DoSomeWork() 的调用,之后是 return; 所编译出的恢复环境的代码了。

另外 00403B34 处的那个调用是在干什么?翻开一看居然调用了 GetCurrentProcess() 和 TerminateProcess()……我说你们真狠,连一个体面的结束都不给,至少也得返回一个验证失败然后逐层退到入口函数来退出,手动回收各种资源啊,不要用终止进程的方法偷懒啊!
看 00403B3B 的代码的时候,发现了“Majiro”的字样。一查,原来就是这引擎的名字。此外,澄空上已经有人分析了一些。
好了,分析了这么多,我们知道了:只有检测到不正常的情况下,才会调用 TerminateProcess() 直接终止进程,否则什么都不做。
原型估计是这样的:
1 | void EnsureDiskIsPlacedIn(); |
调用估计是这样的:
1 | void OnLoad() { |
5、代码修改
说了这么多,大家应该意识到了,我们直接将这个控制逻辑短路了不就好了?我们往里面放一个 JMP 跳转不就行了?
在32位环境下,我们要放的是一个 JMP(空间限制,无法 JMP SHORT)长度是5字节(1字节操作码和附加信息+4字节操作数),于是我们要找一个合适的容器放下这个指令。我选择的就是位于 00403A5A 的这一句:
1 | MOV DWORD PTR SS:[ESP+10], 0 |
明显是一个对局部变量赋值的语句。可以看到,长度是8字节,满足要求。于是使用汇编功能(空格或双击指令),写入 JMP 00403B39,选上“保持长度”(Keep size)和“用 NOP 填充”(Fill rest with NOPs)。汇编,写入,可以看到多余的3个字节被 NOP 指令填充了。然后保存(Edit/Copy to executable,然后 Save file)到一个新的文件(如 StrawberryFeels_cracked.exe)。
然后再用 Locale Emulator 启动。发现了什么?

附记:
我以前看过一点汇编,Win32 汇编程序编写。当时也不太清楚什么道理。不过近年来我通过 C/C++ 对运行机制有了一点了解,加上本学期的微机原理补充了16位汇编的基础,这次我能看懂多一些代码了。问学的16位看32位是否有困难——我想说,原理是一样的,如果死揪课本里的那些东西当然会有困难(有同学考前死背“JMP 指令占3字节”,但是却不了解指令结构,看到32位下的5字节会很惊讶的吧),但是理解了实质后应用那就是指哪儿打哪儿。
试玩了一下,立绘的确不怎么样,游戏系统也是十分糟糕(操作居然都放到了 Windows 菜单里!)。
我没破解区域验证,是因为现在还没人做这个东西的汉化,破解了也没意义——本来就是 Shift-JIS 的内容,用 ANSI 版 API,你却硬要做区域破解,系统自然会按照 GBK 来读;到时候显示在眼前的就是满屏的乱码了。反正我有 LE,能玩。
要问为什么菜单、安装的对话框没有乱码(但是获取安装路径时有)?那是因为使用了资源模板啊!资源区域应该是设为了 1041(日语-日本)了吧!
第一次独立破解,做 NoDVD 的教程什么的也没有,摸索摸索走出一条路。不过确实有大神做用于汉化的破解教程的。原理差不多,我觉得,搜索调用点,修改、短路。因为我读过,也觉得自己有一点储备,所以才有信心认为能独立实践一次。Olly Debug 也是第一次用,感觉真是十分难用,也十分强大。
呜呼,原来以为只需要写一小时的东西,来来去去写了快三个小时,从00:22到03:15。没事,我还能躺30分钟,在起来准备东西,然后和大家四点半集合赶七点的火车……但是我本来预计要在车上看工艺文档的说,估计要全程睡觉了……