深入解析Linux全新mseal系统调用:内存密封技术详解

本文深入探讨Linux 6.10内核引入的mseal系统调用,详细分析其内存密封机制的技术实现原理、内核代码架构,以及如何有效防御权限篡改和内存解除映射攻击等 exploit 技术。

深入解析Linux全新mseal系统调用

mseal是什么(以及不是什么)

内存密封允许开发者在程序运行时使内存区域免受非法修改。当虚拟内存地址(VMA)范围被密封后,拥有代码执行能力的攻击者将无法执行后续虚拟内存操作来更改VMA权限或修改其布局以谋取利益。

与Linux的memfd_create及其memfd_secret变体不同,mseal的安全保证并非提供文件密封功能。mseal是一个专门为利用缓解而设计的系统调用,针对的是寻求代码执行的远程攻击者,而非可能试图窃取内存中敏感秘密的本地攻击者。

内部机制探秘

mseal具有简单的函数签名:

1
int mseal(unsigned long start, size_t len, unsigned long flags)

startlen表示要密封的有效VMA的起始/结束范围,len必须正确页面对齐。flags在当前版本中未使用,必须设置为0。

在6.12内核中,其系统调用定义调用do_mseal

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
static int do_mseal(unsigned long start, size_t len_in, unsigned long flags)
{
    size_t len;
    int ret = 0;
    unsigned long end;
    struct mm_struct *mm = current->mm;     // [1]

    // ... 检查flags == 0,检查页面对齐,计算`end`

    if (mmap_write_lock_killable(mm))          // [2]
        return -EINTR;

    ret = check_mm_seal(start, end);            // [3]
    if (ret)
        goto out;

    ret = apply_mm_seal(start, end);            // [4] 

out:
    mmap_write_unlock(current->mm);
    return ret;
}

do_mseal首先从提供的长度计算结束偏移量,并锁定内存区域[2]以防止对页面的并发访问。[1]处的全局current表示当前正在执行的task_struct(即调用mseal的进程)。引用的字段是表示任务整个虚拟内存地址空间的mm_struct。该系统调用将在其上操作的关键字段是mmap,这是一个vm_area_struct值的列表,表示由mmap创建的单个连续内存区域,如堆栈或VDSO。

[3]处的check_mm_seal调用通过迭代每个VMA来确保目标内存映射是有效范围:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
static int check_mm_seal(unsigned long start, unsigned long end)
{
    struct vm_area_struct *vma;
    unsigned long nstart = start;

    VMA_ITERATOR(vmi, current->mm, start);

    for_each_vma_range(vmi, vma, end) {
        if (vma->vm_start > nstart)
            return -ENOMEM;
        if (vma->vm_end >= end)
            return 0;

        nstart = vma->vm_end;
    }
    return -ENOMEM;
}

魔法发生在apply_mm_seal调用[4]中,它再次遍历每个VMA,并通过mseal_fixup调用为目标区域安排额外的VM_SEALED标志:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
static int apply_mm_seal(unsigned long start, unsigned long end)
{
    nstart = start;
    for_each_vma_range(vmi, vma, end) {
        int error;
        unsigned long tmp;
        vm_flags_t newflags;

        newflags = vma->vm_flags | VM_SEALED;
        tmp = vma->vm_end;
        if (tmp > end)
            tmp = end;
        error = mseal_fixup(vmi, vma, &prev, nstart, tmp, newflags);
        if (error)
            return error;
        nstart = vma_iter_end(&vmi);
    }
    return 0;
}

为确保不需要的内存操作尊重这个新标志,mseal补丁集向以下文件添加了VM_SEALED检查:

1
2
3
4
5
mm/madvise.c    |   12 +
mm/mmap.c       |   31 +-
mm/mprotect.c   |   10 +
mm/mremap.c     |   31 +
mm/mseal.c      |  307 ++++

