内存的初始化与清零问题
View Post
貌似三年没写随笔了,就记录一下最近在搞的事情吧。
我们在写程序的时候,常常会忽略一件事情,那就是即时抹去敏感数据,例如用户口令和密钥,想象一下,在你的程序结束后,密钥被留在了空闲内存区,这时候如果有别的进程申请到它,或者有攻击者读到了内存数据,那就万般皆空了。
事实上,系统开发者们也考虑过这一点,并做了一定程度的防护——在Windows下,系统会在进程结束后自动清零被归还的内存,但是Linux系统不会,可能是出于性能考虑,Linux采用一种Lazy的思想来清零数据。当进程申请一块内存时,Linux只是分配一个虚拟的零页给它,这个时候程序再怎么读也只能从这块buffer中读到0,只有当写入动作发生(例如赋值给buffer中的某一字节)时,系统才会分配一块真正的物理内存(感兴趣的同志可以写个demo试试,只是持续申请内存而不赋值,再看看资源管理器,分给这个demo的内存应该是没有增加的),再将之前的虚拟内存映射并且覆盖写到它上面,Linux的页大小为4KB,此时会进行一次4KB大小的内存写入,举个例子:
1 int main() 2 { 3 unsigned char * buffer = malloc(1024); 4 5 for (int i=0; i<sizeof buffer; i++) 6 printf("%02X ", buffer[i]); 7 8 buffer[3] = 0xff; 9 10 for (int i=0; i<sizeof buffer; i++) 11 printf("%02X ", buffer[i]); 12 free(buffer); 13 return 0; 14 }
假设上面程序在Linux下运行,虽然第3行申请到的buffer没有初始化,但由于 copy-on-write 原则,第5、6行读到的值必然都是0x0,而且此时并没有实际的物理内存被分配给进程,只有第8行执行完毕时,系统才会分配一个page给当前进程,并且这个page的内容会被覆盖写为 00 00 00 ff 00 00 00……
这种机制可以防止一个进程读到其他进程释放掉的内存中的敏感数据,但是,所谓“防君子不防小人”,它能有效防止“合法”读取内存数据,但根本防不了 DMA、cold-boot这类攻击行为。所以,对敏感数据,还是要及时擦除才对。
然而,内存的清零并不是调用一下memset那么简单,如果在上述程序的第12行 free之前加上一句 memset(buffer, 0, sizeof buffer),那只要编译器开了优化,它就会被当作死代码清除掉的——Dead Store Elimination 在GCC -O1时就会被开启,而GCC默认的优化选项是-O2。
关于内存的清零,有一篇 USENIX Security 2017的文章讲的比较全面,这里贴出链接 https://www.usenix.org/system/files/conference/usenixsecurity17/sec17-yang.pdf ,感兴趣的同志可以看看,有时间我再详细说一下这个问题。