1.前言
最近 buy 了一个方向盘,就想去尝试一些汽车模拟类游戏,于是便下载了《欧洲卡车模拟器2》(Euro Trunk Simulator 2)来玩。这个游戏一大好处就是有很多的 mod 下载游玩。
安装mod的过程中遇到了一个很大问题,如果不是 Stream 创意工坊中下载的 mod 需要将其放置到C:\User\##\Documents 的默认目录中保存,但考虑到 mod 的体积很大(almost 50G),就想更改 mod 的默认路径。但是更改时出现了问题,官方并没有给出 mod 路径修改的方式。
于是这篇文章便诞生了,记录一下逆向的过程,便于以后进行考古。
2.逆向过程
欧卡是一款单机游戏,所以对其进行动态调试是很安全的,而逆向的目标是找到相关的路径字符串,然后将其更改,所以动态分析将是主要的分析手段,同时IDA的静态分析界面十分 pretty 于是也用IDA生成了一个*.i64* 文件用来记录一下分析的过程。
2.1 关键API函数
mod 是存放在C:\User\##\Documents\Euro Trunk Simulator 2\mod 目录下的文件,所以主程序在识别 mod 时一定会去遍历那个文件夹。如此便能想到 Windows与之相关的关键API函数。
CreateFileW 函数用来打开或者创建一个新的文件,当找到 mod 文件时一定会用这个函数进行操作。具体说明见 CreateFileW。
FindFirstFileA 函数用来打开一个指定的文件夹并返回其中的第一个文件。具体说明见 FindFirstFileA。
FindNextFileA 与 FindFirstFileA 配合使用,利用其获取到的句柄(handle)来按顺序继续遍历文件夹中剩下的内容。具体说明见 FindFirstFileA 。
SHGetFolderPathW 函数通过 CSIDL 获取 Windows 中常用文件夹的路径,这里就是 Documents 文件夹的路径,所以即使该可执行程序未加密,在其中搜索 我的文档 字符串也是找不到的。具体说明见 SHGetFolderPathW。
2.2 动态调试与交叉引用
选用的动态调试器是:x64dbg。首先将x64dbg打开,然后通过steam运行游戏。下面这一步需要操作快一点,通过x64dbg的 append方式(快捷键:ALT+A)将正在运行的 eurotrucks2.exe程序打开。
2.3 SHGetFolderPathW为线索
由于我们知道欧卡是将 mod的默认目录设置在了我的文档下,所以这个函数一定会被调用,用来获取我的文档路径。
寻找改函数进而获取到对我们最有价值的控制流从而缩小需要逆向的范围是很关键的,下面有两种方式进行寻找——动态(x64dbg)和静态(IDA)。
2.3.1 寻找该API函数
x64dbg中寻找
首先需要知道这个API函数是在那个DLL文件中,这是查阅 SHGetFolderPathW,就可以找到其依赖的 DLL为 Shell32.dll。
在x64dbg的符号一栏中进行过滤,将模块过滤为 Shell32.dll,而函数名称则是SHGetFolderPathW ,获取到该函数在内存中的地址,在其处设置下断点。
IDA中寻找
IDA中可以直接在 Import 引入表中搜索相应的关键字,进而锁定到相关函数。
双击跳转到反汇编窗口中,这个函数只有一个交叉引用(XREF),看到这里就很高兴,这样一步就定位到了关键函数中。
双击交叉引用的函数,跳转跟随,这个函数反汇编代码看上去还是挺多的,大致看一下其中调用的API函数有SHGetFolderPathW,GetModuleFileNameW,GetCurrentDirectoryW 等那么可以大致猜测出这个函数的基本功能就是用来获取相关路径的,所以将其命名为GetDirPath 。
2.2.2 GetDirPath动态调试
前面通过SHGetFolderPathW作为线索找到了GetDirPath 函数,查看这个函数的交叉引用项,是比较多的,并不像SHGetFolderPathW那样只有一个引用对象,所以这时通过x64进行动态分析来排除掉与其他的引用项,找到我们需要的。
通过下面简单的运算将 IDA 中的地址转化为x64中地址。
①通过(按下Ctrl + G)IDA计算出相对于.txt段的偏移=1400E54C0 - 140001000 = E44C0(这里不是RVA,RVA是相对于基址的偏移,而这里是相对于.text段的)。
②在x64的内存布局中找到.text段的基址,加上刚才计算的偏移量就可以得到在本次调试中GetCurrentDirectoryW的地址了。
上面只是介绍如何在 x64dbg 和 IDA中进行地址转化,我们并不一定需要在GetDirPath 函数的开头设置断点。由于我们只关心哪一条控制流会调用SHGetFolderPathW函数,我们在调用这个函数的 call 语句处设置断点。
按下F9,运行到这个断点处。这时在 x64dbg 右下角的栈窗口中寻找返回地址,这个就是调用GetDirPath 时会运行到获取目录控制流的函数 caller。
这个返回地址上的 call 指令调用的函数地址后四位与GetDirPath 的54C0不同,应该不是在此处调用的该函数。
继续在栈中寻找返回地址,第一个以eurotrucks2作为段标记的地址。
可以看到这里 call 调用的函数地址后四位为54C0与GetDirPath的相同。应该就是我们寻找的 caller。
将这个地址转化到 IDA 中查看1400D6E8A,包含这个地址的函数较短,我分析了一下大致功能,就是在获取到的 我的文档路径后面加上了 Euro Trunk Simulator 2 的字符串,所以我将其命名为GetDocumentPathWithEts2。同样的,这个函数的引用项比较多,我们还是通过动态分析来排除。
然而在GetDocumentPathWithEts2中对于GetDirPath的调用是一定会发生的,不存在什么控制流筛选,所以通过动态调试的方法进行排除有些不方便了。到这里我们先把SHGetFolderPathW 这条线给暂停。
2.4 FIndFIrstFileW为线索
加载 mod 时需要遍历 mod文件夹,那么这个函数一定会被调用,我们不知道他会被用来遍历哪些文件夹,因此设置一个条件断点就很重要了,由于这个函数第一个参数就是需要遍历文件夹的目录,在x64处理器中第一个参数是通过 rcx 进行传递的。
因此我们在设置断点时就以rcx中的内容作为条件,**[rcx]**解引用的方式会获取到以 rcx为地址的8 bytes 的数据这个就是文档路径的前8个字节,并且传递的参数是 LPCTR 类型的也就是宽字节,所以也就是路径字符串的前4个字节转化为宽字节就行了,也就是 “C:\U”,即 55002f003a0043。
使用F9运行,在内存窗口中观察 rcx 寄存器指向地址的变化。直到出现了,/mod/*这样的字符串结尾时停止,这时调用这个函数的 caller 就是为了来遍历 mod文件夹。
由于当前函数还没有进入,所以栈顶还停留在返回地址处。我们使用 [rsp] 跳转到相应的位置。
将这个地址转化到 IDA 中查看该函数的交叉引用。很不幸,这个函数没有被直接调用的交叉引用项,其唯一有意义的引用是存放到一个数据段中的表中,所以这个函数很有可能是某个类的 虚函数。
2.4.1 TravelDir函数
大致看一下这个函数的功能,在FindFirstFileW上方的函数是将ASCII的字符串转化为宽字符串,这个可以直接通过动态分析,查看前后的结果得到。
向上看,有操作将 “/*” 添加到指定字符串的结尾,遍历文件夹时通配符很重要,那么这个函数的功能就能大致猜测出来了,将传入的 路径添加 “/*” 之后转化为宽字符串,然后去遍历其中的内容,所以我将这个函数命名为TravelDir。
在该函数的开头设置断点,查看参数寄存器中的值,也就是 rcx, rdx, r8, r9。
重新运行后,当程序第一次在该函数处停止时,第二个参数 rdx中是对一个路径的引用,当然这个不是我们希望寻找的路径。
按下F9继续运行,当程序第二次在断点处停下时,第二个参数 rdx 就是我们对 我的文档\mod路径的引用
这时我们在栈帧中寻找这个函数的调用者,使用 [rsp] 在代码中跟随。
2.4.2 Caller of TravelDir
同样这个函数也是一个通过表进行引用的 虚函数。按照上面的方式进行如法炮制,寻找我们需要的参数。
在第一次停下时,第三个参数 r8 是对于另一个路径的引用。
第二次停下是,第三个参数 r8 是我们期望的参数。
继续根据栈帧向前推。
这个函数是有两个交叉引用项,但是由于其控制流图中必然会调用之前的函数,所以还是通过动态分析来筛选谁是他的调用者。
2.4.3 Caller of the Caller of TravelDIr
重新运行,第一次停止时第一个参数 rcx 指向的内存中有一个路径。
第二次停止时第一个参数 rcx 指向的一个路径,而且还是我们想要的。
继续在栈帧中回溯。这回的函数就不是 虚函数 了而是直接的调用。
2.4.4 Caller3 of the TravelDir
重新运行,这回第一次停下,在第一个参数 rcx 中就指向了我们希望的路径。
继续在栈帧中回溯。
2.4.5 Caller4 of the TravelDir——Critical Discovery
在 IDA 中大致浏览了一下这个函数,有一个关键的性的字符串 “%s/mod/”,这个字符串一定就是在文档目录后加上 mod 子目录的操作。
在上方的 47DB 处设置断点,观察取值。这个地方的判定是由于 IDA 在此处有一个引用。
运行之后,在出入到 r8 寄存器中的是一个取值,而这个路径就是文档中“Euro Truck Simulator 2”的路径。
2.5 锁定关键位置
在2.4 中我们已经找到了关键函数了,那么这个位置是否真的有效还需要进行试验尝试。
这个函数会在 r8寄存器传入的字符串后append 上 “/mod”,如果将这个字符串换成别的如果能够成功那么就是可行的了,同时之前在分析时也看到了 “…/…/” 这样的相对路径,真好拿来使用。
运行完上一条指令后将 r8 的值更改为 “…/…/” 字符串的地址。
运行函数之后成功的将路径变为了 “…/…/mod”。所以这个地方是非常有价值的。
3. patch方式
非常幸运的一点是,mov r8, [rdi + 1A8] 与 lea r8, addr指令是等长的,所以不需要进行额外的跳转才做,会使得 patch 十分的优雅。
4. patch程序
如果对逆向的过程不感兴趣,只是想要更改欧卡的 mod 默认路径,直接使用这里的程序 即可,如果想要了解Patch的源代码,可以看起中的 .cpp 文件。
使用方式:
①在 Stream 中打开本地的安装路径
②将下载的EuroPatch.exe复制到其中。
③双击运行,如下的输出则表示成功Patch。
④将“文档\Euro Truck Simulator 2\mod”目录下的文件剪切到打开的 本地安装目录中