这是本系列文章的第二篇,我们将继续为读者介绍如何通过破解三星手的固件,让其变身为NFC安全研究的利器。
实现固件更新
考虑到这些细节,我决定写一个工具来实现固件更新。如果成功的话,就可以对它们进行修改,看看是否能在引导加载器级别对芯片发动攻击。如果能够通过这种方式发动攻击的话,我就不仅可以部署定制的固件,同时也能让漏洞更难以修复。
我写了一个C应用程序,可以部署到我的手机上,以便与芯片硬件进行交互。通过重放我在追踪的固件更新中发现的命令并检查响应,可以快速编写一个工具。需要说明的是,用于设置模式的IOCTL是从原始内核源代码中识别出来的。
这允许对特定功能进行评估,包括一些命令会在原始命令之外,还会发送后续数据载荷的事实,正如在更新开始时发送的签名和SHA-1哈希值那样。
固件更新协议
我们发现固件更新有特定的顺序,并使用递增编号的命令。
0: 重置
1: 开机信息
2:开始更新
4:更新4096字节区段
5:全面更新
这些都是通过查看内核源码再次确认过的。
这个协议的结构和使用的序列意味着固件的SHA-1哈希值在更新过程结束时进行了相应的验证。这意味着,即使不能执行,也可以将修改后的固件写入芯片。
我们注意到,在这个序列中没有出现编号为0x03的命令。这在很大程度上暗示着可能存在一个隐藏的命令。
我推测,如果存在隐藏的命令,那么从编号0x06到0xFF也有对应的命令存在。我在更新序列的每一个节点上遍历执行每一条命令,这些节点包括:更新发生之前、更新期间和更新之后。错误响应可以反映这些命令的使用情况:响应2意味着命令无效,命令9意味着有效载荷长度太小。
通过上述方法,我们发现隐藏的命令3的功能与命令4相同,唯一的区别就是命令3的有效载荷长度为512字节。我们想了很长时间,也没有找到利用它的方法,不过,好在我们又发现了另外一条隐藏命令:命令6。
隐藏的命令6
利用错误响应信息,我发现这个命令需要8个字节的有效载荷数据,并且只在固件更新发生之前起作用。最初,我发送了八个空的字节,收到了一个不同的错误值,但没有收到数据。因此,我开始在八个字节中逐位进行递增。
最终,当来到第五个字节的第一个二进制位时,命令返回了四个字节的数据:00 20 00 20,或0x20002000。这引起了我的极大兴趣,因为这很可能是一个堆栈指针,并且经常作为Vector表的一部分出现在大多数Cortex-M和Securcore芯片组的最开始部分。
递增到下一个二进制位时,我发现除了返回原来的四个字节外,还返回了额外的四个字节:BD 02 00 00 (0x000002BD)。我认为这很可能就是向量表的下一个地址中的复位向量。此时,很明显,隐藏的命令6允许从芯片中读取任意内存,前四个字节是32位地址,后四个字节是响应大小。通过读取内存中的递增地址,我就证明了这一点。
利用这个命令,可以转储引导加载器、固件、RAM和硬件寄存器。
转储引导加载器
将地址0x00000000处的内存逐步拼接在一起,可以生成一个包含完整引导加载器的二进制文件,其大小为8KB,其结构与标准Cortex-M芯片组的相同。现在,我就可以对固件进行静态分析了,但是,由于嵌入式固件没有字符串或函数引用,因此,这个过程可能比较费劲。
分析引导加载器
之后,我们将引导加载器加载到IDA中,发现反汇编的效果很好。
这样的话,我们就可以考察与更新有关的重要信息了。
上面的代码位于引导加载器的开头附近,其作用是检查用于启动核心固件或bootloader的GPIO引脚。这表明,在地址0x3000处找到了一个魔术值(0x5AF00FA5),它正是部署固件的开始地址,并在更新文件中设置为0xFFFFFFFF。如果这个值不存在,引导加载器就无法启动更新后的固件。最初,我试图将这个值直接放到要发送的固件中,然而这并不奏效。
若要停止运行接收I2C命令的代码其实非常简单,具体如上图所示;另外,在固件更新之前,命令0、1、2和6是可用的。
最后,我们还发现了相关的RSA公钥,其实这一点都不难发现,因为大量的高熵字节后面跟着0x00010001(65537),传统上该数字常常用于RSA密钥的指数。
寻找内存损坏漏洞
我想在这个引导加载器中找到一个内存损坏漏洞,这样的话,我就可以部署自定义固件了。然而,这种方式其实也有一些限制。
首先,我只有一部手机,如果我使用fuzzing过程中将NFC芯片变成了废砖,我就不得不重新再买一部新手机。我可不想破财,所以,我要尽可能安全地攻击它。
其次,所有的通信都是通过I2C进行的,而通过这种方法进行调试会比较困难。
综合考虑,我觉得最好的方法是尽可能地通过模仿引导加载程器来挖掘漏洞。
Unicorn Engine
Unicorn Engine是一款优秀的工具,可以在代码中模拟特定架构的固件。与标准的QEMU不同,它是以应用程序的形式运行的,不仅可以通过C或Python的库进行仿真,还允许hooking仿真过程中的每一步。
就这里来说,Unicorn Engine的固件分析设置非常简单:映射所有有效的内存,在地址0x00000000处加载引导加载器,并将程序计数器设置为复位向量即可。
之所以如此简单,主要是由于该引导加载器没有使用线程,而是使用了一个无限循环来检查I2C命令。
然而,这里还是有一些复杂的地方。我们知道,嵌入式芯片通常做的第一件事情就是将硬件外设为标准配置,并验证其工作是否正常。由于这些过程并没有被仿真,所以,固件会不断重启,因为它总是认为初始化失败。对于这个问题,我是通过取消初始设置功能来进行补救的。
这样一来,固件就可以运行了,并开始不断地从地址0x40022030处读取内存。这是一个硬件地址,用来读取芯片外设的特定元素的信息。在考察它读取这个地址的过程中,我发现它在以无限循环的方式寻找特定的二进制位,直到找到正确的二进制位为止。我认为这很可能是I2C接口的状态寄存器,这意味着我们的方向是对的。利用Unicorn的hooking功能,我们能够让这个地址总是返回随机值,这样特定的位就被设置了。
接下来,我发现芯片会不断地从地址0x40022038处读入字节。由于这是连续读入数据,所以,我们认为这里很可能是芯片的I2C FIFO缓冲区。于是,我们让代码通过这个地址发送引导加载器命令,看看会发生什么。
之后,我发现固件开始向地址0x40022034写入数据,该地址介于前面两个地址之间。通过打印这些写入的值,我发现它发送的响应与原始的引导加载器非常相似。这意味着我对芯片的仿真已经大功告成了,接下来,我们就可以开始进行fuzzing测试了。
破坏内存的机会
现在,我终于可以随意地进行模糊测试,而不用担心芯片被烧坏了,同时,我们还获得了调试功能的支持。这其中会存在一些机会,包括使用16位值的有效载荷大小,这比芯片上可用的8KB RAM要大,以及在更新过程中使用命令后的分块附加数据。
我认为最有可能出现内存损坏的地方位于固件更新初始化函数中。这个函数的工作原理是:先发送固件的SHA-1哈希值长度(0x14)和签名长度(0x80),同时,还发送包含该哈希值和签名的分块数据。
最初,我们通过缩短这些值,以试图操纵哈希值和签名之间的数据量,很可惜,这种方法并没有奏效。
利用仿真的引导加载器,我增加了哈希值的长度,并使分块数据与该长度相匹配。我发现,利用这种方式,可以通过这个命令发送越来越多的数据,直到最后仿真的引导加载器因为内存超出边界而出现异常:它试图访问芯片上8KB的RAM之外的内存区域。
这很可能意味着我发现了一个缓冲区溢出漏洞,我们可以通过该漏洞来覆盖堆栈并操纵当前存储在堆栈中的链路寄存器(Link Register)的值。也就是说,这是一个可用的内存损坏漏洞。
我想为这个漏洞编写shellcode,以协助运行定制的固件。由于这是一个嵌入式芯片,尽管不会存在任何ASLR或复杂的缓冲区溢出的缓解措施,但是,却存在一个意想不到的障碍。
大多数Cortex-M风格的芯片组能够同时从闪存和RAM中执行代码。这意味着,只要你发现了一个缓冲区溢出漏洞,你就可以很轻松地编写出相应的shellcode,然而,通过调查这款芯片的相关营销材料发现,它是基于SC000 SecurCore架构的,这是一个类似于Cortex-M的标准,其中提供了一些没有公开定义的安全限制。我的shellcode之所以在真正的芯片上无法奏效,我认为,主要原因就是无法从RAM中执行,所以,我不得不寻求其他方法。
对于这种情况来说,通常ROP漏洞还是很有希望的。通过操纵堆栈来调整寄存器并跳转到函数的尾部,可以仅用现成的代码库执行特定的计算。但是,应注意的是,引导加载程序进程的堆栈非常小,在发生缓冲区溢出时仅由4个32位字组成。
由于这两种方法都不可行,因此我决定再想想其他简单的方法。最初,我想通过漏洞覆盖固件开头的幻数,以使其能够显示为合法的固件,但是我发现,从引导加载器跳入该固件同样有效。于是,我通过使用指向地址0x016d的指针覆盖存储的链接寄存器来完成该操作。需要注意的是,所有Thumb代码总是设置最低位为1,以使其与标准32位ARM代码区分开来,这就是为什么指针总是出现off-by-one问题的原因。
这里的代码会将固件的地址加载到内存(0x3000)中,读取其复位向量,并存储在0x300c中,然后跳转到相应的代码处。这种简单的方法绕过了所有签名检查,这意味着我直接跳入了未签名的固件中。
我在物理芯片上实施了这种攻击,并对版本号进行了修改——因为它很容易通过NCI检索,所以,我们就使用了该版本号。
现在,我可以通过这种方法运行任意的自定义固件。我已经向三星披露了此漏洞,因为它破坏了芯片上的所有完整性保护机制。
在我看来,可以通过两种方法来修复该漏洞:
方法1:
从主固件中修复引导加载器,铲除缓冲区溢出问题。
这可能会使芯片崩溃,因为核心引导加载器会被覆盖。
方法2:
给内核打上补丁,禁用过长的哈希值和签名。
这种方法的缺点是可通过修改内核或直接访问I2C直接绕过。
由于这些缓解措施都无效,所以,该漏洞很难修复。
进一步研究
由于我是在老款芯片上发现这个漏洞的,所以,我认为新款芯片上也可能存在这个安全漏洞。于是,我浏览Samsung Semiconductor的网站时,发现了一个移动NFC芯片的列表。
在网上搜索这些芯片,但是并没有找到多少有用的信息,但我认为它们很可能用在了新型的三星智能手机中。通过ROM存档,我下载了S6型号之后所有非美版本的三星手机的ROM,并着手提取它们。
在这里,我们的目的是识别存储在手机“/vendor/”分区中的固件二进制文件,因为这些文件很可能与S6中的文件命名相似。有时候,供应商分区并不存储在ROM中,所以,我们只能在手机本身中找。所以,有时寻找固件文件会非常困难。
所以,我决定购买三星S9,因为我发现它使用了S3NRN82 NFC控制器,这是智能手机中的最新芯片。我猜测,它很可能也存在于三星S8和S10中,不过,我还是觉得S9才是最明智的选择。
由于手机允许OEM解锁和自定义ROM,因此,我们轻松得到了root访问权限。
S3NRN82固件
通过观察固件文件发现,它的格式与S3FWRN5固件的非常类似,这意味着它很可能使用了类似的通用架构。不过,我注意到堆栈指针从0x20002000变成了0x20003000,这意味着可用的RAM更多了。此外,重置矢量也变小了,从0x3021变成了0x2021,这意味着引导加载器可能也变了。此外,固件的大小增加了32KB,这意味着有更多的闪存可用。
复现漏洞
我想尽可能地复现原始的漏洞,以证明它仍然存在。因此,我采用了同样的方法。
我发现隐藏的命令3和6现在已经不存在了,所以,我无法再从芯片中任意读取内存了;不过,新的命令7却是可用的,然而,它除了重启芯片之外,什么都做不了。
由于我无法读取内存,所以我进行的任何漏洞利用都必须是“盲式”的。这样的话,事情会变得更加难办,因为引导加载器很可能受到意想不到的改变。
最后,我发现固件更新工具也没法用了,看起来好像是由于SHA-1哈希值与签名不再有效所致。
小结
在本文中,我们继续为读者介绍了如何通过破解三星手的固件,让其变身为NFC安全研究的利器。在本系列的下一篇文章中,我们继续为读者贡献更多精彩内容,敬请期待。
本文翻译自:https://www.pentestpartners.com/security-blog/breaking-samsung-firmware-or-turning-your-s8-s9-s10-into-a-diy-proxmark/如若转载,请注明原文地址: