202509
security

在 GrapheneOS 中实现 hardened_malloc 的每堆隔离、防护区和完整性检查

探讨 GrapheneOS 中 hardened_malloc 的实现,包括每堆隔离、防护区和完整性检查机制,以缓解资源受限移动设备上的堆利用攻击。提供工程化参数和监控要点。

在资源受限的移动设备如 Android 手机上,堆内存漏洞利用是常见的安全威胁。GrapheneOS 通过集成 hardened_malloc 分配器,引入每堆隔离(per-heap isolation)、防护区(guard regions)和完整性检查(integrity checks)等机制,有效缓解这些攻击。这种设计不仅提升了系统的鲁棒性,还考虑了移动平台的性能约束,确保安全增强不以牺牲可用性为代价。

每堆隔离的实现原理

每堆隔离的核心是通过将不同大小的内存分配分离到独立的堆区域中,实现细粒度的内存管理。hardened_malloc 将小分配(small allocations)组织成多个大小类(size classes),每个类对应一个预映射的巨大虚拟内存区域,例如总计约 3TB 的 slab 区域,但实际仅在需要时激活部分页。这些区域在初始化时以 PROT_NONE 保护,仅于分配时切换为 RW,从而避免不必要的物理内存占用。在单 arena 配置下,所有线程共享这些区域,但通过锁机制确保并发安全。

证据显示,这种隔离防止了相邻分配间的直接溢出传播。例如,小分配的 slab 之间插入随机大小的 guard slabs,这些防护区充当缓冲,阻止堆溢出(heap overflow)从一个 slab 扩散到下一个。对于大型分配(large allocations),每个分配动态通过 mmap 创建独立区域,前后附加随机数量的 guard pages(通常数页到数十页),进一步隔离潜在漏洞影响。

在资源受限设备上,这种设计利用扩展地址空间(extended address space)达到 48 位,支持 ASLR 熵增加到 33 位,确保区域起始地址高度随机化。GrapheneOS 的 secure app spawning 使用 exec 而非 fork,进一步随机化每个进程的地址布局,避免 zygote 进程的地址预测性。

防护区的工程化参数

防护区的有效性依赖于其大小和随机性。在 hardened_malloc 中,小分配的 slab 防护区大小基于大小类动态计算,通常为 1-4 页不等,并随机放置于 64 GiB 的超大区域内(实际使用 32 GiB)。大型分配的 guard pages 数量从 1 页到最大 0x18000 字节(约 96 页)随机生成,建议在配置时设置最小 guard_size 为 4 页,以平衡安全与内存开销。

可落地参数包括:

  • CONFIG_CLASS_REGION_SIZE:设置为 32 GiB(默认),确保每个大小类有足够空间容纳数百万 slab。
  • guard_size 范围:在 allocate_pages_aligned 函数中,随机化 1-64 页,针对移动设备上限设为 32 页,避免过度碎片化。
  • MTE 集成:对于支持 Armv8.5+ 的设备(如 Pixel 8+),启用 -fsanitize=memtag 编译旗标,仅对小分配(<128KB)应用 4-bit tags,忽略大型分配以节省性能。

监控要点:通过 proc/maps 观察 guard 区域的 PROT_NONE 比例,若低于 80%,则可能面临内存压力;使用 Frida 钩子跟踪 mmap 调用,警报异常 guard 大小变异。

完整性检查的机制与验证

完整性检查通过运行时验证确保内存访问的合法性。对于小分配,hardened_malloc 嵌入 MTE tags 或 canaries(无 MTE 时)。分配时,为每个 slot 分配唯一 tag(排除相邻和历史 tag),存储在 arm_mte_tags 数组中;释放时,将 tag 置为 0,并清零整个 slot。free 操作前,进行多层验证:指针对齐检查、bitmap 状态确认、canary 值比对(8 字节,slab 级共享)。

如 Synacktiv 的分析所述,“hardened_malloc 使用 MTE 来标记 small allocations,检测 out-of-bounds 或 use-after-free 访问”。

对于大型分配,无 MTE 支持,但依赖 hash 表(regions_a/b)存储 metadata(地址、大小、guard_size),并在 free 时验证 hash 匹配。双 quarantines 机制进一步增强:random quarantine(固定大小数组,随机插入)和 FIFO queue(长度依大小类,如 8 字节分配为 8192 槽),延迟重用 1-2 万次分配,挫败 UAF 利用。

证据在于崩溃日志:无效 tag 触发 SIGSEGV/SEGV_MTESERR,canary 损坏引发 SIGABRT,日志中包含 "hardened_malloc: fatal allocator error",便于调试。

可落地清单:集成与优化

要将这些机制集成到自定义 Android 环境中,遵循以下清单:

  1. 源代码集成

    • 从 GrapheneOS GitHub 克隆 hardened_malloc(commit 7481c885)。
    • 修改 libc 的 malloc/free 钩子,替换 scudo 默认分配器。
    • 编译旗标:添加 -march=armv8-a+memtag(MTE 设备),-fno-omit-frame-pointer 以便栈追踪。
  2. 配置参数调整

    • N_ARENA=1(移动单线程友好)。
    • SLAB_QUARANTINE_RANDOM_LENGTH=8192(小分配),LARGE_QUARANTINE_SIZE=0x2000000(>32MB 直接 unmap)。
    • 启用 CONFIG_EXTENDED_SIZE_CLASSES 以覆盖 16B 到 128KB 范围。
  3. 性能与安全权衡

    • 基准测试:malloc(8) 循环,监控延迟增加 <5%(quarantines 引入)。
    • 回滚策略:若崩溃率 >1%,禁用 quarantines,转为基本 guard;无 MTE 设备优先 canaries。
    • 资源监控:虚拟内存峰值 <4TB,物理使用通过 madvise(MADV_DONTNEED) 优化 slabs。
  4. 测试与验证

    • 使用 syzkaller 模拟 heap exploits,验证 MTE 阻挡率 >95%。
    • Frida 脚本钩 sub_slab_alloc,检查 tag 唯一性。
    • 压力测试:高负载下(如多 app 并发),确保无 OOM 因 guard 碎片。

这些机制在 GrapheneOS 中证明了其价值:在 Pixel 设备上,heap exploits 利用难度提升数倍。开发者可根据具体硬件调整参数,如在低端设备减小 quarantine 大小至 4096,以维持流畅性。总体而言,hardened_malloc 提供了一个平衡安全与效率的框架,适用于任何注重隐私的移动 OS。

(字数:约 1050 字)