【问题标题】:.NET Secure Memory Structures.NET 安全内存结构
【发布时间】:2009-07-22 17:37:20
【问题描述】:

我知道 .NET 库提供了一种以受保护/安全方式存储字符串的方法 = SecureString。

我的问题是,如果我想存储一个字节数组,保存它的最好、最安全的容器是什么?

【问题讨论】:

    标签: c# memory object security


    【解决方案1】:

    了解 System.String 类型的漏洞很重要。让它完全安全是不可能的,SecureString 的存在是为了最小化暴露的风险。 System.String 是有风险的,因为:

    • 它们的内容在别处可见,无需使用调试器。攻击者可以查看页面文件 (c:\pagefile.sys),它会保留已换出到磁盘的 RAM 页面的内容,以便为需要 RAM 的其他程序腾出空间
    • System.String 是不可变的,使用后无法擦除字符串的内容
    • 垃圾收集堆压缩堆,但不会重置已释放内存的内容。这可以将字符串数据的副本留在内存中,您的程序完全无法访问。

    这里的明显风险是字符串内容在使用字符串很久之后仍然可见,从而大大增加了攻击者可以看到它的几率。 SecureString 提供了一种解决方法,将字符串存储在非托管内存中,它不受垃圾收集器的影响而留下字符串内容的杂散副本。

    现在应该清楚如何创建自己的安全数​​组版本,并具有与 SecureString 提供的相同类型的保证。您确实没有有不变性问题,使用后擦洗数组不是问题。这本身几乎总是足够好,隐含在减少暴露的可能性是你不会长时间保持对数组的引用。因此,在垃圾回收之后,未清理的数组数据副本幸存的几率应该已经很低了。您也可以降低这种风险,仅针对小于 85,000 字节的数组显示。要么按照 SecureString 的方式进行,要么使用 Marshal.AllocHGlobal()。或者通过pinning 数组 GCHandle.Alloc() 更容易。

    【讨论】:

      【解决方案2】:

      从 .Net 2.0 开始使用 ProtectedData.Protect 方法,看起来将范围设置为 DataProtectionScope.CurrentUser 应该会产生与安全字符串相同的预期效果

      取自此处的示例用法

      http://msdn.microsoft.com/en-us/library/system.security.cryptography.protecteddata.protect.aspx

      using System;
      using System.Security.Cryptography;
      
      public class DataProtectionSample
      {
      // Create byte array for additional entropy when using Protect method. 
          static byte [] s_aditionalEntropy = { 9, 8, 7, 6, 5 };
      
          public static void Main()
          {
      // Create a simple byte array containing data to be encrypted. 
      
      byte [] secret = { 0, 1, 2, 3, 4, 1, 2, 3, 4 };
      
      //Encrypt the data. 
              byte [] encryptedSecret = Protect( secret );
              Console.WriteLine("The encrypted byte array is:");
              PrintValues(encryptedSecret);
      
      // Decrypt the data and store in a byte array. 
              byte [] originalData = Unprotect( encryptedSecret );
              Console.WriteLine("{0}The original data is:", Environment.NewLine);
              PrintValues(originalData);
      
          }
      
          public static byte [] Protect( byte [] data )
          {
              try
              {
                  // Encrypt the data using DataProtectionScope.CurrentUser. The result can be decrypted 
                  //  only by the same current user. 
                  return ProtectedData.Protect( data, s_aditionalEntropy, DataProtectionScope.CurrentUser );
              } 
              catch (CryptographicException e)
              {
                  Console.WriteLine("Data was not encrypted. An error occurred.");
                  Console.WriteLine(e.ToString());
                  return null;
              }
          }
      
          public static byte [] Unprotect( byte [] data )
          {
              try
              {
                  //Decrypt the data using DataProtectionScope.CurrentUser. 
                  return ProtectedData.Unprotect( data, s_aditionalEntropy, DataProtectionScope.CurrentUser );
              } 
              catch (CryptographicException e)
              {
                  Console.WriteLine("Data was not decrypted. An error occurred.");
                  Console.WriteLine(e.ToString());
                  return null;
              }
          }
      
          public static void PrintValues( Byte[] myArr )  
          {
                foreach ( Byte i in myArr )  
                  {
                       Console.Write( "\t{0}", i );
                   }
            Console.WriteLine();
           }
      
      }
      

      【讨论】:

      • 我已经从这个例子中创建了一个 nuget 包,这些例子帮助了我们谢谢。 [这里是 Nuget 包][1] [1]:nuget.org/packages/SecureArrays
      • 需要注意的是,对于那些使用 .Net Core 和 .Net 5.0+ 开发的用户,此功能仅适用于 Windows 操作系统。
      【解决方案3】:

      没有“最好”的方法来做到这一点 - 您需要识别您试图防御的威胁,以便决定要做什么,或者是否需要做任何事情。

      需要注意的一点是,与不可变的字符串不同,您可以在完成处理后将字节数组中的字节清零,因此您不会遇到与 SecureString 设计的相同的问题解决。

      加密数据可能适用于某些问题,但您需要确定如何保护密钥免受未经授权的访问。

      我发现很难想象以这种方式加密字节数组会有用的情况。详细说明您正在尝试做的事情会有所帮助。

      【讨论】:

      • 出于安全目的,您永远不应依赖对数组进行归零。 GC 可以随时重新定位数组的内容,因此很可能在内存中的某个地方留下一个或多个副本。使用 System.Security.Cryptography.ProtectedData 是要走的路。使用 Marshal.AllocHGlobal 自行开发并非不可能,但有太多方法可以做到这一点。
      【解决方案4】:

      RtlZeroMemoryVirtualLock 的组合可以做你想做的事。 VirtualLock 如果您想阻止数据交换到磁盘和 RtlZeroMemory 以确保内存归零(我尝试使用RtlSecureZeroMemory,但在 kernel.dll 中似乎不存在)下面的类将存储任何数组内置类型的安全。我将解决方案分为两个类以分离出与类型无关的代码。

      第一个类只是分配并保存一个数组。它会在运行时检查模板类型是否为内置类型。不幸的是,我在编译时想不出办法。

      using System;
      using System.Collections.Generic;
      using System.Linq;
      using System.Runtime.InteropServices;
      
      /// <summary>
      /// Manage an array that holds sensitive information.
      /// </summary>
      /// <typeparam name="T">
      /// The type of the array. Limited to built in types.
      /// </typeparam>
      public sealed class SecureArray<T> : SecureArray
      {
          private readonly T[] buf;
      
          /// <summary>
          /// Initialize a new instance of the <see cref="SecureArray{T}"/> class.
          /// </summary>
          /// <param name="size">
          /// The number of elements in the secure array.
          /// </param>
          /// <param name="noswap">
          /// Set to true to do a Win32 VirtualLock on the allocated buffer to
          /// keep it from swapping to disk.
          /// </param>
          public SecureArray(int size, bool noswap = true)
          {
              this.buf = new T[size];
              this.Init(this.buf, ElementSize(this.buf) * size, noswap);
          }
      
          /// <summary>
          /// Gets the secure array.
          /// </summary>
          public T[] Buffer => this.buf;
      
          /// <summary>
          /// Gets or sets elements in the secure array.
          /// </summary>
          /// <param name="i">
          /// The index of the element.
          /// </param>
          /// <returns>
          /// The element.
          /// </returns>
          public T this[int i]
          {
              get
              {
                  return this.buf[i];
              }
      
              set
              {
                  this.buf[i] = value;
              }
          }
      }
      

      下一节课做真正的工作。它告诉垃圾收集器将数组固定在内存中。然后它会锁定它,所以它不会交换。处理后,它将数组归零并解锁,然后告诉垃圾收集器取消固定它。

      /// <summary>
      /// Base class of all <see cref="SecureArray{T}"/> classes.
      /// </summary>
      public class SecureArray : IDisposable
      {
          /// <summary>
          /// Cannot find a way to do a compile-time verification that the
          /// array element type is one of these so this dictionary gets
          /// used to do it at runtime.
          /// </summary>
          private static readonly Dictionary<Type, int> TypeSizes =
              new Dictionary<Type, int>
                  {
                      { typeof(sbyte), sizeof(sbyte) },
                      { typeof(byte), sizeof(byte) },
                      { typeof(short), sizeof(short) },
                      { typeof(ushort), sizeof(ushort) },
                      { typeof(int), sizeof(int) },
                      { typeof(uint), sizeof(uint) },
                      { typeof(long), sizeof(long) },
                      { typeof(ulong), sizeof(ulong) },
                      { typeof(char), sizeof(char) },
                      { typeof(float), sizeof(float) },
                      { typeof(double), sizeof(double) },
                      { typeof(decimal), sizeof(decimal) },
                      { typeof(bool), sizeof(bool) }
                  };
      
          private GCHandle handle;
      
          private uint byteCount;
      
          private bool virtualLocked;
      
          /// <summary>
          /// Initialize a new instance of the <see cref="SecureArray"/> class.
          /// </summary>
          /// <remarks>
          /// You cannot create a <see cref="SecureArray"/> directly, you must
          /// derive from this class like <see cref="SecureArray{T}"/> does.
          /// </remarks>
          protected SecureArray()
          {
          }
      
          /// <summary>
          /// Gets the size of the buffer element. Will throw a 
          /// <see cref="NotSupportedException"/> if the element type is not
          /// a built in type.
          /// </summary>
          /// <typeparam name="T">
          /// The array element type to return the size of.
          /// </typeparam>
          /// <param name="buffer">
          /// The array.
          /// </param>
          /// <returns></returns>
          public static int BuiltInTypeElementSize<T>(T[] buffer)
          {
              int elementSize;
              if (!TypeSizes.TryGetValue(typeof(T), out elementSize))
              {
                  throw new NotSupportedException(
                    $"Type {typeof(T).Name} not a built in type. "
                    + $"Valid types: {string.Join(", ", TypeSizes.Keys.Select(t => t.Name))}");
              }
      
              return elementSize;
          }
      
          /// <summary>
          /// Zero the given buffer in a way that will not be optimized away.
          /// </summary>
          /// <typeparam name="T">
          /// The type of the elements in the buffer.
          /// </typeparam>
          /// <param name="buffer">
          /// The buffer to zero.
          /// </param>
          public static void Zero<T>(T[] buffer)
              where T : struct
          {
              var bufHandle = GCHandle.Alloc(buffer, GCHandleType.Pinned);
              try
              {
                  IntPtr bufPtr = bufHandle.AddrOfPinnedObject();
                  UIntPtr cnt = new UIntPtr(
                       (uint)buffer.Length * (uint)BuiltInTypeElementSize(buffer));
                  RtlZeroMemory(bufPtr, cnt);
              }
              finally
              {
                  bufHandle.Free();
              }
          }
      
          /// <inheritdoc/>
          public void Dispose()
          {
              IntPtr bufPtr = this.handle.AddrOfPinnedObject();
              UIntPtr cnt = new UIntPtr(this.byteCount);
              RtlZeroMemory(bufPtr, cnt);
              if (this.virtualLocked)
              {
                  VirtualUnlock(bufPtr, cnt);
              }
      
              this.handle.Free();
          }
      
          /// <summary>
          /// Call this with the array to secure and the number of bytes in that
          /// array. The buffer will be zeroed and the handle freed when the
          /// instance is disposed.
          /// </summary>
          /// <param name="buf">
          /// The array to secure.
          /// </param>
          /// <param name="sizeInBytes">
          /// The number of bytes in the buffer in the pinned object.
          /// </param>
          /// <param name="noswap">
          /// True to lock the memory so it doesn't swap.
          /// </param>
          protected void Init<T>(T[] buf, int sizeInBytes, bool noswap)
          {
              this.handle = GCHandle.Alloc(buf, GCHandleType.Pinned);
              this.byteCount = (uint)sizeInBytes;
              IntPtr bufPtr = this.handle.AddrOfPinnedObject();
              UIntPtr cnt = new UIntPtr(this.byteCount);
              if (noswap)
              {
                  VirtualLock(bufPtr, cnt);
                  this.virtualLocked = true;
              }
          }
      
          [DllImport("kernel32.dll")]
          private static extern void RtlZeroMemory(IntPtr ptr, UIntPtr cnt);
      
          [DllImport("kernel32.dll")]
          static extern bool VirtualLock(IntPtr lpAddress, UIntPtr dwSize);
      
          [DllImport("kernel32.dll")]
          static extern bool VirtualUnlock(IntPtr lpAddress, UIntPtr dwSize);
      }
      

      要使用该类,只需执行以下操作:

      using (var secret = new SecureArray<byte>(secretLength))
      {
          DoSomethingSecret(secret.Buffer);
      }
      

      现在,这个类做了两件你不应该轻易做的事情,首先,它固定了内存。这会降低性能,因为垃圾收集器现在必须绕过它无法移动的内存。其次,它可以锁定内存中操作系统可能希望换出的页面。这会缩短系统上的其他进程,因为现在它们无法访问该 RAM。

      为了最大程度地减少SecureArray&lt;T&gt; 的不利影响,请不要大量使用它,而只在短时间内使用它。如果您想将数据保留更长时间,则需要对其进行加密。为此,您最好的选择是 ProtectedData 类。不幸的是,这会将您的敏感数据放入不安全的字节数组中。您可以从那里做的最好的事情是快速复制到SecureArray&lt;byte&gt;.Buffer,然后在敏感字节数组上复制SecureArray.Zero

      【讨论】:

      【解决方案5】:

      您可以使用 SecureString 来存储字节数组。

        SecureString testString = new SecureString();
      
        // Assign the character array to the secure string.
        foreach (byte b in bytes)
           testString.AppendChar((char)b);
      

      然后您只需反转该过程以取回字节。


      这不是唯一的方法,您始终可以使用 MemoryBuffer 和 System.Security.Cryptography 之外的东西。但这是唯一专门设计用于以这种方式确保安全的东西。您必须使用 System.Security.Cryptography 创建所有其他内容,这可能是您的最佳选择。

      【讨论】:

      • 那么这是 .NET 库中唯一的“安全”结构吗?
      • 如果字节的值为 0,这可能会导致问题 - 因为您要将它们放入以空字符结尾的字符串中。
      • SecureString 是否以空值终止?
      • 我不这么认为。没有 .NET 字符串以 null 结尾。
      • 不,这不是唯一的方法,你总是可以使用 MemoryBuffer 和 System.Security.Cryptography 之外的东西。但这是唯一专门设计用于以这种方式确保安全的东西。您必须使用 System.Security.Cryptography 创建所有其他内容,这可能是您的最佳选择。
      【解决方案6】:

      一个选项:

      您可以将字节存储在内存流中,并使用System.Security.Cryptography 命名空间中的任何提供程序进行加密。

      【讨论】:

      • 没错,但在加密之前,它很容易受到攻击。
      • 是的 - 但使用任何技术都是如此 - 如果它是静态的,您可以提前对其进行加密,并读取加密的字节 - 但如果它是动态的,它总是会在某个时候不受保护地存在(包括如果您将其输入 SecureString)。这是关于减少易受攻击的窗口而不是消除它。
      • SecureString 已被弃用。
      猜你喜欢
      • 2015-06-14
      • 1970-01-01
      • 2015-07-16
      • 1970-01-01
      • 2013-07-14
      • 2012-05-05
      • 2011-01-31
      • 2018-07-11
      • 1970-01-01
      相关资源
      最近更新 更多