【摸鱼】新游戏神尾的引擎的一点解析



没错就是最近出的c龙王

楼主 展鸿丶  发布于 2020-02-06 18:55:00 +0800 CST  
这个引擎代号是SystemC,应该是自家用的引擎。
封包是FPK格式,GARbro、Crass、Arc等常用的工具都有解包代码。
出于研究目的,先不看这些代码,还是自己动手分析一下好了。
那么就开始了。

楼主 展鸿丶  发布于 2020-02-06 18:59:00 +0800 CST  
IDA载入EXE开始分析。
按照惯例,先搜一波封包扩展名。












马上就搜到了几个。
一看 data.fpk 就觉得里面应该存放了脚本之类的。
因为其它的封包文件名都是bgm cg什么的。
data.fpk这个字符串在好几处地方都有出现,我又找了几处,发现这个函数比较好分析。


F5走起。


可以看到一个 id.ddf 和一个 data.fpk
非常有可能是从 data.fpk 里读取 id.ddf 这个文件的函数。
跟进去看看。


发现IDA自动把这个参数命名成了lpFileName
这说明这个参数最终可能传给了CreateFile这个API用来打开该文件。
我们继续跟进这个 if 的两个函数,先进第一个看看

楼主 展鸿丶  发布于 2020-02-06 19:07:00 +0800 CST  
然后马上就发现了CreateFile函数






通过函数的调用约定 __thiscall 可以得知 该函数是一个类方法。
所以现在开始整理这个类的数据结构。


切换到Structures窗口。
按下键盘上的INS键新建一个结构体。


为什么叫 CFPK 呢?
你看上面的图片,不是写着 CFPK::Create 呢么。
所以这个类名就是 CFPK。


结构体创建好之后,回去看刚才的函数。




CreateFile的返回值就是文件句柄,HANDLE型的。
并且你可以看到,它把文件句柄存入了类地址偏移524字节的地方。
我们现在来修改结构体,给结构体增加一个文件句柄的成员。
切换回结构体窗口。
在结构体上按一下D键






增加了一个类成员,选中field_0按一下N就可以修改它名字。我改成 m_hFile 了
上面看到,文件句柄被存到524字节的地方,所以,现在需要在m_hFile前面填上这些字节。
选中m_hFile,然后右键菜单选择Expand Struct type.. 或者快捷键Ctrl+E。


点击OK 就在m_hFile的前面增加了524个字节。


现在 m_hFile 这个成员已经调整好了。

楼主 展鸿丶  发布于 2020-02-06 19:22:00 +0800 CST  
现在回到函数,选中变量v3,右键,选择 Convert to struct*
意思是转换为结构体指针。


然后在列表中选中我们刚才创建的 CFPK 结构体。




点击OK之后 就变成了这样 :


但是 好像还是不对,突然想起来 还没给 m_hFile 设置类型。
不慌,选中它,然后按Y键,输入 HANDLE


点击OK 之后就变成这样了


这样看代码,就会非常清晰,我们还可以给v3改个名,同样是按N。


按照这种做法,不断完善结构体。


为此,需要去了解Win32 API的细节,比如说这个sao红的CreateFile
除此之外还有很多这样的API

楼主 展鸿丶  发布于 2020-02-06 19:30:00 +0800 CST  
这里还有一个技巧,分析日文程序的时候,我们在中文的系统上会看到乱码。
但是IDA是可以调整设置的。
选中一个字符串,然后按Alt+A
点击Change encoding


在空白的地方右键,选择 Insert...
插入一个新的编码




然后输入 CP932 表示日文编码




点OK。
然后回到最外面那个窗口,点击Set default encoding


设置程序默认编码,选择刚才加入的CP932即可。


OK
回到函数窗口,再按一下F5,就可以看到日文了。


楼主 展鸿丶  发布于 2020-02-06 19:37:00 +0800 CST  
然后改一下这个函数的名字,还是选中,按N


现在就好看点了。




