Nintendo Switch RCM漏洞分析
深山野老
2020年02月24日 19:56

概述

cut-off

本篇文章对任天堂的Switch RCM漏洞做一个介绍,国外黑客通过dump并逆向芯片内的BootRom,找到了Switch在RCM模式时通过USB传输数据的一个漏洞,通过该漏洞可以执行用户自己的代码,从而达到破解运行自制系统和软件的目的。黑客原始的演讲视频如下

视频中详细地介绍了如何绕过芯片的安全机制dump芯片内BootRom,并对RCM漏洞做了非常详细地分析。本篇文章可以看做是视频后半部分的文字版本,更详细的信息请参见视频。

Nintendo Switch

cut-off

硬件架构

cut-off

1. 总览

Switch的SOC采用的是英伟达的Tegra X1,CPU部分是4核Cortex A57 + 4核Cortex A53,armv8 64位架构;GPU部分是英伟达的GeForce Maxwell,如下图所示。采用Tegra X1芯片的设备除了Switch之外还有英伟达自己的Shield安卓TV盒子、谷歌Pixel C平板。该SOC的官方开发板也是可以在网上买到的,数据手册只要在英伟达官网注册账号就可以下载。

Tegra硬件架构

2. BPMP

在Tegra X1数据手册中,将4核Cortex A57 + 4核Cortex A53称为Complex CPU,除此之外还有一个arm7架构的BPMP(Boot and Power Management Processor),用于运行芯片的BootRom和唤醒Complex CPU等。如下图所示。从图中还可以看出,用于运行BootRom的SRAM大小为64kB * 4 = 256kB。

BPMP

cut-off

Boot启动流程

cut-off

下图展示的是Tegra X1开发板上的系统启动流程,Switch上的流程类似,不同的是从BootRom之后引导的是任天堂自己的bootloader和地平线操作系统。图中红色部分表示代码是运行在BPMP上,蓝色表示代码运行在CCPLEX(CPU Complex)上,可以看出上电初始阶段的boot都是运行在BPMP上的。图中还有一个RCM分支,用于当设备变砖后,通过USB恢复bootloader和系统的目的。

Boot流程

虽然Switch和开发板的后续bootloader和系统都不一样,但是他们使用的BootRom是一样的,BootRom是烧录在芯片中的启动代码,一旦烧录进芯片就无法再被更改。所以通过开发板研究的BootRom漏洞同样适用于Switch。

cut-off

RCM漏洞

cut-off
  1. RCM简介RCM表示Recovery Mode,以前刷过安卓手机的同学应该知道,当由于某种原因手机变砖后,可以通过Recovery Mode重新刷入系统固件,以达到挽救手机的目的。一般RCM模式下可以通过SD卡或者USB重新刷入固件,Tegra X1的RCM模式下是通过USB来刷固件的。

  2. USB简介2.1 概述由于RCM漏洞是BootRom在处理USB数据过程中的一个漏洞,所以有必要对USB的知识做一个基本的介绍,更详细的USB资料可以参见这个USB系列博文https://blog.csdn.net/qq_16777851/category_9283392.html。

USB通用标识

2.2 端点

在USB体系中,设备分为主机和从机,比如我们的电脑就是主机,U盘、鼠标等等都是从机。主机和从机之间通过端点来建立通信,端点(Endpoint)是USB从机上的一个数据缓冲区,用于接收和发送USB各种数据,如下图所示。

USB端点

2.3 控制传输

USB主机和从机之间通过端点来进行通讯和数据传输,根据传输的类型不同,可以将端点分为四种类型:控制传输、等时传输、中断传输、批量传输。控制传输用于访问并配置从机,比如端点0就是一个控制传输端点,通过该端点可以给USB从机发命令以查看从机的状态等等信息。

2.4 批量传输

批量传输用于需要大量传输数据的端点,比如主机和U盘之间传输数据就是通过批量传输端点来进行的。

2.5 标准USB请求

所有USB从机都响应默认控制管道上(端点0)主机的请求,这些请求是使用控制传输进行的。请求包的数据格式如下图所示。

控制传输请求数据格式

  • bmRequestType:这个字段包含了传输方向、命令类型、接受者类型信息。

  • bRequest:具体的请求类型,比如获取状态信息、设置地址等等。

  • wValue:请求的参数,根据不同的请求而变。

  • wIndex:根据不同的请求意义不同。

  • wLength:表示要传输的数据的长度,这个字段可以为0,表示无数据传输。在输入请求下(主机获取从机数据),从机返回的数据长度不应多于wLength,但可以少于此长度。这个字段比较重要,因为Tegra X1的BootRom对此字段的处理有漏洞。

2.6 RCM使用的USB端点

