记一次 GameMaker 游戏逆向

记一次 GameMaker 游戏逆向

最近发现一款游戏 心流小筑(Chill Pulse),本质上是一个帮助专注的游戏,有播放音乐,番茄钟等等的功能,但是截至写这篇文章的时候,游戏还没有添加音乐的功能,只能播放内置的几首歌曲,对我来说较为不方便,所以尝试进行一下逆向。 相关技术搜索 先大致浏览了一下游戏目录,除了一堆音乐ogg文件以外,

最近发现一款游戏 心流小筑(Chill Pulse),本质上是一个帮助专注的游戏,有播放音乐,番茄钟等等的功能,但是截至写这篇文章的时候,游戏还没有添加音乐的功能,只能播放内置的几首歌曲,对我来说较为不方便,所以尝试进行一下逆向。

相关技术搜索

先大致浏览了一下游戏目录,除了一堆音乐ogg文件以外,可以看到 data.win 文件,查找了一下发现是GameMaker游戏引擎的东西,因此可以确认这个游戏是GameMaker开发的。

先大致在互联网上查询了一下GameMaker mod制作相关的项目,发现有以下几个项目:

UndertaleModTool

GMML

GS2ML

根据进一步研究发现,UndertaleModTool主要是为Undertale的mod开发做支持,但是他理论上支持所有GameMaker的游戏。

GS2ML是一个基于UndertaleModTool的GameMaker Studio 2 mod开发框架,它主要用到了UndertaleModTool中的反编译器和编译器。

GML语言

通过UndertaleModTool的GUI工具,可以轻松的解包 data.win 并提取其中的资源,这边主要关注游戏的GML脚本语言。

可以看到,游戏的音乐都是在这个函数中定义的, album_list 存放ogg文件名(GameMaker音频资源名), album_name_list 存放实际显示的名称。

添加自定义音乐

理论上来说,只需要通过 GS2ML 在游戏启动前加载我们自己的音频 ogg 资源,再添加到这个列表就行了。幸运的是,GS2ML有充分的Hook支持和AddSound函数,正好可以满足我们的需求。

样例代码如下:

data.HookFunction("all_music", "#orig#()\narray_push(album_list[0], mymusic)\narray_push(album_name_list[0], \"My Music\")");

为自定义音乐单独添加一个分类

上面的代码是将自定义音乐追加到第一个分类(即 Lofi 这个分类)的,为了完美主义,我想再添加一个音乐分类。

再经过查找可以发现,音乐分类的定义在一个 _music_genre 列表中,而且并不在一个独立的函数中,而是被存放于 gml_Object_obj_controller_Draw_64 这个对象的脚本中,这个脚本反编译后一共有3000行,较为棘手。

我尝试直接将反编译后的代码复制到hook的代码里面,但是可能是因为 UndertaleModTool 本身的编译器和反编译器具有缺陷,导致游戏不稳定,使用特定功能会直接导致游戏崩溃,只能另辟蹊径。

可以发现,GML语言编译的结果是字节码,所以我想到直接向原函数中插入字节码,来实现对 _music_genre 的修改。

以下是原 _music_genre 的定义的字节码表示:

理论上只需要多加几行 push.v 字节码,并将 call.i 后面的 argc 修改就好了。

这里发生了一点小插曲,因为我修改之后游戏会直接崩溃,后面发现是因为这个函数中的一些 goto 字节码,在插入代码之后的 JumpOffset 错位,所以需要额外修正。

以下是实现这个功能的部分代码:

var newGenres = new List<string>();
newGenres.Add("Example");

var draw = "gml_Object_obj_controller_Draw_64";
var code = data.Code.ByName(draw);
var codeLocals = data.CodeLocals.ByName(draw);

AsmCursor cursor = new AsmCursor(data, code, codeLocals);

cursor.GotoNext("pop.v.v local._music_genre"))
cursor.index -= 1;
cursor.Replace($"call.i @@NewGMLArray@@(argc={6+newGenres.Count})");
cursor.index -= 6;
foreach( var genre in newGenres)
{
    cursor.Emit($"push.v self.{genre}");
}
var cur = cursor.GetCurrent().Address;
for ( int k = 0; k < code.Instructions.Count; k++)
{
    var inst = code.Instructions.ElementAt(k);
    if (UndertaleInstruction.GetInstructionType(inst.Kind) == UndertaleInstruction.InstructionType.GotoInstruction)
    {
        if (inst.Address < cur && inst.Address + inst.JumpOffset >= cur)
        {
            inst.JumpOffset += 2 * newGenres.Count();
        }
        if (inst.Address >= cur && inst.Address + inst.JumpOffset < cur)
        {
            inst.JumpOffset -= 2 * newGenres.Count();
        }
    }
}

成果展示

LICENSED UNDER CC BY-NC-SA 4.0
Comment