我之前已经讨论过为什么PlayStation2没有任何很好的入口点软件漏洞来启动自制软件。您需要购买预安装了漏洞的存储卡,打开控制台以阻止光盘托盘传感器,或者安装ModChip。作为有史以来最畅销的游戏机,它理应受到更好的抨击。
我最初试图解决这个问题是利用与早期PAL区域PS2捆绑在一起的BASIC解释器。虽然我成功地制造了第一个基于软件的入口点攻击,仅使用控制台附带的硬件就可以触发,但由于必须通过控制器或键盘手动输入有效负载的要求,以及仅限PAL身份的限制,攻击受到了很大批评。我决定以不切实际为由注销这个漏洞,因此继续为PlayStation2寻找更好的攻击方案。
我们可以攻击PlayStation2的其他不可信输入来源;支持在线多人或USB存储的游戏几乎肯定会被利用。但与任天堂64不同的是,我们别无选择,只能通过调制解调器等接口开发游戏,而PlayStation2有一个关键的不同之处:它的主要输入是光学介质(CD/DVD光盘),任何人都可以很容易地用现成的消费硬件刻录这种格式。这就留下了一个有趣的问题,我从小就想解决这个问题:
有没有可能只烧录我们自己的自制游戏,然后像我们启动官方光盘一样,在未经修改的主机上启动它们(而不需要经过任何用户交互,如光盘交换或在游戏中触发网络攻击)?
最终,通过利用控制台的DVD播放器功能,我成功地实现了我的目标。这篇博客文章将描述逆转和利用DVD播放器的技术细节和过程。我的所有代码都可以在GitHub上找到。
显然,我们不能简单地刻录包含ELF文件的光盘,然后指望PS2引导它;我们需要利用与解析受控数据相关的某种软件漏洞。控制台支持播放烧录的DVD视频光盘,这暴露了我们可能利用的重大攻击面来实现我们的目标。
如果我们考虑一下DVD视频由什么组成,就会发现有相当多的主要组件,每个组件都有潜在的漏洞:
不幸的是,虽然完整的DVD视频规范是在付费墙后面,但它主要由MPEG等开放格式组成,只是以专有容器格式(VOB)捆绑在一起。对于专有方面,有一些可免费访问的非官方参考。
IFO文件格式可能是使用的最简单的格式,它负责存储将视频文件链接在一起的元数据。
互动机器允许DVD视频中的互动菜单和游戏。它有32组指令,而且很有趣,因为它可能被用来动态操纵内部存储器状态来启动利用漏洞攻击,或者它可以用来创建带有菜单的通用DVD,该菜单允许您选择固件版本并触发适当的利用漏洞攻击。
显然,在真实的硬件上进行大多数测试是不切实际的,因为刻录数百张测试光盘将是浪费和时间效率低下的。我们需要一个具有调试器支持的仿真器,这就是我们遇到的第一个障碍:PlayStation2最流行的仿真器PCSX2不支持播放DVD视频,而且没有人有兴趣添加支持。
我要感谢KrHacken帮我解决了第一个路障。事实证明,PCSX2确实支持DVD播放器;它只是不能自动加载它,因为它位于加密存储中,而PCSX2不支持解密。有一些公共工具可以从EROM存储中解密和提取DVD播放器。然后可以将其重新打包成ELF,以便轻松加载到PCSX2中。
由于发布了大量不同的PlayStation2型号,每个型号的DVD播放器固件(>;50.)都略有不同,因此在本文期间,我将重点介绍一款DVD播放器:3.10E,因为它恰好是我拥有的控制台的固件。
我将继续使用Ghidra进行反编译,就像我在以前的文章中一直使用的那样。DVD播放器不包含任何符号,所以代码片段中的所有名称都是我通过逆向工程分配的。
DVD播放器将尝试读取的第一个文件是VIDEO_TS.IFO。在内存中搜索文件内容,然后在那里设置内存写入断点以跟踪文件的写入位置,我们会快速找到读取IFO解析代码使用的光盘内容的API getDiscByte,地址为0x25c920。它是一种流读取器,可以将多个扇区缓存到RAM缓冲区中,然后在需要时自动查找更多数据:
byte getDiscByte(Void){byte ret;if(currentDiscBytePointer<;endDiscBytePointer){ret=*currentDiscBytePointer;}Else{currentDiscBytePointer=&;Buffer;setOffset=setOffset+number OfSectorsRead;getDiscByteInternal();ret=*currentDiscBytePointer;}currentDiscBytePointer=currentDiscBytePointer
通过搜索调用,我们还可以快速找到获取更大数据的包装器:getDiscU16(0x25c980)、getDiscU32(0x25c9b8)和getDiscData(0x25c9f0),这是最有趣的,因为它读取任意长度的数据:
void getDiscData(uint size,byte*Destination){byte b;uint i;i=0;if(size!=0){do{i=i+1;b=getDiscByte();*Destination=b;Destination=Destination+1;}While(i<;size);}return;}。
我做的第一件事是搜索对getDiscData的调用,希望找到一个大小可控且没有边界检查的调用。
果然,我们很快就发现了大约4个这种性质的明显的缓冲区溢出漏洞。回到IFO文件格式,我们可以看到,解析文件中大小可变的数据结构需要许多16位数组长度。DVD播放机错误地只期望达到DVD规范所允许的最大长度,因此它错过了拒绝长度较大的光盘的检查。由于所有复制都是在静态分配的内存缓冲区上完成的,因此指定的长度大于允许的长度将导致缓冲区溢出。例如,下面是0x25b3bc处的反编译:
large1=getDiscU16();large2=getDiscU16();large3=getDiscU16();Ignred=getDiscU16();getDiscData(Uint)large1+(Uint)large2+(Uint)large3)*8,&;DAT_0140bdd4);
这是最有趣的,因为它允许所有getDiscData缓冲区溢出的最大复制大小(0xffff*3*8=0x17FFE8字节)。它复制到位于0x0140bdd4的静态分配缓冲区,因此通过指定可能的最大复制大小,我们可以控制从0x140bdd4到0x158BDBC(0x140bdd4+0x17FFE8)的地址空间。
如您所见,我们可以使用上述漏洞控制相当大的内存区域。然而,扫描内存最初是非常令人失望的;指针非常少,而且它们看起来都不是对Corrupt特别感兴趣!
虽然该区域中没有有趣的指针,但有一些索引,如果损坏,可能会导致进一步的越界内存损坏。
注意,像这样的大量读取并不总是从IFO文件复制连续的数据,因为一旦超过文件大小,扇区就会开始重复,但通常假设getDiscData调用写入的所有数据都可以控制,因为它源自光盘上的某个地方。此外,在写入一定数量之后,我们可能会溢出到getDiscByte函数使用的内部状态,但我们将在稍后讨论这一点。
在0x25e388处,我们调用函数指针数组中的条目,其中我们可以从溢出控制0x141284a处的16位fpIndex:
这允许我们跳转到存储在0x5b9d40至0x5b9d40+0xffff*4=0x5F9D3C之间的任何地址。
这个原语不太理想,因为我们的溢出bug都不允许我们控制从中读取跳转目标的内存。更糟糕的是,该内存区域的大部分是从DVD播放器的只读部分映射而来的,因此我们不太可能在不出现其他错误的情况下影响该内存区域的内容。
在函数指针之后,我们确实看到了一些开关案例标签的地址,这有点有趣,因为这允许我们跳到函数中间并执行其结尾,而无需执行其序言,从而允许我们错位堆栈指针并返回到堆栈上的意想值。我经历了所有这些,不幸的是,我只能用它跳到0。
我决定转储整个可能的跳转目标区域,将它们分组为4个字节,看看其中是否有任何一个指向我们通过溢出漏洞控制的内存……。令人惊讶的是,结果是:索引0xe07e(地址0x5f1f38)指向0x1500014,这在我们的控制范围内!这并不完美,因为它是缓存的虚拟地址,因此我们可能会遇到缓存一致性问题,但它可以工作。
非常幸运的是,恰好有一个我们可以使用的有效跳转目标,它已经指向我们可以控制的记忆。由于具有不同地址空间的其他DVD播放器版本可能不会有这样的奢侈,我将简要介绍另一个损坏原语,以防它对任何试图利用自己的游戏机版本的人有用。
If(*(int*)(&;DAT_01411e54+indexForOOBW*4)==0){Error=getBuffer(文件名,0,&;Buffer,1,0);If(Error<;0)Goto Lab_0025c79c;lVar3=FUN_002161f8(0x140de40,pcVar4,0xc);If(lVar3==0){uVar2。DAT_01411e54+indexForOOBW*4)!=0)转到LAB_0025c7ac;}错误=-3;}
由于indexForOOBW是一个32位值,因此通过大型溢出损坏它可能会允许写入此路径中的任意地址。
有一个约束,即在您编写它之前,值必须为0(根据该代码段中的第一行),但这不会显著增加利用它的难度。您可以很容易地将某处延迟槽中的NOP重写到某个寄存器的跳转中,该寄存器恰好在执行时受到控制。或者,一种更好的方法是用上面提到的OOB调用链接这个OOB写入;您可以将我们可以用作跳转目标的地址之一(恰好是0)覆盖到任意的新跳转目标。
当我短暂地尝试这个原语时,它在调用getBuffer时失败了,因为在函数的前面,它通过Sprintf(filename,";vts_%02d_0.ifo";,indexForOOBW)生成文件名,而文件";vts_1364283729_0.ifo";不存在。我们无法正常创建此文件,因为代码有一个最大文件名长度,当我们尝试像这样的大型索引时会遇到这个长度(我认为它可能是15或16个字节)。您可以绕过长度限制,但仍然使用此错误损坏相当大的内存区域,或者可能通过另一个溢出损坏足够多的内部数据结构,从而诱使调用认为这些大型索引文件存在。因为我的控制台不需要它,所以我没有完全分析这种可能性,只是继续利用OOB调用。
此时,我们有了利用大型读取溢出的非常清晰的路径:我们将fpIndex重写为0xe07e,并将我们的有效负载溢出到0x1500014。然后,当代码使用损坏的fpIndex索引到函数指针数组中时,它将触发跳转到我们的有效负载。
我们遇到的第一个问题是,我们要损坏的第一个东西fpIndex(0x141284a)位于内存中currentDiscBytePointer(0x1411fe4)和endDiscBytePointer(0x1411fe8)之后,因此那些影响getDiscByte输出的值在我们尝试损坏fpIndex时可能已经损坏,并且可能已被重定向到不再指向设置为IFO文件内容的内存。
解决方案是中断写入currentDiscBytePointer,以便在我们将要破坏它的时候找出它的值,并确保我们只覆盖它已经拥有的值。我们还可以将endDiscBytePointer更改为0xffffffff,以防止调用getDiscByteInternal,如果在我们处于半损坏状态时调用它会导致更多混淆。
由于溢出现在已到达fpIndex,并且仍在从IFO文件复制受控内容,因此我们可以在损坏currentDiscBytePointer时中断并查看它,以定位从IFO复制的位置。一旦我们发现了这一点,我们就可以将文件中的那些字节修改为7E e0(0xe07e的小端表示),以指向我们的跳转目标。
类似地,我们可以在写入0x1500014时中断,以计算出我们的有效负载将从文件的哪个位置复制,并将其设置为某个占位符的值。
现在运行攻击并在OOB调用(0x25e388)中断,我们面临一个新问题:索引在我们的损坏和调用使用之间被重写。如果我们不能避免这种写入,这种利用方法可能会走入死胡同。
在我们的大型读取之后中断写入fpIndex,我们看到它写在此函数内部的0x25E970处:
int setFpIndex(Void){IF(DAT_01412856!=0){IF(DAT_0141284E==';\0';){IF(DAT_01412854==0){fpIndex=3;}Else{fpIndex=4;}}Else{IF(DAT_01412854==0){fpIndex=5;}Else{fpIndex=6;}}返回0;}返回-1;}。
注意到不是所有路径都写入fpIndex吗?如果将0x1412856处的16位值(我们也可以在溢出时破坏它)设置为0,它将保留fpIndex不变,并返回-1以指示失败。
导致setFpIndex的调用链紧接在OOB调用本身(0x25e378)之前,并且也不检查setFpIndex!的返回值。这意味着我们可以绕过fpIndex的初始化,并且仍然可以到达OOB调用,而它仍然包含我们损坏的值:
callSetFpIndex(puVar6+((Uint)dat_01412841-1)*8);(*(代码*)(&;PTR_LAB_005b9d40)[(uint)fpIndex])(puVar6+((Uint)dat_01412841-1)*8);
在这一点上,我们跳到了受控内容的记忆中,这应该意味着任意代码的执行!但是,我们将有效负载写入缓存的虚拟地址映射,并从那里执行它,这会在我们需要考虑的硬件上产生两个潜在的故障源:
有效载荷在执行时可能没有从数据高速缓存转储清除到主存储器,
自从有效负载到达主存储器以来,指令高速缓存可能没有被刷新,因此我们可以改为执行陈旧的指令高速缓存,
第一个问题是可以解决的:我们可以将大副本扩展到可能的最大大小(0xffff*3*8),甚至可以利用其他大副本写入尽可能多的数据,以确保将我们的有效负载从数据缓存中清除,而不是其他内容。在我的漏洞攻击中,我坚持使用这个最大可能大小,但是如果您愿意的话,您可以对这个数字进行微调,将引导时间优化几分之一秒。
第二个问题并不是真正可以解决的。因为我们不控制目标跳转地址,所以我们不能跳转到未缓存的虚拟地址来绕过指令缓存,而且据我所知,在我们的有效负载被写入之后,没有办法操纵程序动态加载新代码,从而导致指令缓存刷新。然而,事实证明这甚至不是问题,因为指令高速缓存在启动期间被刷新,并且我们的有效负载不会覆盖任何现有代码,因此不会有任何覆盖有效负载地址的陈旧指令高速缓存(PS2 CPU没有推测性执行或任何其他会导致在非体系结构执行路径上创建指令高速缓存条目的情况)。
考虑到高速缓存一致性似乎不是问题,我尝试了一个简单的有效负载,它只是重新启动浏览器菜单来验证有效负载是否可以在硬件上执行,并刻录了一张测试光盘:有效负载应该从光盘读取ELF,然后执行它。这看起来很简单,但有几个不同的考虑因素:
我从一个基本的crt0.s开始,它将使用ExecPS2系统调用启动main,重新初始化内核的内部状态,从而销毁其他线程,以防止它们损坏我们的有效负载使用的任何内存:
.Section.text.Startup.global_start_start:#la$a0,0x7f#la$v1,0x01#syscall 0x01#ResetEE la$a0,main la$a1,0 la$a2,0 la$a3,0.global ExecPS2ExecPS2:la$v1,7 syscall 7#ExecPS2。
我第一次尝试从磁盘加载ELF是使用与从IFO文件读取数据相同的高级函数调用(pointToIFO(0x25c880),然后是所需大小的getDiscData)。当我尝试这样做时,它只能获取单个扇区(0x800字节)的数据,这可能是由于之前缓冲区溢出造成的损坏。
我没有尝试修复这个问题,而是决定使用最低级别的函数getBufferInternal(0x2986a0),它只调用SifCallRpc(0x2096e8)来请求IOP协处理器获取数据,然后等待完成。这件事运作得很完美。
下一个考虑事项是将ELF文件加载到何处。运行readelf-l将告诉我们目标不是位置独立的二进制文件,需要在特定位置加载:
readelf-l BOOT.ELFElf文件类型为EXEC(可执行文件)入口点0x1d00008有1个程序头,从偏移量52开始。程序头:类型偏移量VirtAddr PhysAddr FileSIz MemSiz FLG Align Load 0x000060 0x01ca1450 0x01ca1450 0x5ed6d 0x5ee30 RWE 0x10。
#定义SifIopReset((void(*)(char*,int))0x84fe0)#定义SifIopSync((int(*)(Void))0x85110)#定义SifInitRpc((void(*)(Int))0x84180)#定义SifExitRpc((void(*)(Void))0x84310)#定义负载大小0x5ed6d#定义MEM_SIZEInt lbaOffset=8338-285;字符已忽略[]=";";;getBufferInternal(已忽略,0,lbaOffset,(void*)Destination-0x60,(payload_size+0x60+0x7ff)/0x800,0);//初始化(i=0;i<;MEM_SIZE-payload_size;i++){((char*)Destination+payload_size)[i]=0;}SifIopResp的BSS段。la$a0,0;syscall 0x64";);//FlushCache数据写回ASM易失性(";la$v1,0x64;la$a0,2;syscall 0x64";);//FlushCache指令失效//void ExecPS2(void*entry,void*gp,int argc,char**argv);//ExecPS2((void*)entry,0,0,0。
初始有效载荷有许多不理想的地方。它的可移植性不是很好,因为我们依赖于硬编码从IFO文件到有效负载文件的偏移量,以及目标ELF的基地址。我们还需要确保目标ELF加载地址不与我们在加载和引导期间仍然调用的任何函数重叠。
为了进行上述改进,我们需要更多的空间。初始有效载荷(现在称为阶段1)位于IFO文件中的偏移量0x2bb4处。
..