BootRom在RCM模式下使用了两种USB端点,控制传输端点(ep0)和批量传输端点(ep1)。批量传输端点用于主机和设备之间传输RCM消息,控制端点用于主机获取设备的一些信息,比如状态信息、获取描述符信息等等,如下图所示。

RCM使用USB端点

3. BootRom处理RCM数据

以下展示的数据结构和伪代码都是黑客通过逆向分析BootRom得到的。

3.1 RCM数据

主机发送给设备的RCM数据由两个部分组成,RCM消息加RCM Payload数据,如下图所示。设备会对RCM消息进行合法性检查,如果是合法的就接着处理RCM Payload数据。要注意的是,BootRom是将RCM消息和Payload数据一块接收完成后再对RAM消息做合法性验证的,这就为利用RCM漏洞执行非法的用户代码提供了可能性。

RCM数据组成

3.2 RCM消息结构

RCM消息的数据结构如下图所示。从图中可以看出主机发送到设备上的RCM数据都是经过签名的,BootRom程序会对RCM数据做各种合法性检查,如果发现是不合法的消息就不会做出进一步的反应。如果想让RCM消息变成合法消息,就需要破解RCM消息的签名算法,RCM数据使用的是RSA加密,想要破解这个基本是不可能实现的。

RCM消息结构

从图中还可以看出数据结构中的第一个成员len_insecure表示RCM消息加上Payload数据整体的大小,最后一个成员表示RCM的Payload数据的大小。

3.3 整体流程

BootRom处理RCM消息的伪代码流程如下图所示。

RCM消息处理流程

总体上分为4个步骤:

  • 处理USB主机控制传输请求,将设备的基本信息传输给主机。

  • 接收RCM消息,这里只是接收数据到缓冲区而不会对数据做检查。

  • 接收到RCM消息之后就会对消息的合法性做一系列的检查,如果不合法直接返回不进行后面的操作。

  • 如果所有的合法性检查都通过之后,保存用户代码的地址,然后跳转执行。

可以看出如果想让芯片执行用户代码,就必须准备一个合法的RCM消息,否则就没戏。

3.4 iRAM内存布局结构

在分析rcm_receive_message函数之前我们先来看看,BootRom运行RCM时,SRAM中的内存功能布局,如下图所示。

SRAM内存布局

  • 有两个USB的DMA Buffer,用于接收和发送USB数据,大小分别为16kB。

  • 栈大小为12kB,紧接在第二个USB DMA Buffer之后,用来存放局部变量和函数调用的返回地址信息。

  • 用于接收存放RCM Payload数据的缓冲区大小为3 * 64kB = 192kB。

  • 还有一个全局变量区域,设备接收到的RCM消息就存放在全局变量区,如上图所示。

3.5 接收RCM消息

我们来看看rcm_receive_message这个函数的功能,rcm_receive_message的伪代码流程如下图所示。

RCM数据接收流程

  • 通过read_ep1_out_async函数通知设备到ep1端点读取数据,前面我们已经知道了RCM有两个端点,其中ep1端点用于批量传输功能,主机就是通过ep1端点来发送数据给设备的。注意这个函数是异步执行的,也就是说只是通知设备去接收数据,并不会等待数据接收完成才返回。同时我们也可以看到每次尝试接收的数据大小是0x4000,这个大小就是一个USB DMA Buffer的大小16kB,也就是每次设备都尝试从主机接收一个USB DMA Buffer的大小的数据。

  • 当第一次执行while循环时,由于数据不会立马接收到,所以执行完步骤1会直接到步骤4,步骤4 就是等待数据接收完成才返回。当数据接收完成时,此时数据是存放在USB的DMA Buffer中的。

  • 如果设备接收到了数据,接下来会判断接收的数据的大小,如果小于680字节表示当前接收的是RCM消息,因为RCM消息的总大小是677字节。这里要说的是主机发送数据到设备是分批次的,先发送RCM消息然后再发送RCM Payload数据。

  • 如果接收的是RCM消息,则将接收的数据从USB DMA Buffer拷贝到全局变量区,同时查看RCM消息第一个成员以确定接下来还有多少RCM Payload数据需要传输。

  • 如果接收的数据大小大于680字节,表示接收的RCM Payload数据,就将数据从USB DMA Buffer拷贝到RCM Payload区域。

3.6 BootRom处理批量传输流程

我们来看看设备是如何通过USB来传输数据的,现在我们知道RCM是通过ep1端点来从主机获取数据的,这个伪代码流程如下图所示。

RCM批量传输

还记得我们在USB简介章节说的除了ep0端点之外,USB从机的其他端点在未被主机配置之前是不能和USB主机通讯的么,所以在使用ep1之前需要主机对ep1进行配置。如上图所示,红框内的代码检查ep1端点是否被主机配置过,如果配置过则跳出while循环进行数据读取,如果没有配置过,则调用绿框中的函数等待主机进行配置。

