ns2jb 发布 Switch 2 软件越狱,适配最新固件 23.0.1
Jailbreaking the Switch 2
ns2jb 是首个针对 Switch 2 的纯软件、需联网引导的越狱,适配最新固件 23.0.1,可像普通 sysmodule 及内置 fs sysmodule 一样运行任意原生代码。借助该越狱可运行自制程序、逆向与修改 Switch 2 游戏、备份存档,并通过 MMIO 访问多数外设。作者称越狱本身耗时约两周,而用 Codex 修改《Mario Kart World》仅花一天。
ns2jb 是针对当前最新固件 23.0.1 的 Switch 2 纯软件越狱
简介
你可以从 ns2jb 仓库获取该漏洞利用的副本。
ns2jb 是针对 Switch 2 的纯软件、需连线的越狱。它允许以任意普通 sysmodule 的身份,以及内置的 fs sysmodule 的身份运行任意原生代码。这让我们能够运行自制程序、逆向工程和修改 Switch 2 游戏、从 Switch 备份存档,并通过 MMIO 访问 Switch 2 的大部分外设。据我所知,这是 Switch 2 上的首个此类漏洞利用。
为了展示该越狱的能力,Codex 修改了一款 Switch 2 独占游戏《马力欧卡丁车世界》。感谢我在 Dabney House 的朋友们,他们帮我挑选要添加的角色并试玩测试了这款游戏。
搭建环境
我做的第一件事是在 Switch 1 和 Switch 2 周围搭建一套测试装置,这样 Codex 理想情况下就能在没有我参与的情况下越狱 Switch 2:

编辑:很多人问我是否真的用了这个提示词。不,这只是一句玩笑式的配文。我为用户态代码执行设定了一个 /goal,但没有成功。越狱本身花了大约两周的大量努力。不过《马力欧卡丁车世界》完全是在一天内凭感觉写出来的,纯粹为了好玩。
JetKVM 让 Codex 可以通过 USB 向 Switch 发送手柄输入,并通过 HDMI 接收视频输出。我们还需要一种按下电源键的方式——当 Switch 2 崩溃时,需要按一下按钮才能重启。
这就是从亚马逊买的蓝牙电灯开关的用途。当拨杆压下时,它会按下电源键。一旦电源键被完全按下,拨杆剩余的扭矩就会抬起机构。
获取代码执行
WebKit 是主机越狱的热门目标,所以 Codex 选择它作为初始代码执行的入口并不令人意外。
通过浏览器实现代码执行
它声称通过利用经自定义 Wi-Fi 强制门户加载的 WebKit n-day 漏洞,实现了“原生控制流劫持”。

