貌似三年没写随笔了, 就记录一下最近在搞的事情吧.
我们在写程序的时候, 常常会忽略一件事情, 那就是即时抹去敏感数据, 例如用户口令和密钥, 想象一下, 在你的程序结束后, 密钥被留在了空闲内存区, 这时候如果有别的进程申请到它, 或者有攻击者读到了内存数据, 那就万般皆空了.
事实上, 系统开发者们也考虑过这一点, 并做了一定程度的防护 -- 在 Windows 下, 系统会在进程结束后自动清零被归还的内存, 但是 Linux 系统不会, 可能是出于性能考虑, Linux 采用一种 Lazy 的思想来清零数据. 当进程申请一块内存时, Linux 只是分配一个虚拟的零页给它, 这个时候程序再怎么读也只能从这块 buffer 中读到 0, 只有当写入动作发生 (例如赋值给 buffer 中的某一字节) 时, 系统才会分配一块真正的物理内存(感兴趣的同志可以写个 demo 试试, 只是持续申请内存而不赋值, 再看看资源管理器, 分给这个 demo 的内存应该是没有增加的), 再将之前的虚拟内存映射并且覆盖写到它上面, Linux 的页大小为 4KB, 此时会进行一次 4KB 大小的内存写入, 举个例子:
- int main()
- {
- unsigned char * buffer = malloc(1024);
- for (int i=0; i<sizeof buffer; i++)
- printf("%02X", buffer[i]);
- buffer[3] = 0xff;
- for (int i=0; i<sizeof buffer; i++)
- printf("%02X", buffer[i]);
- free(buffer);
- return 0;
- }
假设上面程序在 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 , 感兴趣的同志可以看看, 有时间我再详细说一下这个问题.
来源: https://www.cnblogs.com/weir007/p/11544119.html