二进制漏洞分析-5.华为安全监控漏洞(SMC MNTN OOB 访问)
2023-11-19 10:44:10 Author: 安全狗的自我修养(查看原文) 阅读量:12 收藏

CVE-2021-22437 漏洞 SMC MNTN OOB 访问(整数溢出)

CVE-2021-39993 漏洞 SMC MNTN OOB 访问(共享控制结构)

如果你是想谈业务合作,直接翻到底联系作者。

  1. 不闲聊,直接说重点,我能不能做,你有没有预算,相关先介绍(我介绍技术、或者方案产品,你介绍需求带预算)就完事。

  2. 云桌面开发相关: 虚拟化(usb、usb透传、显示器、网卡、磁盘、声卡、摄像头)模块。

  3. 安全产品(dlp/edr/沙箱)开发相关:文件单/双缓冲透明加解密、网络防火防墙、所有通用外设管控(用户层版/驱动层版)模块、其它管理(恶意进程、模块等)。

  4. 通用开发相关:注入、hook、产品方案编写与设计、非黑灰产逆向、具体单独小功能编写。

如果你想系统学习二进制漏洞技能,往最后翻,或者直接翻到底联系作者。

漏洞详情

除了充当内核向安全元件 (SE) 发送命令和接收响应的直通之外,安全监视器还提供了一种检索 SE 日志的方法。这与安全虚拟机管理程序的日志记录系统类似,后者使用共享内存缓冲区。

在内核方面,SE 的日志由“hisee mntn”驱动程序管理。此驱动程序可以在 SMC (0xC500CC00) 中找到并利用 SMC 与安全监视器进行通信。此 SMC 在下面的枚举中定义了四个命令。drivers/hisi/mntn/hisee/hisee_mntn.cHISEE_MNTN_IDhisee_mntn_smc_cmd

▸ drivers/hisi/mntn/hisee/hisee_mntn.h
typedef enum {
HISEE_SMC_INIT = 1,
HISEE_SMC_GET_LOG, /*save all log data when hisee reset*/
HISEE_SMC_LOG_OUT, /*get log print of hisee when it is running*/
HISEE_SMC_GET_VOTE /*get vote value of hisee pwr state*/
} hisee_mntn_smc_cmd;

内核使用第一个命令 HISEE_SMC_INIT 来通知安全监视器要使用的共享内存缓冲区的物理地址和大小。该命令是从函数调用的,该函数通过调用 来分配共享缓冲区。hisee_mntn_probedma_alloc_coherent

▸ drivers/hisi/mntn/hisee/hisee_mntn.c
static int hisee_mntn_probe(struct platform_device *pdev)
{
// ...
atfd_hisi_service_hisee_mntn_smc(
(u64)HISEE_MNTN_ID, /* SMC handler ID */
(u64)HISEE_SMC_INIT, /* SMC MNTN Command ID */
hisee_log_phy, /* Shared log buffer physical address */
(u64)hisee_info.log_len); /* Shared info size */
// ...
}

这些命令HISEE_SMC_GET_LOG并用于使安全监视器将 SE 的日志写入共享内存缓冲区,第一个命令在重置 SE 时写入,第二个命令在执行时写入。HISEE_SMC_LOG_OUT

易受攻击的命令是HISEE_SMC_INIT。它在函数(SMC 的处理程序)中处理。hisee_mntn_smc_handlerHISEE_MNTN_ID

uint64_t hisee_mntn_smc_handler(
uint32_t smc_fid,
uint64_t cmd
uint64_t buf_addr,
uint64_t buf_size,
uint64_t x4,
void *cookie,
void *handle,
uint64_t flags) {
// ...
switch (cmd) {
case HISEE_SMC_INIT:
// Check (improperly) that the buffer is in the address range 0x3c000000-0x40000000.
if (buf_addr >= 0x3c000000 && buf_addr + buf_size - 1 < 0x40000000) {
// Fill the control structure at the beginning of the log buffer.
header = (hlog_header_t*)buf_addr;
header->addr = buf_addr + 0x18;
header->max_size = buf_size - 0x18;
header->real_size = 0;
// Save the log buffer physical address in a global variable.
g_hisee_log_buffer = buf_addr;
}
break;
// ...
}
}
return 0;
}