回来继续分析这个 CFPK::Create 函数


发现这个类还有很多目前为止的成员
这里可以先把成员写上,具体名字和用途,后面再分析也不迟。
为了重新看到偏移量,需要重设一下。
右键选择 Reset pointer type 即可




现在又看到熟悉的 this + xxx 了。
然后重复刚才的步骤,给结构体添加成员。
举个例子 this + 268
选中268,右键选择 Hex 改成16进制显示。






然后回到结构体窗口,在 CFPK 里找到 10C 这个位置。


按下D 新建一个成员,然后按Y 暂时先把类型设置为int




测试效果如下




重复上面的步骤把刚才看到的几个成员全部补上
名字可以先默认,用途暂未清楚。

楼主 展鸿丶  发布于 2020-02-06 19:50:00 +0800 CST  


从CreateFile往下看。
if ( hFile != -1 ) 判断文件没有打开失败。
然后执行 sub_478100 这个函数,跟进去看看。


这里已经可以看到ReadFile了。读取文件数据的。
可以判断这里就是读取FPK文件的代码。
首先,它从文件头部读取了4个字节,存到Buffer这个变量里。


这个Buffer看起来是个整数。
看它走了个if,应该是用来确定文件类型的。
想知道实际上程序会怎么执行,就需要调试程序。
好在IDA支持调试。
先打个断点,在代码窗口 行号的左边 点一下,这行代码会变红。
如果程序执行到这一行,就会暂停下来。
暂停下来,就可以慢慢搞了。




准备调试,在IDA窗口的上面。有个选择框。
选择Local win32 debugger




接着需要改一下调试器设置


点击Edit exceptions








每一行都改成这样。
改完之后是这样的


关掉这个窗口,然后 点OK 就行了。


现在 点击 IDA窗口上面那个绿色的三角形箭头,就可以开始调试。

楼主 展鸿丶  发布于 2020-02-06 20:10:00 +0800 CST  




然后IDA可能会弹一些警告,选是即可。
过了一会,程序停在这个地方。


然后按下F5。
可以看到刚才红色那行变成紫色了。
说明程序执行到这里。遇到刚才我们打的断点,在这暂停了。


然后几次 F8,让程序一行行执行代码。








这时如果你把鼠标指针移到变量上,比方说刚才那个Buffer。
IDA会显示出该变量当前的值。




可以看到Buffer现在是0x325(十六进制)

楼主 展鸿丶  发布于 2020-02-06 20:18:00 +0800 CST  
由于本帖是摸鱼贴,所以会时不时更新。


实际上关于FPK封包已经分析完了。
目前我还没有在网上找到这个引擎的封包工具,可能有大佬写过吧。
简单描述一下FPK封包的数据格式。
文件头4字节是封包内的文件数量。
如果它是个负数,说明FPK封包里的文件表是加密的。
if (count & 0x80000000){
encrypted = true;
}
然后执行 count &= 0xFFFFFF
count才是正确的数值。


文件头部4字节之后是封包内的文件的数据。
再接着是FPK封包的文件表。
再接着是文件表解密用的KEY(4字节)
最后是文件表的位置(4字节)
文件结构如下
FPK
{
EntryCount : 4 bytes
EntryData
EntryTable
TableKey
TablePos(相对于整个文件)
}
读取封包过程如下:
1、先读头部的EntryCount,判断是否加密。并还原出正确的数值。
2、移动读写位置到 倒数8字节处,读取TableKey和TablePos。
3、移动读写位置到TablePos处,读取文件表。文件表的大小可以通过 EntryCount 乘以 单个Entry 的大小来计算。
Entry 大小为36字节。
4、读取完文件表之后需要解密文件表。对文件表进行异或(xor)即可。key就是TableKey
for (...) {
( (int*)table )[ i ] ^= TableKey
}

楼主 展鸿丶  发布于 2020-02-06 20:37:00 +0800 CST  
Entry结构如下:


