通过操纵游戏记忆而实现的《塞尔达传说》



《塞尔达传说》的第一部分是不朽的经典。一个普通的玩家需要几天的时间才能完成它,但是对于最有经验的赛车手来说,这是一个半小时的问题。但是,Sockfolder打开的非常令人困惑和复杂的错误使用户可以直接从游戏中执行任意代码,以在不到三分钟的时间内完成游戏。

简而言之,这种情况如下:

  1. 在名称输入屏幕上输入代码。
  2. 我们进入第二个地牢,鸣笛。
  3. 我们到达墓地,我们叫十个鬼魂。
  4. 我们等待必要的条件,在生物处于特定位置时暂停游戏。
  5. 休息一下,同时按A和B,就这样!

是的,太棒了。现在,让我们仔细看看游戏中正在发生的事情以及这个惊人的《塞尔达传说》漏洞是如何执行的。



如何导致错误


因此,首先我们在名称输入屏幕上输入代码。文件名存储在内存中,每个字符对应于内存中的特定字节。我们给文件起了奇怪的名字,因为实际上我们将必要的字节写入内存,然后这些内存将成为汇编代码。只有两个限制:

  • 我们只能处理三个文件,也就是说,程序只能包含3 * 8个字符= 24个字节;
  • 如果要从第二个任务开始,则需要输入一个ZELDA文件的前五个字符。



我们需要从第二个任务开始,其原因我将在后面解释。我们称呼第一个ZELDA文件是因为该名称最适合。现在开始游戏。在进入墓地之前,我们的行为与通常的游戏几乎没有什么不同。我们需要做的主要事情是尽量避免两次死亡。 “死亡”既指死亡本身,又指向菜单的频繁过渡。通过进一步的说明,您将理解为什么会这样。



吹口哨后,我们转到此墓地屏幕以完成错误。在此触发该错误,因为使用哨子时,会从墓碑中创建一个对象,该对象会打开一个隐藏的楼梯。这是发生错误所必需的。所有这些都是由于精灵的限制。在《塞尔达传说》中,屏幕一次最多可以包含11个精灵。如果您尝试创建第12个Sprite,则游戏将不允许您执行此操作。为了重现错误,我们打破了溢出限制并创建了第12个精灵。



创建哨子对象时,游戏会忘记检查屏幕上之前有多少个精灵。因此,当我们创建最大数量的子画面时,将在子画面表外部创建子画面,覆盖内存,并发生意外状态。

内存中有一个小片段,从偏移量350开始,该片段存储十一个精灵的标识符。屏幕加载时会创建一个精灵,它从该精灵可能具有最小偏移的位置开始。这意味着子画面正在寻找具有最小偏移量的位置,该位置从偏移量350开始及以后。当要创建精灵时,游戏会在精灵表中搜索一个空值,以将其替换为精灵标识符,从而创建它。与在加载屏幕时创建精灵不同,在尝试创建精灵时,游戏会寻找一个具有最大精灵偏移的位置。这意味着她首先检查是否有可能在位置10(0A)上创建子画面。如果不是,则她检查位置9(09),即偏移量359,依此类推。如果所有精灵位置都被占用,则游戏“投降”并且不会创建精灵。



但是,开发人员忘了执行边界检查,以便游戏在尝试创建“ whistle”对象时可能“放弃”,并且游戏继续在精灵表之外的位置搜索值为00的字节,以便在那里记下对象标识符。但是她首先在哪里寻找这个意思?实际上,该表是存储有关Sprite信息的较大数组的一部分。当哨子在表格的开头找到要创建的位置时,它将开始从数组的结尾开始搜索值00的位置。

在墓地屏幕上,数组的此片段包含有关幻影动作当前状态的信息。现在让我们谈谈鬼魂的行动状态。墓地里的幽灵随机移动。这是因为他们的行为受行为状态决定的行为控制。这些动作状态可以具有0(00)到5(05)之间的任何值,具体取决于虚影的动作。它们写在包含有关精灵信息的数组的末尾。

通常,所有这些动作状态都对应于功能表中定义幻影动作的位置。由于状态只能有6个值,因此该表具有存储所有6个动作所需的大小。在此,动作状态0(00)对应于重影的加速度。这很重要,因为当哨子开始寻找创建其精灵的位置时,它将填充在内存中找到的第一个位置,值为00。幻影加速度对应于动作0,记录在代码中的值为00,因此游戏将其视为一个空位置并将标识符写入其中。吹口哨5E。



然后,游戏尝试在表的位置5E执行代码,但是由于表仅包含不超过05的值,因此游戏将“垃圾”数据作为远远超出表的代码执行。如果一切正确,“垃圾”数据将引导我们找到用文件符号记录的代码,游戏将进入Zelda,我们将进行整个游戏。

但是,为了使错误起作用,我们需要破坏表中动作的第三状态。因此,必须加速创建的第三个重影,并且所有后续的重影必须执行其他操作(其代码不等于00)。这是重要的信息。现在让我们看看执行错误时会发生什么。

错误执行


如果一切正确,游戏将尝试在表格的位置5E执行数据。它开始读取为从偏移量602开始的代码数据。与Link动作状态相关联的信息存储在内存中的偏移量602和603,因此我们可以对其进行控制。当Link站立时,偏移602和603的值为00。但是,当我们按下按钮B使用哨子时,在602的值为10,而当我们按下A使用剑时,在603的值为01。



因此,当我们同时按下这两个按钮时,相邻数据将为1001。游戏将此数据解释为BPL分支命令,以转到偏移量605,并以代码形式执行其中的数据。如果我们的健康状况不太低,则605和606的值将为00;如果我们的健康状况较低,则其值为40。要快速通过游戏,这些值必须等于00,因此您需要尝试保持运行状况,直到错误完成。值00对应于BRK(中断)指令。由于此处不执行任何操作,因此我们需要将值设置为00,并且游戏需要继续执行代码。