我们发现的漏洞是双重的。首先,对缓冲区地址和大小执行的检查不考虑整数溢出。其次,安全监视器使用的控制结构位于共享内存区域中,并包含可任意修改的指针。

整数溢出

第一个问题在于对共享内存缓冲区的地址和大小执行的检查。要满足的两个条件是:

  • buf_addr >= 0x3C000000

  • buf_addr + buf_size <= 0x40000000

但是,这些检查允许整数溢出。可以提供高于 0x40000000 的缓冲区地址,然后调整缓冲区大小以使加法溢出。然后,结果将小于 0x40000000,并且检查将通过。buf_addr + buf_size

例如,我们可以将地址0xDEADBEEF和大小0xFFFFFFFF21524111。

  • 0xDEADBEEF大于0x3C000000,因此它通过了第一次检查;

  • 0xDEAD_BEEF + 0xFFFF_FFFF_2152_4111 = 0x0(在 64 位平台上),从而通过第二次检查。

然后,我们可以滥用函数的其余部分,在相对于我们选择的缓冲区地址的地址执行写入,例如,以下 3 个语句。

header->addr = buf_addr + 0x18;
header->max_size = buf_size - 0x18;
header->real_size = 0;

注意:对 / 可以取的值仍有限制。它只能从 0x3C000000 到 0xFFFFFFFFFFFFFFFF,这意味着我们不能单独使用此整数溢出写入安全监视器的内存部分,因为它是在地址 0x14000000 处映射的。buf_addrheader

共享控制结构

第二个问题与用于跟踪共享内存缓冲区内当前位置的控制结构有关。hlog_header

typedef struct hlog_header {
uint64_t addr;
uint64_t max_size;
uint64_t real_size;
} hlog_header_t;

此结构位于内核提供的共享内存缓冲区的开头。因此,内核能够随时修改其字段,包括在安全监视器使用它们时。最重要的是,字段 (将写入日志的地址)可以在 执行检查后很长一段时间内由内核修改。addrhisee_mntn_smc_handler

我们将在下一节中看到如何将这些错误组合在一起,以构建一个漏洞利用,从而在 NS-EL1 的 EL3 中实现可靠的代码执行。

开发

在本节中,我们将描述漏洞利用和我们构造的不同原语,以便从内核获取 EL3 的任意代码执行。

任意 Memset 原语

我们制作的第一个原语允许我们执行任意内存范围。为此,我们使用命令 HISEE_SMC_GET_LOG,该命令在调用 hisee_save_log 函数之前设置为 0。memsetg_hisee_log_buffer->real_size

uint64_t hisee_mntn_smc_handler(
uint32_t smc_fid,
uint64_t cmd,
uint64_t buf_addr,
uint64_t buf_size,
uint64_t x4,
void *cookie,
void *handle,
uint64_t flags) {
// ...
switch (cmd) {
case HISEE_SMC_GET_LOG:
if (g_hisee_log_buffer) {
g_hisee_log_buffer->real_size = 0;
ret = hisee_save_log(0xf0e23800, 0x7b0);
}
break;
}
// ...
}
return 0;
}

hisee_save_log执行从 0xF0E23800 到 的memcpy_s。正如我们在 HISEE_SMC_INIT 中看到的,指向共享内存缓冲区开头的控制结构。因此,通过使用第二个错误,我们可以(但不能)指向任何地址,包括安全监视器的部分。g_hisee_log_buffer->addr + g_hisee_log_buffer->real_sizeg_hisee_log_bufferg_hisee_log_buffer->addrg_hisee_log_buffer

uint64_t hisee_save_log(uint64_t addr, uint64_t size) {
// ...
// Sanity-check on the argument values.
if (!size || !g_hisee_log_buffer || !get_hisee_state())
return -1;
// Copy the logs to the shared memory buffer.
res = memcpy_s(
g_hisee_log_buffer->real_size + g_hisee_log_buffer->addr,
g_hisee_log_buffer->max_size,
addr,
size);
if (res)
return -1;
// Increment the current size in the control structure.
g_hisee_log_buffer->real_size += size;
return res;
}