这里有一个需要提到的是 hash 值。它是对 name 进行计算得到的。算法如下:


这段代码在 sub_00476DB0 里。

楼主 展鸿丶  发布于 2020-02-06 20:45:00 +0800 CST  
这里分析一下每个Entry的类型




在封包里,文件有三种形式存在。
一种是经过ZLC2压缩算法压缩过。(实际上是被魔改过的LZ77算法)
一种是经过RLE0压缩算法压缩的。
一种是未经任何压缩的原始数据。




ZLC2
解压算法在 sub_00477B90 里。




这是被魔改过的算法,直接去网上扒的LZ77代码是解压不了的。






然后就是说一下到底修改了什么。
需要对LZ77算法有一定了解才可以理解。
可以搜得到LZ77算法图解。
简单说有三处地方改过。
1、滑动窗口缓冲区改过,使用标准的算法无法还原成它改过的那样。
2、标志位反了过来。原版是1不压缩,0压缩。
3、解压时的偏移量算法改过。原版是从滑动窗口头部开始计算的位移量,改成了从尾部开始的回退量。
解压算法经过几个小时的努力,也算是还原了出来。能解压data.fpk里的所有文件。

楼主 展鸿丶  发布于 2020-02-06 20:58:00 +0800 CST  
其实如果只是简单要求封包的话,可以不压缩数据,直接封进去就可以,引擎是可以读取的。
但是出于学术研究精神,我还是花了不少时间,把压缩算法还原了出来。(而且还找到了引擎的BUG)
具体代码我之后应该会放到某个黑白网站。
目前想做的是分析一下剧本文件的结构。虽然说这个游戏的剧本就是明文未经编译的。但是也有一堆奇怪的文件放在一起。这需要点时间来分析。

楼主 展鸿丶  发布于 2020-02-06 21:02:00 +0800 CST  
鸽了一天继续更新帖子。
今天来搞搞脚本文件。


打开ACT_A.txt可以看到明文脚本,但实际上游戏并不读取这个文件,莫非临时工放错了?
管它呢,有总比没有的好。
用编辑器打开ACT_A.DAT能看到一些文本(UTF8)和二进制数据。
复制其中的一段文本,发现可以在ACT_A.txt中一一对应。


看来这个文件有可能是处理ACT_A.txt得到的产物。
再尝试用编辑器打开ACT_A_JA.PTR发现全都是二进制数据,没有明文。
打开ACT_A_JA.TXD发现里面是UTF8文本,并且是连续的,只有一行。


这样的文本应该不可能直接解析。故猜测应该需要配合ACT_A_JA.PTR文件来使用。
通过ACT_A_JA.PTR的扩展名来猜测,PTR应该是指针(Pointer)的缩写。
所以猜测PTR文件里应该储存了文本的索引信息。



看十六进制发现除了文件头之外其它数据难以识别,还是逆代码吧。IDA走起。
老规矩,直接搜一波扩展名。
直接定位到目标函数。
这个函数还有遗留的信息。函数名是CScript::LoadTextID。


看到一堆字符串操作的函数,是用来拼接文件名的,略过,直接往下拉。


一看到一个很清晰的读取文件流程。
首先CFPK::Read读取16字节文件头。
文件头里有一个Count表示文件里Entry的数量,老套路了。
直接给出文件头:


接下来就是循环读取每个Entry的代码。
通过CFPK::Read的参数确定每个Entry的大小为12字节。
读取之后entry的数据并没有直接解析。而是存到了std::vector里。
那个if实际上是std::vector的push_back操作,被编译器优化成这样了而已。


很显然,这里面的v4就是std::vector的指针。找一下它是哪里来的。
往回翻可以看到它是CScript里的一个成员。




由于这个函数里并没有解析Entry数据的代码,所以尝试翻翻前后的函数。
因为一般来说一个类的函数都是放在一起的。

楼主 展鸿丶  发布于 2020-02-08 12:19:00 +0800 CST  
往下翻了几个函数之后发现了一个用到刚才那个std::vector的函数。
(名字什么的忽略,猜的)


