34

修志龙_ZenonXiu · 2022年08月18日 · 上海市

Memory安全和硬件Memory Tagging技术(2)

本文大部分内容来自ARM Memory Tagging Extension and How It Improves C/C++ Memory Safety
https://www.usenix.org/system...
原作者 KOSTYA SEREBRYANY

现有的工具和实践
编程实践和测试工具减少了内存bug的可能性。测试驱动的开发流程和如AddressSanitizer或Valgrind的动态测试工具一起可以帮助避免内存bug. Fuzzing(和理想化的Fuzz驱动开发流程)可以处理下一层的bug. 有些内存bug可以通过静态分析发现。

基于软件代码加固技术使攻击者发现内存安全问题越来越难。Stack cookies, 不可执行代码的内存, ASLR, 控制流程完整性(control flow integrity)(LLVM CFI, mircosoft CFG, Shadow Call Stack)和其他技术帮助避免内存安全性问题演变为串改程序流程,这是很多hack的最终目标。加强内存分配器,比如Sudo加强的分配器或是Chrome的Partition Alloc是hack更难,在有些情况下甚至不可能。

基于硬件的方案也开始出现。Arm Pointer Authentication(指针认证),已经在最近的Apple硬件设备上使用。加密认证函数返回地址可以避免return-oriented-programming (ROP)。Intel Control-flow Enforcement技术有希望很快出现,以另一种方式解决ROP问题, 它将返回地址放在有特殊访问权限的单独的栈里实现。

这些工具是我们的软件更加稳定安全,但是这还不够。现有的手段仅仅避免了一些攻击,几乎忽略了如data-oriented的其他攻击。

在基于硬件方案中,有两种最突出,SPARC的ADI技术和arm的MTE技术,它们都实现memory tagging(内存标签)或叫memory coloring(内存着色)。SPARC ADI 2016年就有了量产的硬件。这篇文章我们重点是arm MTE技术。

