.NET : 如何保护内存中的敏感数据?

      我们程序的威胁来自于各个方面.在互联网高度发达的今天, 安全性问题已经是企业软件开发所必须面对的最重要的问题. 从安全学的一般意义上来讲,安全性主要体现在两个方面:

  • 敏感数据的泄露
  • 敏感数据的破坏

      从具体上来说, .NET 元数据机制的设计, 既方便了反射等强大特性的实现, 又同时给代码安全及程序运行时安全带来了巨大的隐患.迄今为止, 还未发现比较有效元数据可见性控制方法. 当然, 这不在本文的讨论范围之内. 我还是更愿意在这篇文章在针对.NET的内存分配机制讨论一个更具体的问题: 如何保护在内存中存储的敏感数据?

 

String的驻留机制带来的安全性问题

      String是代码中使用频率很高的对象类型. 为了提高字符串的处理速度, 节省内存空间, Microsoft为.NET String类设计了驻留机制. 其大概的逻辑模型是, 大部分String存储在一个类似的Hash Table中, string的内容是哈希表的key, 该key对应的value是string的内存地址. 这样内容相同的string实际上只是对应内存堆上同一个字符串.之所以说是大部分而不是全部, 是因为有一部分动态创建(concat)的string, 是不会进入这样一个虚拟的hash Table中的. 本文的最后附上String类的源代码, 有兴趣的同学可以研究研究:D

      这就带来了最主要的问题, 你无法准确控制或者预测一个特定字符串的生命周期. 一个以string形式呈现的敏感数据(比如密码)很有可能在内存中一直存在, 而你却预测它在超出某个特定函数的作用域的时候就被垃圾回收了. 这样, 当发生操作系统换页的时候(而这也往往是可能发生的), 这个敏感数据就被保存到本地文件pagefile.sys当中, 或者当操作系统休眠的时候, 敏感数据进入hiberfil.sys中.一个可能的敏感数据泄漏过程是:

 

Drawing1

 

使用SecureString类

      现在既然String靠不住了,我们能有什么简单的方法来特别的保护我的敏感数据吗? 幸运的是, .NET从Version 2.0开始, 为我们提供了一套基于DPAPI的解决方法 - SecureString.

      SecureString类具有以下特性:

  • SecureString中的内容是加密之后的,而不是平文;

  • 使用windows的加密方案DPAPI ;

  • SecureString只能在基于NT的平台上使用

 

C#代码示例:

    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数组的生命周期是可以预期的, 它在超出自己的作用域之后,就被回收. 
  • MakeReadOnly方法, 一旦使用了该方法后, SecureString的内容就不能再被修改,从而保证了加密后的数据不能再被修改,否则将引发异常.
  • SecureString的解密,是通过将其内容复制到一个长指针中,然后利用DPAPI, 最终获得String.该String不会进入上文所说的那个虚拟Hash Table中.
  • ZeroFreeBSTR()方法. 因为使用COM Interop引入了非托管资源,所以一定不能忘记使用ZeroFreeBSTR来释放指针,否则会造成内存泄漏.
  • SecureString类重写了基类的ToString()方法,不过该方法不会返回所持有的加密内容, 而总是返回System.Security.SecureString.


敏感数据已经足够安全了吗?

      这个问题的答案很让我们沮丧, 不是. 有两个问题:

  • 用户的输入往往先被处理成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

 

声明: 本文为作者Shining Sun原创, 禁止用于任何商务用途! 如需转载, 请务必注明作者及作品出处.

posted @ 2008-12-15 16:50  Jeffrey Sun  阅读(4093)  评论(17编辑  收藏  举报