给出一个结构:
struct CryptoKey {
std::vector<unsigned char> key;
~CryptoKey() { memset(key.data(),0,key.size()); }
};
编译器标题为以消除对memset
的调用,因为这将节省时间,并且没有定义行为的程序可以区分。 (假设析构函数返回后变量key
将不复存在。)
然而,像这样的代码在加密应用程序中很有用,因为秘密存储在内存中的时间越少,攻击者提取它的机会就越少。 (memset
不提供安全性,但确实提供了“纵深防御”。)
我的问题是,哪些真正的编译器实际做消除了这样的memset
调用(显然,启用了优化)?
答案 0 :(得分:7)
也许最好说好的编译器会尝试消除memset调用,开发人员不应该依赖编译器实现的差异来避免这种优化。这些编译器通常具有无法优化的安全替代方案。
memset的安全版
C11引入了memset_s,其中一个特性是不会被优化出来的。
与memset不同,任何对memset_s函数的调用都应严格按照(5.1.2.3)中描述的抽象机的规则进行评估。也就是说,对memset_s函数的任何调用都应假定s和n指示的内存在将来可以访问,因此必须包含c指示的值。
Windows特定
在Windows上还有其他选择。 SecureZeroMemory
或使用#pragma optimize
pragma关闭优化。
常见的子表达式优化
加密安全存在一个更广泛的问题:出于优化原因,编译器有权复制缓冲区。归零可能不会删除所有副本,编译器可能已应用优化,将堆复制到堆栈以消除常见的子表达式。因此,除了避免优化归零之外,还应注意编译器没有插入额外的副本。
答案 1 :(得分:1)
这里的优化器的问题是你的memset根本没有写给成员。是的,key
将不复存在,但不是key.data
。该内存将返回std::allocator
。并且std::allocator
很可能会读取相邻内存以确定key.data
来自的内存块。典型的实现将这样的数据存储在分配的块的头部中,即在负的偏移处。标题不会更新以反映块是空闲的,或者将空闲块与其他空闲块合并。
这甚至可以内联,因此优化器会看到一个函数执行memset
然后执行标头访问。期望优化器能够发现memset
是无害的,这是不合理的。尽管如此,分配器可能会保留一个归零块池。