二进制漏洞分析-1.​华为Security Hypervisor漏洞
2023-11-14 08:15:24 Author: 安全狗的自我修养(查看原文) 阅读量:11 收藏

华为Security Hypervisor漏洞

此通报包含有关以下漏洞的信息:

  • CVE-2021-39979 漏洞 OOB 使用日志记录系统进行访问

漏洞详情

华为的安全管理程序是一个特权组件,通过在EL2上执行来在运行时监督内核。简而言之,它通过利用地址转换的第二阶段以及捕获系统寄存器的修改来确保强制执行物理内存的访问权限。我们在一篇专门的博客文章中详细介绍了它的内部工作原理,如果您想了解有关此组件的更多信息,请您查看。

内核能够通过执行指令来调用安全虚拟机管理程序。根据经验,后者应该不信任来自前者的任何信息。在我们对这个组件的评估中,我们观察到几乎总是如此,除了一个地方:日志记录系统。此系统使用多个共享内存缓冲区,内核和虚拟机管理程序都可以访问这些缓冲区。HVC

在内核方面,“hisi hhee exception”驱动程序的 hhee_logger_init 函数通过调用 、 和 HVC 来检索这些日志缓冲区的物理地址。HHEE_LOGBUF_INFOHHEE_CRASHLOG_INFOHHEE_PMFBUFLOG_INFO

▸ drivers/hisi/hhee/hhee_log.c
int hhee_logger_init(void)
{
// ...
/* get logging information */
/* normal buffer */
ret_res = hhee_fn_hvc((unsigned long)HHEE_LOGBUF_INFO, 0ul, 0ul, 0ul);
ret = cb_init(ret_res.a0, ret_res.a1, &normal_cb, NORMAL_LOG);
// ...

/* crash buffer */
ret_res = hhee_fn_hvc((unsigned long)HHEE_CRASHLOG_INFO, 0ul, 0ul, 0ul);
ret = cb_init(ret_res.a0, ret_res.a1, &crash_cb, CRASH_LOG);
// ...

/* monitor buffer */
ret_res = hhee_fn_hvc((unsigned long)HHEE_PMFBUFLOG_INFO, 0ul, 0ul, 0ul);
ret = cb_init(ret_res.a0, ret_res.a1, &monitor_cb, MONITOR_LOG);
// ...
}

每个日志缓冲区都由 cb_init 函数映射,然后可供内核访问。

▸ drivers/hisi/hhee/hhee_log.c
int cb_init(uint64_t inlog_addr, uint64_t inlog_size,
struct circular_buffer **incb, unsigned int logtype)
{
// ...
log_addr = (uint64_t)(uintptr_t)ioremap_cache(inlog_addr, inlog_size);
// ...
tmp_cb = (struct circular_buffer *) kzalloc(sizeof(struct circular_buffer),
GFP_KERNEL);
// ...
tmp_cb->virt_log_addr = log_addr;
tmp_cb->virt_log_size = inlog_size;
tmp_cb->logtype = logtype;
*incb = tmp_cb;
return 0;
}

在虚拟机管理程序端,日志缓冲区的初始化函数是从init_log_buffers调用的。

void init_log_buffers(saved_regs_t *regs) {
// ...
init_irq_infobuf();
init_logbuf_info(0x134e0000, 0x10000);
init_crashlog_info(0x134f0000, 0x1000);
init_monitorlog_info(0x134f1000, 0xf000);
init_pmfbuflog_info(0x134d4000, 0xc000);
// ...
}

例如,缓冲区(跨越区域)由函数 init_logbuf_info 初始化。LOGBUF_INFO0x134E0000-0x134F0000

void init_logbuf_info(log_buffer_t *buf, int64_t size) {
buf->data_size = size - 0x28;
buf->head_size = 0x28;
buf->total_off = 0x28;
buf->field_18 = 0;
buf->data_ptr = (char *)buf + 0x28;
g_logbuf_info = buf;
}

每个日志缓冲区都以控制结构 开头,定义如下:log_buffer_t

