基于虚拟机字节码的文本修改思路
前言
大部分的Gal引擎为了提高运行效率或加密或防止修改等目的都会使用私有的VM,也就是会把明文脚本编译成字节码的脚本,由于是私有的VM所以没有现成的工具来解析,所以为了修改文本,我们不得不分析其VM,而分析VM依据OP数量和结构,工作量会有不同程度的增加,但总的来说分析VM的工作量都比较大,由于我们是为了修改文本数据,所以从文本修改替换的角度来考虑一下这个问题。
结构
依据个人的分析经验,可以总结一下Gal引擎VM架构实现存储文本数据的结构,大概来说可以划分成这两种
-
指令与数据分离
那么什么是指令与数据分离?其实就是VM要使用到字符串数据的时候,是用一个偏移来指向字符串数据的,即大多数此类实现的VM的脚本文件会有个区域(通常在脚本文件末尾)来存放字符串,也就是游戏的文本统一存放在一个区域,我们可以称之为字符串区段。一句话总结就是,代码区段使用指向字符串区段的偏移来读取字符串数据
其抽象结构类似如下:
.code [instr][instr][instr][instr][instr][instr] .str [text][text][text]
从这里就不难发现,如果指令与数据分离了,那么我们就可以很轻松的改变文本长度,因为指令中存储了一个偏移来指向文本,那么我们只需要找到相关指令并修改这个偏移,就可以把文本数据附加到脚本文件的末尾,这样的好处是不会破坏原来的字符串区段,这就使得没有修改的偏移依然指向有效数据,同时新增的字符串也可以随意改变长度
-
指令与数据混合
那么什么是指令与数据混合?这就不言而喻了,就是字符串数据紧接在指令后面
.code [instr][text][instr][text][instr][instr][text][instr][instr]
那么也不难发现,如果指令与数据混合了,修改起来会很麻烦,首先就是不能够随便变化文本的长度,缩短文本长度的话,倒是可以,如果增长会造成其后的指令相对脚本文件开头的偏移改变,这时候如果内部有相对或绝对的跳转都又可能会出现问题,然而事实是依据以往的分析经验大多数游戏都会有相对或绝对的跳转,或者脚本文件的结构上还会设计label来标识某个代码块,方便进行跳转,所以基本不能直接增长文本。
那么直接解析呢?首先如果要直接解析的话,最基本的都得知道op\instr的长度,如果op\instr是定长的,那么倒是简单了一些,但仍然需要知道哪些op是跳转的op,并要分析出其结构,才能够正确的替换文本,如果op\instr是变长的,也就是op\instr的值本身没存储长度信息,也没办法直接从op\instr解析出长度信息,或没有这种结构,每一个op都有不确定的参数个数,也就是有不同的长度,说白了就是每一个op都有自己的结构,这样就没办法确定下一个op在何处开始了,所以就要分析每一个op,至少是要分析出每一个op的长度,才能解析脚本文件,如果要正确替换文本的话,还是得找到跳转相关的指令,这就要解析每一个op的功能了,或者至少也得用取巧的方法分析出跳转相关的指令,反正两种方法都得花费不少的时间来修正跳转的问题,没几个op还好说,要是遇到op有上百个的情况,这种的话工作量会巨大。
当然也不一定是这样的结构,有种是以块来存储数据的,它二进制脚本里的数据,其实并不是真正的VM字节码,而是一种类似序列化后的数据,在加载脚本的时候,再反序列化成内部的对象来执行代码。
讨论
那么有没有方法可以避免指令与数据混合这种结构带来的工作量巨大的分析呢?
答案是肯定的,在此我们从直观的思路出发,递进简略讨论这个问题,以不同的实现难度给出三种方法,并主观讨论其实现的效果和优劣以及改进的方案。(以下讨论的方法其实也适用指令与数据分离的结构,只不过这种结构很少需要走到这一步)
-
利用字符串搜索定位文本读取的位置实现动态替换
动态替换也是有部分汉化补丁使用的,有点类似VNR的内嵌功能的味道,或者说找H-Code的意思,好像是这个说法来着。
如果说我们基本没去逆VM,或者说不具备这样的能力的话,那么其实可以用搜索的功能,搜索当前游戏显示的字符串,找存储字符串的buffer,或者找读取字符串的位置,有点类似于那种拿CE搜所谓"基址"的操作,找到后写个替换就结束了,不过可能由于信息不足,你只能选择遍历相同的字符串来替换。这种我个人是不推荐的,首先,如果你没找到足够的信息来定位唯一的字符串,就只能选择遍历匹配原文字符串的方法,这就会带来性能上的问题。其次由于是随便找,碰运气到位置,运气不佳,没搞清楚这地方的作用,可能会出现问题,导致整个实现稳定性堪忧,因为这种实现出问题的其实我也见过好几个(。
-
Hook VM
这个的话有点接近我们的最终答案,在我们对VM有一点了解之后,我们可以Hook VM的dispatch或具体OP的处理函数或位置,来实时获取VM的状态数据(哪个是PC哪个是Code指针哪个是需要的指令),从而达到更加精准的字符串替换。或说通俗一点我们可以Hook VM中处理文本渲染,压入文本数据的OP,同时配合VM的状态数据来达到精准的替换。
这个是我比较认可的方法,这种实现的效果肯定是更好的(相比胡乱搜内存找一个位置,或者从系统API来替换文本而言),首先就可以消除搜索字符串的操作,因为如果你要搜索字符串第一个是性能问题,第二个可能会有不同位置但原文字符串相同,但译文不一样的情况,第三是可能还涉及到字符串过滤的问题,因为有些字符串并不是文本的字符串,可能是一些文件名之类的。
当然再稍微深入一点,还有种方法是可以构造我们自己的OP,来实现出一个类似 指令与数据分离 的结构。
总而言之,如果你对VM结构有一点了解,就可以达到这种比较精准和高效的替换了。不过这显然还是需要写Hook代码,不同游戏还要找位置,搜特征,也涉及到代码注入等问题,通用性比较差,但是对于有一点了解的人来说,这种方法确实是方便(从逆向分析的工作量上来看)又高效(从文本替换的角度上看)。
-
利用VM指令构造VM下的Inline Hook
这个是我认知里能提供的一个比较好的方案,前提是要大概分析一遍VM,也就是说你要对VM有一定程度的了解。
从上面的Hook VM的思想上再深入一点,Hook VM其实就是Hook Native代码,也就是本机代码,更具体一点,其实就是修改x86汇编,从另一个角度来看,VM也可以看作一种类似x86 cpu的东西,而里面跑的字节码其实就可以比作是x86汇编代码,那么从这个角度上来说,能不能构造一个在VM下的Hook,也就是利用VM的字节码写一个Hook,而不修改任何x86汇编代码?
显然是可以的,这个的关键就是找到VM的Jmp指令,就和x86下写一个 Inline Hook 一样通过Jmp指令跳转到目标代码块,执行完毕后跳转回去,当然x86下很方便,因为指令都是已知的,也有方便的调试工具和各种库来实现这种功能,而Gal引擎的VM显然大多数都是闭源,特别是Gal引擎基本都是会采用自己写的VM,所以基本是不可能直接找到工具来编辑脚本中的指令的,也找不到指令功能的说明文档,除非有人逆了,所以这就要求对VM要有一定的了解,目的其实就是找到那个Jmp指令。
当然Jmp指令可不是随便一个都行,首先这个指令要能够修改VM的PC,不然怎么Jmp?其次这个指令不能造成副作用,或者说副作用可以被抵消,因为不像x86汇编我们可以使用pushad之类的指令来保存环境,一个私有的VM连有什么指令都不清楚,哪来的保存环境?如果真有的话也需要分析找到这种指令,所以找到这样的指令是前提,当然一般来说VM都有这种指令,因为Gal脚本通常都会有GOTO之类的写法来跳转和切换不同的场景,选择分支之等。
不过类似的指令其实会有很多,比如Call指令其实也需要修改PC,也有一种类似Jmp的效果,特别是Gal引擎还会有一种脚本文件之间的Call指令,当然还有什么Label_Call之类的,各种各样的,你看着像是修改了PC,但有很多副作用,或者说没办法方便传递跳转偏移等一系列问题。
所以关键是能不能够找到一个副作用可以抵消或者没有副作用的单纯的跳转指令,就像是x86下的Jmp指令一样,如果你找到的是类似x86下的Call指令,又没找到Ret指令,显然会出问题,所以这又要求要准确把握指令的含义及其操作了VM中的什么数据。
找到这样的指令后,就可以在文本数据之前或者文本数据的位置,写上这个指令,跳转到脚本文件末尾,然后在末尾写上Push Str的OP指令和数据,然后再跳转回之前指令的结尾,这就有点类似于在x86下Inline Hook的意思了,只不过这是利用VM自身的指令来实现的。
所以,通过这种方法就可以避免Hook动态替换文本,或者工作量巨大的VM指令完全解析和分析外加写编译反编译工具,而且这种方法就和完全反编译和重新编译脚本一样,可以通用同一个引擎不同的游戏而无需写Hook,DLL注入之类的。
关于这个思路具体实现,可以查看以下项目
-
Seraph_Tools(已经分析好了,现在还没写出来,实在没时间)
这两个引擎都是指令和数据混合的架构,实现替换文本的思路相同,都是借助VM的类Jmp指令,从Push Str的位置跳出来,在脚本末尾写上Push Str,然后再跳回原来指令的结束位置,以此来替换文本,从而避免去完整解析脚本和使用Hook的方法动态替换。
感悟
写完这篇文章记录一下自己的感悟吧,2024年2月26日03点41分。
这些思路也是一步步从实际出发自然而然地往下思考的,说实话,看着很简单,其实一开始还真想不到,或者一开始也没能力做这个,有时候没思路了,就把电脑关了,其实关了还是心心念念,或者说在那死磕,搞了半天也没看出什么名堂。
写这文章也是,一口气写了好几个小时,其实后面思路就有点混乱了,或者说已经开始有点表达不出想要的意思了,但还是刻意去改来改去,所以花了更多时间,但又时不时会做出这种行为。
其实有些时候并不需要刻意去想脑子里就会自然浮现出一些想法,然后再考虑这些到底合不合理,有没有可行性,反正就有种自然而然的感觉,特别是一个人安静的时候,脑子里就会在琢磨一些事情,这种时候比较放松,思路就会更清晰一点,确实很神奇,只不过相比问别人答案,过程确实比较长,当然也得有人问才是(
所以其实相比问别人我还是喜欢自己考虑问题,有些时候虽然有的东西理解起来很简单,其实仔细想想可能并没完全理解到精髓上,只是把别人说的话记在脑子里而已,没准过几天就忘了。
逆引擎的VM其实是我一直比较感兴趣的,但奈何网上资料,不能说少吧,那基本是没有,当然像是那种打CTF的玩具VM那规模和复杂度基本没啥参考价值,Github有一些开源编译反编译工具,但也看不到他们分析的内部结构和过程,只有分析后写的工具,说实话这样的参考价值就低了很多。
咱也没什么人脉,还是只能靠自己慢慢磨了,说实话分析VM真的是非常烦人,不过像是现在这种VM遍地走的时代,没办法,还是得硬着头皮上啊,不然很多编译的脚本文本替换不了也白搭,当然我也不是什么汉化组的没这个急切的需求,主要还是想锻炼一下自己分析的能力,加强对基础的认识。
反正就是悟了亿段时间有点体会了,如果是汉化的话,主要也是这个目的,为什么不写具体例子呢?,其实我认为思路还是比较重要的,有思路才有方向嘛,好嘛,主要是我菜,而且完全解析VM工作量也很大,要写成文章就更费时间了,以后可能会写吧。
标签:字节,instr,虚拟机,VM,Hook,指令,字符串,文本 From: https://www.cnblogs.com/Dir-A/p/18034944