软件安全的重要性无需赘述。Google的研究表明Memory访问的安全性漏洞导致的问题占了所有软件漏洞的很大部分,而memory safety问题中 buffer overflow,use after free 原因导致又占很大一部分。
Google发现
https://www.cvedetails.com/pr...
其中23%是overflow,7%是memory corruption导致的。
另一项对Chrome的研究表明,70%的安全性问题是内存访问安全问题,其中 Use-after-free 由占其一半。
https://www.zdnet.com/article...
Microsoft对过去12年的问题研究也印证了这点,70%是内存安全导致的。
https://www.zdnet.com/article...
内存安全这个名词被软件和安全工程师用来描述访问操作系统内存不产生错误的应用。
内存安全bug发生在软件有意无意访问超过分配的大小和地址的内存。
大家在读安全漏洞的报告时经常会看到,buffer overflow, 页面错误,空指针,堆栈溢出/破坏,use after free等,这些都是内存安全漏洞。
UAF是何方妖怪?
Use-after-free(UAF)漏洞是由于错误使用动态分配的内存。虽然可以free一块内存,程序不能清除指向这块内存的指针。
因为动态分配(malloc)的内存可以被重复分配。对于放在动态分配内存中数据,它可以被free,但这个free不意味着数据就被删除了,这块free的内存还是可能被指针引用。
举个例子,
#include<stdio.h>
#include<malloc.h>
#include<stdio.h>
#include<malloc.h>
struct Buf{
int a;
int b;
};
int main(){
struct Buf *buf;
struct Buf *newbuf;
buf=(struct Buf *)malloc(sizeof(struct Buf));
buf->a=10;
buf->b=20;
printf("%d %d\n", buf->a, buf->b);
free(buf);
newbuf=(struct Buf *)malloc(sizeof(struct Buf));
newbuf->a=100;
newbuf->b=200;
printf("%d %d\n", newbuf->a, newbuf->b);
buf->a=30; //Use after free
printf("%d %d\n", newbuf->a, newbuf->b);
return 0;
}
大家认为这3个print可能会得到什么结果?
可能的结果是,
10 20
100 200
30 200
虽然buf被释放了,但再次为newbuf分配一样大小的内存时,它可能分配到前面同一内存, 指向buf的指针还是有可能可以访问这块内存。
Buffer overflow这只老妖
Buffer overflow 发生在访问的数据超过了buffer的范围,因此超过的访问会破坏临近的内存。
攻击者会探索buffer overflow的问题,然后利用它们改变程序执行流程,或获取私有信息。常见的攻击包括,
- 基于Stack的buffer overflow,这是一种常见的,泄露/破坏在代码执行时态的栈内存的方式。
- 基于Heap 的buffer overflow, 它更难实施,通常采用大量分配超过程序运行时使用的内存的方式。
妖怪们后面的神仙
西游记里面的大多妖怪都来自某个神仙。
这些妖怪(问题)的很大的原因时C/C++允许使用指针(神仙),因此C/C++代码更容易出现内存安全问题,因为他们没有自带检查访问/覆盖内存中数据的机制。Linux,Windows, Mac OSX都是使用C/C++编写的。
Java, JavaScript, PERL, C#编程语言自带安全机制,减少了这些安全问题。
有何法宝?
检查出和避免这些问题对安全至关重要,可以有两种思路,
- 地址随机化, ASLR (address space layout randomization)。此方法通过对程序代码, stack,heap放置位置(地址)的随机化,虽然不能避免攻击,但是可以改变buffer overflow问题导致的后果。这个不是本文讨论范围。
- AddressSanitizer(ASAN):面向C/C++语言的内存错误问题检查工具,可以检测如下内存问题:
使用已释放内存, 堆内存越界(读写),栈内存越界(读写),全局变量越界(读写),函数返回局部变量,内存泄漏
ASAN工具主要由两部分组成:
- 编译器插桩模块
- 运行时库
运行时库会接管malloc和free函数。malloc执行完后,已分配内存的前后(称为“红区”)会被标记为“中毒”状态,而释放的内存则会被隔离起来(暂时不会分配出去)且也会被标记为“中毒”状态。
编译器插桩模块:加了ASAN相关的编译选项后,代码中的每一次内存访问操作都会被编译器修改。
ASAN机制维护了一个shadow内存,它追踪内存中的每个byte,它有这个byte是否能访问的系统。无效内存byte叫做red zones, 我们也说这个内存有毒。
当在ASAN编译使能ASAN功能时 (GCC和CLANG都能支持,需在编译链接加上-fsanitize=address 选项),编译的代码会访问内存之前先做检查。如果访问的内存时有毒的,ASAN会追踪这个程序并产生诊断报告,否则可以正常访问。
举个例子,下面C/C++对*p访问会编译成右边代码,
p是个指针,IsPoisoned函数检查访问内存对应的shadow内存。
如果检查后,内存是有效的,那么允许写这个内存地址。
ASAN维护一个查找表,每8byte内存对应shadow内存的一个byte。
它们地址对应关系为shadow=Addr>>3+Offset.
我们来看一个编译代码的例子,
void func (int * p)
{
*p+=1;
}
使用armv8-a clang 11.0.0编译,在使用
-O2 -fomit-frame-pointer -fsanitize=address 编译选项时
func(int*):
str x30, [sp, #-16]! // 8-byte Folded Spill
lsr x8, x0, #3 // p>>3
mov x9, #0x1000000000 //offset
ldrsb w8, [x8, x9] //shadow byte value= *(p>>3+offset)
cbnz w8, .LBB0_2 // if ‘shadow byte value’!=0, it is poisoned
.LBB0_1: //not poisoned, then *p=*p+1;
ldr w8, [x0]
add w8, w8, #1
str w8, [x0]
ldr x30, [sp], #16 // 8-byte Folded Reload
ret //function return
.LBB0_2: //poisoned, ASAN report
and w9, w0, #0x7
add w9, w9, #3 // =3
cmp w9, w8
b.lt .LBB0_1
bl __asan_report_load4
asan.module_ctor: // @asan.module_ctor
str x30, [sp, #-16]! // 8-byte Folded Spill
bl __asan_init
bl __asan_version_mismatch_check_v8
ldr x30, [sp], #16 // 8-byte Folded Reload
ret
因为每个内存访问都要软件检查,ASAN会带来很大的性能影响。
ASAN 可在 32 位和 64 位 ARM 以及 x86 和 x86-64 上运行。 ASan 的 CPU 开销约为 2 倍,代码大小开销在一半到 2 倍之间,并且内存开销很大(具体取决于您的分配模式,但约为 2 倍。
没有银弹?
软件实现代价太高,因此需要hardware来加速,通过硬件管理shadow内存和做检查。
SPARC的ADI和arm的MTE技术是hardware Memory Tagging的实现。
Memory安全和硬件Memory Tagging技术(1)
Memory安全和硬件Memory Tagging技术(2)
Memory安全和硬件Memory Tagging技术(3)
Memory安全和硬件Memory Tagging技术(4)
Linux Kernel MTE相关文档