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

本文涉及的产品
密钥管理服务KMS,1000个密钥,100个凭据,1个月
简介: 我们程序的威胁来自于各个方面.在互联网高度发达的今天, 安全性问题已经是企业软件开发所必须面对的最重要的问题. 从安全学的一般意义上来讲,安全性主要体现在两个方面:敏感数据的泄露敏感数据的破坏      从具体上来说, .

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

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

      从具体上来说, .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就应该修改成如下:

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

目录
相关文章
|
3月前
|
存储 开发框架 .NET
"揭秘.NET内存奥秘:从CIL深处窥探值类型与引用类型的生死较量,一场关于速度与空间的激情大戏!"
【8月更文挑战第16天】在.NET框架中,通过CIL(公共中间语言)可以深入了解值类型与引用类型的内存分配机制。值类型如`int`和`double`直接在方法调用堆栈上分配,访问迅速,生命周期随栈帧销毁而结束。引用类型如`string`在托管堆上分配,堆栈上仅存储引用,CLR负责垃圾回收,确保高效且自动化的内存管理。
56 6
|
3月前
|
存储 编译器 C语言
【C语言篇】数据在内存中的存储(超详细)
浮点数就采⽤下⾯的规则表⽰,即指数E的真实值加上127(或1023),再将有效数字M去掉整数部分的1。
369 0
|
7天前
|
开发框架 监控 .NET
【Azure App Service】部署在App Service上的.NET应用内存消耗不能超过2GB的情况分析
x64 dotnet runtime is not installed on the app service by default. Since we had the app service running in x64, it was proxying the request to a 32 bit dotnet process which was throwing an OutOfMemoryException with requests >100MB. It worked on the IaaS servers because we had the x64 runtime install
|
23天前
|
监控 算法 应用服务中间件
“四两拨千斤” —— 1.2MB 数据如何吃掉 10GB 内存
一个特殊请求引发服务器内存用量暴涨进而导致进程 OOM 的惨案。
|
22天前
|
存储 C语言
数据在内存中的存储方式
本文介绍了计算机中整数和浮点数的存储方式,包括整数的原码、反码、补码,以及浮点数的IEEE754标准存储格式。同时,探讨了大小端字节序的概念及其判断方法,通过实例代码展示了这些概念的实际应用。
45 1
|
26天前
|
存储
共用体在内存中如何存储数据
共用体(Union)在内存中为所有成员分配同一段内存空间,大小等于最大成员所需的空间。这意味着所有成员共享同一块内存,但同一时间只能存储其中一个成员的数据,无法同时保存多个成员的值。
|
28天前
|
监控 Java easyexcel
面试官:POI大量数据读取内存溢出?如何解决?
【10月更文挑战第14天】 在处理大量数据时,使用Apache POI库读取Excel文件可能会导致内存溢出的问题。这是因为POI在读取Excel文件时,会将整个文档加载到内存中,如果文件过大,就会消耗大量内存。以下是一些解决这一问题的策略:
68 1
|
30天前
|
SQL XML 关系型数据库
入门指南:利用NHibernate简化.NET应用程序的数据访问
【10月更文挑战第13天】NHibernate是一个面向.NET的开源对象关系映射(ORM)工具,它提供了从数据库表到应用程序中的对象之间的映射。通过使用NHibernate,开发者可以专注于业务逻辑和领域模型的设计,而无需直接编写复杂的SQL语句来处理数据持久化问题。NHibernate支持多种数据库,并且具有高度的灵活性和可扩展性。
39 2
|
1月前
|
缓存 安全 Java
使用 Java 内存模型解决多线程中的数据竞争问题
【10月更文挑战第11天】在 Java 多线程编程中,数据竞争是一个常见问题。通过使用 `synchronized` 关键字、`volatile` 关键字、原子类、显式锁、避免共享可变数据、合理设计数据结构、遵循线程安全原则和使用线程池等方法,可以有效解决数据竞争问题,确保程序的正确性和稳定性。
37 2
|
1月前
|
存储 编译器
数据在内存中的存储
数据在内存中的存储
41 4