这个函数比较短,关键是看这个v5。


v5->length是长度。
v5->offset是偏移量。
关键在于:
Q_memcpy(v6, &v7[v5->offset], v12);
这一行,它从TextBuffer里复制了一部分文本出来,然后又把文本从UTF8转为Unicode。
到这里已经清楚PTR的Entry的数据结构了。
还差一个。


直接给出结构:


至于那个TextBuffer,想都不用想,肯定是来自对应的TXD文件了。
于是马上写代码验证一下。


输出如下:


PTR文件解析完毕。

楼主 展鸿丶  发布于 2020-02-08 12:34:00 +0800 CST  
根据以上分析结果,如果要做汉化,就需要重新构建PTR和TXD文件。
首先是要从PTR里提取出TextID,从TXD中提取出对应的文本。
然后把译文存入TXD文件并记录偏移量和长度,最后重新构建PTR文件即可。

楼主 展鸿丶  发布于 2020-02-08 12:39:00 +0800 CST  
还有DAT和SPT文件的解析。
还是搜一波扩展名。定位目标函数。




从文件头部读取4字节为Count

然后计算所有Entry的大小,一次性读取。
可以看到每个Entry大小为156字节。
往下翻到一个调用了其数据的函数。


正如其名,DAT就是场景(Scene)文件。
按照刚才的套路,写一波代码把数据读出来看看再说。


输出如下:


可以很简单地发现其对应关系。
故猜测DAT是每个ACT的索引文件。再去看代码加以验证。


的确如此。

楼主 展鸿丶  发布于 2020-02-08 12:59:00 +0800 CST  
最后还有SPT文件。
这个文件是逻辑脚本文件。控制游戏里的各种行为。
显示CG、立绘、输出文字、播放语音、特效、等等。
还有很多很多功能,以至于命令数量都到了三位数。
下面是读取代码。


还是文件头4字节的Count。
然后每个Entry32字节。
下面是Entry结构。


经过分析,其调用方式就是简单的 switch( Cmd )
一层套一层。
其中最长的switch伪代码竟然有四五千行。
(实在是无力吐槽)
当然我还没有闲到有命去分析每个命令和参数的作用。

楼主 展鸿丶  发布于 2020-02-08 13:13:00 +0800 CST  
引擎的架构大概是个状态机。
区分不同的游戏状态,例如:标题界面状态、播放影片状态、阅读文本状态、设置界面状态、显示对话框状态、等等等等。
开始游戏时会读取DAT文件。
其中,ACT_A、ACT_B是指章节A、章节B。也就是 ChapterA、ChapterB。
每个章节里有很多场景(Scene)
文件名A0100_100.spt
就表示:章节A、场景0100、100号脚本。这个100号的实际意义我也不太清楚。
但txt文件里是可以找到对应的。
引擎以Scene为单位播放游戏内容。


红色框是起始场景,ye框是结束场景,最后的文本是每个场景的标题。

楼主 展鸿丶  发布于 2020-02-08 13:22:00 +0800 CST  
不得不吐槽一下这个引擎,每次打开一个文件,都要载入一次封包。
CFPK::Create() // 打开FPK封包,读取文件表。
CFPK::Open() // 在封包中搜索文件。
CFPK::Read() // 读取封包中的文件。
CFPK::Close()
CFPK::Delete()


是的你没看错,每当你点一下屏幕,播放一个语音,它就会走一遍甚至多遍上面的流程。
我打断点确认过,确实没错。太屑了。


楼主 展鸿丶  发布于 2020-02-08 14:25:00 +0800 CST  

楼主:展鸿丶

字数:6471

发表时间:2020-02-07 02:55:00 +0800 CST

更新时间:2020-08-29 13:15:34 +0800 CST

评论数:101条评论

帖子来源:百度贴吧  访问原帖

 

热门帖子

随机列表

大家在看