逆向工程简介:破解游戏数据格式


引言


陌生数据文件的反向工程可以描述为一个渐进的理解过程。 在许多方面,它类似于一种科学方法,仅适用于人创造的抽象对象,不适用于自然世界。 我们首先收集数据,然后使用此信息提出一个或多个假设。 我们检验假设并应用这些检验的结果来澄清它们。 如有必要,重复该过程。

培养逆向工程技能基本上是一个实践问题。 通过积累经验,您可以首先直观地了解需要探索的内容,需要寻找的模式以及更方便使用的工具。

在本文中,我将详细讨论对旧计算机游戏中的数据文件进行反向工程的过程,以演示如何完成此工作。

一点背景


当我尝试在Linux上重新创建Chip's Challenge时,一切就开始了。

芯片挑战赛最初于1989年发布,用于如今已被人们遗忘的Atari Lynx便携式游戏机。 当时,Atari Lynx是一款令人印象深刻的汽车,但它与Nintendo Game Boy同时出现,最终占领了市场。

Chip's Challenge是一款具有顶视图和瓷砖图的益智游戏。 与大多数此类游戏一样,每个关卡的目标都是到达出口。 在大多数级别中,输出受芯片连接器保护,只有收集一定数量的计算机芯片才能绕过该连接器。

图片

视频: Atari Lynx实际操作一级演练

从第一关开始,新游戏的名称为“第1课”。 除了芯片和芯片插槽以外,钥匙和门还出现在芯片上。 在其他级别,会出现(通常)沿着可预测路线移动的障碍物,例如陷阱,炸弹,水和生物。 各种各样的对象和设备使您可以创建许多难题和时间限制。 要完成游戏,您需要经过140多个级别。

尽管Lynx最终失败了,但是Chip's Challenge被证明非常受欢迎,并被移植到许多其他平台上,最终出现在Microsoft Windows上,并在此广泛传播。 在游戏中,形成了一个小而专门的粉丝群,随着时间的流逝,编写了一个关卡编辑器,使玩家可以创建无数个关卡。

这就是我的故事开始的地方。 我决定要创建一个基本的开源游戏引擎版本,以便可以在Linux和Windows上玩“ 芯片大挑战 ”,并使运行粉丝创建的所有关卡更加容易。

事实证明,关卡编辑器的存在对我来说是一个奇迹,因为我可以探索游戏逻辑的隐藏功能,创建自己的关卡并进行测试。 不幸的是,原始Lynx游戏没有关卡编辑器;它仅出现在Windows下更为知名的端口中。

图片

Windows端口不是由原始开发人员创建的,因此其中对游戏逻辑进行了许多更改(并非所有更改都是有意的)。 当我开始编写引擎时,我想在其中重新创建Lynx上原始游戏的逻辑,以及Windows更为知名的版本。 但是Lynx缺少关卡编辑器,这严重限制了我详细研究原始游戏的能力。 Windows端口具有一个优势:级别存储在单独的数据文件中,从而简化了其检测和逆向工程。 Lynx的游戏发行在包含子画面图像,声音效果和机器代码以及一起执行的关卡数据的ROM盒中。 没有任何关于数据在ROM的128 KB转储中的位置或外观的提示,没有这些知识,我就无法为Lynx版本创建级别编辑器。

有一次,在一个悠闲的研究过程中,我遇到了MS-DOS下的Chip's Challenge端口的副本。 与大多数游戏的早期版本一样,其逻辑比Windows版本更接近原始版本。 当我查看程序数据以了解其存储方式时,我惊讶地发现级别数据分配在单独的目录中,并且每个级别都存储在其自己的文件中。 拥有如此方便的分离级别数据,我建议对级别数据文件进行反向工程并不难。 这样,您就可以为MS-DOS下的游戏版本编写一个关卡编辑器。 我认为这是一个有趣的机会。

但是随后, Chip's Challenge社区的另一位成员警告了我一个有趣的事实。 事实证明,MS-DOS的级别文件的内容是ROM Lynx的字节转储。 这意味着,如果我可以解码MS-DOS文件,则可以使用此知识来读取和更改Lynx ROM转储中的级别。 然后,您可以直接为Lynx上的原始游戏创建一个关卡编辑器。

突然,我的头等大事是MS-DOS的反向工程级别文件。

资料档案


这是包含所有数据文件的tarball目录的链接。 如果您想重复执行本文中介绍的所有步骤,或者尝试自行解码数据文件,请提供。
合法吗 好问题。 由于这些文件只是MS-DOS程序的一小部分,它们本身是无用的,并且由于我仅出于教育目的而发布它们,因此我认为这属于合理使用的要求。 我希望所有有关方面都同意我的看法。 (但是,如果我收到律师的威胁信,则可以更改文章,以有趣的方式显示数据文件,然后声明这是一个模仿。)

先决条件


我将假定您知道十六进制演算,即使您不知道十六进制值的解码,并且您也对Unix shell有所了解。 本文显示的shell会话在标准Linux系统上运行,但是几乎使用的命令是常见的Unix实用程序,并且广泛分布在其他类似Unix的系统上。

初看


这是包含来自MS-DOS下端口的数据文件的目录列表:
  $ ls水平
 all_full.pak cake_wal.pak eeny_min.pak iceberg.pak课程_5.pak mulligan.pak playtime.pak southpol.pak totally_.pak
 Alphabet.pak castle_m.pak elementa.pak ice_cube.pak lesson_6.pak nice_day.pak potpourr.pak special.pak traffic_.pak
 amsterda.pak地下墓穴.pak fireflie.pak icedeath.pak lesson_7.pak nightmar.pak问题.pak螺旋.pak trinity.pak
 Apartmen.pak cellbloc.pak firetrap.pak icehouse.pak课程_8.pak now_you_.pak refracti.pak spooks.pak trust_me.pak
 arcticfl.pak chchchip.pak floorgas.pak invincib.pak龙虾_.pak坚果和.pak反向_.pak steam.pak undergro.pak
 balls_o_.pak chiller.pak强制_e.pak i.pak lock_blo.pak on_the_r.pak rink.pak条纹.pak up_the_b.pak
 beware_o.pak chipmine.pak force_fi.pak i_slide.pak loop_aro.pak oorto_ge.pak roadsign.pak suicide.pak vanishin.pak
 blink.pak citybloc.pak force_sq.pak jailer.pak memory.pak open_que.pak sampler.pak telebloc.pak受害者.pak
 blobdanc.pak Colony.pak财富_.pak跳跃_.pak metastab.pak海外_.pak scavenge.pak telenet.pak vortex.pak
 blobnet.pak走廊.pak four_ple.pak kablam.pak mind_blo.pak pain.pak scoundre.pak t_fair.pak wars.pak
 block_fa.pak cypher.pak four_squ.pak knot.pak mishmesh.pak paranoia.pak seeing_s.pak the_last.pak writers_.pak
 block_ii.pak deceptio.pak glut.pakladder.pak miss_dir.pak部分_.pak short_ci.pak the_mars.pak yorkhous.pak
 block_n_.pak deepfree.pak goldkey.pak lemmings.pak mixed_nu.pak pentagra.pak收缩in.pak the_pris.pak
 block_ou.pak digdirt.pak go_with_.pak课_1.pak mix_up.pak perfect_.pak skelzie.pak three_do.pak
 block.pak digger.pak grail.pak课_2.pak monster_.pak pier_sev.pak slide_st.pak time_lap.pak
 bounce_c.pak doublema.pak hidden_​​d.pak课_3.pak morton.pak ping_pon.pak slo_mo.pak酷刑c.pak
 brushfir.pak Drawn_an.pak hunt.pak课程_4.pak mugger_s.pak playhous.pak socialis.pak tossed_s.pak 
如您所见,所有文件都以.pak结尾。 .pak是应用程序数据文件的标准权限,但不幸的是,这没有提供任何有关其内部结构的信息。 文件名是级别名称的前八个字符,但有一些例外。 (例如,在级别文件“ BLOCK BUSTER”和“ BLOCK BUSTER II”的名称中,省略了“ buster”一词,以使它们不匹配。)
  $ ls水平| 厕所
      17148 1974 
目录中有148个数据文件,游戏实际上有148个级别,因此这里的所有内容都相同。

