C# 确保 List<T> 中元素的随机内存分配
C# ensuring shuffled memory allocation for elements in List<T>
这里关于托管环境中的内存分配的大多数答案都是 "You don't need to bother with such things, let the platform handle it"。事实上,在某些情况下你确实需要担心这些事情。
我有一个对象列表,在内存转储的情况下,我不想在连续的连续地址行上按顺序分配。我要他们分散。
有什么办法(技巧或技巧)可以确保对象不是按顺序分配的,并且不能被识别为内存转储中的序列?
谢谢!
编辑: 这样做的原因是我试图绕过 MySQL for .NET 要求我将敏感数据作为连接字符串传递的方式。我的想法是将所有内容保存在一个分散的字符列表中,然后 assemble 在 connection.Open() 之前,然后立即将其处理掉。有没有更好的办法?我不想像那样将数据库凭据保存在内存中。
结论:似乎不可能通过分散敏感数据来防止内存转储。在我的特定情况下,我将尽可能使用 SecureString,并将寻找其他方法来避免将未加密的密码暴露给 MySQL。
更新: 经过一番挖掘,这就是我最终做的事情:我所有的 SQL 都是由本地服务执行的,帐户仅限于存储过程和本地主机。为了请求 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 中存储尽可能短的时间。
这是我能想到的最好的。有新内容我再更新,谢谢大家
你想通过这个达到什么目的?
如果是安全的话,那么你可以加密你的data/properties,或者使用SecureStrings就足够了。 GC 就是优化内存,而不是对其进行碎片化(这实际上是您所要求的)。
内存布局
由于 .NET 中列表的内部结构,将 .NET 对象移动到内存中的随机位置没有任何好处。事实上,您的对象可能已经在 "random" 地址,垃圾收集器可能会随时更改它(基于内存需求,而不是由于列表排序)。
唯一的例外是数组,它们被分配为一个块。正是这些数组将成为问题,因为 ...
在内部,列表是 "pointers" 对象的数组,因此与对象所在的位置无关,一旦有人访问了该数组,他就能够按顺序访问所有对象。
下图有望说明这一点:
使用windbg debugger and a .NET extension called sos,下面的命令可以很容易地找到一个数组并打印其中的对象:
!dumpheap -type List
!dumparray
!dumpobj
在您的情况下,使用 SecureString(正如其他人已经提到的)将确保密码在内存中加密并且没有人可以轻易(仍然有可能)泄露它。
覆盖内存
关于评论中的后续问题
Can you please tell me if setting myByteArray[n] = 0; actually zeroes out the previous value, or allocates new memory for the 0 and stores a reference to it, while the original value at n still resides somewhere waiting for the GC? It is related to my question.
让我们编写以下程序:
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 IMHO 不是一个好的统计程序,所以我使用了 HxD,它提供了以下统计数据:
结论:未装箱的值被覆盖,而装箱的则没有。
这里关于托管环境中的内存分配的大多数答案都是 "You don't need to bother with such things, let the platform handle it"。事实上,在某些情况下你确实需要担心这些事情。 我有一个对象列表,在内存转储的情况下,我不想在连续的连续地址行上按顺序分配。我要他们分散。 有什么办法(技巧或技巧)可以确保对象不是按顺序分配的,并且不能被识别为内存转储中的序列? 谢谢!
编辑: 这样做的原因是我试图绕过 MySQL for .NET 要求我将敏感数据作为连接字符串传递的方式。我的想法是将所有内容保存在一个分散的字符列表中,然后 assemble 在 connection.Open() 之前,然后立即将其处理掉。有没有更好的办法?我不想像那样将数据库凭据保存在内存中。
结论:似乎不可能通过分散敏感数据来防止内存转储。在我的特定情况下,我将尽可能使用 SecureString,并将寻找其他方法来避免将未加密的密码暴露给 MySQL。
更新: 经过一番挖掘,这就是我最终做的事情:我所有的 SQL 都是由本地服务执行的,帐户仅限于存储过程和本地主机。为了请求 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 中存储尽可能短的时间。 这是我能想到的最好的。有新内容我再更新,谢谢大家
你想通过这个达到什么目的?
如果是安全的话,那么你可以加密你的data/properties,或者使用SecureStrings就足够了。 GC 就是优化内存,而不是对其进行碎片化(这实际上是您所要求的)。
内存布局
由于 .NET 中列表的内部结构,将 .NET 对象移动到内存中的随机位置没有任何好处。事实上,您的对象可能已经在 "random" 地址,垃圾收集器可能会随时更改它(基于内存需求,而不是由于列表排序)。
唯一的例外是数组,它们被分配为一个块。正是这些数组将成为问题,因为 ...
在内部,列表是 "pointers" 对象的数组,因此与对象所在的位置无关,一旦有人访问了该数组,他就能够按顺序访问所有对象。
下图有望说明这一点:
使用windbg debugger and a .NET extension called sos,下面的命令可以很容易地找到一个数组并打印其中的对象:
!dumpheap -type List
!dumparray
!dumpobj
在您的情况下,使用 SecureString(正如其他人已经提到的)将确保密码在内存中加密并且没有人可以轻易(仍然有可能)泄露它。
覆盖内存
关于评论中的后续问题
Can you please tell me if setting myByteArray[n] = 0; actually zeroes out the previous value, or allocates new memory for the 0 and stores a reference to it, while the original value at n still resides somewhere waiting for the GC? It is related to my question.
让我们编写以下程序:
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 IMHO 不是一个好的统计程序,所以我使用了 HxD,它提供了以下统计数据:
结论:未装箱的值被覆盖,而装箱的则没有。