我们程序的威胁来自于各个方面.在互联网高度发达的今天, 安全性问题已经是企业软件开发所必须面对的最重要的问题. 从安全学的一般意义上来讲,安全性主要体现在两个方面
敏感数据的泄露
敏感数据的破坏
从具体上来说, .NET 元数据机制的设计, 既方便了反射等强大特性的实现, 又同时给代码安全及程序运行时安全带来了巨大的隐患.迄今为止, 还未发现比较有效元数据可见性控制方法. 当然, 这不在本文的讨论范围之内. 我还是更愿意在这篇文章在针对.NET的内存分配机制讨论一个更具体的问题: 如何保护在内存中存储的敏感数据的驻留机制带来的安全性问题
是代码中使用频率很高的对象类型. 为了提高字符串的处理速度, 节省内存空间, Microsoft为.NET String类设计了驻留机制. 其大概的逻辑模型是, 大部分String存储在一个类似的Hash Table中, string的内容是哈希表的key, 该key对应的value是string的内存地址. 这样内容相同的string实际上只是对应内存堆上同一个字符串.之所以说是大部分而不是全部, 是因为有一部分动态创建(concat)的string, 是不会进入这样一个虚拟的hash Table中的. 本文的最后附上String类的源代码, 有兴趣的同学可以研究研究
这就带来了最主要的问题, 你无法准确控制或者预测一个特定字符串的生命周期. 一个以string形式呈现的敏感数据(比如密码)很有可能在内存中一直存在, 而你却预测它在超出某个特定函数的作用域的时候就被垃圾回收了. 这样, 当发生操作系统换页的时候(而这也往往是可能发生的), 这个敏感数据就被保存到本地文件pagefile.sys当中, 或者当操作系统休眠的时候, 敏感数据进入hiberfil.sys中.一个可能的敏感数据泄漏过程是
使用SecureString类
现在既然String靠不住了,我们能有什么简单的方法来特别的保护我的敏感数据吗? 幸运的是, .NET从Version 2.0开始, 为我们提供了一套基于DPAPI的解决方法
类具有以下特性:
中的内容是加密之后的,而不是平文
使用windows的加密方案
只能在基于NT的平台上使用
代码示例
public void MethodA()
{
//using DPAPI to encrpt the sensitive content
System.Security.SecureString password = new System.Security.SecureString();
char[] pass = { 'p', 'a', 's', 's', 'w', 'o', 'r', 'd' };
for (int i = 0; i < pass.Length; i++)
{
password.AppendChar(pass[i]);
}
password.MakeReadOnly();
//pass the encrypted password through memory or file
}
public void MethodB(System.Security.SecureString password)
{
string decryptedPassword = "";
//copy the secure content to a long pointer
IntPtr ptr = System.Runtime.InteropServices.Marshal.SecureStringToBSTR(password);
try
{
//Convert secure content to string using DPAPI
decryptedPassword = System.Runtime.InteropServices.Marshal.PtrToStringBSTR(ptr);
//using the decrypted password to check
}
finally
{
System.Runtime.InteropServices.Marshal.ZeroFreeBSTR(ptr);
password.Dispose();
}
}
这段代码中有几个值得说明的地方
代码写得有些粗糙, 仅为示意。
使用Char数组来保存敏感数据的原始值. 因为Char数组的生命周期是可以预期的, 它在超出自己的作用域之后,就被回收。
方法, 一旦使用了该方法后, SecureString的内容就不能再被修改,从而保证了加密后的数据不能再被修改,否则将引发异常。
的解密,是通过将其内容复制到一个长指针中,然后利用DPAPI, 最终获得String.该String不会进入上文所说的那个虚拟Hash Table中。
方法. 因为使用COM Interop引入了非托管资源,所以一定不能忘记使用ZeroFreeBSTR来释放指针,否则会造成内存泄漏。
类重写了基类的ToString()方法,不过该方法不会返回所持有的加密内容, 而总是返回System.Security.SecureString。
敏感数据已经足够安全了吗
这个问题的答案很让我们沮丧, 不是. 有两个问题
用户的输入往往先被处理成string, 然后才能传递到我们的处理函数, 比如command line parameters, 或者
的很多函数都要求string参数, 而非SecureString, 比如ADO.NET的Connect函数
幸运的是, 对于这两个问题,我们除了祈祷Microsoft尽快更新Framework以外, 在当前条件下还有些办法来处理
针对第一个问题,重写Command Line或者Textbox,添加对SecureString的支持(不详述
针对第二个问题,利用GC特性来处理
第二个问题的主要安全隐患是来自于string的特性, 即不可变性(immutable). 为了防止GC的自作聪明处理我们的数据, 从而造成敏感数据泄漏, 我们需要对GC做一些处理, 此时上面代码的MethodB就应该修改成如下:
代码示例
public unsafe void MethodB(System.Security.SecureString password)
{
int pwdLength = password.Length;
IntPtr passwordPtr = IntPtr.Zero;
//allocate a pinned memory to store the password in string form
string decryptedPassword = new string('\0', pwdLength);
GCHandle gch = GCHandle.Alloc(decryptedPassword, GCHandleType.Pinned);
try
{
//copy the secure content to a long pointer
passwordPtr = System.Runtime.InteropServices.Marshal.SecureStringToBSTR(password);
var pPassword = (char*)passwordPtr;
var pDecryptedPassword = (char*)gch.AddrOfPinnedObject();
for (int index = 0; index < pwdLength; index++)
{
pDecryptedPassword[index] = pPassword[index];
}
}
finally
{
if (IntPtr.Zero != passwordPtr)
{
System.Runtime.InteropServices.Marshal.ZeroFreeBSTR(passwordPtr); } }}
我们用GCHandleType.Pinned标志, 申请了一块固定位置的内存来存储密码, 这段明文密码是独立于string类的虚拟hash table的.这可以在一定程度上减少因不当权限访问造成的敏感数据泄露
到这里,string是可以用了, 但是换页的问题还没有解决啊
是的, 你可能已经觉得麻烦了.我们不得已而为之, 实在是因为.NET Framework对于SecureString的支持还不够完善, 或者说是部分的. 上面虽然解决了String的不可变特性造成的问题, 但是重新引入系统换页的问题. 怎么办
在这种情况下,我们只能求助于Windows API. Windows API对于页的操作为我们提供了2个接口: AllocateuserPhysicalPages 和 VirtualLock, 这两个函数可以将我们在上例中所取得的密码存储地址pDecryptedPassword 锁定在内存中,强制不换页. 不过这么做要万分小心, 因为一旦pDecryptedPassword 所指向的密码内容被强制不换页, 那该程序的整个workset都会一直被强制在内存中, 一直到程序结束. 这可能给系统的其他程序带来糟糕的体验
关于使用VirtualLock来强制Page In的修改, 就不再讨论了
总结
事物总是两面性的, .NET给我们带来了快速实现, 关注业务的好处, 却缺少了譬如C++般精确操作内存这样的灵活性, 因而在安全性方面如果Framework不够完善, 我们就会多多少少有些掣肘. 总之, 在现有条件下, 尽力实现系统安全性, 是我们的目标. 本文没有讨论系统设计的安全性考虑等这些概念性理论性的东西, 而是从最具体的String类入手讨论, 希望对您有一些启发