memcpy_s 的一个有趣的属性是,如果它所做的任何检查失败,它会调用函数 reset_memory

uint64_t memcpy_s(char *dst, uint64_t dst_len, char *src, uint64_t src_len) {
// Sanity-check on the argument values, and the source and destination buffers.
if (src_len > dst_len || !dst || !src || dst_len >= 0x80000000 || !src_len
|| (src >= dst || dst < src + src_len) && (dst >= src || src < dst + src_len)) {
// Call reset_memory to ensure no uninitialized memory from the destination buffer leaks.
return reset_memory(dst, dst_len, src, src_len);
}
// Call memcpy that does the actual copy.
memcpy(dst, src, src_len);
return 0;
}

reset_memory是将目标内存重置为零的调用的包装器。memset

uint64_t reset_memory(char *dst, uint64_t dst_len, char *src, uint64_t src_len) {
// ...
if (dst_len < 0x80000000) {
if (dst && src) {
if (src_len <= dst_len) {
if (src < dst && dst < src + src_len || dst < src && src < dst + src_len) {
memset(dst, 0, dst_len);
}
} else {
memset(dst, 0, dst_len);
}
} else {
if (dst) {
memset(dst, 0, dst_len);
}
}
}
// ...
}

通过查看 memcpy_s 和 reset_memory 中的条件,我们可以看到,如果我们提供的目标长度小于源长度(即 0x7B0),我们就可以执行任何小于 0x7B0 大小的目标缓冲区。memset

但是,在我们可以使用这个原语之前,仍然需要满足一个条件,即通过第一个签入hisee_save_log

if (!size || !g_hisee_log_buffer || !get_hisee_state())
return -1;

对于此调用,大小和日志缓冲区指针将为非零,但我们仍然需要确保get_hisee_state返回非零值。当 extract_bit_11_from_0xfff0a434 函数也返回非零值时,就是这种情况。

uint64_t get_hisee_state() {
uint64_t state = 0;
spin_lock(g_lock);
if (extract_bit_11_from_0xfff0a434()) {
state = 1;
// ...
}
spin_unlock(g_lock);
return state;
}

当设置地址 0xFFF0A434 处的四字的第 11 位时,就会发生这种情况。

uint64_t extract_bit_11_from_0xfff0a434() {
return ((*(uint64_t*)0xfff0a434) >> 11) & 1;
}

为了确保设置了这个位,我们可以使用影响命令HISEE_SMC_INIT的第一个错误(整数溢出)来写入地址0xFFF0A434(大于 0x3C000000)。为此命令指定缓冲区地址时,将写入以下值:1 << 11 = 0x800X

抵消可能的值
addr0x00X + 0x18
max_size0x08从 到-(X + 0x18)-(X + 0x18) + 0x40000000
real_size0x100

我们可以通过调整给定给命令的缓冲区大小来完全控制字段的 3 个最低有效字节。这足以填补hisee_save_log开始时的条件,并开始努力制作读/写基元。max_size

任意读写基元

控制流劫持

该漏洞的下一步是使用我们的原语制作任意读/写原语。我们可以通过更改数据部分中存在的众多函数指针之一来劫持监视器的安全执行流程来做到这一点。memset

这个想法是找到一个可以从 SMC 轻松访问的功能指针,一旦我们将它的一个或多个字节设置为零,它就会指向一个有趣的小工具。这高度依赖于安全监视器版本。在我们的测试设备上,版本是 .v1.5(debug):6458010

我们选择修改运行时服务的 SMC 处理程序指针之一。bl31_secap_svc

ops_t bl31_secap_smc_handlers[] = {
/* 0x14028178: */ {0xca000001, func_14003148},
/* 0x14028188: */ {0xca000002, func_14003140},
/* 0x14028198: */ {0xca000008, func_1400382c},
};

