逆向工程奇幻头晕

Fantastic Dizzy是由Codemasters在1991年创建的一款益智平台游戏。 她是“ 头晕系列”的成员 。 尽管Dizzy系列仍然很受欢迎,并且创造了业余游戏( Dizzy Age ),但似乎没有人参与原始游戏的反向开发。


我写了一些简单的工具来提取,查看和打包原始游戏的资源。 工具发布在GitHub上

解压EXE


二进制文件PCDIZZY.EXE以Microsoft EXEPack格式打包。 尽管有许多Linux工具可以解压缩此类可执行文件,但它们似乎都不支持用于Fantastic Dizzy的版本。 因此,为了解压缩可执行文件,我使用了UNP的DOS版本。 解压缩可执行文件后,可以将其下载到IDA。 方便地,二进制文件的解压缩版本仍然运行良好,因此可以使用DOSBox调试器对其进行调试。

资料档案


游戏中有两个数据文件:DIZZY.NDX和DIZZY.RES。 扩展名以及文件大小为我们提供了有关扩展名的提示。 NDX文件约为8 KB,而RES文件约为800 KB。 由于游戏是用C语言编写的,因此我们可以在IDA中搜索fopen调用,以查看数据文件的打开位置。 在用汇编器编写的DOS游戏中,为此,您需要查找int 21h指令(打开文件ah = 3d)。 Dizzy二进制文件包含围绕fopen的包装函数,该函数可用于指定主要名称和文件扩展名。 它带我们到下面的代码块:


它加载DIZZY.RES和DIZZY.NDX文件,并将文件指针保存在全局变量中。 当对DOS二进制文件进行逆向工程时,会出现一个烦人的问题:它们中的寄存器是16位的,但是在某些情况下指针可以是32位的。 此处的FILE *指针大小为32位,并从do_open_file返回到ax:dx。 请注意,字符串也是32位指针,并且dizzy_basename传递给堆栈上的调用函数(并且此混淆的IDA自动分析-被认为是fopen的模式参数)。

通过查找外部参照中g_dizzy_res / ndx的出现,您可以找到文件的读取位置。 DOSBox调试器在这一点上非常有用,因为很有可能发生许多随机文件读取操作,并且使用IDA确定读取偏移量将是一个非常单调的过程。 在此处可以找到有关构建和使用DOSBox调试器的良好指导。

当同时使用IDA和DOSBox调试器时,很明显,NDX文件被用作RES文件的索引。 NDX文件中的每个条目占用16个字节; 它将片段标识符,其大小和偏移量存储在RES文件中。 查看RES数据的读取方式,您可以看到在NDX文件中首先检查了标志字节。 如果未设置位0x80,则直接从RES文件读取数据,否则执行更复杂的代码路径。 该标志是为大多数片段设置的,因此我们很有可能假设它表示用于这些片段的某种压缩。

压缩方式


压缩路径通过从RES片段的底部读取两个32位字(表示初始大小和最终大小)开始,然后调用解压缩功能。 1991年,简单的行程编码(RLE)和字典压缩很流行,例如各种Liv-Zempel算法。 拆包周期的开始看起来像这样:


使用get_next_token函数获取用于拆包的令牌,该函数读取ax:dx中源数据的下一部分,并移位cl。 cl寄存器用作位移的位置,在达到八位后返回零。 在循环的开始,读取令牌并检查低位。 如果设置了标志,则代码很简单:


它只是保存当前字节,接收下一个令牌并继续工作。 如果清除该标志,则选择更长的代码路径,该路径以rep movsb指令结尾。 这表明在压缩中使用了某种字典。

压缩算法很有趣,原因有几个。 首先,它使用可变位长编码。 绝对值编码为1标志和8位数据值。 奇怪的是,比特流被编码为小字节序。 通过在十六进制编辑器中观察RES文件,这会使解压缩的分析有些复杂。 例如,如果片段的前三个字节被编码为绝对值,则数据的排列方式如下:

 : AAAAAAAA BBBBBBBB CCCCCCCC DDDDDDDD  1: 6543210F 7  2: 543210F 76  3: 43210F 765 

另外,如果计数器c1在接收到下一个令牌时返回零,则拆包器可以在读取时跳过该字节。 我不知道这是优化,错误还是游戏开发人员为解决我的工具问题而创建的骇客。

如果清除了该标志,则解压缩程序将从解压缩数据的初始部分执行复制。 在这种情况下,以下位对要复制的长度和偏移进行编码。 偏移量以10或13位编码,所需的选项指示该标志。 这似乎是一个非常奇怪的选择,因为它会使代码复杂一点,最多只能节省2位。