例如,mprotectpkey_mprotect在最终调用mprotect_fixup时将强制执行此检查:

1
2
3
4
5
6
int mprotect_fixup(..., struct vm_area_struct *vma, ...)
{
    if (!can_modify_vma(vma))
        return -EPERM;
    }
}

为了确定系统调用是否应该继续,can_modify_vma(在mm/vma.h中定义)将测试指定vm_area_struct中是否存在VM_SEALED

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
static inline bool vma_is_sealed(struct vm_area_struct *vma)
{
    return (vma->vm_flags & VM_SEALED);
}

static inline bool can_modify_vma(struct vm_area_struct *vma)
{
    if (unlikely(vma_is_sealed(vma)))
        return false;

    return true;
}

从其他内存管理系统调用的更改中,我们可以确定VMA密封后不允许的操作:

  • 使用mprotectpkey_mprotect更改权限位
  • 使用munmap取消映射
  • 使用mmap(MAP_FIXED)将密封映射替换为可变/未密封的映射
  • 使用mremap扩展或缩小其大小
  • 使用mremap(MREMAP_MAYMOVE | MREMAP_FIXED)迁移到新目标
  • 使用具有以下破坏性标志的madvise调用

目前,可以通过直接系统调用在6.10+内核上调用mseal。以下是一个基本的包装器实现:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
#include <sys/syscall.h>
#include <unistd.h>

#define MSEAL_SYSCALL 462

long mseal(unsigned long start, size_t len)
{
    int page_size;
    uintptr_t page_aligned_start;

    page_size = getpagesize();
    page_aligned_start = start & ~(page_size - 1);
    return syscall(MSEAL_SYSCALL, page_aligned_start, len, 0);
}

mseal有助于缓解哪些利用技术?

从不允许的操作中,我们可以辨别出内存密封将防止的两种特定利用场景:

  1. 篡改VMA权限:不允许设置可执行权限可以阻止基于shellcode的攻击复活
  2. 通过任意取消映射/重新映射内存区域进行"打孔":缓解利用重新填充攻击者控制数据的仅数据利用

强化NX保护

即使存在像ROP这样的代码重用技术,攻击者可能仍希望在利用过程中获得shellcode能力。内存密封的最直接用例是禁止VMA权限修改;一旦发生这种情况,想要利用传统shellcode的利用将无法关闭可执行位。

以下示例展示了如何使用mseal增强堆栈的NX保护:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
int main(void)
{
    unsigned char exec_shellcode[] = "...";

    void (*exec_ptr)() = (void(*)())&exec_shellcode;
    void *exec_offset = (void *)((int64_t) exec_ptr & ~(getpagesize() - 1));

    if (mseal(exec_offset, getpagesize()) < 0)
        handle_error("mseal");

    mprotect(exec_offset, getpagesize(), PROT_READ|PROT_WRITE|PROT_EXEC);
    exec_ptr(); // 现在会段错误
    return 0;
}

缓解基于取消映射的仅数据利用

禁止mprotect还可以防止密封区域变为可写,这在存在可能增强利用原语的数据变量时很有价值。Chrome维护者确定,如果攻击者可以将损坏的指针传递给取消映射/重新映射系统调用,他们可以在内存中"打孔",并重新填充攻击者控制的数据。

这种技术不仅适用于具有JIT编译器的浏览器!类似的技术是用户空间堆利用的House of Muney。这种技术依赖于这样一个事实:对于大分配块(大于M_MAP_THRESHOLD可调参数),malloc和free将分别直接调用mmap和munmap,没有中间空闲列表缓存任何释放的块。

使用mseal构建更强大的软件

mseal是Linux内核中最新的安全缓解措施!随着glibc集成的完成和成熟,我们期望看到改进的迭代来满足特定需求,包括充实flags参数的最终用途。

强化软件是复杂的,因为导航和评估新的安全缓解措施在理解风险和回报权衡方面可能具有挑战性。

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计