我们针对的 SMC 处理程序指针位于地址 0x140281A0,并指向位于 0x1400382C 的函数。我们之所以选择这个,是因为0x14003800有一个有趣的小工具,可以让我们调用任意函数:BLR X2

0x14003800: cbnz x2, 0x14003820
[...]
0x14003820: blr x2

通过使用我们的原语将最低有效字节设置为 0,我们能够将指针值从0x140038 2C 更改为 0x14003800(上面的小工具)。这是我们通过执行下面列出的操作来实现的。memset

  • 将四字中的第 11 位设置为 0xFFF0A434,以便get_hisee_state返回 true。

  • 调用缓冲区大小为 (因为将从中减去 0x18) 的HISEE_SMC_INIT0x18+1

  • 将控制结构的字段从内核设置为0x140281A0,即我们的函数指针。addr

  • 通过调用 HISEE_SMC_GET_LOG 命令触发函数指针 LSB。memset

现在让我们详细介绍一下如何利用这个小工具来构建更强大的基元。

我们劫持的函数指针用于 (0x14003838),它只是数组中 SMC 处理程序的包装器。从 NS-EL1 传递的参数(除 之外)将移入 - 在调用站点。bl31_secap_handlerbl31_secap_smc_handlerssmc_fidX0X3

0x1400394c:  mov x2, x3
0x14003950: mov x0, x22
0x14003954: mov x1, x21
0x14003958: mov x3, x4
0x1400395c: blr x6 /* address of the blr x2 gadget */

查看调用处理程序(现在是我们的小工具)的程序集,我们可以看到我们控制了以下寄存器:BLR X2

  • X0 == X22

  • X1 == X21

  • X2

  • X3 == X4

X2必须包含调用小工具后我们将跳转到的小工具的地址。现在我们只需要找到一个有趣的小工具,它使用我们控制的任何其他寄存器来增强我们当前的原语。BLR X2

我们将从寻找任意写入小工具开始。

基本任意写入

再一次,在像显示器一样大的代码库中,找到这样的小工具并不难。但是,我们仍然需要考虑到分支指令是 ,这意味着我们丢失了寄存器中的原始返回地址。尽管如此,我们可以重用与第一个漏洞相同的技术:可以使用具有相同尾声(或至少相同堆栈帧大小)的小工具来检索存储在堆栈帧中的返回地址。这样,我们就可以干净地返回到运行时服务调度程序。BLRLRbl31_secap_handler

下面的结语在返回之前向堆栈添加了 0x50 个字节:bl31_secap_handler

