.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中.一个可能的敏感数据泄漏过程是:

使用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就应该修改成如下:

C#代码示例

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原创, 禁止用于任何商务用途! 如需转载, 请务必注明作者及作品出处。

时间: 2024-10-04 00:15:29

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

.NET内存中敏感数据的保护方案

从具体上来说,.NET元数据机制的设计,既方便了反射等强大特性的实现,又同时给代码安全及程序运行时安全带来了巨大的隐患.迄今为止,还未发现比较有效元数据可见性控制方法.当然,这不在本文的讨论范围之内.我还是更愿意在这篇文章在针对.NET的内存分配机制讨论一个更具体的问题:如何保护在内存中存储的敏感数据? String的驻留机制带来的安全性问题 String是代码中使用频率很高的对象类型.为了提高字符串的处理速度,节省内存空间,Microsoft为.NET String类设计了驻留机制.其大概的逻

修改内存-汇编中使用debug更改内存中的内容问题

问题描述 汇编中使用debug更改内存中的内容问题 为了学习汇编,我经常使用debug中的指令修改主板内存中存的数据,我想问的是,我这样总是修改联系的话会不会使电脑内存出现问题呢?有牛人说虽然我们经常修改的是那些可以修改的内存内容,但是有的机器甚至连主板ROOM内容都能修改,这样练习练习岂不是我们很有可能将来得换一块主板?哈哈,不知道我说的哪里有问题,请大神指教!谢谢 我是在虚拟机中安装操作系统,在用debug修改内存内容的,也不知道这样做是不是会影响虚拟机中的系统的正常性能,反正是不会影响原本

免杀新姿势:利用线程将恶意代码注入到内存中

本文讲的是免杀新姿势:利用线程将恶意代码注入到内存中, 产生存放远程攻击线程的进程 在这篇文章中我不想一步一步解释我编写的C#代码,但是我会展示下它能够绕过杀毒软件,并且操作非常简单,而且实用. 首先说明一下: 1. 我是在三年前发现这个攻击方法的,当我在做免杀的时候我发现了很多都是以0x0地址开始的进程.在我的win7系统中这种恶意代码绕过了我的杀毒软件,只是在内存中可以找到,然后以系统权限运行.所以,当然是NSA干的咯! 2. 这并不意味着以0x0开始的进程都是进行恶意注入的. 就像刚才所说

.Net程序集的不同加载方式,以及其在内存中格式

.Net程序集除了正常的有框架自动按需载入外,我们还可以通过反射手动载入程序集. 其中反射载入程序集有一种方式就是 以字节流的方式载入程序集.而不是直接从磁盘文件载入. 很多.Net压缩壳,和整体保护壳都采用了这种方式. 这种方式载入的程序集和从磁盘文件载入的程序集,其内存属性是不一样的. 字节流载入的内存属性是 MEM_MAPPED . 文件方式载入的内存属性是  MEM_IMAGE . (注:指使用api函数 VirtualQuery 获取的内存信息) 在 .Net 1.1中 这两种内存影像

linux系统 动态共享库-动态共享库加载到内存中,怎样才能保证只有一份副本

问题描述 动态共享库加载到内存中,怎样才能保证只有一份副本 这几天一直在看关于动态共享库的知识,但看到一个地方,就不懂了,在网上搜索了一下!也没有找到答案!希望有哪位高手,帮小弟解答一下,小弟不胜感激! 问题:动态共享库,顾名思义,可以在多个进程间,进行共享!在系统中只保存一份副本,现在假设Process A已经将Lib A Load到物理内存,但当同样共享Lib A的Process B开始运行时,它是如何知道Lib A已经被加载到内存中,且如何找到这块物理内存?

mysql cluster存储引擎NDB,如何设置哪些数据不被LOAD到内存中?

问题描述 mysql cluster存储引擎NDB,如何设置哪些数据不被LOAD到内存中? mysql cluster存储引擎NDB,如何设置哪些数据不被LOAD到内存中?

ADO.Net与ADO在数据内存中的差异讨论

ado|数据 数据的内存中表示形式 在 ADO 中,数据的内存中表示形式为记录集.在 ADO.NET 中,它为数据集.它们之间有重要的差异. 表的个数 记录集看起来像单个表.如果记录集将包含来自多个数据库表的数据,则它必须使用 JOIN 查询,将来自各个数据库表的数据组合到单个结果表中. 相反,数据集是一个或多个表的集合.数据集内的表称为数据表:明确地说,它们是 DataTable 对象.如果数据集包含来自多个数据库表的数据,它通常将包含多个 DataTable 对象.即,每个 DataTabl

PHP 直接在共享内存中存储数据集

共享内存是一种在相同机器中的应用程序之间交换数据的有效方式.一个进程可创建一个可供其他进程访问的内存段,只要它分配了正确的权限.每个内存段拥有一个惟一的 ID(称为 shmid),这个 ID 指向一个物理内存区域,其他进程可在该区域操作它.创建并提供了合适的权限之后,同一台机器中的其他进程就可以操作这些内存段:读取.写入和删除. 这表明使用 C 语言编写的应用程序可与使用其他语言(比如 Java 或 PHP)编写的应用程序共享信息.它们都可以共享信息,只要它们可访问和理解该信息.共享内存在针对大

C/C++变量在内存中的存储

在C++语言中,有如下代码: char a; int b; int c; a='1'; b=1; printf("a = %x \r\n",a); printf("b = %x\r\n",b); c=a; printf("c = %x\r\n",c); 输出的结果是 a = 31 b = 1 c = 31 请按任意键继续. . . 这里 int型是4Byte char型是1Byet. a='1' 这条语句执行后,会在内存中分配1个字节大小的内存空