【问题标题】:C# ensuring shuffled memory allocation for elements in List<T>C# 确保 List<T> 中元素的随机内存分配
【发布时间】:2016-06-15 10:00:42
【问题描述】:

这里关于托管环境中内存分配的大多数答案都说“您不需要为这些事情烦恼,让平台来处理它”。事实上,在某些情况下,您确实需要担心这些事情。 我有一个对象列表,在内存转储的情况下,我不想在连续地址的直线行上按顺序分配。我要他们分散。 有什么办法(一个技巧或黑客会做)来确保对象不是按顺序分配的,并且不能被识别为内存转储中的一个序列? 谢谢!

编辑: 这样做的原因是我试图规避 MySQL for .NET 要求我将敏感数据作为连接字符串传递的方式。我的想法是将所有内容保存在一个分散的字符列表中,然后在 connection.Open() 之前组装它,然后立即处理它。有没有更好的办法?我宁愿不要像那样将数据库凭据保存在内存中。

结论:似乎不可能通过分散敏感数据来防止内存转储。在我的具体情况下,我将尽可能使用 SecureString,并将寻找其他方法不将未加密的密码暴露给 MySQL。

更新:经过一番挖掘,这就是我最终要做的事情:我所有的 SQL 都由本地服务执行,帐户仅限于存储过程和 localhost。为了请求 SP 调用,客户端必须通过 SHA512 哈希密码 + salt 进行身份验证。在客户端,密码从文本框中逐个字符地读取,并附加到安全字符串上,然后将文本替换为系统密码字符。然后对安全字符串进行哈希处理,并将原始数据清零,如下所示:

public static string GetSHA512(SecureString secureInput)
    {
        if (secureInput == null)
            throw new ArgumentNullException("securePassword");

        IntPtr unmanagedString = IntPtr.Zero;
        byte[] dataUnmanaged = new byte[secureInput.Length];
        try
        {
            unmanagedString = Marshal.SecureStringToGlobalAllocAnsi(secureInput);
            Marshal.Copy(unmanagedString, dataUnmanaged, 0, dataUnmanaged.Length);

            using (SHA512 shaM = new SHA512Managed())
            {
                byte[] hash = shaM.ComputeHash(dataUnmanaged);
                int i = 0;
                while (i < dataUnmanaged.Length)
                {
                    dataUnmanaged[i] = 0;
                    i++;
                }
                StringBuilder hex = new StringBuilder(hash.Length * 2);
                foreach (byte b in hash)
                    hex.AppendFormat("{0:x2}", b);
                return hex.ToString();
            }
        }
        finally
        {
            int i = 0;
            while (i < dataUnmanaged.Length)
            {
                dataUnmanaged[i] = 0;
                i++;
            }
            Marshal.ZeroFreeGlobalAllocAnsi(unmanagedString);
        }
    }

生成的哈希值进一步使用随机盐进行哈希处理,并与盐一起发送到服务进行确认。服务用盐重新散列其散列并进行比较。一旦获得批准,将通过不断更改客户端和服务器跟踪的唯一会话 ID 来进行进一步的身份验证。任何劫持和会话立即失效。这样,密码只使用一次,只传输双散列,并尽可能短地存储在 SecureString 中。 这是我能想到的最好的。如果有新内容我会进一步更新,谢谢大家。

【问题讨论】:

  • 没有。垃圾收集器无论如何都会重新排列它们。即使它们在内存中没有“排序”,它们也很容易找到。让我提一下WinDbg + SOS + !dumpheap
  • 感谢您的评论。我根本不精通内存转储,所以我不知道 SOS 是什么,但根据您的估计,从内存转储中按照原始顺序从集合中查找对象有多容易?另外,你是说GC会按照集合中的顺序对内存中的对象进行排序?
  • @Daniel “从内存转储中按照原始顺序从集合中查找对象有多容易?”。这是一个相当简单的任务。
  • 找到有问题的列表肯定需要一些时间,因为转储中通常有很多列表(如果不是 HelloWorld)。完成后,很简单:stackoverflow.com/q/2198805/480982
  • 使用SecureString 和加密代替。

标签: c# memory memory-management allocation


【解决方案1】:

内存布局

由于 .NET 中列表的内部结构,将 .NET 对象移动到内存中的随机位置没有任何好处。事实上,您的对象可能已经位于“随机”地址,而垃圾收集器可能随时更改它(基于内存需求,而不是由于列表排序)。

唯一的例外是数组,它们被分配为一个块。正是这些数组才是问题所在,因为...

在内部,列表是指向对象的“指针”数组,因此与对象所在的位置无关,一旦有人可以访问该数组,他就可以按顺序访问所有对象。

下图希望能说明这一点:

使用 调试器和名为 的.NET 扩展,以下命令可以很容易地找到一个数组并打印其中的对象:

!dumpheap -type List
!dumparray
!dumpobj

在您的情况下,使用 SecureString(正如其他人已经提到的那样)将确保密码在内存中被加密,并且没有人可以轻易地(仍然有可能)泄露它。

覆盖内存

关于cmets的后续问题

你能告诉我是否设置 myByteArray[n] = 0;实际上将先前的值清零,或者为 0 分配新内存并存储对它的引用,而 n 处的原始值仍然驻留在某个等待 GC 的地方?这与我的问题有关。