0x14003974:  ldp x19, x20, [sp,#0x10]
0x14003978: ldp x21, x22, [sp,#0x20]
0x1400397c: ldp x29, x30, [sp],#0x50
0x14003980: ret

我们正在使用的任意写入小工具,在地址 0x14001850 处找到,执行相同的操作,因此将正确返回:

0x14001850:  str w21, [x0]
0x14001854: ldr x25, [sp,#0x40]
0x14001858: ldp x21, x22, [sp,#0x20]
0x1400185c: ldp x29, x30, [sp],#0x50
0x14001860: ret

在这个阶段,一旦我们将 SMC 处理程序指针替换为小工具的地址,我们现在可以使用下面列出的参数从内核调用相应的 SMC,并开始使用我们的任意写入。BLR X2

  • X0:被劫持的SMC ID,0xCA000008;

  • X1:我们要写入的地址;

  • X2:我们要写的值;

  • X3:要由小工具调用的任意写入小工具的地址。BLR X2

完全任意读写

该漏洞的下一步是通过再次修改 的 SMC 处理程序指针来获得更好的读/写基元。bl31_secap_svc

  • SMC 0xCA000002(地址 0x14028190)的处理程序指针更改为 0x14001A74,这是一个任意读取小工具:

0x14001a74:  ldr w0, [x0,x1]
0x14001a78: ret
  • SMC 0xCA000008(地址 0x140281A0)的处理程序指针更改为 0x1400AC70,这是一个任意写入小工具:

0x1400ac70:  str x1, [x0]
0x1400ac74: ret

在这个阶段,我们有了稳定的读/写原语,现在可以开始执行任意代码了。

双重映射安全监视器

为了在安全监视器中执行代码,我们使用与第一个漏洞相同的策略:第二次将安全监视器映射为可写。映射安全监视器代码和数据部分的第三级 EL3 页表通过查找映射物理地址0x14000000的描述符来定位。通过将描述符复制到页表的未使用空间并更改新描述符的 和 位来实现双重映射。然后,可以使用这种从虚拟地址 0x14058000 开始的读写映射来修补监视器代码。APPXN

在 EL3 中获取代码执行

最后,我们将第一个处理程序指针(用于 SMC 0xCA000001)更改为 0x14003140,在此地址写入 shellcode,并调用此 SMC 来劫持安全监视器并在 EL3 处执行代码。bl31_secap_smc_handlers

漏洞利用摘要


步骤0:HISEE_SMC_INIT中的整数溢出和内核控制的共享结构配对,以产生有限的任意写入。这个有限的原语不足以直接控制安全监视器,但它可以首先用于制作一个原语。hlog_headerbzero


步骤1:将日志从安全监视器复制到内核共享内存缓冲区时,将使用函数 memcpy_s。为了创建一个基元,我们滥用了 memcpy_s 的错误情况,即它调用 reset_memory 来重置目标内存区域。但是,要使reset_memory成功返回,需要设置地址 0xFFF0A434 处的 dword 的第 11 位,这是通过使用我们的有限写入原语调用来确保的。bzero


步骤2:我们的原语现在可用于从内核修改安全监视器地址空间中的可写数据。在我们的漏洞利用中,我们以运行时服务的函数指针为目标。bzerobl31_secap_svc


步骤3:通过设置第三个函数指针0x1400382C的最低有效字节,我们可以将 SMC 处理程序重定向到在地址 0x14003800 处找到的小工具。BLR X2


步骤4:我们将寄存器指向的任意写入小工具的地址放在内存中,并调用被劫持的 SMC。这有效地执行了任意调用小工具,然后调用任意写入小工具。x2


步骤5:现在,我们可以使用这个临时写入小工具来制作一个写入原语,方法是将第一个 SMC 处理程序的函数指针替换为读取小工具的地址。


步骤6:我们对运行时服务的第二个函数指针执行相同的操作,并将其替换为任意写入小工具的地址。


步骤7:然后,我们使用 read 原语来查找 EL3 页表。在其中一个页面中,我们找到了映射虚拟地址的描述符0x14200000,即安全监视器的基址。


步骤8:由于 WXN 机制的原因,我们无法直接使代码段可写。相反,我们通过复制映射代码和数据部分的描述符来创建物理内存的第二个映射。


步骤9:在编写副本时,我们更改描述符的 and 位,以使映射可写。现在,我们可以使用 write 原语修补安全监视器的代码,并在 EL3 上执行代码。APPXN

受影响的设备

我们验证了这些漏洞是否影响了以下设备:

  • 麒麟710:P30 精简版 (MAR)

  • 麒麟970:P20 专业版 (EML)

请注意,其他型号可能已受到影响。

补丁

第一个漏洞(整数溢出)被指定为 CVE-2021-22437,并在 2021 年 9 月的安全更新中进行了修补。第二个漏洞是共享控制结构,被分配为 CVE-2021-39993,并在 2021 年 12 月的安全更新中进行了修补。

时间线

  • 2021年5月31日,华为PSIRT收到漏洞报告。

  • 2021年6月18日 - 华为PSIRT确认该漏洞报告。

  • 2021 年 9 月 1 日 - 第一个问题已在 2021 年 9 月的更新中修复。

  • 2021 年 12 月 1 日 - 第二个问题已在 2021 年 12 月更新中修复。

二进制漏洞(更新中)

其它课程

windows网络安全防火墙与虚拟网卡(更新完成)

windows文件过滤(更新完成)

USB过滤(更新完成)

游戏安全(更新中)

ios逆向

windbg

恶意软件开发(更新中)

还有很多免费教程(限学员)

更多详细内容添加作者微信


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