现在,让我们检查一下这些文件是什么。 xxd是用于转储十六进制数据(hexdump)的标准实用程序。 让我们看看第1课中的外观。
  $ xxd级/第_1.pak节
 00000000:1100 cb00 0200 0004 0202 0504 0407 0505 ................
 00000010:0807 0709 0001 0a01 010b 0808 0d0a 0a11 ................
 00000020:0023 1509 0718 0200 2209 0d26 0911 270b。#......“ ..&..'。
 00000030:0b28 0705 291e 0127 2705 020d 0122 0704.(..)..''....“ ..
 00000040:0902 090a 0215 0426 0925 0111 1502 221d .......&。%....“。
 00000050:0124 011d 0d01 0709 0020 001b 0400 1a00。$ ....... ......
 00000060:2015 2609 1f00 3300 2911 1522 2302 110d。&... 3.)..“#...
 00000070:0107 2609 1f18 2911 1509 181a 0223 021b ..&...)......#..
 00000080:0215 2201 1c01 1c0d 0a07 0409 0201 0201 ..“ .............
 00000090:2826 0123 1505 0902 0121 1505 220a 2727(&。#.....!..“。”
 000000a0:0b05 0400 060b 0828 0418 780b 0828 0418 .......(.. x ..(..
 000000b0:700b 0828 0418 6400 1710 1e1e 1a19 0103 p ..(.. d .........
 000000c0:000e 1a17 1710 0e1f 010e 1314 1b29 1f1a .............)..
 000000d0:0012 101f 011b 0c1e 1f01 1f13 1001 0e13 ................
 000000e0:141b 001e 1a0e 1610 1f2d 0020 1e10 0116 .........-。  ....
 000000f0:1024 291f 1a01 1a1b 1019 000f 1a1a 1d1e。$).............
 00000100:2d02 
什么是hexdump实用程序? 十六进制转储是显示二进制文件确切字节的标准方法。 大多数字节值不能与可打印的ASCII字符关联,或者它们的外观令人难以理解(例如制表符)。 在十六进制转储中,各个字节作为数值输出。 值以十六进制显示,因此为名称。 在上面的示例中,一行输出显示16个字节。 最左边的列显示文件中行的位置,也以十六进制显示,因此每行中的数字增加16。字节显示在八列中,每列显示两个字节。 右侧的十六进制转储显示了用字符显示时字节的外观,只有所有不可打印的ASCII值都用点代替。 这使查找可嵌入二进制文件的字符串变得容易。
显然,这些文件的逆向工程不会归结为仅浏览内容并浏览其中可见的内容。 到目前为止,还没有什么可以告诉我们数据执行的功能。

我们期望看到什么?


让我们退后一步,弄清楚情况:我们希望在这些数据文件中找到哪些特定数据?

最明显的是该级别的某个“地图”:表明墙壁和门以及其他所有位置的数据,这使得该级别独一无二。


(对我们来说幸运的是,游戏迷们辛苦了,并收集了所有148个关卡的完整地图,因此我们可以使用它们来了解每个地图上应该包含的内容。)

除地图外,每个级别还应具有其他几个属性。 例如,您可能会注意到每个级别都有一个名称,例如“第1课”,“完全匹配”,“草稿和季度”等等。 不同级别也有不同的时间限制,因此我们可以假定此信息也包含在数据中。 此外,每个级别都有自己的组装芯片数量。 (我们可以假设该数字仅与该级别的芯片数相对应,但事实证明,在某些级别上,打开芯片插槽所需的芯片数量超过了所需数量。至少对于这些级别,应以显式形式表示最小数量。)

我们希望在级别数据中找到的另一条数据是提示文本。 在某些级别上,有一个“提示按钮”-地面上有一个大问号。 当芯片安装在其上时,将显示工具提示文本。 提示按钮大约在20个级别。

最后,每个级别都有一个密码-四个字母的序列,允许玩家从该级别继续游戏。 (此密码是必需的,因为Lynx没有数据存储。无法在控制台上保存游戏,因此您可以在使用密码打开控制台后继续玩游戏。)

因此,这是我们的相关数据列表:

  • 等级图
  • 级别名称
  • 密码等级
  • 时间限制
  • 筹码数
  • 工具提示文字

让我们粗略估计数据的总大小。 确定时间限制和芯片数的最简单方法。 这两个参数的值都可以在0到999的范围内,因此它们很可能以整数值的形式存储,总大小为4个字节。 密码始终由四个字母组成,因此很可能将其存储为另外四个字节,即只有8个字节。 级别名称的长度从4到19个字符不等。 如果我们假设需要另一个字节来完成该行,则该字节为20个字节,即小计为28个字节。 最长的工具提示文字超过80个字节; 如果将此值舍入为90,则总共将获得118个字节。

那水平计划呢? 大多数关卡的大小为32×32瓦片。 不存在较大的级别。 某些级别较低,但是逻辑上假设它们只是简单地嵌入在32×32卡中是合理的;如果我们假设一个图块需要一个字节,那么整个电路就需要1024个字节。 也就是说,通常,我们得到的每个级别的估计值为1142字节。 当然,这只是一个初步的粗略估计。 这些元素中的某些元素可能以不同的方式存储,或者根本没有存储在级别文件中。 或者它们可能包含我们没有注意到或不知道的其他数据。 但是到目前为止,我们已经奠定了良好的基础。

确定了我们期望在数据文件中看到的内容之后,让我们回到研究它们实际包含的内容的角度。

有什么没有


尽管乍看之下数据文件看起来完全令人费解,但您仍然可以注意到其中的几点。 首先,这是我们不到的。 例如,我们看不到关卡的名称或提示的文本。 通过研究其他文件,您可以理解这不是巧合:
  $字符串级别/ * | 少一点
 :!!;#
 &>'':: 4#
 、、、!
 -54“;
 /&67
 !)60
 <171
 *(0 *
 82>'= /
 8> <171 &&
 9>#2')(
 ,)9
  0小时
 `@PX
 )“” *
 24 ** 5
 ;))<
 B777:.. 22C1
 E,,F
 -GDED
 EGFF16G ;; H <
国际电工委员会
 9K444
 = MBBB >> N9“ O” 9P3?
 1-24行/ 1544行(更多) 
除了ASCII垃圾的任意片段,这里什么都看不到。

大概在这些文件中的某处有级别名称和提示,但是它们不是以ASCII格式存储的,或者已经进行了一些转换(例如,由于压缩)。

还应注意以下几点:文件几乎不超过256个字节。 考虑到最初我们估计它的大小超过1140字节,这是非常小的。

-S选项以大小降序对文件进行排序。

  $ ls -lS级别| 头
总共592
 -rw-r-r-- 1面包箱面包箱680 2015年6月23日mulligan.pak
 -rw-r-r-- 1面包箱面包箱675 2015年6月23日
 -rw-r-r-- 1个面包箱面包箱671 2015年6月23日balls_o_.pak
 -rw-r-r-- 1个面包箱面包箱648 2015年6月23日cake_wal.pak
 -rw-r-r-- 1个面包箱面包箱647 2015年6月23日citybloc.pak
 -rw-r-r-- 1个面包箱面包箱639 2015年6月23日four_ple.pak
 -rw-r-r-- 1个面包箱面包箱636 2015年6月23日trust_me.pak
 -rw-r-r-- 1个面包箱面包箱625 2015年6月23日block_n_.pak
 -rw-r-r-- 1个面包箱面包箱622 2015年6月23日mix_up.pak 

最大的文件仅占用680个字节,这不是很多。 最小的是什么?

-r选项告诉ls颠倒顺序。

  $ ls -lSr级别| 头
总共592
 -rw-r-r-- 1面包箱面包箱206 2015年6月23日kablam.pak
 -rw-r-r-- 1个面包箱面包箱214 2015年6月23日fortune_.pak
 -rw-r-r-- 1个面包箱面包箱219年6月23日digdirt.pak
 -rw-r-r-- 1面包箱面包箱226 Jun 23 2015 lesson_2.pak
 -rw-r-r-- 1个面包箱面包箱229 2015年6月23日lesson_8.pak
 -rw-r-r-- 1面包箱面包箱237 2015年6月23日partial_.pak
 -rw-r-r-- 1个面包箱面包箱239年6月23日knot.pak
 -rw-r-r-- 1面包箱面包箱247 2015年6月23日cellbloc.pak
 -rw-r-r-- 1个面包箱面包箱248 2015年6月23日torturec.pak 

最小的文件仅占用206个字节,比最大的文件小三倍以上。 考虑到我们期望近似相同的电平大小这一事实,这是一个相当大的范围。

在我们的初步评估中,我们假设该卡每个图块将需要一个字节,而仅需要1024个字节。 如果我们将此估算值减半,即每个图块仅需要4位(或每字节2个图块),则该卡仍将占用512字节。 512小于680,但仍大于大多数级别。 在任何情况下-4位将仅提供16个不同的值,并且在游戏中还有更多不同的对象。

即,很明显,卡未以开放形式存储在这些文件中。 它们使用更复杂的编码,提供了更有效的描述,并且/或者以某种方式进行了压缩。 例如,在“第1课”级别,我们可以看到缺少“空”图块的条目将如何显着减少地图数据的整体大小。

我们可以查看最大文件的地图:


57级地图:变形迷宫


98级卡:收缩

然后将它们与最小文件的映射进行比较:


第106级卡:KABLAM


112级卡:《财富》偏爱

这种比较支持我们的想法,即小数据文件对应于更简单的级别,或包含更多的冗余。 例如,如果通过某种行程编码对数据进行压缩,则可以轻松解释不同文件的大小间隔。

如果文件实际上是加密的,那么我们很可能必须在继续解密卡数据之前解密压缩。

我们同时研究几个文件


我们对第一个数据文件的简要研究使我们可以做一些假设,但没有发现任何具体的东西。 下一步,我们将开始探索几个数据文件的模式。 目前,我们假定所有148个文件都使用相同的排序方案来编码数据,因此在这些文件中查找重复的模式将有助于我们开始使用。

让我们从图块的最开始开始。 文件顶部最有可能用于存储“元数据”,以告诉我们有关文件内容的信息。 通过仅查看十六进制转储的第一行,我们可以对前16个字节执行简单而快速的比较,并在其中查找突出的模式:

  $代表f的水平/ *; 做xxd $ f |  sed -n 1p; 完成| 少一点
 00000000:2300 dc01 0300 0004 0101 0a03 030b 2323#............. ##
 00000000:2d00 bf01 0300 0015 0101 2203 0329 2222 -...“ ..)”
 00000000:2b00 a101 0301 0105 0000 0601 0207 0505 + ...............
 00000000:1d00 d300 0200 0003 0101 0402 0205 0102 ................
 00000000:2d00 7a01 0300 0006 1414 0701 0109 0303 -.z .............
 00000000:3100 0802 0200 0003 0101 0502 0206 1313 1 ...............
 00000000:1a00 b700 0200 0003 0100 0502 0206 0101 ................
 00000000:1a00 0601 0300 0005 0001 0601 0107 0303 ................
 00000000:2000 7a01 0200 0003 0202 0401 0105 0028 .z ............(
 00000000:3a00 a400 0200 0003 2828 0428 0205 0303:.......((。(。
 00000000:2600 da00 0300 0004 0507 0901 010a 0303&...............
 00000000:2400 f000 0300 0004 0303 0504 0407 0101 $ ..............................
 00000000:2a00 ef01 0300 0005 0101 0614 0007 0303 * ...............
 00000000:2c00 8c01 0300 0004 0303 0500 0107 0101,......
 00000000:2a00 0001 0300 0004 0303 0501 0107 0404 * ...............
 00000000:1b00 6d01 0200 0003 0101 0502 0206 0003 ..m .............
 00000000:1e00 1701 0200 0003 0202 0401 0105 0013 ................
 00000000:3200 ee01 0f00 0015 0101 270f 0f29 1414 2 .........'..)..
 00000000:2a00 5b01 0300 0005 0303 0601 0107 1414 *。[.............
 00000000:2c00 8a01 0200 0003 0202 0401 0105 0303,......
 00000000:1d00 9c00 0216 1604 0000 0516 0107 0205 ................
 00000000:2000 e100 0200 0003 0101 0402 0205 0303 ...............
 00000000:2000 2601 0300 0004 0303 0502 0207 0101.&.............
 00000000:1f00 f600 0132 0403 0000 0532 3206 0404 ..... 2 ..... 22 ...
 1-24行/ 148行(更多) 

查看此转储,您可以看到在每一列中都有一些相似的值。

从第一个字节开始,我们很快意识到它的值在非常有限的值范围内,十六进制为40 (或十进制大约为20–60 )。 这是一个非常具体的功能。

更有意思的是,每个文件的第二个字节始终为零,无一例外。 第二个字节可能未使用,或者是一个占位符。 但是,还有另一种可能性-前两个字节一起代表以Little-Endian顺序存储的16位值。

什么是小端? 当存储一个多于一个字节的数值时,必须首先选择存储字节的顺序。 如果首先存储一个代表数字较小部分的字节,则这称为直接顺序( little-endian ); 如果您首先存储表示大部分数字的字节,则这是相反的顺序( big-endian )。 例如,我们以相反的顺序(十进制)写出十进制值:“ 42”行表示“四十二”,而不是“四二十”。 小尾数是许多微处理器系列的自然顺序,因此,它通常更受欢迎,但网络协议除外,后者通常需要大尾数。

如果继续分析,我们很快就会看到文件中的第三个字节与前两个字节不同:其值在很大范围内变化。 但是,第四个字节始终是0102 ,而01是最常见的。 这也向我们暗示了这两个字节构成了另一个16位值,该值大约在十进制值0-700的范围内。 该假设还可以通过以下事实得到证实:如果第四个字节的值为02 ,则第三个字节的值通常较低,如果第四个字节为00 ,则通常较大。

顺便说一句,值得注意的是,这部分是默认情况下十六进制转储格式成对显示字节的原因-这使得读取16位整数序列更加容易。 使用16位计算机时,十六进制转储格式已标准化。 尝试用xxd xxd -g1替换xxd以完全禁用分组,并且您会发现识别一行中间的字节对是一项繁重的工作。 这是一个简单的例子,说明用来研究陌生数据的工具如何使我们注意到某些类型的模式。 最好在xxd默认情况下突出显示此模式,因为它非常普遍(即使在今天,到处都使用64位计算机时)。 但是,如果没有帮助,了解如何更改这些参数会很有用。

让我们继续进行视觉探索,看看是否从16位整数值保留了此模式。 第五个字节通常具有非常低的值:经常遇到0203 ,最大值似乎是05 。 文件的第六个字节通常等于零-但有时它包含更大的值,例如322C 。 在这一对中,关于间隔中分布的值的假设没有得到特别证实。

我们仔细研究初始值


我们可以使用od生成十六进制转储来测试我们的猜测。 od实用程序类似于xxd ,但是提供了更多的输出格式选择。 我们可以使用它来将输出转储为16位十进制整数:

od实用程序的-t选项指定输出格式。 在这种情况下, u代表无符号十进制数, 2代表每条记录两个字节。 (您也可以使用-d选项指定此格式。)

  $代表f的水平/ *;  do od -tu2 $ f |  sed -n 1p; 完成| 少一点
 0000000 35476 310242572577782819 8995
 0000000 45447 3 5376 257802 10499 8738
 0000000 43417259259128 262 1794 1285
 0000000 29211 27682575161282513
 0000000 45 378 3 1536 5140 263 2305 771
 0000000 49 520 2 768 257 517 1538 4883
 0000000 26183 2768 1517 1538257
 0000000 26262 31280 256262 1793 771
 0000000 32378 27685142601281 10240
 0000000 58164 2768 10280 10244 1282 771
 0000000 38218 3 1024 1797 265 2561 771
 0000000 36 240 3 1024 771 1029 1796 257
 0000000 42495 3 1280 257 5126 1792 771
 0000000 44 396 3 1024 771 5 1793 257
 0000000 42 256 3 1024 771 261 1793 1028
 0000000 27365 2768257257517538768
 0000000 30 279 2 768 514 260 1281 4864
 0000000 50 494 15 5376 257 3879 10511 5140
 0000000 42347 3 1280 771262 1793 5140
 0000000 44 394 2 768 514 260 1281 771
 0000000 29 156 5634 1046 0 5637 1793 1282
 0000000 32225 27682575161282771
 0000000 32 294 3 1024 771 517 1794 257
 0000000 31 246 12801 772 0 12805 1586 1028
 1-24行/ 148行(更多) 

此输出表明我们对前几个字节的猜测是正确的。 我们看到第一个16位值在20-70的十进制范围内,第二个16位值在100-600的十进制范围内。 但是,后续值的表现不太好。 某些模式出现在其中(例如,在第四位置,通常为1024),但是它们没有文件的第一个值固有的可重复性。

因此,我们首先假设文件的前四个字节是特殊的,并且由两个16位值组成。 由于它们位于文件的开头,因此很可能是元数据,并有助于确定如何读取文件的其余部分。

实际上,第二个值间隔(100–600)非常接近我们之前注意到的文件大小间隔(208–680)。 也许这不是巧合? 让我们提出一个假设:存储在文件第三和第四字节中的16位值与文件总大小相关。 现在我们有了一个假设,我们可以对其进行检验。 让我们看看这个地方的大型文件是否始终具有较大的值,而小型文件具有较小的值。

要以字节为单位显示文件大小而不显示任何其他信息,可以将wc-c选项一起使用。 同样,您可以在od中添加选项,以仅显示我们感兴趣的值。 然后,我们可以使用命令替换将这些值写入shell变量并一起显示:

od实用程序的-An选项禁用最左边的列,该列显示文件中的偏移量,并且-N4告诉od在文件的前4个字节之后停止。

  $代表f的水平/ *; 做大小= $(wc -c <$ f); 数据= $(od -tuS -An -N4 $ f);  echo“ $ size:$ data”; 完成| 少一点
 585:35 476
 586:45447
 550:43,417
 302:29,211
 517:45 378
 671:49520
 265:26183
 344:26,262
 478:32,378
 342:58 164
 336:38218
 352:36,240
 625:42495
 532:44,396
 386:42256
 450:27365
 373:30 279
 648:50 494
 477:42347
 530:44,394
 247:29 156
 325:32,225
 394:32,294
 343:31,246 

查看此输出,您可以看到这些值是近似相关的。 较小的文件通常在第二位置具有较低的值,而较大的文件具有较大的值。 但是,相关性不准确,值得注意的是,文件大小总是明显大于存储在其中的值。

此外,文件大小较大时,前16位值通常也较大,但是匹配也不是很完整,您可以轻松地在第一个位置找到具有较大值的中型文件的示例。但是,也许如果我们将这两个值加在一起,它们的总和将与文件大小更好地关联?

我们可以使用read将输出中的两个数字提取od到单独的变量中,然后使用Shell算术来找到它们的总和:

Shell命令read不能在竖线的右侧使用,因为传输到管道的命令是在子命令处理器(子外壳)中执行的,子命令处理器在退出时将其环境变量带到位接收器。因此,我们需要使用流程替换功能bash,并将输出定向od到一个临时文件,然后可以将其重定向到command read

$代表f的水平/ *; 做大小= $(wc -c <$ f); 读取v1 v2 <<(od -tuS -An -N4 $ f); sum = $(($ v1 + $ v2));
    echo“ $ size:$ v1 + $ v2 = $ sum”; 完成| 少一点
585:35 + 476 = 511
586:45 + 447 = 492
550:43 + 417 = 460
302:29 + 211 = 240
517:45 + 378 = 423
671:49 + 520 = 569
265:26 + 183 = 209
344:26 + 262 = 288
478:32 + 378 = 410
342:58 + 164 = 222
336:38 + 218 = 256
352:36 + 240 = 276
625:42 + 495 = 537
532:44 + 396 = 440
386:42 + 256 = 298
450:27 + 365 = 392
373:30 + 279 = 309
648:50 + 494 = 544
477:42 + 347 = 389
530:44 + 394 = 438
247:29 + 156 = 185
325:32 + 225 = 257
394:32 + 294 = 326
343:31 + 246 = 277
1-24行/ 148行(更多) 

这两个数字的总和也与文件大小大致相关,但是它们仍然不完全匹配。它们有什么不同?让我们展示它们的区别:

$代表f的水平/ *; 做大小= $(wc -c <$ f); 读取v1 v2 <<(od -tuS -An -N4 $ f); diff = $(($ size-$ v1-$ v2));
    echo“ $ size = $ v1 + $ v2 + $ diff”; 完成| 少一点
585 = 35 + 476 + 74
586 = 45 + 447 + 94
550 = 43 + 417 + 90
302 = 29 + 211 + 62
517 = 45 + 378 + 94
671 = 49 + 520 + 102
265 = 26 + 183 + 56
344 = 26 + 262 + 56
478 = 32 + 378 + 68
342 = 58 + 164 + 120
336 = 38 + 218 + 80
352 = 36 + 240 + 76
625 = 42 + 495 + 88
532 = 44 + 396 + 92
386 = 42 + 256 + 88
450 = 27 + 365 + 58
373 = 30 + 279 + 64
648 = 50 + 494 + 104
477 = 42 + 347 + 88
530 = 44 + 394 + 92
247 = 29 + 156 + 62
325 = 32 + 225 + 68
394 = 32 + 294 + 68
343 = 31 + 246 + 66
lines 1-24/148 (more) 

差值或“剩余”值显示在输出的最右侧。该值并不完全落入恒定模式,但似乎保持在40-120的有限范围内。同样,文件越大,通常其剩余价值就越大。但是,有时小文件也具有较大的残差值,因此这并不是我们想要的常数。

但是,值得注意的是,残基的值永远不会为负。因此,这两个元数据值指示文件的各个部分的想法仍然很有吸引力。

(如果您足够小心,您可能已经看到一些提示,提示尚未注意到的连接。如果尚未提示,请继续阅读;机密将很快被发现。)

更大的跨文件比较


在这一点上,一次能够交叉比较超过16个字节将是很好的。为此,我们需要不同类型的可视化。一种好的方法是创建一个图像,其中每个像素表示文件之一的单独字节,而颜色表示该字节的值。如果每个数据文件由单行图像像素指示,则图像一次可以显示所有148个文件的一部分。由于所有文件的大小都不同,因此我们使用每个文件的前200个字节来构建矩形图像。

最简单的方法是构建灰度图像,其中每个字节的值对应于不同的灰度级别。使用我们的数据创建PGM文件非常简单,因为PGM标头仅包含ASCII文本:

 $ echo P5 200 148 255 >hdr.pgm 

PGM? PGM, «portable graymap» (« ») — , : ASCII, . — PBM («portable bitmap», « »), 8 , PPM («portable pixmap», « »), 3 .

P5是PGM文件格式的初始签名。接下来的两个数字200148,指定图像的宽度和高度,最后一个数字255,表示每个像素的最大值。PGM标头以新行结尾,后跟像素数据。(值得注意的是,PGM标头通常被分成三行单独的文本,但是PGM标准只要求元素之间用空格字符分隔。)

我们可以使用该实用程序head从每个文件中提取前200个字节:

$代表f的水平/ *; 做-c200 $ f; 完成> out.pgm

然后,我们可以将它们与标题连接起来并创建显示的图像:

xview-这是一个用于在窗口中显示图像的旧X程序。您可以用自己喜欢的图像查看器替换它,例如displayImageMagick 的实用程序,但是请记住,令人惊讶的是,有许多图像查看器不接受重定向到标准输入的图像文件。

$ cat hdr.pgm out.pgm | xview /开发/标准输入


如果您难以考虑深色图像中的细节,则可以选择其他配色方案。使用pgmtoppmImageMagick 的实用程序将像素转换为不同的颜色范围。此版本将创建一个“负”图像:

$ cat hdr.pgm out.pgm | pgmtoppm白黑色| xview /开发/标准输入



此版本将低值显示为黄色,将高值显示为蓝色:

$ cat hdr.pgm out.pgm | pgmtoppm黄蓝色| xview /开发/标准输入


颜色的可见性是一个非常主观的问题,因此您可以尝试并选择最适合您的颜色。尽管如此,200×148的图像还是很小的,因此最好通过增加其尺寸来增加可见性:

$ cat hdr.pgm out.pgm | xview -zoom 300 / dev / stdin


图像较暗,这意味着大多数字节包含较小的值。靠近左侧边缘的明显是大部分明亮像素的明显条带与此形成鲜明对比。该条位于文件的第三个字节中,正如我们上面所说,该值在整个值范围内变化。

尽管在第三个字节之外没有很多高值,但是当它们出现时,它们通常由序列组成,从而在图像中产生短而明亮的条纹。这些系列中的一些会定期中断,从而产生虚线效果。 (也许,通过正确的颜色选择,可能会在较深的颜色中注意到此类序列。)

通过仔细研究图像,可以理解,主要在左侧是小的垂直条纹。这些频段告诉我们大多数文件中的某些可重复性。但是并不是在所有文件中-有时会出现一些像素带中断的像素行-但这足以确定实际图案的存在。这种图案消失在图像的右侧,条纹上的深色背景让位于更嘈杂和不确定的位置。 (似乎在图像的最左侧部分也缺少条纹,但是,我再说一遍,当使用不同的配色方案时,您可能会发现它们比此处看起来更靠近左侧边缘。)

条纹由相对较暗的像素背景的较亮像素的细线组成。因此,此图形模式应与数据文件的模式相关,在数据文件中,稍大的字节值之间均等散布着稍大的值。似乎条纹大约在图像的中间被耗尽。因为它显示了文件的前200个字节,所以您应该期望字节模式在大约100个字节之后结束。

这些模式在不同的数据文件中发生更改的事实应引起我们这样一个问题:在前200个字节之后,文件的外观将是什么样的。好了,我们可以轻松地用实用程序替换该实用head程序,tail然后看看最后200个字节是什么样的:

$代表f的水平/ *; 做尾巴-c200 $ f; 完成> out.pgm; 猫hdr.pgm out.pgm | xview -zoom 300 / dev / stdin


我们立即看到数据文件的这一区域非常不同。在这里,字节更为常见,尤其是在文件末尾。(但是,像以前一样,他们更喜欢聚在一起,用明亮的水平条纹覆盖图像。)似乎高字节值的频率几乎增加到了尽头,它突然中断并被大约十到十二个字节的低值所代替。而且这里的模式也不是通用的,但是它太标准了,不能巧合。

也许在文件中间,可能还有我们尚未考虑的其他领域。我们要做的下一件事是以这种方式检查整个文件。但是由于所有文件的大小都不同,因此无法将它们放置在漂亮的矩形像素阵列中。我们可以用黑色像素填充每行的末尾,但是最好调整它们的大小以使所有行的宽度相同,并且不同文件的比例区域或多或少匹配。

实际上,我们可以多做一些事情。您可以使用Python及其库处理图像PIL(“枕头”):

文件showbytes.py:

 import sys from PIL import Image # Retrieve the full list of data files. filenames = sys.argv[1:] # Create a grayscale image, its height equal to the number of data files. width = 750 height = len(filenames) image = Image.new('L', (width, height)) # Fill in the image, one row at a time. for y in range(height): # Retrieve the contents of one data file. data = open(filenames[y]).read() linewidth = len(data) # Turn the data into a pixel-high image, each byte becoming one pixel. line = Image.new(image.mode, (linewidth, 1)) linepixels = line.load() for x in range(linewidth): linepixels[x,0] = ord(data[x]) # Stretch the line out to fit the final image, and paste it into place. line = line.resize((width, 1)) image.paste(line, (0, y)) # Magnify the final image and display it. image = image.resize((width, 3 * height)) image.show() 

当我们调用此脚本时,使用数据文件的完整列表作为参数,它将创建完整的图像并将其显示在单独的窗口中:

 $ python showbytes.py级别/ * 


不幸的是,尽管这张图片是完整的,但它并没有向我们展示任何新东西。(但实际上它显示得更少,因为调整大小破坏了条纹上的图案。)可能,要研究整个数据集,我们需要一个更好的可视化过程。

表征数据


考虑到这一点,让我们停一会儿并完成一次完整的数据普查。我们需要知道数据文件是否优先使用某些字节值。例如,如果每个值通常都相等地重复,那么这将有力地证明文件实际上已被压缩。

要完全

重写值,Python中仅几行就足够了:census.py文件:

 import sys data = sys.stdin.read() for c in range(256): print c, data.count(chr(c)) 

将所有数据放入一个变量后,我们可以计算每个字节值的出现频率。

$猫水平/ * | python ./census.py | 少一点
0 2458
1 2525
2 1626
3 1768
4 1042
5 1491
6 1081
7 1445
8958
9 1541
10 1279
11 1224
12,845
13908
14859
15 1022
16679
17 1087
18,881
19 1116
20 1007
1189年21
22 1029
23,733
1-24行/ 256行(更多) 

我们看到,最常见的是字节值0和1,下一个频率是2和3,之后字节数继续减少(尽管恒定性较低)。为了更好地可视化此数据,我们可以将输出传输到gnuplot并将此普查转换为直方图:实用程序

选项命令在完成工作后不要关闭带有图形的窗口-pgnuplotgnuplot

$猫水平/ * | python ./census.py | gnuplot -p -e'使用框绘制“-”


非常值得注意的是,前几个字节的值比其他所有字节的值更常见。以下几个值也很常见,然后大约50个值的频率沿着平滑的概率曲线开始下降。但是,存在一个高值子集,它们彼此均匀分开,其频率似乎相当稳定。通过查看原始输出,我们可以确保此子集包含可以被8整除的值。

值数量上的这些差异表明字节值存在几种不同的“类”,因此逻辑上看一下这些类的分布方式是合乎逻辑的。第一组字节值将是最低的值:0、1、2和3。然后第二组字节的值可以是4到64之间的值。第三组字节的值可以是64以上的值,这些值可以被8整除。不可被8的整数除以大于64的值将是第四组和最后一组。

考虑到所有这些,我们可以更改最后写入的图像生成脚本。与其以单独的颜色显示每个字节的实际值,不如显示每个字节所属的组。您可以为四个组分别分配唯一的颜色,这将帮助我们查看某些值是否确实出现在某些位置。

Showbytes2.py文件:

 import sys from PIL import Image # Retrieve the full list of data files. filenames = sys.argv[1:] # Create a color image, its height equal to the number of data files. width = 750 height = len(filenames) image = Image.new('RGB', (width, height)) # Fill in the image, one row at a time. for y in range(height): # Retrieve the contents of one data file. data = open(filenames[y]).read() linewidth = len(data) # Turn the data into a pixel-high image, each byte becoming one pixel. line = Image.new(image.mode, (linewidth, 1)) linepixels = line.load() # Determine which group each byte belongs to and assign it a color. for x in range(linewidth): byte = ord(data[x]) if byte < 0x04: linepixels[x,0] = (255, 0, 0) elif byte < 0x40: linepixels[x,0] = (0, 255, 0) elif byte % 8 == 0: linepixels[x,0] = (0, 0, 255) else: linepixels[x,0] = (255, 255, 255) # Paste the line of pixels into the final image, stretching to fit. line = line.resize((width, 1)) image.paste(line, (0, y)) # Magnify the final image and display it. image = image.resize((width, 3 * height)) image.show() 

我们将红色,绿色,蓝色和白色分为四个组。(同样,您可以尝试选择其他颜色来满足您的偏好。)

 $ python showbytes2.py级别/ * 


借助此图像,我们可以预先确认将数据文件分为五个部分的正确性:

  1. 我们之前发现的四字节标头。
  2. , , (.. ).
  3. , (. 64).
  4. , .
  5. , .

这些颜色可以清楚的是,在第四部分,其由高字节值为主,作为图像中看到灰度特别是高字节值整除8.为主

从上面的图片,我们知道,在第二部分,即条纹的部分延伸到几乎完全红色的区域。实际上,在第一批图像中,我们看到带有条纹的部分(从左到右)平均逐渐变亮。

我们再次看到,来自主要第三部分的绿色像素会不时地由间歇的绿色和红色像素(蓝色或白色)形成点状图案。但是,这种模式不是特别规则,并且可能是虚构的。

当然,将文件分为五个部分是非常任意的。高字节值可被八整除的第四部分可能恰好是第三部分的末尾。或者可能会发现,最好将较大的第三部分分成我们尚未确定的几个部分。在此阶段,零件的发现有助于我们更多地寻找进一步研究的地方。就目前而言,足以让我们知道字节值的一般组成在某些部分发生了变化,而对它们大小的大概了解将有助于我们继续进行研究。

结构搜索


接下来我们要寻找什么?好吧,和以前一样,最简单的启动方法是从文件顶部开始。或者说,靠近顶部。由于我们已经很自信地将第一部分标识为一个四字节的标头,因此让我们仔细看看接下来的内容-我们称为第二部分或部分频段的区域。这些条带是该结构存在的最强烈暗示,因此最好在此处寻找存在该图案的新证据。

(暂时,我们假定剥离模式在前四个字节之后立即开始。从外观上看,这并不明显,但是似乎是可能的,检查字节值应该可以迅速向我们展示事实。)

让我们回到十六进制转储,这一次着重于第二部分。请记住,我们希望找到一个重复模式,该模式的值略高,平均分布在略低的值之间。

期权-s4订单xxd跳过前4个字节的文件。

$代表f的水平/ *; 做xxd -s4 $ f | sed -n 1p; 完成| 少一点
00000004:0200 0003 0202 0401 0105 0303 0700 0108 ................
00000004:0201 0104 0000 0504 0407 0202 0902 010a ................
00000004:0300 0004 0303 0504 0407 0505 0801 0109 ................
00000004:0300 0009 0202 1203 0313 0909 1401 0115 ................
00000004:0203 0305 0000 0602 0207 0505 0901 010a ................
00000004:0203 0304 0000 0502 0206 0404 0901 010a ................
00000004:0300 0005 022a 0602 2907 0303 0902 000a ..... * ..).......
00000004: 0203 0305 0000 0605 0507 0101 0802 0209 ................
00000004: 0300 0007 0303 0901 010a 0707 0b09 090c ................
00000004: 0300 0004 0101 0903 030e 0404 1609 0920 ...............
00000004: 0200 0003 1313 0402 0205 0013 0701 0109 ................
00000004: 0500 0006 0505 0701 0109 0606 0e07 070f ................
00000004: 0100 0003 0101 0a03 030b 0a0a 0e32 3216 .............22.
00000004: 0300 0004 0705 0903 030a 0606 0b08 080c ................
00000004: 0200 0003 0701 0402 0209 0501 0a08 080b ................
00000004: 0200 0003 0202 0901 010a 0303 0b05 010d ................
00000004: 0200 0003 0202 0403 0305 0101 0904 040a ................
00000004: 0300 0007 0303 0f01 0115 0707 2114 1422 ............!.."
00000004: 0200 0003 0202 0403 0309 0101 0a04 040b ................
00000004: 0231 3103 0202 0500 0006 0303 0701 0109 .11.............
00000004: 0200 0003 0202 0b32 320c 0303 0e08 0811 .......22.......
00000004: 0201 0103 0000 0902 020a 0303 0b09 090c ................
00000004: 0200 0003 0202 0a01 010b 0303 0d0b 0b0f ................
00000004: 0300 0005 0303 0701 0109 0001 0b05 051b ................
lines 27-50/148 (more) 

通过仔细查看字符串中的字节序列,我们可以看到一种模式,其中第一,第四,第七和第十个字节大于其最近的邻居。这种模式是不完美的,有例外,但是它绝对足够稳定,可以创建在所有图像上看到的条带的视觉可重复性。 (这足以证实我们的假设,即条纹模式在四字节标头之后立即开始。)

这种模式恒定性清楚地表明文件的这一部分是一个数组或表,并且数组中的每个记录的长度为三个字节。

您可以配置十六进制转储格式,以便更轻松地将输出显示为一系列三联式:

该选项-g3将分组设置为三个字节,而不是两个字节。选件-c18 每行设置18个字节(3的倍数),而不是16。

$代表f的水平/ *; 做xxd -s4 -g3 -c18 $ f | sed -n 1p; 完成| 少一点
00000004:050000 060505 070101 090606 0e0707 0f0001 ..................
00000004:010000 030101 0a0303 0b0a0a 0e3232 161414 ............. 22 ...
00000004:030000 040705 090303 0a0606 0b0808 0c0101 ..................
00000004:020000 030701 040202 090501 0a0808 0b0101 ..................
00000004:020000 030202 090101 0a0303 0b0501 0d0302 ..................
00000004:020000 030202 040303 050101 090404 0a0302 ..................
00000004:030000 070303 0f0101 150707 211414 221313 ............!..“ ..
00000004:020000 030202 040303 090101 0a0404 0b0001 ..................
00000004:023131 030202 050000 060303 070101 090505 .11 ...............
00000004:020000 030202 0b3232 0c0303 0e0808 110b0b ....... 22 .........
00000004: 020101 030000 090202 0a0303 0b0909 0c0a0a ..................
00000004: 020000 030202 0a0101 0b0303 0d0b0b 0f2323 ................##
00000004: 030000 050303 070101 090001 0b0505 1b0707 ..................
00000004: 022323 030000 040202 050303 030101 070505 .##...............
00000004: 031414 050000 060303 070505 080101 090707 ..................
00000004: 030000 050202 060303 070505 080101 090606 ..................
00000004: 030202 040000 050303 070404 080005 090101 ..................
00000004: 030202 040000 050303 090404 1d0101 1f0909 ..................
00000004: 020000 050303 060101 070202 0f0300 110505 ..................
00000004: 050000 070101 0c0505 0d0007 110c0c 120707 ..................
00000004: 030202 050000 060303 070505 080101 090606 ..................
00000004: 020000 030101 050202 060505 070100 080303 ..................
00000004: 020000 030202 050303 090101 0a0505 0b0302 ..................
00000004: 022c2c 030000 040202 020303 050101 060202 .,,...............
lines 38-61/148 (more) 

在以这种方式格式化的转储中,此模式的其他一些功能开始出现。和以前一样,每个三元组的第一个字节通常大于其周围的字节。您还可以注意到,每个三元组的第二个和第三个字节通常是重复的。在第一列中,我们将看到第二个和第三个字节的大多数值相等0000。但是非零值也经常成对出现,例如01012323。这种模式也不是完美的,但有太多共同点,并非巧合。再看一下右边的ASCII列,我们会看到,当我们拥有与可打印ASCII字符相对应的字节值时,它们通常成对出现。

另一个值得一提的模式不能立即引起注意:每个三元组的第一个字节从左到右增加。尽管这种模式不太明显,但其稳定性很高。在找到第一个不匹配项之前,我们需要查看很多行。字节通常以较小的值增加,但是它们不表示任何常规模式。

在研究原始图像时,我们注意到带有条纹的部分在每个文件中结尾不在同一位置。从左侧创建图案带到右侧创建随机噪声都有过渡,但是此过渡发生在不同点的每一行像素。这意味着必须有一些标记,以便读取数据文件的程序可以了解数组在哪里结束以及另一组数据开始。

让我们回到第一级转储来检查整个文件:

 $ xxd -s4 -g3 -c18级/节_1.pak
00000004:020000 040202 050404 070505 080707 090001 ..................
00000016:0a0101 0b0808 0d0a0a 110023 150907 180200 ...........#......
00000028:22090d 260911 270b0b 280705 291e01 272705“ ..&..'..(..)..''。
0000003a:020d01 220704 090209 0a0215 042609 250111 ...“ .........&。%..
0000004c:150222 1d0124 011d0d 010709 002000 1b0400 ..“ .. $ ....... ....
0000005e:1a0020 152609 1f0033 002911 152223 02110d ...&... 3.)..“#...
00000070:010726 091f18 291115 09181a 022302 1b0215 ..&...)......#....
00000082:22011c 011c0d 0a0704 090201 020128 260123“ .............(&。#
00000094:150509 020121 150522 0a2727 0b0504 00060b .....!..“ .''......
000000a6: 082804 18780b 082804 18700b 082804 186400 .(..x..(..p..(..d.
000000b8: 17101e 1e1a19 010300 0e1a17 17100e 1f010e ..................
000000ca: 13141b 291f1a 001210 1f011b 0c1e1f 011f13 ...)..............
000000dc: 10010e 13141b 001e1a 0e1610 1f2d00 201e10 .............-. ..
000000ee: 011610 24291f 1a011a 1b1019 000f1a 1a1d1e ...$).............
00000100: 2d02 

通过研究三连音的序列,我们可以临时假定此数据中具有波段的部分在17个三连音之后以offset结束00000036。这是不准确的,但是每个三元组的第一个字节会不断增加其值,然后再减少第十八个三元组。另一个证明:在第18个三元组中,第二个字节与第一个字节具有相同的含义。我们还没有注意到这一点,但是如果我们回头看一看,我们将看到第一个字节永远不会等于第二个或第三个字节。

如果我们的标记理论正确,那么有两种可能性。首先,很可能在带状部分之后有一些特殊的字节值(在第十七个三元组之后)。其次,可能存在一个存储在某处的值,该值等于带有条纹的零件的大小。此大小可以等于17(即,表示三元组的数目),或51(表示,一部分中的字节总数),或55(51 + 4,即该部分结束的文件的偏移量)。

对于第一个选项,双字节值可能是该部分结尾的标记(假设这样的序列永远不会在第二部分中出现)。但是,仔细研究其他几个数据文件与该想法相矛盾,因为这种模式不会在其他任何地方出现。

对于第二个选项,显然它将在第一部分中寻找大小指示器。看得出-四字节文件标头中的第一个16位值为17:

 $ od -An -tuS -N4级别/ lesson_1.pak
    17203 

如果我们的理论是正确的,那么此值并不决定带有条纹的部分的大小,而是三字节记录的数量。为了测试这个想法,让我们回到计算中,我们将两个16位整数值的总和与文件大小进行比较。这次,我们将第一个数字乘以三得到字节的实际大小:

$代表f的水平/ *; 做大小= $(wc -c <$ f); 读取v1 v2 <<(od -tuS -An -N4 $ f); diff = $(($ size-3 * $ v1-$ v2));
    echo“ $ size = 3 * $ v1 + $ v2 + $ diff”; 完成| 少一点
585 = 3 * 35 + 476 + 4
586 = 3 * 45 + 447 + 4
550 = 3 * 43 + 417 + 4
302 = 3 * 29 + 211 + 4
517 = 3 * 45 + 378 + 4
671 = 3 * 49 + 520 + 4
265 = 3 * 26 + 183 + 4
344 = 3 * 26 + 262 + 4
478 = 3 * 32 + 378 + 4
342 = 3 * 58 + 164 + 4
336 = 3 * 38 + 218 + 4
352 = 3 * 36 + 240 + 4
625 = 3 * 42 + 495 + 4
532 = 3 * 44 + 396 + 4
386 = 3 * 42 + 256 + 4
450 = 3 * 27 + 365 + 4
373 = 3 * 30 + 279 + 4
648 = 3 * 50 + 494 + 4
477 = 3 * 42 + 347 + 4
530 = 3 * 44 + 394 + 4
247 = 3 * 29 + 156 + 4
325 = 3 * 32 + 225 + 4
394 = 3 * 32 + 294 + 4
343 = 3 * 31 + 246 + 4
lines 1-24/148 (more) 

是的进行此更改后,标头中的总量始终比整个数据文件的大小小四倍。而且由于首标中的字节数也是4,所以很明显这不是巧合。第一个数字为我们提供表中三字节条目的数量,第二个数字为我们提供构成数据文件其余部分的字节数。

我们找到了一个常数公式,这意味着我们现在已经完全了解第一部分中的数字是什么意思。

(顺便说一下,这是细心的读者可以注意到的非常秘密的模式。对方程的仔细研究表明,当文件具有相同的第一个数字时,它们将具有相同的残差。这是因为差异始终是值的两倍。第一个数字。这是一个不明显的模式,但是一个刻苦或成功的观察者会注意到它。)

因此,我们可以说文件包含三个主要部分:

  1. 四字节报头;
  2. 三字节记录表;
  3. 文件的其余部分,据说其中包含大部分数据。

(因此,我们之前大致确定的其他部分应该是第三部分的小节。)

解释元数据


给定该方案,逻辑上假设第二部分的表中的条目是解释第三部分的数据所必需的一些元数据。

但是此表包含什么样的元数据?

上面我们注意到,有两个迹象表明数据文件可能已被压缩。 (现在这似乎更加合理,因为我们知道每个文件的第三部分(大概包含每个级别的数据)的大小仅为100-600字节。)如果是这样,则主数据之前的表很有可能包含压缩元数据-开箱时使用的字典。例如,在通过霍夫曼算法编码的数据之前,通常会有一个字典,将原始字节值映射到位序列。尽管我们不希望这些文件通过霍夫曼算法进行编码(由于数据在字节级别显示出清晰的模式,也就是说,它们几乎不是比特流),但尝试将此表解释为解压缩字典还是明智的。

(当然,并不是每种压缩类型都使用存储的字典。例如,使用deflate算法gzipzlib允许您直接从数据流中重新创建字典。但是这种情况是例外,而不是规则。)

通常,字典条目由两部分组成:键和价值观。当然,有时隐含一个键,例如,当它不是按顺序排列在查找表中而是按数组排列时。但是,我们已经注意到三字节记录似乎由两部分组成-特别是,每个记录的第一字节遵循的模式明显不同于第二字节和第三字节的模式。考虑到这一点,一个合理的第一个假设是将第一个字节视为键,而将其余两个字节视为值。

如果是这种情况,那么解释条带部分的最简单方法之一就是,第一个字节是压缩数据中需要替换的字节值,第二个和第三个字节是需要替换的值。尽管尚不清楚多少,但此方案创建的结果肯定会更大。尽管如此,这是一个逻辑假设,并且很容易验证。我们可以用Python编写一个实现此解压缩方案的简短程序:

File decompress.py:

 import struct import sys # Read the compressed data file. data = sys.stdin.read() # Extract the two integers of the four-byte header. tablesize, datasize = struct.unpack('HH', data[0:4]) data = data[4:] # Separate the dictionary table and the compressed data. tablesize *= 3 table = data[0:tablesize] data = data[tablesize:datasize] # Apply the dictionary entries to the data section. for n in range(0, len(table), 3): key = table[n] val = table[n+1:n+3] data = data.replace(key, val) # Output the expanded result. sys.stdout.write(data) 

现在,我们可以使用示例数据文件检查此脚本:

$ python ./decompress.py <levels/lesson_1.pak | xxd
00000000: 0b0b 0b0b 0404 0000 0a0a 0109 0d05 0502 ................
00000010: 0200 0100 0000 0101 0100 0009 0702 0209 ................
00000020: 1100 0125 0100 2309 0700 0009 0d1d 0124 ...%..#........$
00000030: 011d 0a0a 0105 0500 0100 2000 1b02 0200 .......... .....
00000040: 1a00 2009 0709 1100 011f 0033 001e 0100 .. ........3....
00000050: 2309 0709 0d23 0000 0023 0a0a 0105 0509 #....#...#......
00000060: 1100 011f 0200 1e01 0023 0907 0001 0200 .........#......
00000070: 1a00 0023 0000 1b00 0009 0709 0d01 1c01 ...#............
00000080: 1c0a 0a01 0105 0502 0200 0100 0001 0000 ................
00000090: 0107 0509 1101 2309 0704 0400 0100 0001 ......#.........
000000a0: 2109 0704 0409 0d01 010b 0b0b 0b08 0804 !...............
000000b0: 0402 0200 0608 0807 0707 0502 0202 0078 ...............x
000000c0: 0808 0707 0705 0202 0200 7008 0807 0707 ..........p.....
000000d0: 0502 0202 0064 0017 101e 1e1a 1901 0300 .....d..........
000000e0: 0e1a 1717 100e 1f01 0e13 141b 1e01 1f1a ................
000000f0: 0012 101f 011b 0c1e 1f01 1f13 1001 0e13 ................
00000100: 141b 001e 1a0e 1610 1f2d 0020 1e10 0116 .........-. ....
00000110: 1024 1e01 1f1a 011a 1b10 1900 0f1a 1a1d .$..............
00000120: 1e2d 0000

但是,结果并不明显。当然,生成的数据流比原始数据流部署得更多,但数量却很少。绝对不足以包含我们期望找到的所有数据。显然,这种拆包方案比必要的要简单一些。

如果我们仔细检查结果输出,我们将很快看到它以许多重复的字节开始。0b04000a-他们都成对出现。查看压缩的原件,我们会发现所有这些对都是由于字典替换而产生的。但是在此过程中,我们立即注意到所有这些重复的含义也对应于字典中的条目。也就是说,如果我们再次应用字典,那么数据将再次扩展。也许我们拆包不够?

我们的第一个猜测可能是执行第二遍,第二次应用每个字典条目以进一步扩展数据。第二个猜测可能是对字典执行多次遍历,重复该过程,直到所有与字典键相似的字节都被替换为止。但是,如果我们仔细查看字典的结构,我们会发现,当我们将所有值一次扩展时,我们仅从右至左而不是从左至右应用字典条目

以此假设为前提,我们可以看到更合理的压缩算法的结构。该程序将获取源数据并对其进行扫描,以查找最常见的双字节序列。然后,它将两个字节的序列替换为数据中未找到的一个字节值。然后,重复该过程,继续进行直到数据包含两个以上的双字节序列重复为止。实际上,这种压缩算法具有一个名称:被称为“重新配对”压缩,是“递归对”的缩写。

(这可能解释了我们在字典中看到的一些模式。在压缩过程中,字典是从左到右构建的,因此在解压缩时,应从右到左应用它。由于字典条目通常是指以前的条目,因此逻辑上第二和第三字节通常是比第一个小。)

尽管重新配对压缩不会产生令人印象深刻的结果,但是它具有一个优势:可以用最少的代码来实现解压缩器。当需要最小化压缩数据和解压缩代码大小时,我本人在某些情况下使用了重新配对

因此,我们可以在Python程序的一行中进行更改,以从右到左应用字典:

File decompress2.py:

 import struct import sys # Read the compressed data file. data = sys.stdin.read() # Extract the two integers of the four-byte header. tablesize, datasize = struct.unpack('HH', data[0:4]) data = data[4:] # Separate the dictionary table and the compressed data. tablesize *= 3 table = data[0:tablesize] data = data[tablesize:datasize] # Apply the dictionary entries to the data section in reverse order. for n in range(len(table) - 3, -3, -3): key = table[n] val = table[n+1:n+3] data = data.replace(key, val) # Output the expanded result. sys.stdout.write(data) 

如果我们尝试这个版本,输出将大得多:

 $ python ./decompress2.py <levels/lesson_1.pak | xxd | less
00000000: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000010: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000020: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000030: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000040: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000050: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000060: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000070: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000080: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000090: 0000 0000 0000 0000 0000 0000 0000 0000 ................
000000a0: 0000 0000 0000 0000 0000 0000 0000 0000 ................
000000b0: 0000 0000 0000 0000 0000 0000 0000 0000 ................
000000c0: 0000 0000 0000 0000 0000 0000 0000 0000 ................
000000d0: 0000 0000 0000 0000 0000 0000 0000 0000 ................
000000e0: 0000 0000 0000 0000 0000 0000 0000 0000 ................
000000f0: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000100: 0000 0000 0000 0000 0000 0101 0101 0100 ................
00000110: 0101 0101 0100 0000 0000 0000 0000 0000 ................
00000120: 0000 0000 0000 0000 0000 0100 0000 0101 ................
00000130: 0100 0000 0100 0000 0000 0000 0000 0000 ................
00000140: 0000 0000 0000 0000 0000 0100 2300 0125 ............#..%
00000150: 0100 2300 0100 0000 0000 0000 0000 0000 ..#.............
00000160: 0000 0000 0000 0000 0101 0101 011d 0124 ...............$
00000170:011d 0101 0101 0100 0000 0000 0000 0000 ................
1-24行/ 93行(更多) 

我们在此输出中看到很多空字节,但这很可能对应于几乎空的卡。非零字节似乎彼此相邻分组。由于我们希望找到32×32的卡,因此让我们将输出重新格式化为每行32字节:

$ python ./decompress2.py <levels / lesson_1.pak | xxd -c32 | 少一点
00000000:0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 ................................
00000020:0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 ................................
00000040:0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 ................................
00000060: 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 ................................
00000080: 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 ................................
000000a0: 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 ................................
000000c0: 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 ................................
000000e0: 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 ................................
00000100: 0000 0000 0000 0000 0000 0101 0101 0100 0101 0101 0100 0000 0000 0000 0000 0000 ................................
00000120: 0000 0000 0000 0000 0000 0100 0000 0101 0100 0000 0100 0000 0000 0000 0000 0000 ................................
00000140: 0000 0000 0000 0000 0000 0100 2300 0125 0100 2300 0100 0000 0000 0000 0000 0000 ............#..%..#.............
00000160: 0000 0000 0000 0000 0101 0101 011d 0124 011d 0101 0101 0100 0000 0000 0000 0000 ...............$................
00000180: 0000 0000 0000 0000 0100 2000 1b00 0000 0000 1a00 2000 0100 0000 0000 0000 0000 .......... ......... ...........
000001a0: 0000 0000 0000 0000 0100 2300 011f 0033 001e 0100 2300 0100 0000 0000 0000 0000 ..........#....3....#...........
000001c0: 0000 0000 0000 0000 0101 0101 0123 0000 0023 0101 0101 0100 0000 0000 0000 0000 .............#...#..............
000001e0: 0000 0000 0000 0000 0100 2300 011f 0000 001e 0100 2300 0100 0000 0000 0000 0000 ..........#.........#...........
00000200: 0000 0000 0000 0000 0100 0000 1a00 0023 0000 1b00 0000 0100 0000 0000 0000 0000 ...............#................
00000220: 0000 0000 0000 0000 0101 0101 0101 1c01 1c01 0101 0101 0100 0000 0000 0000 0000 ................................
00000240: 0000 0000 0000 0000 0000 0000 0100 0001 0000 0100 0000 0000 0000 0000 0000 0000 ................................
00000260: 0000 0000 0000 0000 0000 0000 0100 2301 2300 0100 0000 0000 0000 0000 0000 0000 ..............#.#...............
00000280: 0000 0000 0000 0000 0000 0000 0100 0001 2100 0100 0000 0000 0000 0000 0000 0000 ................!...............
000002a0: 0000 0000 0000 0000 0000 0000 0101 0101 0101 0100 0000 0000 0000 0000 0000 0000 ................................
000002c0: 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 ................................
000002e0: 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 ................................
lines 1-24/47 (more) 

仔细查看非零值的模式后,我们将看到该图在输出中清晰可见。实际上,我们可以使用“彩色”转储工具使该模式更加可见,该工具为每个字节值分配一种颜色,从而简化了对重复值模式的搜索:这

xcd是非标准工具,但是您可以从此处下载注意-r实用程序选项less,它告诉您清除控制序列。


将其与绘制的第一级地图进行比较:


毫无疑问,我们看到了关卡地图的数据。您可以确定我们已经正确确定了拆包算法。

数据解释


通过将字节值与地图图像进行比较,我们可以确定对00空白图块进行01编码,对墙进行编码并23表示芯片的内容。1A表示红色门,1B-蓝色门,依此类推。我们可以为构成整个关卡地图的芯片,钥匙,门和所有其他瓷砖分配准确的值。

现在,我们可以进入下一层,找到在那里出现的对象的字节值。并继续进行下一个级别,直到获得字节值和它们编码的游戏对象的完整列表。

正如我们最初建议的那样,该卡恰好在1024个字节之后(在offset处000003FF)结束。

这次,要删除转储的前32行,我们使用sed由于每行有32个字节,因此我们将跳过前1024个字节。


映射数据紧随其后是384个字节的序列(其值在00000400-范围内0000057F),几乎所有这些值都等于零,但非零值也落在它们之间。在此之后,出现了完全不同的字节模式,因此逻辑上假设此384字节序列是一个单独的部分将是合乎逻辑的。

如果再看几个级别,我们会很快注意到这种模式。384字节的部分实际上包括三个小节,每个小节长128个字节。每个小节都以一些非零字节开头,后跟零,以填充小节的其余部分。

某些级别包含大量数据。对于其他人(例如,对于第一级别)而言,仅是最小的。将级别与他们的卡进行比较,我们很快就会注意到,文件这一部分中的数据量与每个级别的“暴民”数量直接相关。在这种情况下,“小怪”的数量包括该级别的所有生物,地块(它们不会独立移动,但可以被推动)和主要角色Chip(玩家)。也就是说,小怪没有在地图本身上指示,而是在这三个缓冲区中编码。

在我们了解到这三个小节包含有关该级别的暴民的数据后,就不难弄清楚每个小节中包含的内容。

前128个字节的子节是确定暴民类型的字节值列表。例如,第二级缓冲区如下所示:

 $ python ./decompress2.py <levels/lesson_2.pak | xxd | less
00000400: 0608 1c1c 0808 0000 0000 0000 0000 0000 ................
00000410: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000420: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000430: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000440: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000450: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000460: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000470: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000480: a870 98a0 6868 0000 0000 0000 0000 0000 .p..hh..........
00000490: 0000 0000 0000 0000 0000 0000 0000 0000 ................
000004a0: 0000 0000 0000 0000 0000 0000 0000 0000 ................
000004b0: 0000 0000 0000 0000 0000 0000 0000 0000 ................
000004c0: 0000 0000 0000 0000 0000 0000 0000 0000 ................
000004d0: 0000 0000 0000 0000 0000 0000 0000 0000 ................
000004e0: 0000 0000 0000 0000 0000 0000 0000 0000 ................
000004f0: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000500: 6060 6060 5868 0000 0000 0000 0000 0000 ````Xh..........
00000510: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000520: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000530: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000540: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000550: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000560: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000570:0000 0000 0000 0000 0000 0000 0000 0000 ................
行64-87 / 93(更多) 

将此与水平图进行比较:


在这个级别上,有六个小怪:三个错误,两个块和一个筹码。第一个128字节的子项包含一个字节06,三个字节08和两个字节1C得出06Chip代表什么08(一个错误和1C一个块)的结论是合理的

(持续数据文件从该卡并在我们的字典小怪填充的水平相比,我们很快发现在这个理论有缺陷,该芯片可以被称为四个不同的值,即040506或者07。这组符号实际上包含所有小怪。当我们仔细研究不同的值时,我们将最终理解,值0、1、2或3被添加到指示类型的字节值中,该值指示生物的初始方向:北,东,南或西。也就是说,例如,字节值06表示Chip向南看。)

其他两个小节的意义不太明显。但是,在研究了这些小节中的重复值并比较了这些小怪的卡片后,我们将理解第二小节中的字节存储每个小怪的X坐标,而第三小节中的字节存储每个小怪的Y坐标。由于存储在这些小节中的坐标实际上向左移动了3位,即:乘以8。这个小事实解释了我们在价值普查中发现的“蓝色”群体。 (进行此移位的原因尚不清楚,但是当小怪移动时,很可能使用低三位来表示动画。)

处理了这一部分之后,我们现在可以看到有多少数据文件仅剩下几个字节了:

注意什么xxd接受option的-s十六进制值。

$代表f的水平/ *; 做python ./decompress2.py <$ f | xxd -s 0x580 | sed -n 1p; 完成| 少一点
00000580:9001 0c17 1701 1120 1717 00 ....... ...
00000580:0000 0c17 1b13 0c0d 101f 011e 1a20 1b00 .............
00000580:f401 0c18 1e1f 101d 0f0c 1800 ............
00000580:2c01 0c1b 0c1d 1f18 1019 1f00,...........
00000580:9001 0c1d 0e1f 140e 1117 1a22 00 ............“
00000580:2c01 0d0c 1717 1e01 1a01 1114 1d10 00,..............
00000580:2c01 0d10 220c 1d10 011a 1101 0d20 1200,...“ ........ ..
00000580:5802 0d17 1419 1600 X .......
00000580:0000 0d17 1a0d 0f0c 190e 1000 ............
00000580:f401 0d17 1a0d 1910 1f00 ..........
00000580:f401 0d17 1a0e 1601 110c 0e1f 1a1d 2400 .............. $。
00000580: ee02 0d17 1a0e 1601 0d20 1e1f 101d 0114 ......... ......
00000580: 5802 0d17 1a0e 1601 1901 1d1a 1717 00 X..............
00000580: 5e01 0d17 1a0e 1601 1a20 1f00 ^........ ..
00000580: c201 0d17 1a0e 1601 0d20 1e1f 101d 00 ......... .....
00000580: 2c01 0d1a 2019 0e10 010e 141f 2400 ,... .......$.
00000580: 5000 0d1d 201e 1311 141d 1000 P... .......
00000580: e703 0e0c 1610 0122 0c17 1600 ......."....
00000580: 5802 0e0c 1e1f 1710 0118 1a0c 1f00 X.............
00000580: 8f01 0e0c 1f0c 0e1a 180d 1e00 ............
00000580: 0000 0e10 1717 0d17 1a0e 1610 0f00 1b1d ................
00000580: 2c01 0e13 0e13 0e13 141b 1e00 ,...........
00000580: 8f01 0e13 1417 1710 1d00 ..........
00000580: bc02 0e13 141b 1814 1910 00 ...........
lines 1-24/148 (more) 

快速检查剩余部分中的第一对字节会提示我们它们包含另一个16位整数值。如果我们这样看待它们,则大多数值将以十进制表示为整数:

该命令od使用-j代替将其移至原始offset -s值得注意的是该命令printf:除了提供格式设置外,这是一种显示文本而无需在末尾挂换行的便捷方法。

$代表f的水平/ *; 做printf“%-20s” $ f; python ./decompress2.py <$ f | od -An -j 0x580 -tuS -N2; 完成| 少一点
等级/ all_full.pak 400
级别/字母.pak 0
等级/ amsterda.pak 500
等级/ Apartmen.pak 300
水平/ arcticfl.pak 400
等级/balls_o_.pak 300
等级/ beware_o.pak 300
等级/blink.pak 600
等级/ blobdanc.pak 0
等级/ blobnet.pak 500
等级/ block_fa.pak 500
等级/ block_ii.pak 750
等级/ block_n_.pak 600
等级/ block_ou.pak 350
等级/块.pak 450
等级/ bounce_c.pak 300
等级/ brushfir.pak 80
等级/cake_wal.pak 999
等级/ castle_m.pak 600
等级/ catacomb.pak 399
等级/ cellbloc.pak 0
等级/ chchchip.pak 300
等级/ chiller.pak 399
水平/ chipmine.pak 700
1-24行/ 148行(更多) 

如果再次转到最初由我们期望在文件中找到的数据创建的列表,我们将意识到此数字是完成级别的时间(如果该值为零,则该级别没有时间限制)。

在这两个字节之后,数据变得更加易失。对于大多数级别而言,文件中剩余约十个字节这一事实严重限制了它们的内容:

$ python ./decompress2.py <levels / all_full.pak | xxd -s 0x0582
00000582:0c17 1701 1120 1717 00 ..... ... 

例如,在此级别仅剩余九个字节。我们考虑到这种有限的大小,以及这9个字节包含17两次出现的value值的事实,这些值以两对的形式收集。我们将立即注意到,数字17模式与L“ ALL FULL”级别的名称中的字母模式相对应名称长度为八个字符,因此末尾的空字节很可能是行字符的末尾。发现了这一点之后,您可以简单地查看所有其他级别,并使用它们的名称来构建完整的字符列表:

00行尾
01空格键
02 -- 0B数字0-9
0C -- 25字母AZ
26 -- 30标点符号

对于大多数级别,数据文件在此处结束。但是,该名称中仍有几十个数据。如果我们看一下列表,将会看到带有提示按钮的关卡,而剩余的数据包含了关卡提示行的文本,该行用与关卡名称相同的字符集编码。

之后,我们到达所有文件的末尾。现在,我们对这些级别的方案有了完整的描述。我们的任务完成了。

未完成的业务


细心的读者可能会注意到,最初我们希望在这些文件中找到两个我们从未见过的元素。我们将解释两者的缺失:

第一个要素是筹码数,即玩家为了通过筹码连接器必须收集的筹码总数。正如我们最初所说,通常等于一个级别的芯片总数,但这并不总是会发生。因此,必须以某种方式获得此数据。可以通过研究有额外筹码的那些级别的卡片来找到答案。事实证明,使用两个不同的值来表示筹码。值23我们最初发现的,但作为价值31表示不会影响打开芯片连接器所需总量的芯片。 (但是,从游戏性的角度来看,两种类型的筹码都相同。如果31关卡上只有一种类型的筹码,那么在该级别上您将无法收集到任何数量的筹码。)

第二个元素是四字母级别的密码。它不会隐藏在关卡数据中的任何位置。不幸的是,无法通过进一步研究数据文件来解决此问题。我们不得不得出结论,密码只是存储在其他地方。最可能的解释是:它们被硬编码在程序本身的某个位置。但是,后来变得很清楚,密码不是直接存储的。从熟悉代码本身的人员那里,我了解到,密码是使用伪随机数生成器动态生成的,伪生成器以特定顺序初始化。因此,不能直接更改级别的密码,这只能通过更改汇编代码来完成。

后记


通过对级别文件中的数据进行完全的反向工程,我可以开始编写一个可以对级别数据进行编码和解码的程序。多亏了她,我才能够为《Chip's Challenge for Lynx 》版本创建期待已久的关卡编辑器,并且该工具的存在极大地提高了我学习游戏逻辑的能力,并提高了其仿真的质量。

但是...我必须承认,我本人以上述未描述的方式对数据文件进行了反向开发。我从另一端开始-定义了字符串数据。我开始研究前八个级别的文件。由于它们是从“第1课”到“第8课”被调用的,因此我在其中搜索了相同子字符串的数据。而且我很幸运:没有压缩这些级别的名称之一,因此所有八个名称都以原始格式存储在数据文件中。当然,这些行没有存储在ASCII字符中让我感到很尴尬,但是标题中的双S字符帮助我检测到在所有八个数据文件中都重复的模式。找到名称后,我认识到字母,数字和空格字符的字符编码。然后,我将此编码应用于文件的其余部分,并在名称之后看到提示行,并开始观察异常:

强大的实用程序tr可以轻松地将您自己的数据文件字符集转换为ASCII。

$ tr'\ 001- \ 045''0-9A-Z'<levels / lesson_1.pak | xxd
00000000:4600 cb00 3000 0032 3030 3332 3235 3333 F ... 0..200322533
00000010:3635 3537 0020 3820 2039 3636 4238 38466557。8 966B88F
00000020:0058 4a37 354d 3000 5737 4226 3746 2739 .XJ75M0.W7B&7F'9
00000030:3928 3533 2953 2027 2733 3042 2057 3532 9(53)S''30B W52
00000040:3730 3738 304a 3226 375a 2046 4a30 5752 70780J2和7Z FJ0WR
00000050:2059 2052 4220 3537 0055 0050 3200 4f00 Y RB 57.U.P2.O.
00000060:554a 2637 5400 3300 2946 4a57 5830 4642 UJ&7T.3。)FJWX0FB
00000070:2035 2637 544d 2946 4a37 4d4f 3058 3050 5&7TM)FJ7MO0X0P
00000080:304a 5720 5120 5142 3835 3237 3020 3020 0JW Q QB85270 0
00000090: 2826 2058 4a33 3730 2056 4a33 5738 2727 (& XJ370 VJ3W8''
000000a0: 3933 3200 3439 3628 324d 7839 3628 324d 932.496(2Mx96(2M
000000b0: 7039 3628 324d 6400 4c45 5353 4f4e 2031 p96(2Md.LESSON 1
000000c0: 0043 4f4c 4c45 4354 2043 4849 5029 544f .COLLECT CHIP)TO
000000d0: 0047 4554 2050 4153 5420 5448 4520 4348 .GET PAST THE CH
000000e0: 4950 0053 4f43 4b45 542d 0055 5345 204b IP.SOCKET-.USE K
000000f0: 4559 2954 4f20 4f50 454e 0044 4f4f 5253 EY)TO OPEN.DOORS
00000100: 2d30 -0 

例如,在帮助文本中,有两个位置,S的序列和空格由右括号代替。这些异常现象为我提供了足够的证据来推论计算压缩的存在并获得有关其性质的一些信息。后来,我将异常字节值与出现在数据文件开头附近的字节相关联。(例如,在上面显示的十六进制偏移量转储中,00000035有一个右括号,后跟一个大写字母S和一个空格。)据此,我计算出的压缩方案与本文中描述的过程类似。其他一切都非常简单。

在我看来,可以从中吸取教训:没有独特的方法来检查不熟悉的数据文件。任何适合您的工具都是进行逆向工程的正确工具。

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


All Articles