3.7 BootRom处理控制传输流程

RCM通过handle_ep0_control_transfer函数来进准备数据和USB主机进行通讯,响应主机的各种请求,比如获取状态、设置地址等等。我们来重点关注下主机获取状态这个请求,如下图所示。

RCM控制传输流程

流程大致分为以下几步:

  • USB标准请求的数据格式之前已经介绍过,第一个字段包含了请求的方向,如果是设备到主机的请求则进入上图中的第一步处理分支。

  • 如果请求的类型是GET_STATUS,则会进一步判断是什么的状态,比如图中的设备和端点状态。图中对要返回给主机的数据大小计算不同,如果是设备状态,返回数据大小为2个字节,如果是端点状态,则大小是请求数据包中的wLength!其实根据USB协议,GET_STATUS返回的数据大小应该固定为2个字节!

  • 当知道要返回的数据和大小后,回到第四步对返回的数据大小做调整,因为假设主机请求获取10个字节的数据,但是设备上对应的数据有20个,那么设备只能分2此返回数据,每次传输10个,这就是图中第四步做的工作。

  • 当一切准备就绪之后,通过memcpy将数据拷贝到USB DMA Buffer然后发送给主机。这就是图中第五步做的事情。

3.8 漏洞

正常情况下,GET_STATUS返回的是2个字节的数据,memcpy其实是将栈中的2个字节数据拷贝到DMA Buffer中,因为status这个变量是函数中的局部变量,对应的内存在栈区,如下图所示。

GET_STATUS正常拷贝操作

但是我们看看RCM控制传输流程的第3步,在这个步骤中,返回的数据长度是由USB请求中的数据长度字段决定的,而这个字段是由主机发送控制的!!!正常情况下,这个wLength应该是2,但是如果这个值不是2而是一个更大的值呢!比如是8kB的话,memcpy就会将栈区后面的Payload区域的数据也拷贝到DMA Buffer中了!如下图所示。

wLenght字段改为8kB大小

那么如果我们再将wLength改大一点,比如(16+12)kB呢,由于在DMA Buffer区域之后是栈区,那么memcpy会用Payload区域的数据覆盖栈区!如下图所示。

wLength改为28kB大小

前面我们说过,栈中除了保存局部变量数据之外还保存了函数调用的返回指针!而覆盖到栈区的是Payload区域的数据,这个区域的数据是我们可以控制的,如果我们精心设计Payload区的数据,让其覆盖栈中的函数返回指针,那么当BootRom的函数执行完返回时就会跳转到我们自己的代码执行了!

我们来看看到现在为止的整个函数调用链,如下图所示。

函数调用链

可以看出,当执行完memcpy函数后,由于返回指针已经被我们给替换,所以不会返回到原来的memcpy之后代码执行,而是跳转执行我们自己的代码了!

4. 利用漏洞执行非法用户代码流程

现在我们来总体看下RCM漏洞执行非法代码的流程。

  • 设备进入RCM模式。

  • 使用USB连接设备和主机。

  • 通过RCM传输非法数据给设备。

  • 不要结束传输过程,可以通过将RCM消息结构中的大小字段设置一个比RCM Payload数据还大的值。

  • 主机发送一个GET_STATUS请求,并且将其中的wLength字段设置一个较大的值。

  • Payload数据被memcpy覆盖了栈区,从而控制了返回指针

  • 执行用户非法代码

5. Switch如何进入RCM模式

  • 将eMMC从主板上移除。

  • 按住音量增加键 + Home键。这里的Home键不是Joycon上的Home键,而是右Joycon插槽中的Pin10,通过将Pin10和Pin7或者Pin1短接就相当于按下Home键。

cut-off

总结

cut-off

通过前面的分析可以看出由于两个原因导致了RCM漏洞可以执行非法代码:

  • 核心错误是GET_STATUS时本来应该返回两个字节,结果却复制了wLenth个数据。

  • 另外一个错误是在一次处理中同时接收RCM消息和RCM Payload数据,然后再做RCM消息合法性验证。如果处理逻辑是先只接受RCM消息做验证,那么即使用户使用了RCM漏洞拷贝了过多数据覆盖栈区,但是由于RCM Payload数据还没接收并保存到Payload区域,那么覆盖到栈区的也仅仅是垃圾数据而已,虽然这种情况下返回指针同样被垃圾数据覆盖,但是当函数返回时只会造成系统崩溃而不会跳转到非法代码执行。

最后想说的是,这个世界上没有绝对安全的系统,只要有人系统就有漏洞~