由于我们使用了剑,因此游戏将继续在下一个奇数字节中执行指令。下一条指令(不是BRK)在偏移量08处,但是由于我们使用了剑,因此游戏会跳过它并在09和0A处执行代码。字节09中的值始终为10,这意味着我们再次处理BPL分支指令。偏移量623(62E)处的字节与音乐有关。她尝试增加音乐播放的过程,但有时会跳到较低的值。

这意味着要完成此错误,我们需要将其应用于特定的音乐。如果值很小,我们就有机会跳到内存中具有明显变化的值的区域。它们的变化是如此随机,以至于我们无法实时控制它们以接收游戏必须遵循的指令。因此,这里的过渡很可能导致游戏停滞。



但是,在此混乱数据之后,仍然存在安全数据区域。因此,我们将尝试跳到那里。我们需要60A中的更多数据才能准确跳过不安全区域并获得永久性安全数据。如果我们去那里,那么一切都井然有序。但是在630偏移处,有一个Link的死亡计数器。如果Link两次死亡,该值将为02,从而形成指令并停止游戏。这就是为什么不要两次死亡很重要的原因。因此,结果,我们继续到偏移量638,这是存储文件名的表的开始。如果我们到达这里,游戏将执行我们在文件屏幕上输入的代码。从这里开始...

代号


文件名按顺序存储在内存中。这意味着我们执行的第一个字节是一个名为ZELDA的文件。幸运的是,这些字节执行的代码是安全的,因此我们可以继续进行其他操作。由于名称ZELDA并不重要,因此让我们看看其余代码的作用。首先,我们执行三个PLP命令(从堆栈中拉出,从堆栈中拉出)。当我们调用一个函数时,它将两个值写入堆栈。这些值与执行函数时在代码中的位置相对应。因此,游戏可以在执行功能后找出要返回的位置。

我们首先从堆栈中提取三个值,然后执行return函数。我们这样做是为了返回执行错误之前的位置。否则,游戏将无法完成作为代码执行数据的操作,因此将冻结。但是在获得破坏内存并结束游戏的代码之前,我们需要谈谈其他一些值。

内存偏移量10处的值对应于我们所在的世界的数量。如果我们在地上世界,则值为00。对于第一个地牢,值为01,对于第二个地牢,值为02,依此类推。由于Zelda位于第9个地牢中,因此我们需要将此值设为09。但是,我们在地上世界中执行错误,该值为00。因此,我们需要找到一种方法将其等于09。偏移量11的值对应于某些数据关于游戏状态。当游戏加载时,值为00,当游戏未加载时为-01。我们需要加载第九个地牢,因此该值应为00。

偏移量12处的值对应于状态的某些部分。屏幕加载时,该值通常为02,未加载时通常为05。由于我们要加载地牢,因此该值应为02。

我们要更改的最后一个值对应于我们所处的任务。偏移量62D处的值对应于第一个或第二个任务,并具有值00或01。我们从第二个任务开始,但想在第一个任务中结束,因为这是我们混淆游戏的方式。因此,我们需要将此值更改为00。这将使我们陷入第一个和第二个任务的奇怪混合。我们需要这种混合状态来执行特定目的的代码。当我们解释代码的作用时,我们将对其进行研究。



好吧,我们为自己设定了目标,让我们继续进行代码。首先,我们具有用于偏移量11的LSR(逻辑右移)功能。该功能将值分为两半,并将其余部分写入进位。 11是内存中的一个存储位置,用于存储与游戏状态相对应的值,在此指令之前,其值为01。当我们将其除以2时,由于除法是整数,因此将得到00与剩余的01。在偏移量11处,写入值00,在传输中写入01。

然后,我们有了ROX函数,可以用索引X移0D(向左旋转,向左旋转)。由于我们已经覆盖了所创建的第三个幽灵的动作状态,因此X等于3。 0D + X是0D + 03,它是十六进制10。因此,此函数向左偏移10。该函数将偏移10处的值乘以2,并加连字符。值从0开始。零乘以2等于0,而0 + 1等于1,因此将值写入偏移量10。

现在,再次重复ROX操作以索引X偏移量0D。X的值没有改变,仍然是3,因此,这是在偏移量10处向左的另一弯。1 * 2 = 2,并且由于我们已经使用了传输,因此其值为0。2 + 0 = 2,因此将值2写入偏移量2。

接下来,我们对偏移量0D进行第三次ROX操作。 2 * 2 = 4,并且4 + 0 = 4,所以该值写在偏移量10处

。下一条指令是在偏移量12处的LSR。我们从值5开始,因此除以2得到的余数为1。2写在偏移量12处。和1-在转移中。

然后最后一个ROX在偏移量10处向左旋转。4 * 2 =8。转移值为1,我们将其加到乘积中,得到9。

返回前的最后一条指令是在偏移量62D处向右逻辑移位。此处的值为1,因为我们处于第二个任务中。并且由于我们除以2,所以该值等于0。因此我们进入了混合任务模式。

现在,我们执行return函数,并且游戏使用代码中记录的值更新其状态的值。这样就完成了错误,然后进入Zelda的房间。我们之所以到达那里是因为混合任务使游戏迷惑了。她不知道确切在哪里放置Link。这样我们就通过了比赛,来到了塞尔达的房间。



我希望这可以帮助您了解执行任意代码并执行快速游戏错误时在《塞尔达传说》游戏内部发生的情况。

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


All Articles