我们希望能在微内核服务(在 Switch 上称为“sysmodule”)中获得代码执行,这样我们就能运行自制程序、修改游戏、下载存档等。 不幸的是,这种用一个 JavaScript 函数对象替换另一个的做法无法推广到现有函数之外——我们需要一种新方法来发起任意 IPC 调用。这看起来很难。
为什么?
- WebKit 具有仅执行内存(XOM),所以即使拥有任意内存读写能力,我们也不知道能够执行任意 IPC 调用的 gadget 在哪里。
- 即使我们知道,也许是通过解析重定位数据,由于 PAC 的存在,我们也不清楚如何为它构造签名指针。
- 而且即使我们能通过某种签名 gadget 做到这一点,由于 CFI 会对大多数调用点进行类型检查,我们也不清楚如何调用这个指针。
Codex 已重置目标以完成这些步骤。这是一次附带押注;它可能会成功1,但说实话:WebKit 的可及性和任天堂 BBP 奖励可能已经激励了许多在我之前的人去研究这里2。
在它运行的同时,让我们探索其他选项。
通过 smilehax2 实现代码执行
我的想法是针对 Switch 1 游戏,原因有两个:
- 它们是由开发者构建的。我们不太可能需要处理 PAC 和 CFI,尤其是因为 Switch 1 不支持 PAC。
- 由于 Switch 1 的信任根已被攻破,我们可以解密代码并读取它。
也许我们仍然可以获得浏览器攻击面的好处——从 JavaScript 进行编排、利用现有的 n-day 等——同时通过找到一个内嵌浏览器的 Switch 1 游戏来避免 WebKit 缓解措施的缺点?类似于 Electron,但针对游戏,这样我们就能实现像这个 Discord RCE 一样的漏洞利用。目前我不能公开披露这种方法的发现,但幸运的是,还有其他 Switch 1 游戏。
我特别感兴趣的是 SmileBASIC。这是因为它之前被攻破过,在 smilehax 中,在 3DS 上实现了用户态代码执行。它是一个巨大的 C++ 代码库;既然 BASIC 解释器中存在可利用的内存损坏,那肯定还能再找到一个。
Codex 在解释器中找到了一个有用的 32 位整数溢出。当从数组中移除元素时,解释器检查了 position + count <= array.length,但求和可能回绕并变为负数。为了填补移除元素造成的空缺,它本应将剩余元素左移,如下所示:
memmove(
backing + position * 8, // start of the gap
backing + (position + count) * 8, // elements after the gap
(length - position) * 8 // bytes to copy
);假设我们有一个长度为 4 的字符串数组 R,然后运行 REMOVE R,&H60000000,&H3FFFFFFF。position + count 回绕为 0x9fffffff。作为负的有符号 32 位整数,它小于 array.length。乘以 8 也会回绕,因此我们最终执行了 memmove(backing, backing - 8, 32),将前 8 个字节拉入数组。该命令确实报告了 Out of memory 错误,但这无关紧要,因为复制已经发生了。
这很有用,因为 SmileBASIC 中任何数组之前的 8 个字节都包含指向该数组本身的指针!在 REMOVE 之后,对 R[0] 的操作将把数组后备存储当作字符串来对待。
由于指针占据了 R[0] 的长度字段,字符串长度变得巨大,我们可以执行越界读写。我们通过复制 R[0] 的子串来读写内存。
这是一个 Switch 1 游戏,所以 SmileBASIC 代理可以在我的 Switch 1 上研究漏洞利用,而 WebKit 代理则在 Switch 2 上工作。一段时间后,它获得了 ROP。

