现有的工具和实践
编程实践和测试工具减少了内存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构架的一部分。它暂时还不没有实际硬件支持 。
这个扩展保护了两类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的指令。
当用户代码请求分配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];
array[14] = 0;
有多种软件策略可能提高这种情况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) 来加固你产品中的二进制文件。
原作者:KOSTYA SEREBRYANY