深入解析Linux全新mseal系统调用及其安全防护机制

本文详细分析了Linux 6.10内核引入的mseal系统调用,探讨其内存密封技术原理、内核实现机制及对权限篡改和内存解除映射攻击的防护效果,包含具体代码示例和漏洞利用场景。

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

mseal是什么(与不是什幺)

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

该syscall由Chrome安全团队为支持V8 CFI策略而引入,经过多次重写后最终登陆内核,未来还将通过glibc 2.41+扩展到浏览器之外的应用场景。

与提供文件密封的memfd_create不同,mseal是专门针对远程攻击者代码执行攻击的利用缓解措施,而非防护本地敏感数据泄露。

内部机制剖析

mseal具有简单的函数签名:

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

其中start和len表示要密封的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的进程)。check_mm_seal调用[3]通过遍历每个VMA来验证目标内存映射范围的正确性。

关键操作发生在apply_mm_seal调用[4],它再次遍历每个VMA并通过mseal_fixup调用为目标区域设置VM_SEALED标志。

为确保不需要的内存操作尊重这个新标志,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 +++

例如,mprotect和pkey_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
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;
}

从其他内存管理syscall的变化可以看出,VMA密封后不允许的操作包括:

  • 使用mprotect和pkey_mprotect更改权限位
  • 使用munmap解除映射
  • 使用mmap(MAP_FIXED)替换密封映射
  • 使用mremap扩展或缩小大小
  • 使用mremap(MREMAP_MAYMOVE | MREMAP_FIXED)迁移到新目标
  • 使用具有破坏性标志的madvise调用

目前可通过直接syscall调用在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. 通过内存区域的任意解除映射/重新映射进行"打洞":缓解利用用攻击者控制数据重新填充内存区域的数据only攻击

强化NX保护

即使存在ROP等代码重用技术,攻击者可能仍希望在利用过程中获得shellcode能力。典型工作流程:

  1. 通过某些目标功能将shellcode喷洒到不可执行的栈/堆区域
  2. 利用目标漏洞启动初始ROP链,调用mprotect和PROT_EXEC来定位持有shellcode的区域并关闭NX位
  3. 跳转到该区域执行传统shellcode

CVE-2018-7445针对Mikrotik RouterOS SMB守护程序的攻击就是典型例子。基于socket的shellcode被喷洒到不可执行堆上,来自栈溢出的精心构造的ROP链修改堆内存权限后执行shellcode。

内存密封的最直接用例是禁止VMA权限修改;一旦实现,想要利用传统shellcode的攻击将无法切换可执行位。

mseal将在glibc 2.41+中引入,动态加载器将在预定的VMA集上应用密封。但目前不会自动为栈或堆完成此操作,因为这些区域可能在运行时扩展。

因此,软件开发者有责任保护这些区域,因为他们有上下文来确定何时何地适当应用密封。

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

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

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

    /* 密封包含shellcode的栈页! */
    if (mseal(exec_offset, getpagesize()) < 0)
        handle_error("mseal");

    // 漏洞触发,指令指针被劫持

    mprotect(exec_offset, getpagesize(), PROT_READ|PROT_WRITE|PROT_EXEC);
    /* 现在出现段错误,因为权限更改未实际发生 */
    exec_ptr();
    return 0;
}

当调用mprotect时,上述can_modify_vma检查会生效,防止权限更改发生,shellcode尝试现在失败。

适应实际软件的简单策略可能涉及谨慎引入宏化版本的mseal代码片段,并在可能存在不可信数据的选定栈帧中迭代密封页面:

1
2
3
4
5
6
7
#define SIMPLE_HARDEN_NX_SINGLE_PAGE(frame) \
  do { \
    void *frame_offset = (void *)((int64_t) &frame & ~(getpagesize() - 1)); \
    if (mseal(frame_offset, getpagesize()) == -1) { \
      handle_error("mseal"); \
    } \
  } while(0)

即使密封的VMA被重用作另一个具有密封逻辑函数的帧,再次调用mseal将被视为无操作,因此不会出现错误。开发者应注意边缘情况,如激进使用导致的自动栈扩展或自定义功能如栈拆分。

缓解基于解除映射的数据only攻击

禁止mprotect还防止密封区域变为可写,这对于存在修改后可能增强利用原语的数据变量很有价值。但在mseal的初始阶段,Chrome维护者合理化了一种更简单、更强大的技术,具有绕过CFI(控制流完整性)的额外好处。

他们确定,如果攻击者可以将损坏的指针传递给解除映射/重新映射syscall,他们可以在内存中"打洞",并可以用攻击者控制的数据重新填充。这不会违反CFI保证,因为前向和后向边缘CFI仅覆盖被篡改的控制流转换(例如栈返回地址和函数指针)。

这对于实现JIT编译器的浏览器特别有吸引力。V8的Turbofan可以创建在RW和RX之间切换的区域,帮助重新填充过程和更改权限。因此,攻击者可以利用JIT编译过程,将可执行代码从热路径JavaScript发射到未映射区域,覆盖关键数据,然后利用修改产生代码执行。

我们认为这是一种数据only利用技术,因为它不涉及直接劫持控制流或需要泄漏指针,而是篡改内存中的特定数据,以影响控制流向攻击者期望的方向。在CFI等缓解措施的时代,这已成为利用过程中非常有效的技术。因此,内存密封可以通过禁止打洞场景来防止这些特定的数据only技术。

这种数据only技术不仅适用于具有JIT编译器的浏览器!类似技术将是用户空间堆利用的House of Muney。正如Max Dulin在其文章中指出的,Qualys使用这种技术对Qmail中的古老错误执行了真实攻击。

该技术依赖于这样一个事实:对于大分配块(大于M_MAP_THRESHOLD可调参数),malloc和free将分别直接调用mmap和munmap,没有缓存任何释放块的中间空闲列表(这大大简化了利用)。由于大小元数据存在于分配块的顶部,将其篡改为不同的页面大小并释放它将导致对块相邻内存区域的munmap。Dulin使用任意munmap来定位.gnu.hash和.dynsym区域,并用另一个更大的mmap块重新填充后,启用覆盖单个尚未解析的PLT条目,复活了GOT覆盖式攻击!

幸运的是,mseal的glibc集成中预期的VMA集将自动缓解此问题,无需开发者干预,因为映射的二进制代码和动态库将免受任何此类重新映射/解除映射技巧的影响。为了额外强化,开发者可以选择性密封他们知道在程序生命周期内永远不会扩展或解除映射的mmap分配。如果预期攻击者控制的数据可能被写入mmap块并可能变为可写/可执行,这将具有防止先前利用场景的额外好处。

使用mseal构建更强大的软件

可能还有许多其他用例和场景我们没有涵盖。毕竟,mseal是Linux内核中最新的成员!随着glibc集成的完成和成熟,我们期望看到syscall的改进迭代以满足特定需求,包括充实flags参数的最终用途。

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

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