typedef struct log_buffer {
int64_t data_size; /* Size of the data section */
int64_t head_size; /* Size of the header section */
int64_t curr_off; /* Current offset in the buffer */
// ...
void* data_ptr; /* Pointer to the data section */
} log_buffer_t;

此控件结构包含一个指针、一个偏移量和两个大小字段,如下图所示:

虚拟机监控程序可以使用 write_str_to_logbuf_info 函数将字符串写入此日志缓冲区。格式化字符串的参数后,此函数通过调用 write_chr_to_logbuf_info 函数一次写入一个字符的日志缓冲区。

int write_str_to_logbuf_info(const char *fmt, ...) {
char buf[0x88];
// Format the log string arguments.
// ...

hvc_log_lock();
int count = 0;
char *buf_ptr = buf;
// Iterate over the characters of `buf`.
for (char c = *buf_ptr; c; buf_ptr++) {
// Increment the count of written characters.
count++;
// Append the character to the log buffer.
write_chr_to_logbuf_info(c);
}
hvc_log_unlock();
return count;
}

write_chr_to_logbuf_info将字符写入循环缓冲区并更新控制结构字段。

int write_chr_to_logbuf_info(char c) {
uint64_t head_size;
uint64_t curr_offset;
uint64_t data_off;

// ...
// Ensure the log buffer is initialized.
if (!g_logbuf_info)
return -1;

// Get the `head_size` and `curr_offset` fields of the control structure.
head_size = g_logbuf_info->head_size;
curr_offset = g_logbuf_info->curr_offset;

// Check if the circular buffer needs to wrap around.
if (curr_offset >= head_size + g_logbuf_info->data_size) {
// Reset the current offset to the beginning of the buffer.
g_logbuf_info->curr_offset = head_size;
data_off = 0;
} else {
// Compute the offset of the next character.
data_off = curr_offset - head_size;
}

// Write the character at the current offset.
*(char *)(g_logbuf_info->data_ptr + data_off) = c;
// Increment the current offset.
++g_logbuf_info->curr_offset;
return c;
}

由于控制结构位于共享内存中,并且其字段的值未由安全虚拟机监控程序验证,因此内核可以更改它们,以便将日志字符串写入安全虚拟机监控程序地址空间中的选定地址。

日志记录函数与由 和 初始化的其他日志缓冲区类似。init_crashlog_infoinit_pmfbuflog_info

开发

本节介绍我们开发的漏洞,用于从内核获取安全虚拟机管理程序中的任意代码执行。

约束写入原语

当一个字符串被记录时,它的每个字符都写在 address ,其中 ranges:data_ptr - head_size + curr_offcurr_off

  • from offset ,标题的末尾和数据部分的开头;head_size

  • to ,数据部分的末尾,因此是日志缓冲区的末尾。head_size + data_size

日志缓冲区是循环的,因此当到达末尾时,它将重置为 .curr_offhead_size

虽然只需修改字段即可将字符串写入任意地址,但记录的字符串不受内核控制。然而,在我们的漏洞利用中,我们只需要这个能力来写入一个非零字节。其背后的原因将在下一节中解释强制整数下溢。但现在,让我们解释一下我们是如何构造这个原语的。data_ptr

(0x8400FF03) HVC 的处理程序调用一个函数,该函数将字符串记录到缓冲区中。通过将控制结构的字段设置为 1,只需要一个字符即可填充缓冲区并循环回开头。此外,每个字符都会覆盖前一个字符,只在内存中留下字符串的最后一个字节。在我们的例子中,这是换行符 (,即 )。下图显示了日志缓冲区在此配置中的外观。ARM_STD_HVC_VERSION"PMF:cpu %lu\ttid %u\tts %llu\n"LOGBUF_INFOdata_size'\n'0x0A

强制整数下溢

使用受约束的写入原语,我们决定以第 2 阶段页表为目标。它们由虚拟机管理程序使用简单的堆分配器进行分配。堆区域跨越地址 0x12F14C00-0x134D1000,执行这些分配的函数alloc_memory_inner