对系列的长度进行编码看起来有些奇怪。 解包器读取这些位,直到达到零位为止。 然后,用于编码长度的位数为2加上非零位数。 例如,当编码长度为58(0x3a)时,比特流如下所示:

 11110 111010 

编码需要11位。 小长度的编码更好,因为最小位长度为2。最多复制3个长度只需要3位即可编码,最多7个需要5位,依此类推。 我不确定这种编码是否是常用技术。

DOSBox调试器对于重构解压缩算法也非常有用。 如果您不知道解压缩后的数据是什么样子,那么很难理解解压缩程序是否正常工作。 使用调试器,您可以逐步执行整个解压缩算法,并保存未包装内存的转储以进行比较。

另一个有用的功能是NDX文件中的标志,指示资源已压缩。 由于原始游戏支持解压缩的资源,因此我们无需压缩算法就可以重新打包RES文件。 在随后的游戏发布中修改和重新打包片段是测试我们对数据格式的假设的一种好方法。

等级


Fantastic Dizzy是一款开放世界的游戏。 级别是具有垂直或水平滚动的区域。 玩家在各个级别之间移动,到达级别的末尾或进入和离开建筑物。 尽管通过16位标识符(ID)对RES文件中的片段进行了引用,但游戏的二进制文件实际上包含一个具有片段标识符的匹配级别名称表。 每个级别由几个片段组成:标题,一层或多层,磁贴和调色板。 这里很少有冗余,因为某些级别使用相同的调色板和图块,但不重用相同的片段,因此RES文件包含许多重复资源。

层对一个级别的图块进行编码。 对于世界的不同部分或背景图层,您可以使用其他图层。 例如,在tree1.stg级别上,针对树顶部的不同部分有八层,并且有一个公共背景层。 但是,水下级别分为sea1.stg和sea2.stg,每个级别都有一个前景层和一个背景层。

背景层是不滚动的固定宽度背景,例如,游戏中具有树顶的部分中的森林。 位于角色前面和后面的前景和背景图块与您可以在其上行走的图块位于同一层中。 例如,屏幕快照从游戏开始就显示了树顶的级别:


树顶水平

它是tree1.stg的第七层:


第七层树1.stg

值得注意的是,玩家可以在小屋前面经过,但可以在两棵树后面经过。 所有图块信息都包含在一层中的一个图块地图数组中。 该层片段中的图块被编码为两个字节,并且低9位用于图块索引。 我没有完全理解较高的位,但是至少它们包含有关瓦片调色板移动的信息,并且可能还包含有关碰撞的信息。

作为游戏中的关卡,过场动画,人物肖像和库存控制屏幕也被存储。 这项技术似乎是DOS游戏的标准,可能是因为它使所需的代码量减至最少。


库存管理的“级别”

精灵


Sprite格式并不是特别有趣。 每个子画面都是一个位图,每个像素一个字节,但每个子画面只有16种颜色。 在256色VGA时代,使用有限数量的颜色是一种常见技术,因为对于精灵而言,执行调色板移动或将其与其他调色板一起使用很容易。 此外,它还节省了为精灵分配的空间。

精灵的大小不同,因此单独的片段包含有关精灵大小及其在x和y中的位移的信息。 精灵被分组为集合,但是分组看起来相当随意。 例如,一组精灵包含屏幕保护程序图形,清单对象以及一些非玩家角色。 这使得查看Sprite设置有些棘手,因为所有Sprite的调色板都不相同。


玩家角色精灵

还剩下什么?


逆向工程还有更多事情要做。 我通常对数据文件格式感兴趣,但有些方面我不了解:

  • 对象(键,水果等)的位置在哪里。 似乎它们没有写入级别片段中。 也许它们存储在游戏的二进制文件中,因为玩家可以在一个级别拾取一个对象,然后将其扔到另一个级别。
  • 水平碰撞如何工作。 玩家可能会走在某些瓷砖的前面或后面,并且地板可能是平坦的或倾斜的。
  • 各个级别如何连接。 该信息可以存储在游戏的二进制文件中。
  • 瓷砖调色板在各个级别上的移动并不完全正确。 一些图块显示不正确的颜色。
  • 每个精灵集都有三个片段:标头,表和数据。 带有表和数据的片段对我来说很清楚,但是标头中包含一些有关sprite的信息,例如,x和y的偏移量。 我不完全了解其格式。

Source: https://habr.com/ru/post/zh-CN436674/


All Articles