让我们编写以下程序:

using System;

namespace OverwriteValuesInMemory
{
    class Program
    {
        static void Main()
        {
            // Ensure we have it on the heap
            var program = new Program();
            program.FillAndRefill();
            Console.WriteLine("Create a dump now");
            Console.ReadLine();
            // Access to prevent it from being optimized away
            program.nonboxed[0]=0;
        }

        const int M16 = 16 * 1024 * 1024;
        byte[] nonboxed = new byte[M16];
        object[] boxed = new object[M16];

        void FillAndRefill()
        { 
            Fill(nonboxed, 42);
            Fill(boxed, 23);
            Fill(nonboxed, 0x42);
            Fill(boxed, 0x23);
        }

        private void Fill(object[] array, int value)
        {
            for (int i = 0; i < array.Length; ++i)
                array[i] = value;
        }

        private void Fill(byte[] array, byte value)
        {
            for (int i = 0; i < array.Length; ++i)
                array[i] = value;
        }
    }
}

Ans 然后让我们用 WinDbg + SOS 调试它。某些命令的输出可能会缩短,因为它对结果并不重要,但我想包含命令以使其独立:

0:005> .symfix
0:005> .reload
0:005> .loadby sos clr
0:005> ~0s

我们可以在栈上找到对Program的引用

0:000> !dso
[...]
0042EE4C 02553128 OverwriteValuesInMemory.Program
[...]

或者,在堆中的所有对象中搜索它

0:000> !dumpheap -type Program
 Address       MT     Size
02553128 001c4d44       16  

查看对象的细节

0:000> !do 02553128
Name:        OverwriteValuesInMemory.Program
MethodTable: 001c4d44
EEClass:     001c1394
Size:        16(0x10) bytes
File:        E:\Projekte\SVN\HelloWorlds\OverwriteValuesInMemory\OverwriteValuesInMemory\bin\Release\OverwriteValuesInMemory.exe
Fields:
      MT    Field   Offset                 Type VT     Attr    Value Name
70cd35fc  4000002        4        System.Byte[]  0 instance 04821010 nonboxed
70cced0c  4000003        8      System.Object[]  0 instance 06981010 boxed

我们找到了盒装和非盒装值的 2 个成员,我们可以在原始内存中检查它们。未装箱的数组直接在里面包含值:

0:000> db 04821010 L20
04821010  fc 35 cd 70 00 00 00 01-42 42 42 42 42 42 42 42  .5.p....BBBBBBBB
04821020  42 42 42 42 42 42 42 42-42 42 42 42 42 42 42 42  BBBBBBBBBBBBBBBB

虽然盒装的情况并非如此:

0:000> db 06981010 L20
06981010  0c ed cc 70 00 00 00 01-d8 58 bd 1a e4 58 bd 1a  ...p.....X...X..
06981020  f0 58 bd 1a fc 58 bd 1a-08 59 bd 1a 14 59 bd 1a  .X...X...Y...Y..

查看指针大小的框,我们看到距离为 0x0C (12) 的值,这表明该对象存在一些元数据开销。

0:000> dp 06981010 L10
06981010  70cced0c 01000000 1abd58d8 1abd58e4
06981020  1abd58f0 1abd58fc 1abd5908 1abd5914
06981030  1abd5920 1abd592c 1abd5938 1abd5944
06981040  1abd5950 1abd595c 1abd5968 1abd5974

查看它们中的第一个给我们的预期值是 35 (0x23):

0:000> !do 1abd58d8
Name:        System.Int32
MethodTable: 70cd07a0
EEClass:     7090fd30
Size:        12(0xc) bytes
File:        C:\Windows\Microsoft.Net\assembly\GAC_32\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll
Fields:
      MT    Field   Offset                 Type VT     Attr    Value Name
70cd07a0  400055f        4         System.Int32  1 instance       35 m_value

所以Program 仅引用最后设置的值。这是预期的。但是内存呢?旧值是否潜伏在等待垃圾收集? WinDbg 恕我直言,这不是一个好的统计程序,所以我使用了 HxD,它提供了以下统计信息:

结论:非装箱值被覆盖,而装箱值未被覆盖。

【讨论】:

  • 你好,对不起,我又来了。你能告诉我是否设置 myByteArray[n] = 0;实际上将先前的值清零,或者为 0 分配新内存并存储对它的引用,而 n 处的原始值仍然驻留在某个等待 GC 的地方?这与我的问题有关:)
  • @Daniel:更新了我的答案以包含评论中的问题
  • 非常感谢,信息量真丰富!我会用我最终做了什么来更新我的问题。再次感谢!
【解决方案2】:

你想通过这个实现什么?

如果它是安全的,那么您可以加密您的数据/属性,或者使用SecureStrings 就足够了。 GC 是关于优化内存,而不是对其进行分段(这实际上是您所要求的)。

【讨论】:

  • 感谢您的回复,请查看我的编辑内容。
  • 您只是想保护连接字符串吗?如前所述,使用 SecureString 将其安全地存储在内存中。点击并查看文档。
猜你喜欢
  • 1970-01-01
  • 2011-06-04
  • 2012-02-08
  • 2018-12-16
  • 2021-04-28
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多