#define HEAP_START 0x12F14C00
#define HEAP_SIZE 0x5BA400

uint64_t alloc_memory_inner(uint64_t alignment, uint64_t size) {
uint64_t alignment_mask = alignment - 1;
uint64_t heap_current_ptr = HEAP_START + g_heap_offset;
uint64_t padding = -heap_current_ptr & alignment_mask;

// Return if the remaining size is too small to make the allocation.
if (HEAP_SIZE - g_heap_offset < padding + size)
return 0;

// Update the global heap offset.
g_heap_offset += size + padding;

// Log the global heap offset in another log buffer.
if (snprintf(buf, 0x18, 0x17, "heap_offset 0x%lx.\n", g_heap_offset) >= 0)
write_str_to_pmfbuflog_info(buf, 0x18);

return heap_current_ptr + padding;
}

在正常使用期间,检查不容易受到攻击,并正确防止分配器返回堆区域之外的内存。但是,如果偏移量大于堆大小(例如,通过使用我们的写入原语),则检查将由于 上的整数溢出而通过,并且分配器将返回堆区域之外的内存。HEAP_SIZE - g_heap_offset < padding + sizeHEAP_SIZE - g_heap_offset

由于分配的大小为 0x1000 字节,因此在堆区域之外进行的第一个分配将位于 0x134D0000(并且由于填充而不0x134CF000)。幸运的是,这个区域位于日志缓冲区中,该缓冲区由虚拟机管理程序和内核映射。

要触发第 2 阶段页表的分配,我们需要使用 HVC 访问该函数。此函数将中间物理地址范围作为参数,它将更改其权限。如果此范围在第二阶段未映射,或者当前映射为块,则更改其权限将需要分配新的页表。change_stage2_software_attrs_per_va_range

步骤0:初始状态(假设未进行分配)。


步骤1:我们通过映射和更改位于0x10000000000之后的物理内存区域的权限来填充堆,直到等于。我们可以使用 alloc_memory_inner 编写的日志消息来监控堆偏移量。g_heap_offsetHEAP_SIZE


步骤2:我们通过将其值从 0x5BA400 更改为 0x5BA40A 来绕过使用我们的 write 原语。结果,现在等于 0xfffffffffffffff6,大于 。g_heap_offsetHEAP_SIZEHEAP_SIZE - g_heap_offsetpadding + size


步骤3:我们通过映射和更改内存块的权限,从 0x10000000 开始,触发将在堆区域之外并由内核访问的 0x134D0000 处进行的分配。

对虚拟机管理程序进行双重映射

在执行了 3 个步骤后,我们最终会得到一个可从内核写入的第 2 阶段页表。通过更改此页面中包含的描述符,我们可以再次映射虚拟机管理程序的物理地址范围,并使此映射可写。剩下的就是修补虚拟机管理程序的代码,并调用修改后的函数,以便在 EL2 处执行任意代码。

步骤0:内核可访问的阶段 2 页表的初始内容。


步骤1:我们修改描述符的输出地址,以将 IPA 范围0x10000000映射到 0x10200000 到 PA 范围0x12F00000 0x13100000。我们还更改了属性以使映射读写。


步骤2:使用我们的可写映射,我们使用返回当前异常级别的简单 shellcode 修补 (0xC6001088) HVC 处理程序。HHEE_HVC_LIVEPATCH

受影响的设备

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

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

  • 麒麟810:P40 精简版 (JNY)

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

补丁

此漏洞的编号为 CVE-2021-39979,并在 2021 年 10 月的安全更新中进行了修补。

时间线

  • 2021年7月09日 - 向华为PSIRT发送漏洞报告。

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

  • 2021 年 10 月 1 日 - 此问题已在 2021 年 10 月更新中修复。

二进制漏洞(更新中)

其它课程

windows网络安全一防火墙

windows文件过滤(更新完成)

USB过滤(更新完成)

游戏安全(更新中)

ios逆向

windbg

恶意软件开发

()

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


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