敏感数据已经足够安全了吗?
这个问题的答案很让我们沮丧, 不是. 有两个问题:
- 用户的输入往往先被处理成string, 然后才能传递到我们的处理函数, 比如command line parameters, 或者textbox.
- .NET Framework的很多函数都要求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类入手讨论, 希望对您有一些启发.
下载: String Class