作者论坛账号:ltr0030
虽然很早注册了论坛账号,但都没怎么实际进行过逆向。这次动手逆向一次某游戏mod文件加密逆向过程,在此分享和大家交流学习一下。
该游戏为零几年发布的游戏,官方早已不再更新。游戏目前全靠社区制作mod维持活跃。
一个 mod 由一个 .skudef 描述文件以及一个 .big 资源文件组成。skudef描述文件为纯文本文件,描述了 mod 所需版本号以及所需加载的 big 文件;big 文件为二进制文件,是 mod 代码、图片、音频等文件编译后打包的结果。big 文件可以通过官方工具解包提取其中的音乐和美术资源。
游戏本体有两个可执行文件,一个为游戏启动器,一个为游戏本体。通过在游戏启动器中输入参数,启动器便会将要运行的 mod 以命令行的形式传给游戏本体,从而实现加载 mod 。
例如只需要在游戏启动器中输入 -modConfig d:\rael_1.0.skudef
即可启动上图mod。
co**na 是该游戏由爱好者开发的 mod ,该 mod 完成质量高,在游戏圈内传播广。游玩 co**na 必须通过其网站下载专用的启动器。co**na 由启动器下载真正的mod文件,然后通过专用启动器才可以运行mod。
为什么玩 mod 还需要专用启动器?不用这个启动器不能玩吗?我带着这个疑问,先分析下这个启动器是如何运行游戏的。
运行游戏发现, co**na 的专用启动器是通过执行游戏自带的启动器,然后再启动游戏的。
游戏自带启动器支持直接修改传入的命令行参数,很容易发现 co**na 的专用启动器也是通过传入 -modConfig
命令行参数来运行的。由于 -modConfig
命令行参数后面紧跟的便是 skudef 文件的路径,可以找到 co**na 真正的mod保存文件夹。
找到了真正的 mod 文件后,理所当然的,便尝试以普通 mod 的方式启动它。但奇怪的是,进入游戏后,无事发生,mod没有被启动。
好奇怪,这mod还有什么黑魔法不成?
通过观察发现,该mod的数据文件并不是游戏规定的big文件,而是一个没有听说过的格式:lyi。如下图:
会不会只是后缀名被修改了?先拖进hex查看器中先看看情况。
这是big文件的情况:
可以很容易看出文件以 big4 标识开头。
这是lyi文件的情况:
可以发现文件头与big文件完全不同,没有任何有用信息(看不出文件头)。
好家伙,格式完全不一样,怪不得我自己启动启动不了,这lyi文件游戏不认识啊。这可真是奇了怪了,他的启动器又是怎么让游戏加载的呢?
游戏本体不是开源的,游戏本身的资源加载解析逻辑很难被修改,所以必然是再启动游戏过程中某些过程被mod专用启动器动了手脚。当前便是要找出这个动手脚的地方。
由于启动游戏涉及了三个可执行文件,首先通过Process Monitor看看这三个文件再启动游戏的过程中是哪些进程读取了lyi文件。
首先添加过滤规则,防止事件过多难以阅读:
然后通过mod专用启动器启动游戏,开始捕获直到进入游戏主界面。
如图,_launcher.exe便是mod的专用启动器,_12.game是游戏本体,游戏自带的启动器并没有出现在捕获结果中。
通过捕获的内容发现,mod专用启动器只是打开lyi文件后查询了文件的信息,并没有读取文件内容,也没有修改文件内容。游戏本体则是打开lyi文件后进行了大量的读取,也没有修改lyi文件的内容。
这可真的是奇了怪了,好像游戏本体正常读取了lyi文件,mod专用启动器什么其他的事情也没有做。
于是可以断定,游戏本体exe被动了手脚。一个合理的猜想于是在我的脑海中诞生了,会不会是exe被dll注入了,加入了读取lyi文件的功能,才会出现上面的结果?
这很好验证,直接双击Process Monitor中的一条事件,弹出事件详情窗口,第二个进程选项卡中便列出了此时进程所加载的dll。甚至都不用打开Process Explorer。
点击按path排序一下,映入眼帘的第一个便是一个叫lyi.dll的文件,文件目录正好在mod专用启动器的安装包下。这文件名起的,就是它了。
确定了目标后,接下来便是分析lyi.dll是如何在游戏进程内实现读取lyi文件的功能的。
通过上面的捕获结果可以发现,游戏反复读取了lyi资源文件(仅仅在加载游戏到进入游戏主界面,便有足足一万多次读取),并且在事件详情中也可以看出每次读取的文件位置和文件大小都是不相同的。如图:
我想,如果是将lyi文件直接读入内存,然后直接全部解密,不应该出现这样的调用。并且,mod资源文件本身就有近2GB大小,直接读入内存不是这个32位的游戏可以承受的,不会是这个方法。
分块读入解密也不太可能,因为每次读入的文件位置有前有后,读入的大小相差巨大(从几个字节到几个mb都有),如果是分块解密,应该是按顺序每次读入相同大小的内容,现在的情况反倒是像真的在读取游戏资源一样。
会不会是lyi.dll hook了游戏本身的读取资源函数,然后在游戏正常读取完内容后,再对内容进行修改呢?于是,我观察了ReadFile函数的调用栈,果然,ReadFile函数都是从游戏本体exe中发出的,而非lyi.dll。
如果是这样的话,lyi文件肯定是和原始的big文件一样大了。因为游戏本体并不知道自己读取的是lyi文件,还是按照big文件的偏移和大小来读取,那么如果lyi文件和原始的big文件不一样大,一切都乱套了。所以我猜想,lyi文件必然是在原始的big文件上对每一个字节经过某种运算得到的,比如说经典的xor。
猜到了lyi.dll的大致做法,那么下面寻找解密代码就简单了。
打开x64dbg,选择32位版本,先将调试器附加到正在运行的游戏上,在symbols界面找到kernel32.dll,搜索File函数,将ReadFile与ReadFileEx断点。
因为游戏在运行中不一定会读取资源,但在启动时一定会,所以重新启动游戏。
此时便会遇到一个问题,由于游戏加载的文件很多,并不只有mod资源文件,所以ReadFile的断点命中后,不一定就是我们所需要的,于是我这里用了一个笨方法:游戏启动时就需要播放mod资源文件中的启动视频,所以mod资源文件必定很早就被加载,每次命中断点后,我就在handles界面中搜索lyi文件的句柄是否已经存在,如果存在就记下它,并对ReadFile
和ReadFileEx
断点添加条件:
这里句柄值是470, ReadFile
的第一个参数是文件句柄,第一个参数位于esp + 4
的位置,于是添加条件 [esp + 4] == 470
。
接下来,ReadFile
中断便全是在读取lyi文件了。
ReadFile
的第二个参数是缓冲区,第三个参数是读取的长度。因为资源文件很大,所以解密大概率是就地修改,所以在缓冲区指向的内存上打上写断点。
继续运行,代码停留在如下位置
看得出来,此处是一个循环内部,通过观察与反复的单步执行发现:
ecx
正好指向的是读取缓冲区,并且每次循环都会加一。
esi
在循环开始的值与ReadFile
读取的长度相同,并且每次循环减少1。
循环体每循环一次,缓冲区便会修改一个字节,并且当读取的是lyi文件的文件头时,缓冲区会被修改为以 big4 开头。
看来,这就是解密代码。
解密的核心代码便是这一段
继续单步执行反复观察,可以注意到看到寄存器窗口出现了奇怪的字符串
该字符串的地址为 eax + edi + 0x0c
,直接跟进内存,于是发现了一个长度为512的字符串
可以发现,当eax
为0时,edi + 0x0c
正好指向了当前字符串的开头。并且无论哪一次读取lyi文件,程序停在此处,edi + 0x0c
都指向该字符串。看来,这就是解密用的字符串了。
那么eax便是解密字符串当前使用的字符下标了,从 and eax,1FF
这句代码也可以印证这一点,解密字符串长度为512,0x1ff为511,正好可以防止下标溢出。
那么eax
的值是如何确定的呢?继续向上看,eax
是edx + ecx
的结果,ecx
的内容我们已经知道了,是指向文件内容缓冲区的指针,那edx
呢?继续向上看可以发现edx是读取了栈上esp + 20
的值,然后又减去edi的值。
随后edi又被重新赋值为栈上esp + 14
的内容,并且再也没有被修改。结合前面的分析结果,看来esp + 14
的位置保存的是解密用符串所在的地址。
那edi原来存的是什么内容?继续观察可以发现这一句
ecx
的值是edi赋值给它的。在下面的循环中,ecx
指向了文件读取缓冲区,那么edi开始时必然也是指向了文件读取缓冲区。
当是到这里,我的大脑已经开始混乱了,这几个寄存器在这你赋值给我,我赋值给你干什么玩意呢?而且eax
的值怎么和缓冲区指针扯上关系了,这缓冲区指针不是new出来的么?值应该是随机的啊?不对劲,赶紧再缕一缕,把表达式拆开来写看看:
eax = edx + ecx
eax = dex - edi + ecx
嗯?这个时候ecx
不是等于edi
么?一加一减抵消了,这eax
的值就是一开始的edx
,也就是esp + 20
的内容。在下面的循环中,ecx
每次循环都会被加1,所以eax
的值每次循环也会被加一,这样就能在每次循环中使用解密字符串中下一个字符来解密了。
esp + 20
的内容又是啥?通过向上翻代码发现,esp + 20
的值在本函数中没有被修改过,就是外界传进来的一个普通参数。难道需要向上查调用栈吗?这也太麻烦了。这esp + 20
可能是什么呢?它影响了eax
的初始值,也就是影响了一开始使用解密字符串的位置。
于是我猜,这个esp + 20
一定和当前的文件指针有关,因为游戏读取文件的位置和大小是随机的,你不能保证游戏什么时候就加载什么文件,如果是其它值,那怎么解密呢?解密字符和原始字节不就对不上了吗?
为了证明这一点,给SetFilePointer
和SetFilePointerEx
打上断点。读文件前肯定要用到这两个函数设置读取位置。
然后让游戏继续运行,发现断点停在了SetFilePointerEx
上面
esp + 0x08
是文件指针的低三十二位,esp + 0x0c
是文件指针的高三十二位,高三十二位都是0,所以此时文件指针是0x4B744AF7
。
继续运行:
果然停在了ReadFile
函数,此时参数三是0x0FA3
,说明当前游戏读取了文件从0x4B744AF7
处开始0x0FA3
个字节。
继续运行:
程序停在刚才的解密代码处。一下子就可以看出来,esp + 20
的值与刚才的文件指针一模一样,都是0x4B744AF7
,esi
的值与读取长度也都是0x0FA3
。重复几次,结果都是如此,说明刚才的猜想是正确的。
总结一下,这个解密过程其实很简单。解密时,将当前要被解密的字节的位置与0x1ff按位与,得解密字符串使用的下标,然后将被解密字节加上这个解密字符,解密完成。
知道了解密过程,解密程序的编写便十分容易,就是一个对每一个字节加上特定数值的过程。
做完了这一切,是不是就可以脱离mod专用启动器玩mod了?我迫不及待的尝试了一下,发现还是不能。这是为什么?
我发现,mod专用启动器还向游戏注入了一个LuaBridge.dll的文件
我猜想,游戏本身便支持lua脚本,这个dll应该是用于扩展lua脚本的功能,而mod中用到了这些扩展功能,所以还是离不开mod专用启动器。
但解密是成功的,游戏可以正常读取解密后文件的启动动画,并且游戏官方的big解包工具也可以正常提取音乐等美术资源。
-官方论坛
👆👆👆
公众号设置“星标”,您不会错过新的消息通知
如开放注册、精华文章和周边活动等公告