随着控制流完整性(Control-Flow Integrity, CFI)技术的广泛部署,传统劫持程序控制流的内存破坏攻击得到有效遏制
[1-3]。然而这种防御机制的普及使得数据导向攻击(Data-Oriented Attacks)快速发展
[4-5]。此类攻击仅操作非控制流数据而不篡改代码指针,遵循合法的控制流图路径,通过破坏栈变量等数据结构实现信息泄露
[6]、权限提升
[7-8]乃至任意代码执行
[9]。以WU-FTPD服务器的格式字符串漏洞为例,攻击者可在不破坏控制流的前提下,篡改setuid系统调用使用的pw
pw_uid参数值以获得更高权限,如下列代码。由于不扰乱程序正常执行流程,数据导向攻击具有更强的隐蔽性,使得传统防御措施难以检测与预防。
1. struct passwd { uid_t pw_uid; ...} *pw; ...
2. int uid = getuid();
3. pw->pw_uid = uid;
4. ... //format string error
5. void passive (void) {
6. ...
7. setuid(0);
8. setuid(pw->pw_uid);
9. ...
10. }
针对上述威胁,研究人员提出数据流完整性、SoftBound
[10]和HardScope
[11]等以非控制数据保护为核心的防御方案。这些方法主要依赖影子栈等软件机制实现
[12],普遍存在性能开销偏高、适用场景受限等问题,软件实现自身也可能引入新的漏洞
[13]。同时,实现复杂度与运行时开销的显著增加也限制了这些方案在实际部署与规模化应用的效果。
硬件辅助内存标记技术为缓解上述问题提供了新思路。ARM内存标记扩展(Memory Tagging Extension, MTE)利用硬件功能为每个内存块及其对应指针分配唯一标签,建立双层引用机制
[14-15]。内存访问时,MTE验证指针标签与内存位置标签的匹配性,不匹配则触发安全违规响应。现有研究已将MTE用于增强内存安全防御,但主要集中于堆内存保护,且依赖手动源代码分析进行标记插入。此外,MTE技术集成至现有系统需进行大量代码修改,工程实现难度较大;技术尚属新兴,缺乏充分的测试验证与案例研究。
近些年在利用ARM MTE增强内存安全方面的研究主要有以下3类代表性的工作。1)通过静态分析将内存分配划分为安全类与危险类,并实施编译时混淆保护,从而实现内存防护
[16];2)通过MTE在实际系统中的通用缺陷,针对4位标签导致的碰撞概率高、防护弱问题,提出基于BTree压缩的内存高效标签存储方案;3)基于对象级与页面级多粒度标签融合架构实现,提升检测概率,增强防护能力
[17]。
这些工作也存在共同的局限性。1)防护重心集中于堆内存,对栈内存的自动化分析缺失;2)所有方法均需要人工介入源代码标记过程;3)缺乏兼顾安全性与工程可行性的自动化方案。
本文提出一种面向栈内存保护的自动化MTE防御方法。该方法通过底层虚拟机(Low Level Virtual Machine, LLVM)编译器传递系统性扫描程序,自动识别栈内存分配点并完成标记插入,降低人为错误风险。针对标签执行结束后的清除问题,设计运行时监控组件持续检查标签协议合规性,在检测到违规时进行告警;同时实现标签清理机制,在函数或进程不再需要特定内存段时回收标记,防范内存泄漏等安全漏洞。实验基于基准测试集和50个含已知漏洞的程序展开,运行时开销为52.8%,文件大小增加17.7%,并在含有CVE漏洞的Linux程序上验证有效性。该方法将保护重心从手动、以堆为中心的模式转向自动化栈内存防护,以更少人工干预实现内存安全的系统性增强。
1 线程模型与系统概述
1.1 线程模型
在该威胁模型中,假设存在一个攻击性极强的对手,该对手完全控制目标程序的数据内存。该模型涵盖涉及内存损坏漏洞的各种场景,例如缓冲区溢出。假设已经实施诸如CFI
[1-3]、数据执行保护(Data Execution Prevention, DEP)
[18]和地址空间布局随机化(Address Space Layout Randomization, ASLR)
[19-20]等众所周知的保护措施,以防御控制数据攻击。
1.2 系统概述
本文提出的自动化保护系统旨在保护程序中的所有栈内存空间,利用ARM MTE机制来增强内存安全性。系统的基本工作流程如
图1所示。
该系统始于对每个程序函数的分析,重点是识别与栈内存分配相关的指令;为每个分配的内存块生成随机标签,并将其插入到这些指令中;在系统运行时,不断监视内存访问,并将其与分配的标签进行验证,确保内存安全;当内存块不再使用或函数完成时,从内存块中清除标签,保持系统的完整性和效率。
2 系统设计
通过利用ARM MTE,自动化保护系统在各种编程的上下文中增强了栈内存的安全性。自动化保护系统的具体实现过程,包括系统设计的核心组件和方法论,如内存分配识别、标签生成和插入、运行时监控和执行,以及关键的标签清除过程通过一个代码示例展示,如
图2所示。
2.1 MTE启动和内存分配提取
系统从一个程序分析阶段开始,自动分析程序中的每个功能。该步骤在研究函数代码的同时,也关注在堆栈上分配内存的指令。分析确定并提取出负责在每个函数中分配堆栈内存的特定指令。通过这一方法,系统能够确定代码中分配堆栈内存的确切段。这些点通常对程序的正确操作至关重要,并且容易受到内存漏洞的影响。这种集成的方法确保堆栈内存使用中关键区域的精确和有针对性的识别,为内存保护的后续阶段奠定基础。
在识别函数中的所有堆栈内存分配点后,系统自动插入按分配大小配置的mprotect指令,该指令通过调用PROT_MTE参数激活指定内存区域的MTE机制,确保各栈内存在受到保护的同时,具备标记能力。
2.2 标签生成与插入
内存分配点确定后,系统开始根据MTE协议为每个内存块生成随机标签。该协议为每个内存段分配一个唯一标识符,确保标签随机和不可预测,且符合MTE协议规范。这个过程需要重点考虑内存对齐,这是MTE机制的操作细节而导致的一个关键问题。
MTE机制的工作原理是通过向每16 B的物理内存添加4 bit元数据来标记内存位置,称为标记颗粒。在根据MTE协议标记堆栈分配的过程中,如何在遵守MTE内存对齐要求的同时,使系统适应并处理不同大小的堆栈分配问题至关重要。对于小于16 B的堆栈分配,系统将其填充至16 B块,以确保符合MTE的16 B结构,并为整个颗粒插入1个标签。
对于大于16 B的分配,系统将其对齐至16 B的整数倍,分配足量颗粒并赋予相同标签标识。此统一标记流程确保了跨不同分配规模的防护一致性与机制有效性。
2.3 运行时监控和实施
在系统中,对程序执行过程期间的内存访问监控是保障内存安全的基础。使用根据MTE协议生成和插入的标签,系统不断验证对特定内存块每次访问的合法性。检查访问是否与相应的标签一致,并遵守内存安全原则。如果系统检测到任何未经授权或不正确的内存访问,例如对带有不匹配标记的内存块的访问,将通过引发异常或触发警报来作出适当的响应。
为实现这种程度的监视和保护,系统对栈内存标签进行特殊处理。MTE架构假定栈指针的可靠性,任何不由栈指针管理的栈内存访问都需要系统从内存地址读取相应的标签。这种做法可以有效防止标签不匹配和潜在的系统错误。
两种关键场景需要对标签的特殊处理:第1种涉及函数调用参数的管理。根据ARM架构的调用约定,该方法提取所有系统调用参数,并分析它们是否指向栈内存。如果参数指向栈内存,系统将从该内存中读取标签。由于该设计依赖于静态分析,确定一个参数是否指向栈内存具有挑战性。为解决这一问题,在每次参数调用之前执行一段汇编代码。这段代码确定参数的值是否落在有效的栈内存范围内,并在必要时检索其标签。第2种涉及栈指针的栈内存访问。系统会分析指令是否是栈内存的读取或写入操作。如果涉及栈内存且操作数不是栈指针,系统会读取该内存段的标签。
通过这些专门的进程,系统可以更好地确保所有堆栈内存访问都是合法和安全的,从而保持程序的完整性和可靠性。
2.4 标签清除
标签清除子系统在全部控制流场景下均满足MTE的机密性与完整性要求。该子系统对函数返回语义、非局部跳转、中断/异常入口与返回等事件进行统一建模,并在栈内存生命周期结束时执行同步的标签清除tag-eviction操作,确保任意栈区间在重新分配前其logical tag已被回退为0x0(ARM规范定义的“默认标签”),从而消除元数据残留导致的重用侧信道风险。
2.4.1 常规函数返回路径
编译器在函数 prologue 中记录初始 SP 快照(SP0)。epilogue执行前,通过(SP1-SP0)得出当前帧的栈字节数N,随后以MTE 粒度(16 B)为步长,依次对区间[SP0, SP0+N)内的每一granule发出STG指令,将其logical tag重置为0x0。该过程与返回地址的atomical更新在同一异常级别完成,保证tag清除与栈回收的瞬时一致性。
2.4.2 非局部退出
非局部退出包括longjmp、throw、setjmp 伪造跳转等。编译器在中间表示(IR)层为每个可能触发非局部退出的调用点插入jmp_buf元数据,并在后端生成栈展开描述符(.eh_frame)。运行时若检测到控制流经Unwind_ForcedUnwind或 longjmp离开当前栈帧,unwinder在第2遍扫描中回溯所有已访问的栈区间,并立即调用 STG 序列执行tag擦除;该操作发生在任何潜在栈内容复用之前,防止“tag残留→类型混淆”攻击面。
2.4.3 中断/异常与信号处理程序
内核在entry.S中将SP_EL0切换至独立中断栈之前,通过IRG为新栈分配随机logical tag;在退出路径(eret)中,利用MSR SPSel恢复线程栈后,以异常发生时的 SP_EL0快照为基址,对中断处理期间可能污染的区间[SP0, SP0+max_irq_stack)执行批量STG清零。用户态信号机制沿用同一范式,rt_sigreturn系统调用在恢复ucontext_t之前,由libc的signal trampoline完成tag擦除。
整个过程系统严格遵守MTE的操作参数和约束。标记清理必须符合MTE的体系结构细节,确保安全维护和符合系统体系结构。这种管理标签的方法不仅维护了内存的完整性,并为将来的安全利用做好了准备,符合当代计算环境中内存安全的严格标准。
3 系统实现
3.1 环境设置
在系统的实现阶段,选择QEMU来模拟Aarch64架构上的ARM MTE。为建立相应的虚拟环境,通过使用特定命令行选项,对QEMU进行配置,启用MTE功能。这些选项包括-machine virt,mte=on和-cpu max。这种设置能够彻底测试和评估MTE在各种场景下的性能和有效性。该系统运行在Linux Ubuntu 22版本6.0.1,使用Clang版本17.0.0。
3.2 实现方法
该系统使用LLVM的Clang编译器将C源代码转换为LLVM IR。通过自定义LLVM Pass,对IR文件进行分析并插入MTE标签操作。生成一个集成MTE功能的可执行二进制文件。处理流程包括从编译到LLVM IR,应用LLVM Pass,生成启用MTE汇编代码,并最终编译成可执行文件的全过程。确保MTE的集成,提高内存安全性。有关实现步骤,参考算法如下,演示参见
图2。
算法1 LLVM Pass Workflow for MTE Instrumentation
/*Initiate MTE*/
1. for Each I in entryBlock do
2. If I is AllocaInst then
3. lastAlloca = &I;
4. If lastAlloca is not null then
/*Insert a new basic block*/
5 BasicBlock (MTE_init_BB);
/*Set the new insertion point*/
6 SetInsertPoint(MTE_init_BB) → getFirstNonPHI());
/*Insert the inline assembly code*/
7 getFirstNonPHI ← initTagAsm;
/*Memory Allocation Extraction*/
8. For Each BB in F do
9. For Each I in BB do
10. If I is AllocaInst then
/*Get the allocated memory size*/
11 allocaSize();
/*Resize and align the memory*/
12 newSize = (allocaSize + 15)& 15;
/*Tag Generation and Insertion*/
13 SetInsertPoint(Alloca →
getNextNode(());
14. insertTagAsm();
15. getNextNode ← buildtagAsm;
/*Runtime Monitoring and Enforcement*/
16. For Each BB in F do
17. ForEach I in BB do
18. If I is CallInst then
/*Load tags for arguments*/
19 For Each &arg in CallInst do
/*Insert the inline assembly code*/
20. SetInsertPoint(CI);
21. checkTagAsm();
22. CI ← checkTagAsm;
/*Tag Cleanup*/
23. For Each BB in F do
24. For Each I in BB do
25. If I is Returnor LongJump or Interrupt then
26. SetInsertPoint(i);
/*Insert the inline assembly code*/
27. cleanTagAsm();
28. i ← cleanTagAsm;
3.2 1 MTE初始化
在LLVM Pass中,通过将特定的汇编指令插入主函数指定位置,为ARM64架构初始化MTE。这种集成是激活和配置程序启动时MTE的关键。
在该过程当中,使用的汇编指令包括prctl和mprotect。prctl指令是启用带标签地址ABI的重要指令,允许同步MTE标签检查故障,并在随机生成的集合中使用所有非零标签。mprotect指令用于在之前创建的匿名内存映射上启用MTE。
这些指令在IRBuilder框架中通过内联汇编的方式实现。通过将指定内容嵌入主函数序言位置,确保MTE能够高效地集成到程序中,为整个程序执行期间的内存安全打下坚实基础。
3.2 2 内存分配提取
在LLVM Pass的设计当中,需要对每个函数进行详细分析,以确定堆栈内存分配指令。利用LLVM工具集中的编译器遍历函数中的每条指令。在迭代的过程中,系统会专门查找AllocaInst实例,这是指示堆栈内存分配的指令。通过确定这些分配点,为内存标记奠定基础。
3.2 3 标签生成与插入
该关键阶段,LLVM专注于为先前已识别的每个栈内存分配生成和嵌入标签。这里的主要任务包括修改AllocaInst指令。这些修改确保分配对齐到16 B边界。
为解决这一技术难点,使用内联汇编,将其集成到LLVM IR中。这种集成使得系统能够无缝插入特定的存标记指令,这些指令的目的是为栈上分配的每个内存块分配一个随机标签。
3.2 4 运行时监控和实施
在系统的设计中,运行时监控和强制执行功能在确保程序执行期间的内存安全方面起着重要作用。这个过程被分为两个主要场景,每个场景都通过不同的方法来维护内存完整性。
场景1:即系统调用参数可能来自栈分配的情况,LLVM Pass会介入以确保安全性。在分析每个系统调用的LLVM IR时,LLVM Pass会检查调用的参数是否来自栈分配的内存。如果是,则会插入内联汇编代码来验证内存访问与其对应标签的匹配性。
场景2:处理不涉及栈指针的栈内存访问。在这种情况下,由于LLVM Pass是一个前端编译器,因此在编译期间无法直接确定这些情况。为克服这一问题,通过开发一个使用正则表达式的Python脚本来识别符合此场景的汇编语言指令。该脚本通过分析每一行汇编代码,找到所有栈内存操作。如果某个操作不涉及栈指针,脚本会修改代码以包含ldg指令,强制程序读取内存的标签。
3.2 5 标签清除
在LLVM Pass结束阶段使用标签清理程序。当检测到函数返回,包括涉及非标准退出(如longjmp或异常)的场景时,该程序将被触发。在这个节点上,LLVM Pass会计算函数所使用的栈空间量,并根据计算结果系统遍历已分配栈空间。在遍历过程中,同时清除所有内存块中的标签。这项关键任务使用内联汇编来完成,它允许精确且高效地移除标签。
清理过程对于确保内存重用或释放的安全性至关重要。通过移除标签,LLVM Pass有效减少函数执行完成后分配内存可能遗留的安全风险。
4 实验与评估
对ARM MTE框架在增强数据导向攻击的内存安全性方面的有效性进行评估。通过对GNU Core Utilities
[21]、SPEC CPU 2017
[22]以及50个已知漏洞程序的全面测试,分析MTE保护对性能和文件大小的影响。并通过考察MTE框架在DNStracer和Wget这两个已有记录CVE漏洞且广泛使用的Linux程序中的应用,评估其在现实场景中的有效性。
4.1 性能评估
为衡量系统效率,测试实验使用两个基准测试套件,分别来自GNU Core Utilities和SPEC CPU 2017。使用Clang 17.0.0将所有程序编译为64 bit可执行程序。
1)运行时执行开销。使用GNU Core Utilities和SPEC CPU 2017基准测试套件,通过比较实施MTE前后的基准运行时间,评估MTE保护对系统性能的影响。
图3直观地展示了这些比较结果。值得注意的是,由于两个基准测试套件在执行时间上存在显著差异,为了清晰起见,对SPEC CPU基准测试时间进行了缩放处理。
通过分析显示,平均运行时执行开销为52.8%。需要强调的是,由于实验是在完全虚拟仿真的环境中进行的,与真实实现存在一定的差距,因此这种仿真环境可能会引入比硬件实现更多的延迟。根据ARM的设计原则,基于硬件的MTE将运行得更快,在真正的硬件实现中,观察到的开销会显著降低。
2)文件大小开销。为了评估MTE保护带来的文件大小开销,在GNU核心实用程序和SPEC CPU 2017上进行了测试。该方法将原始文件的大小与受MTE保护的文件进行比较,结果如
图4所示。统计得到平均文件大小开销为17.7%,这一增长主要归因于为MTE保护插入了汇编语言代码。
4.2 安全性分析
为评估该自动化防御方法的实际有效性,选取真实漏洞CVE-2017-9430
[23]作为验证案例,如下所示。
// dnstracer_broker.h
#ifndef NS_MAXDNAME
#define NS_MAXDNAME 1024
#endif
// dnstracer.c
int main(int argc, char **argv)
{
...
char * server_name = “127.0.0.1”;
char * server_ip = ...;
char ipaddress[NS_MAXDNAME];
char argv[NS_MAXDNAME];
int server_root = 0;
...
strcpy(argv0, argv[0]);
...
}
该漏洞存在于DNS追踪工具DNStracer中,对strcpy函数处理长参数时缺乏边界校验机制,导致栈缓冲区溢出风险。当DNStracer接收超长的命令行输入时,main函数中的argv0缓冲区(固定大小1 024 B,由NS_MAXDNAME定义)会被溢出,进而引发栈内存破坏。在未部署MTE机制的场景下,server_root等后续栈变量面临被覆盖的风险,可能导致程序行为失控。而传统防御机制对此类破坏难以有效检测。本方法通过在栈缓冲区中嵌入内存标签,使strcpy越界写入触发段触发错误并终止程序运行,从根源上阻止栈破坏的发生,显著降低缓冲区溢出风险。
在MTE机制部署开销方面,MTE机制部署后的DNStracer可执行程序文件大小为124 184 B,较原始版本的118 064 B仅增加约5.18%,证明该自动化防御方法在提升安全性的同时对资源消耗的可控性。
5 结束语
本文通过利用ARM MTE增强内存安全性,从而解决面向数据的攻击的挑战。该系统通过LLVM编译器传递和高效的MTE机制自动实现堆栈内存保护,大幅减少了对手动安全措施的依赖。对各种基准测试和脆弱程序的测试验证了方法的有效性,在安全性增强和性能之间取得了平衡。在系统架构中,通过遍历的方式对所有内存空间建立了全面保护机制。然而,在日常场景中,没有必要屏蔽程序中的每个内存空间。一般来说,只有特殊的地方需要设防。保护所有空间的方法需要相当大的成本,大大增加了系统的操作负担。在未来的工作中,将设计一个用来增强与内存空间结构有关的识别过程。这一举措包括识别方法的优化,主要集中在缩小内存保护空间。最终目标是将系统开销缩减到尽可能小的程度。