获取 sysmodule 读写权限
好的,现在我们需要攻破一个权限更高的进程。我们感兴趣的是 普通 sysmodule 和 内置 sysmodule。
我们应该追求哪个 sysmodule?我们没有它们的代码,所以不清楚哪些 sysmodule 拥有哪些权限。
我给几个 Codex 实例设定了获取 sysmodule 代码执行的目标。大多数选择专注于 audio 和 ldn。我认为这是因为 audio 和 ldn 在 Switch 2 上出现过漏洞。Codex 也查看了 NV 服务,可能是因为 GPU 驱动通常成果丰硕?
fs sysmodule
与此同时,我开始研究 内置的 fs sysmodule,因为:
- Switch 1 代码显示它能够提供解密后的代码。
- 代码量很大。
- 它可能与 Switch 1 中的
fs模块类似,因为文件系统 API 没有太大变化。
第一个原因对我最有吸引力。即使我们无法从 fs 获得有用的越狱,我们至少可以解密一些其他 sysmodule,以便 Codex 能在其中找到漏洞。
其他 sysmodule 如 ldr 和 ro 看起来很有吸引力,因为它们有权限映射可执行内存3,但它们的攻击面不大,尤其是从 SmileBASIC 来看。我们稍后再回到它们。
路径规范化导致的缓冲区溢出
一个 IFileSystem 句柄 有一个 IPC 命令,GetEntryType。它接受一个路径字符串,并用 Path::Normalize 进行规范化。
包含带反斜杠的父目录组件的路径(例如 /..\)会调用一个辅助函数,该函数分配一个 0x301 字节的临时堆缓冲区。我们的路径被复制进去,但它可能大于缓冲区,从而导致缓冲区溢出。lol
我们需要一个合适的受害者分配对象来溢出进去。Codex 发现 BufferedStorage 对象会为其文件分配一个缓存条目。如果该条目设置了脏位,对文件的读写就会沿着指向缓存数据的指针进行。
我们可以劫持这一点来实现任意读/写4。
从这里开始,我扫描内存以找到 SmileBASIC 的连接对象,并将保存的 PID 设置为 1。 现在,我的进程拥有“FullPermission”,这意味着我可以执行特权文件系统操作。我转储了内部存储和所有普通 sysmodule 代码,这样 Codex 就可以进行离线分析,我也能理解下一步该往哪里走。
以任意普通 sysmodule 的身份获取代码执行权限
Homebrew 和 modding 通常依赖于执行我们自己的原生代码。查看转储后,唯一有权限映射可执行代码的普通 sysmodule 是 ro。所有这些 sysmodule 都带有 CFI、PAC 和 XOM。该模块的接口似乎也非常精简。相比直接在 ro 中寻找内存破坏,追求逻辑漏洞似乎更有成效。
让我们回到关于 fs 提供解密代码的第一点。这对于转储普通 sysmodule 的代码很有用。这个接口的预期功能是什么?加载程序?
游戏和普通 sysmodule 以任天堂内容归档(NCA)的形式分发,其中包含一个称为 ExeFS 的文件系统镜像。它包含主可执行文件 main 和程序元数据 main.npdm。
为了加载程序,内置的 ldr sysmodule 请求 fs 打开 ExeFS,fs 回复一个指向该文件系统的 IPC 句柄。fs 还提供关于 NCA 头的两条加密信息:头的 SHA-256 哈希以及开发者对该哈希的签名。加载器使用这些信息执行两次签名检查:
- 任天堂对
main.npdm的进程权限和开发者公钥的签名是否有效? - 开发者对 NCA 头哈希的签名是否有效?
在我看来这在密码学上是可靠的,因为理论上:
- NPDM 签名固定了进程权限和开发者密钥;
- 该密钥固定了 NCA 头的签名;
- 该签名固定了 NCA 头的哈希;
- 该哈希固定了 NCA 头的内容,包括其文件系统哈希;
- 这些哈希固定了可执行内容。
然而在实践中,我对最后一点存疑,因为确保所提供的文件系统与 NCA 头的文件系统哈希匹配是 fs 模块的职责,而我们在其中拥有 ARW。
从任意读/写到原生代码执行
确实,借助 ARW,我们可以将加载器 IPC 句柄背后的 ExeFS 对象替换为我们自己的临时文件系统。该文件系统仍然需要为加载器的第一次检查提供有效的开发者 main.npdm,但可执行文件 main 本身可以是任意的。
第二次检查将通过,因为当加载器打开文件系统时,fs 已经提交了原始文件系统的有效 NCA 头哈希和签名。
我们执行两次写入:
- 第一次将我们制作的临时文件系统对象放入加载器 ExeFS 的域表条目中。这个临时文件系统包含原始的
main.npdm和我们替换的main。 - 第二个将加载器的 ExeFS 对象放入我们临时文件系统的域表条目中。这只是为了保持引用计数一致,并防止
fs崩溃。
这些写入必须发生在加载器请求 ExeFS 文件系统之后,但在 fs 处理后续文件请求之前。我们如何确保这一点?
fs 进程维护一个其 IPC 连接的列表。当某个连接有新的 IPC 消息时,fs 会处理它。
利用我们的任意读写,我们可以将 fs 等待的加载器 IPC 连接句柄替换为我们从 SmileBASIC 控制的事件句柄。这样我们就能控制 fs 何时处理加载器消息。
有了这个,我们就能以任何我们拥有 main.npdm 的程序身份执行任意代码!
获取内置系统模块的原生代码执行
我们拥有哪些 main.npdm?嗯,全部都有。借助 ARW,我们可以利用加载器的 fsp-srv 连接来复制任何普通系统模块或游戏的 NPDM。
我们将利用这一能力,使用 ro 的 NPDM 来启动我们自己的代码。如前所述,ro 的 NPDM 赋予它调用内核 SVC 的权限,例如 SetProcessMemoryPermission、MapProcessCodeMemory 和 MapProcessMemory。在本例中,我们将使用 MapProcessMemory 来创建 fs 现有代码的可写视图。我们还可以通过读取该视图来转储 fs 的代码。
然而,这些调用需要进程的 KProcess 句柄。为了获得 fs 的句柄,我们将使用 ARW 将 IEventNotifier 的 4 字节句柄覆盖为 0xffff8001,然后从 SmileBASIC 调用 GetEventHandle。内核将此值解释为“当前进程的句柄”,因此 SmileBASIC 获得了 fs 的 KProcess 句柄。
然后我们可以将此句柄发送给我们自定义的 ro 进程,由它来修补 fs 的代码。
结论
以任何普通系统模块的权限执行任意代码似乎非常强大。我们可以在不先利用每个设备驱动的情况下调查硬件。我希望这能帮助自制软件社区向 CFW 迈进。
短期内,我认为有几个下一步会很有趣:
- 将
fs系统模块漏洞移植到 WebKit。既然我们知道了想要利用的漏洞,能够执行任意 IPC 调用就没那么有价值了。我们或许可以通过更改 WebKit 现有文件系统代码所使用的数据,在fs中触发 ARW。 - 此越狱是有条件的,而且我们目前除了
fs之外没有其他方法攻破内置系统模块。fs是一个内置系统模块,因此 SM 会跳过来自fs的服务 ACL 检查,提供与 smhax 相同的能力。我想知道我们能否在其他内置模块中找到漏洞? - 借助
fs中的原生代码执行,我们可以直接编排其加密硬件。例如,它被用于 NCA4 解密。 - 该漏洞也已在 Switch 1 上测试过,但我还没顾得上打包。它不会提供 modchip 的能力,但也不需要任何外部硬件。
目前,我很期待看到所有即将推出的 Switch 2 自制软件、模拟和改装。
脚注
-
它没有成功。↩
-
尝试 WebKit 漏洞本身并不坏。一旦我们在某个系统模块中发现了已知有价值的漏洞,回过头来尝试用纯数据攻击在 WebKit 中利用它可能是有意义的。这是因为 WebKit 可以离线访问,意味着任天堂无法像 eShop 游戏那样将其撤回。然而,在我们研究的这个阶段,不值得以牺牲迭代速度为代价。↩
-
我想知道任天堂是否有一天会仅通过只有内核或安全监视器才能解密的加密 blob 来映射普通 sysmodule 甚至游戏代码。那样的话,任何用户态进程都无法观察到系统上的任何代码——用户态永远不可能存在 RX 映射。 就连负责启动新进程的 loader 模块,也会请求更高异常级别来加载它提供的加密 blob。这会有取舍,比如游戏无法在运行时读取自己的代码。不过,任天堂已经开始移除动态代码功能,比如 JIT sysmodule。我想知道这是否就是 Switch 的发展方向。幸运的是,那一天还不是今天。↩
-
最初,Codex 的读/写原语每次想要更改后备缓冲区指针时,都会在缓存对象上重复缓冲区溢出。这会影响稳定性,尤其是在连续跟随许多指针时,因为每次更改都有可能导致系统崩溃。我们可以使用与此处类似的技巧。我们可以利用一些溢出来将第一个缓存条目的后备缓冲区指针指向另一个缓存条目的元数据。然后我们就可以替换第二个缓存条目的指针,而无需冒更多溢出的风险。↩
来源:Hacker News · pepsipu.com