原帖:https://garyodernichts.blogspot.com/2024/10/looking-into-nintendo-alarmo.html
翻译如下:
任天堂 Alarmo 探秘
在大家都期待任天堂 Switch 后继产品的消息时,任天堂却推出了一款名为 Alarmo 的新产品。这是一个小巧的塑料闹钟,能够用你最喜欢的任天堂游戏音效叫醒你。起初我对购买它有些犹豫,但最终还是决定入手并深入研究一下它的工作原理。
仅仅是个闹钟?
Alarmo 的正面配有一个 2.8 英寸的小型 LCD 显示屏,顶部有返回和通知按钮,还有一个可旋转按压的旋钮,用于确认操作。旋钮还包含一个 RGB LED。和其他闹钟相比,它的独特之处在于配备了 2.4 GHz Wi-Fi,可以下载软件更新和额外主题,并带有 24GHz 毫米波感应器,能对你的动作作出反应。考虑到它高达 99 欧元的售价,我起初不想购买。但几天后,我决定把它当成一个有趣的项目,探索一下它红色塑料外壳内的奥秘,于是入手了一个。
内部结构如何?
由于我在周末前下单,所以等到发货后一周才收到 Alarmo。在此期间,有位推特用户 Spinda 已经找到了板子上的 SWD 调试引脚,因此拿到后,我第一时间就把 Alarmo 打开。
拆开设备非常简单。设备底部 USB-C 接口旁边有一颗十字螺丝,拧下螺丝后,屏幕可以连同电路板一起旋下。在电路板上,我发现了一颗 STM32H730ZBI6 MCU 和一颗 KIOXIA 4GB eMMC 存储芯片。
我将电线焊接到 SWD 引脚,并用我的树莓派连接到 Alarmo 上,通过 OpenOCD 读取和操作 Alarmo 的内存和寄存器。然而,由于一项名为读出保护 (RDP) 的机制,我无法读取 STM 内部存储的启动固件。该机制在检测到调试器后,会阻止对内部闪存的任何访问。要破.解 RDP,我们需要找到一种方法,在不连接调试器的情况下实现代码执行。
所幸 STM32H7 系列的参考手册、示例代码和许多库都可自由获取。调试端口还允许将有效负载加载到内存中并执行,这意味着我们可以尝试许多潜在的操作。
这是 Flash 吗?
由于 Spinda 在 Twitter 上的研究进展迅速,我决定联系她。她建议我查看内存中的 0x70000000 范围。从这里导出数据后,确实发现了大量的 ARM 指令。这片区域似乎包含了大部分固件!
根据参考手册,这就是所谓的 OCTOSPI2 区域。OCTOSPI 是一种低级接口,用于单通道、双通道、四通道或八通道的 SPI 通信。SPI?我是否忽略了电路板上的某个 SPI 闪存?MCU 旁边似乎有一个没有任何标识的小芯片,难道它就是我们要找的?
当我开始逆向分析刚刚导出的固件时,发现固件将 OCTOSPI 区域视为 RAM。而且,确实如此:向此区域的空白部分写入值并重启系统后,该值会复位为 0x55。进一步检查 OCTOSPI 寄存器配置后,确认这里是用于外部 RAM 的 32 MiB HYPERRAM。
遗憾的是,这并不能帮助我们绕过 RDP,因为我们无法在外部 RAM 中持久保存有效负载。
解密内容
那么,这个在 RAM 中的固件到底从哪里加载的?它在启动时从 eMMC 中加载。Spinda 已经利用 RAM 中的固件提供的 eMMC 功能转储了 eMMC 数据。eMMC 中包含一个内容文件夹,里面有每个游戏主题的文件、一个系统文件、一个出厂文件和一个名为 2ndloader.bin 的文件。不幸的是,所有内容文件都是加密的。那么系统是如何解密它们的呢?
STM32H7 配有一个称为 CRYP 的加密处理器。这个外设可以配置来解密内存中的内容。导出的外部 RAM 固件并未配置 CRYP,因此 CRYP 很可能在启动时由内部闪存上的代码进行配置。但是,固件确实使用了 CRYP 接口来解密内容,因此我们也可以利用它!CRYP 接口配置为 AES-128-CTR 模式,这让操作变得简单。CTR 模式中会生成一个密钥流,该密钥流会与明文结合来加密和解密文件。我们可以使用 CRYP 接口生成大量密钥流,然后将其与加密文件组合来解密它们。通过调试接口转储了约 100MB 的密钥流后,我们成功解密了闪存上的所有文件。
额外收获:获取密钥
配置 CRYP 接口时,密钥存入四个 32 位寄存器中。但遗憾的是,这些寄存器是只写的,无法读取密钥。暴力破.解也不可行,因为存在 2^128 种可能组合。
当 Spinda 在深入研究 eMMC 内容时(她在 Twitter 上发现了很多有趣的内容,值得关注!),我与 hexkyz 一起讨论了我们的发现。hexkyz 发现 CRYP 接口存在一个部分覆盖攻击的漏洞。由于密钥分成四个不同的寄存器,我们可以只更新密钥的 32 位,然后尝试所有 2^32 种不同可能性,直到加密处理器生成匹配的输出。这需要对密钥的四个部分都进行测试,总共需测试 4×2^32 种组合,几个小时内就可以完成。我编写了一个小型程序来执行这个操作,并让它连夜运行。第二天早上检查进度时,任务已完成,我成功获取了用于加密和解密 Alarmo 内容文件的 AES-128-CTR 密钥。
sha256(alarmo_content_key)=47238c47d21165fdb2f9a26c128e4b620a39139f6514588f5edb8a16397a9201
初始化向量和加密优化
初始化向量(IV)可以直接从 IV 寄存器中读取,因为这些寄存器并非只写。
更新(2024-10-31):我们发现还可以在 Alarmo 上加密已知的明文块,然后逐步将密钥的部分值置为零,同时转储每部分生成的密文。这样,密钥的暴力破.解可以在 PC 上进行,通过不断增加密钥值,直到生成的输出与那些非零密钥部分的密文匹配。
为了加快这个过程,我利用 AES-NI x86 扩展开发了一款 PC 工具。通过在 PC 上进行,这一破.解时间从数小时减少到几分钟(在一台较新的 PC 上)。我已将这些工具上传到 GitHub 仓库。感谢 @SciresM 和 @PoroCYon 的宝贵建议!
https://github.com/GaryOderNichts/alarmo/tree/main/key_bruteforcer
文件格式概览
现在我们可以解密这些文件了,一起来看看它们的结构。eMMC 上的所有内容文件都是加密的,且前缀带有 CIPH 文件签名标识。我们称之为 CIPH 文件,或加密文件。CIPH 文件主体使用 AES-128-CTR 加密,其加密主体的最后 256 字节是一个 RSA-2048 签名(PKCS#1 v1.5,SHA256)。
所有主题文件以及系统文件和出厂文件都是 .shpac 格式文件。实际上,shpac 文件是 ZIP 文件包裹在 CIPH 文件中的格式。每个 shpac 压缩包都包含各种资源和一个固件二进制文件。这些固件二进制文件以 BINF 文件签名标识开头,因此我们称它们为 BINF 文件。每个 BINF 文件包含一个头部,头部包括加载到内存中的地址、向量表地址和文件在内存中的总大小。
2ndloader
eMMC 内容中最引人注目的是 2ndloader。顾名思义,这是一个从 eMMC 加载的二级加载程序。2ndloader 是唯一不是 shpac 格式的文件;它由内闪存中的加载程序直接加载并解密到 SRAM(地址 @0x24000000)。在正常启动过程中,2ndloader 会从 eMMC 中加密的 system.shpac 文件加载固件,将其复制到外部 RAM,然后跳转执行。
有趣的是,在正常启动时并不会验证 system.shpac 文件的签名,只有在进行固件更新时才会检查。
固件更新
固件更新过程相对简单。系统固件会使用设备的唯一证书查询一个端点,获取最新的固件版本,端点会返回一个到新版 system.shpac 文件的 CDN 链接。设备随后下载该文件并将其存储为 eMMC 上的 system.update.shpac,然后设备重启进入 2ndloader。2ndloader 验证更新文件的签名,并将其覆盖到现有的 system.shpac 上。在这个复制过程中,屏幕上会显示一个小的进度条,接着新 system.shpac 文件的签名会再次被检查。
USB 加载模式
2ndloader 最有趣的部分是 USB 模式。当启动时按住 Alarmo 上的三个顶部按钮时,2ndloader 会在外部 RAM 中设置一个 FAT32 格式的缓冲区,模拟 USB 大容量存储设备。它随后等待一个 MarkFile 文件被放置到该设备中。一旦检测到该文件,它就会从设备读取一个包含 BINF 固件的 CIPH 文件。与其他 CIPH 文件一样,该文件被加密并签名。解密该文件并将其加载到外部 RAM 后,2ndloader 跳转到新加载的复位向量,类似于常规的系统固件加载过程。
他们会检查 USB 载荷的签名,对吧?对吧?嗯,可以说他们“尝试”了……他们的代码看起来大概是这样的:
if (!IsSignatureValid("2:/a.bin")) {
// Do nothing
}
// Continue with loading the firmware
这意味着我们可以通过 USB 加载任意固件二进制文件,甚至不需要拆开 Alarmo。文件仍然需要加密,但现在我们已经拥有密钥,甚至只需要一个足够长的密钥流,就可以轻松实现。
绕过 RDP 保护
现在我们可以运行任意代码,而无需连接调试器,因此可以避免触发闪存的读取保护(RDP)。于是,我编写了一个小的负载,尝试将内部闪存的内容复制到 RAM。不幸的是,这并没有成功,只读到了一堆零。虽然读取保护错误不再被触发,但出现了另一个问题:安全错误标志被设置了。
安全访问模式
原来 STM32H7 还有一个“安全访问模式”的保护机制。在安全访问模式下,MCU 会始终启动进入 STM 制作的安全引导程序。该引导程序支持设置一个包含安全用户代码的安全区域,用于执行各种任务。安全用户代码完成后会跳转到常规应用程序,并被锁定访问。对于 Alarmo 来说,我们虽然不完全了解安全用户代码的确切作用,但它很可能负责设置 CRYP 接口,并加载、解密并跳转到 2ndloader。在跳转到 2ndloader 之前,安全用户区域被锁定,无法再读取。不幸的是,我还未能转储安全引导程序或安全用户区域,因此具体的内容仍然是个谜。
额外发现:利用 2ndloader
在对 2ndloader 进行逆向工程时,我注意到它使用 RSS_exitSecureArea
函数跳转到加载的固件。这是用于锁定并退出安全用户区域的函数。那么,2ndloader 可能仍然在安全模式下运行?因此,我开始寻找在它跳转到加载代码并退出安全区域之前执行代码的方法。还记得 USB 模式下加载的 BINF 文件吗?该文件头部包含了它应加载到的地址。Spinda 问道:“是否可以将其复制到 bin 文件头中指定的任何地址?”这是个好问题!我记得看到的代码验证了地址必须位于外部 RAM 中,但经过再次确认,只有向量表地址需要位于外部 RAM,文件本身可以加载到任何地方。这允许覆盖 2ndloader 中的指令,在调用 RSS_exitSecureArea
之前跳转到自定义代码。
然而,不幸的是,读取内部闪存时仍会触发安全错误。看起来 2ndloader 实际上并未在安全模式下运行,即便它调用了用于退出安全区域的函数。
接下来是什么?
那么接下来呢?现在已经有了一种无需拆开设备就能在 Alarmo 上运行自定义代码的方法。目前,这在软件版本 2.0.0 上仍然有效,而且似乎还没有为更新 2ndloader 设置的系统。当然,理论上可以通过 eMMC 更新 2ndloader,因此我们可以观望接下来会发生什么。
关于安全用户区域呢?目前还没有想法如何转储该区域。由于我们已经拥有内容密钥,可能不会有太多额外的收获,除了进一步确认其工作原理。
当这篇博客文章发布时,我会发布我的测试 USB 负载,它进行显示初始化并显示一张猫的图片。同时,我也会发布用于获取内容密钥的负载。你可以在 GitHub 仓库找到它们:https://github.com/GaryOderNichts/alarmo。或许会有更多人研究在 Alarmo 上编写自定义代码。
致谢
感谢 Spinda,感谢她发现了 SWD 引脚、编写 eMMC 转储代码,以及倾听我关于 2ndloader 的讨论;感谢 hexkyz,他帮忙找到了有关安全区域的资源,并给了我转储密钥的想法。