Arm MTE
2018年9月,arm宣布了Memory Tagging Extension 或MTE,作为armv8.5构架的一部分。它暂时还不没有实际硬件支持 (译者注:实际上2020年,arm公布的CPU路线图里,2021年的Matterhorn会支持MTE, https://techlog360.com/matter... ).

这个扩展保护了两类tag: 地址tag和内存tag。

一个地址tag是存储在每个指针的高4 bits。 MTE采用了现存的tope-byte-ignore功能,这个功能告诉CPU硬件忽略地址的最高byte部分,这允许最高的1 byte用在用户控制的metadata. 因此MTE只能用在64位软件中。

一个内存tag是每16 byte对齐的应用内存(内存粒度)对应的4 bit值,如何存储内存tag是硬件实现相关的。逻辑上,每16 byte的内存现在包含了除128 bit数据之外的4 bit metadata。

每次分配Heap区域时,软件随机选择4 bit tag 并用这个tag标记这个地址和新分配的内存(16 byte)。访问内存时,读写指令会验证地址tag和内存 tag是否匹配,如果不匹配,会导致一个硬件异常。MTE引进了新的操作tag的指令。

让我们看一个例子,https://community.arm.com/dev...

1.png

当用户代码请求分配16 byte的栈时,new()操作将分配大小对齐到16 byte的16 byte内存,然后选择一个随机的4bit tag, 将这个tag放在地址的最高byte中,然后更新这个tag到最新分配的内存中。附近的内存有不同的内存tag, 因此当代码访问越界(p[17])时,MTE会因为指针的tag和内存tag不匹配而导致一个异常。

上图也演示了避免heap-use-after-free, 在delete的时候,delete函数会改变要free内存的内存tag,因此任何试图通过老的指针( dangling)访问这块内存的尝试都会导致一个异常,因为指针还是使用老的tag,而内存tag已经改变,不再和指针tag匹配。

你也许注意到了,用MTE检测bug可能有点问题。是的,4bit tag只有16种可能的值。一个随机的tag和另外一个随机tag不同的可能性为15/16或~93%。软件需要决定通过其他方式增加可能性。比如,为了用完美精度检测连续buffer overflow问题,内存分配器可能要强制附近的内存块的tag永远不相等。

使用MTE,Heap内存在malloc和free时标识,tag的检查是由硬件来做。这意味着在检查heap相关问题时,代码不需要重新编译。MTE也可以识别stack-use-after-return和stack或全局变量访问的buffer overflow 问题,但是这要求使用额外的编译选项重新编译代码。

和AddressSanitizer的比较
AddressSanitizer是一个广泛使用的检测内存安全问题的工具。它使用compiler功能来追踪内存读写(上篇已经介绍)。

MTE概念上和AddressSanitizer类似:都是在运行时检测bug,都需要在malloc/free中实现特殊功能,都需要compiler的支持。

但是,使用地址tag使MTE足够不同:它不需要red zones或隔离来发现bug. 这让MTE 消耗更少的内存,另外,MTE使用硬件来检查,这消除了AddressSanitize需要的compiler对每个内存读写干预带来的消耗。

和AddressSanitizer相比, MTE带来一下好处:

  • MTE检查可以在运行时动态打开和关闭
  • CPU的消耗预计非常小,有希望百分比在很小的个位数,而AddressSanitizer典因为型有2-3倍的slowdown.
  • MTE可以在不需要重新编译来发现heap相关问题
  • 因为比较小的overhead, 一样的二进制代码可以用在测试也可以用作产品化代码
  • MTE的内存overhead在3-5%之间,而AddressSanitizer为2-3倍
  • 对离目标内存边界很远,或目标生命周期很后面的内存访问,MTE比AddressSanitizer更有可能检测出问题。

MTE唯一不好的地方是, 它可能不能检查到在16 byte颗粒之内的buffer overflow.

char *array = new char [13]; // allocates one 16-byte granule
array[14] = 0; // access within the same 16-byte granule

有多种软件策略可能提高这种情况bug检测能力,但需要额外的代价和复杂度。

MTE的使用
接下来展示MTE的几种不同的用法。
首先,MTE将会成为很新版本的用来做测试和fuzzing的AddressSanitizer,它会以小代价发现更多的bug。在很多情况下,它允许测试使用的二进制文件和发行版一样。
第二,MTE可以用作为产品测试机制,一直使能或是随机使能。对用终端软件,比如浏览器,这意味着用户手中设备的bug可以被检测出来,并且如果用户允许,生成bug报告可以发送给提供商。对于服务器端软件,这意味着罕见的bug一旦触发也可以被立即检查出来。

最后, MTE可以当成强大的安全防御,它虽然不能100%避免攻击,但是可能性还是很高,并且第一次攻击失败的尝试会警告用户和软件提供商。我们相信memory tagging会检测到大多数常见内存安全bug,帮助提供商定位和修复它们,从而使攻击者对攻击不感兴趣。

另外一些聪明的使用MTE办法很有可能被发掘出去。MTE可以帮助开发带无硬件watchpoint数量限制,高效率的竞争检测,快速垃圾回收等功能的调试器。

HWSAN
我们可以是使用软件的memory tagging 实施方案- HWSAN (hardware-assisted AddressSanitizer) 来减少内存占用。 HWSAN在思想上和AddressSanitizer类似,但是更小的内存占用使它更适合内存紧张的设备,比如智能手机。今天,这个工具只在64位的arm CPU上支持,因为它要求top-byte-ignore特性,并且需要对内核做些小修改以便让tag过的地址可以通过系统调用传给内核。

兼容性
MTE和HWSAN提供对现有代码的高兼容性。我们通过下很少的代码修改编译了Android平台和Chromium浏览器。
然而,我们也发现了些不兼容的情况。在一个情况中,指向一个特定类型的指针有应用自己定义的metadata放在高16bit 地址中。在另一情况中,一个指针被强制转换位double类型然后再转换回来,导致低地址位丢失。还有一种情况,代码计算一个局部变量和不同栈帧的差值,这用来计算递归深度。所用的情况都被容易地解决了。

相关工作
我写这篇文章是希望让更多人知道memory tagging概念和arm的令人兴奋的MTE技术, 以便其他的CPU厂商也尽快采用它。不同于其他现有的硬件安全扩展,MTE直接针对内存安全bug,它们是很多漏洞的根本原因。除了作为高效的防御手段,MTE也可以作为bug检测工具,但MTE也不是所有内存安全bug的万灵药。

Intra-Object-Buffer-Overflow
有另一类C/C++bug等着要处理,这就是Intra-Object-Buffer-Overflow

struct S {
int array[5];
int another_field;
};

int GetInt(int *p, size_t idx) {
return p[idx];
}

int Foo(S *s) {
return GetInt(s->array, 5);
}

这里,通过访问超出边界的数组,我们可以最终读到在同一结构体里的其他field。在这种情况下, 不论AddressSanitizer, HWASAN, 还是MTE都无法发现这个问题,因为这个访问发生在同一heap(或stack)分配的目标中。Undefined Behavior Sanitizer (UBSan)可以检测一些简单的情况,但是如这个例子更复杂的情况就不行了,因为访问这个内存的GetInt()函数已经失去了在Foo()里面的静态边界信息。

Type-Confusion
另外一类MTE不能解决的bug是Type-Confusion

struct Image {
int pixels[100];
};
struct Secret {
int sensitive_data[200];
};
Secret *secret = new Secret;
...
DrawOnScreen((Image*) secret);

这个代码对不兼容的类型强制类型转换,接下来在DrawOnScreen()的内存访问会错误地访问没有违背边界和生命周期的敏感数据。

一个潜在的方案是使用更严格的C++子集,它不允许一些静态无效强制类型转换(通过编译时报错)和其他一些动态无效强制类型转换(通过如LLVM CFI的技术)。

没有初始化的内存
MTE的另一作用是不管什么时候内存分配被tag,它也可以没有额外代价地被初始化。新的arm指令同时初始化内存和存储内存tag。 因此,为一个应用使能MTE可以防御另一类uses of uninitialized memory的漏洞。

Safer Languages
没有对C/C++内存安全的谈论可以避免安全编程语言如Java,Go, Swift, Rust,它们的确是更安全,在很多情况下,它们是开发新软件的更好选择。

但是它们没有一个是真正的安全。 Go和Swift有数据竞争,Java巨大runtime本身是用C++写的,只有Rust更加解决安全,但是需要更陡峭的学习曲线。

当然,所有的这些语言都有不安全的紧急出口,当不安全的section在使用时,它都会转向C, 更糟的是,这些语言有更少的工具,实践和习惯来避免内存安全问题。再一次,带AddressSanitizer and fuzzing 的Rust可能是最好的选择。MTE会对Rust和其他带有不安全代码的内存安全编程语言有用。

GWP-Asan
GWP-Asan是另外一种bug检测工具,用来发现heap-use-after-free 和heap-buffer-overflows. 它依赖 protected guard pages, 这是在Electric Fence Mallo和其他类似工具使用的老办法。但是它有一个改变:guarded分配是被采样的。这意味着overhead, 和bug检测的可能性可以缩小到可接受的范围。小的bug检测可能性可以通过在大规模的产品应用中提升。
GWP-Asa不是AddressSanitizer 或HWASAN的替代,因为它处理更小的bug子集,并且有很小的检测出问题的可能性。 我们也可以使用MTE实现GWP-Asan类似采样的bug检测机制。

结论
Arm MTE会减少C/C++内存安全问题从灾难程度到可接受范围。希望其他的硬件厂商也是实现它们的memory tagging技术。在这之前,别忘了用现有的工具测试你的软件,比如AddressSanitizer 或HWASAN 和 fuzzers (e.g., libFuzzer) 来加固你产品中的二进制文件。

Memory安全和硬件Memory Tagging技术(1)
Memory安全和硬件Memory Tagging技术(2)
Memory安全和硬件Memory Tagging技术(3)
Memory安全和硬件Memory Tagging技术(4)
Linux Kernel MTE相关文档

推荐阅读
关注数
8647
内容数
61
目录
极术微信服务号
关注极术微信号
实时接收点赞提醒和评论通知
安谋科技学堂公众号
关注安谋科技学堂
实时获取安谋科技及 Arm 教学资源
安谋科技招聘公众号
关注安谋科技招聘
实时获取安谋科技中国职位信息