目录
本文基于 google 文档 https://github.com/google/sanitizers/wiki/Stack-instrumentation-with-ARM-Memory-Tagging-Extension-(MTE) 分析~
1.概述
MTE 属于 ARMv8.5 指令集的拓展功能~ 即 memory tag extension~
要使能MTE功能,需要有支持 MTE 的硬件,kernel,以及编译器和libc(定制malloc等堆内存分配方法)~
其中,支持MTE的硬件和kernel是必要条件~
支持MTE的编译器是实现 stack 检测的必要条件~
支持MTE的libc是实现 heap 检测的必要条件~
2.栈内存检测原理
首先是tag(标签)的概念~
MTE 有两类 tag:
A. 分配标签(allocate tag也可以说是memory tag),每16个字节内存对应一个4位的mem tag
B. 地址标签(即address tag),地址的高4位存放 address tag
这里说的地址可以理解成存放内存地址的寄存器,比如X寄存器或SP寄存器~~~
内存访问指令(基于SP的内存访问指令除外)会比较address和memory tag,不匹配时生成异常~~
编译器(LLVM)可以应用MTE 检测 stack 内存安全,这是通过将 MTE 指令插入到生成的代码中来实现的!
由于对齐要求,所有 stack 变量的大小都会增长到最接近的 16 字节的倍数,并且也按16对齐,因为所有MTE指令都要求其地址操作数按 16 字节对齐~
LLVM 启用 MTE 的方法是,在编译时添加编译选项 -march=armv8+memtag -fsanitize=memtag~
(目前 LLVM 对 MTE 的支持工作还在进行中)
3.MTE 指令
主要的 MTE 指令有
指令名 | 使用方法 | 说明 |
---|---|---|
IRG | IRG Xd, Xn | 将Xn复制到Xd, 并且生成一个随机tag设置给Xd(Xd,Xn表示arm的X寄存器即地址寄存器)~ 此时,Xd 的 tag 即为 address tag! |
STG | STG Xd, [Xn] | 将 Xd 的 tag 设置给 [Xn, Xn + 16) 这块内存~ 此时,[Xn, Xn + 16)这块内存的 tag(mem tag)就等于 Xd 的 tag(address tag)了! |
STZG | STZG Xd, [Xn] | STG 的变体。 在 STG 的基础上,将 [Xn, Xn + 16)这块内存清0(内容设置为0) |
STGP | STGP Xt, Xt2, [Xn] | STG 的变体。 将 Xt,Xt2 这两个寄存器的值写到 [Xn, Xn + 16) 这块内存中(arm64 的 X寄存器size是8字节,但是 MTE 要求 16字节对齐,所以这里会用两个X寄存器来初始化变量所在的内存)~ 并且将 Xn 的 tag 设置给 [Xn, Xn + 16) 这块内存 ~ |
ADDG | ADDG Xd, Xn, #imm1, #imm2 | ADD 的变体。 将 Xn 复制到 Xd~ 将立即数 imm1 加到 Xd 的地址部分~ 将立即数 imm2 加到 Xd 的tag 部分~ |
ST2G | ST2G Xd, [Xn] | STG 的变体。 将 Xd 的 tag 一次性设置到 2 个内存粒度上(MTE 的内存粒度为16个字节,要求16字节对齐)~ 即将 Xd 的 tag 设置给 [Xn, Xn + 32) 这块内存~ |
4.举例说明
4.1 未初始化的局部变量作为函数参数
{ int x; use(&x); }
irg x0, sp // 将sp即栈指针设置给x0(sp一直指向栈顶,此时栈顶的位置即变量x所在的位置),同时生成随机tag,设置给x0 的高4位(即address tag)
stg x0, [x0] // 将x0 的tag 设置给 x0 指向的16字节内存(即变量x所在的位置),此时变量 x 的mem tag 与寄存器 x0 的address tag 相同
bl use // 函数跳转(在use函数中通过 x0 来操作变量 x,由于 tag 相同所以可以操作,如果操作超出16字节则会报错)
stg sp, [sp] // 将 sp 的 tag 设置给 sp 指向的 16 字节内存(即此时的栈顶位置,也就是变量 x)
4.2 初始化为0的局部变量作为函数参数
{ int x = 0; use(&x); }
irg x0, sp // 同上
stzg x0, [x0] // 同上,同时将变量x清0
bl use // 同上
stg sp, [sp] // 同上
4.3 初始化为非0的局部变量作为函数参数
{ int x = 42; use(&x); }
irg x0, sp // 同上
mov w8, #42 // 将x8寄存器的低32位值设置为42(w表示对应x寄存器的低32位)
stgp x8, xzr, [x0] // 将x8的值写到x0(指向的16字节)的低8字节,将xzr(0寄存器,表示全0)的值即全0写到x0的高8字节
bl use // 同上
stg sp, [sp] // 同上
4.4 多个局部变量作为函数参数
{
int x, y;
use(&x, &y);
use(&x, &y);
}
irg x19, sp // 先将sp(指向栈顶,假设为变量x)设置给x19(临时使用),并生成和设置随机 tag 给 x19
addg x1, x19, #16, #1 // 将x19设置给x1,x1的值加16,x1的tag加1~ x1指向变量y
mov x0, x19 // 将x19设置给x0,x0指向变量x~ 这样x0和x1就有不一样的tag了(x0与x19的tag相同)
stg x19, [x19] // 设置变量x的 mem tag
stg x1, [x1] // 设置变量y的 mem tag
bl use // 同上
addg x1, x19, #16, #1 // 第二次调用函数前,重新将x19设置给x1,x1的值加16,x1的tag加1~ x1指向变量y
mov x0, x19
bl use
st2g sp, [sp] // 将栈顶的32个字节(即变量x和y)的mem tag设置为与 sp的address tag 相同
4.5 基于SP寄存器(或偏移)的存取指令
这里要特别说明,基于SP寄存器和立即偏移量的加载和存储指令都不检查 tag。 这允许编译器在上述代码片段中避免为x的标签地址分配一个被调用保存的寄存器,并在调用后加载x的值,就像它没有被标记一样。
基于SP寄存器和立即偏移量的加载和存储行为一般被认为是安全的,因为它们只出现在访问当前函数堆栈上的局部变量的代码中,它们的正确性可以静态地进行验证~~
比如下面的代码片断,指令 ldr w0, [sp]
通过SP寄存器读取栈顶位置的 x 变量值,并写到 w0 寄存器~
{
int x;
use(&x);
return x;
}
irg x0, sp
stg x0, [x0]
bl use
ldr w0, [sp] // 将栈顶的32位数据传入寄存器w0(即x0的低32位)
stg sp, [sp]
这段代码想说明的是,按照一般的 MTE tag 检查规则来看, ldr w0, [sp]
这条指令是会报错的,因为变量 x 当前的 mem tag 与 sp 的 address tag 不相等~
但是实际上并不会报错,因为基于 SP 的内存访问并不会触发 MTE 的 tag 检查~
MTE - 堆内存检测原理: