c# 并从内存中删除敏感数据和所有垃圾收集副本

Posted

技术标签:

【中文标题】c# 并从内存中删除敏感数据和所有垃圾收集副本【英文标题】:c# and removing sensitive data and all garbage collection copies from memory 【发布时间】:2015-03-14 11:04:44 【问题描述】:

我正在尝试保护我的 Windows 服务项目免受内存刮板的影响。我正在尝试存储一些极其敏感的数据。让我们以信用卡号“1234-5678-1234-5678”为例。我需要能够将这些数据存储到两个主要的 C# 对象中,并在完成后将其删除。

我已经能够在 *** 示例的帮助下构建的自定义类中存储和删除敏感数据:

public void DestorySensitiveData()

    try
    
        unsafe
        
            int iDataSize = m_chSensitiveData.Length;
            byte[] clear = new byte[iDataSize];
            for (int i = 0; i < clear.Length; i++)
            
                clear[i] = 70;  //fixed ascii character
            

            fixed (char* ptr = &m_chSensitiveData[0])
            
                System.Runtime.InteropServices.Marshal.Copy(clear, 0, new IntPtr(ptr), iDataSize);
            
        
    
    catch (Exception e)
    

    

通过使用字符数组而不是字符串,我能够覆盖/擦除内存中的敏感数据。如果没有这个类,.NET 和字符串的内存管理可以并将这些数据复制到任何需要它的地方。即使我的课程超出范围或我试图调用 dispose,我也可以运行内存刮擦并以纯文本形式找到我的敏感数据,就像您阅读本段一样容易。

我正在寻找 Streams 的实践或方法。我需要能够将敏感数据移入/移出 System.Diagnostics.Process() 甚至是文件。当我使用它时,数据以纯文本形式显示是“可以的”——当我用完它时,它就不能在内存中的任何地方保留。这包括由内存管理或垃圾收集制作的副本。

不起作用的示例:

Strings/StringBuilders:它们在不断更新自己,到处都是碎片。 SecureString:类在纸面上和 MSDN 上看起来都很棒。但是您必须使用字符串来加载和卸载此对象(请参阅第 1 项) 数组列表。真的是二维的字符串问题。

我什至尝试创建一个单独的 EXE 项目并在该进程中执行我对内存敏感的工作。当我不得不重载输入/输出流时,我有点吃不消了。

流似乎被复制到各处。在我的应用程序中,我创建了一个流,将敏感数据加载到其中然后完成。我加载了我的敏感数据一次,执行完成后,我在原始内存中发现了大约十几个完整的副本。

以前有没有人在 .NET 中遇到过类似的问题?他们是如何解决的?

更新:2015 年 1 月 16 日:

我必须通过在同一台机器上运行的软件发送信用卡进行处理:

        proc.StartInfo.CreateNoWindow = true;
        proc.StartInfo.UseShellExecute = false;
        proc.StartInfo.RedirectStandardInput = true;
        proc.StartInfo.RedirectStandardOutput = true;
        proc.StartInfo.WorkingDirectory = @"<full working path>";

        proc.StartInfo.FileName = @"<vendor EXE>"; 
        proc.StartInfo.Arguments = "<args>";                    
        proc.Start();                                       
        //pTran.ProcessorData.ProcID = proc.Id;
        StreamWriter myWriter = proc.StandardInput;
        StreamReader myReader = proc.StandardOutput;
        // Send request API to Protobase

        //I need a process to send the data
        //even if I use a protected object to hold it, 
        //the 'myWriter' cannot be controlled
        **myWriter.Write(sSensitiveData);**  
        myWriter.Flush();
        myWriter.Close();
        // Read Response API
        string sResponseData = myReader.ReadToEnd();
        Console.Write(sResponseData);
        proc.WaitForExit();
        proc.Close();
        myReader.Close();

这就是我询问 Stream 类并破坏它们包含的内存的原因。与我们作为哈希存储和比较的密码不同,此数据必须由我们的供应商读取。

@Greg:我喜欢你在我的流程和供应商之间相互同意加密的想法。我已经在研究这个角度了。

除了数据加密和允许 GC 到处复制片段之外,有没有办法从 Stream 类型的类中清理内存?

【问题讨论】:

您是否考虑过对值进行强加密并在目标或源计算机上编写或使用一些解密代码..? 您能否更具体地说明为什么 SecureString 不能满足您的需求? 是的! SecureString 对我不起作用的原因是我必须将此信息发送给第 3 方供应商,然后阅读响应。我正在创建一个 System.Diagnostics.Process() 并重载输入/输出以传递数据。我知道的唯一可以做到这一点的对象是 Stream/StreamWriter 类。我还有一个 File I/0 选项(需要调用 sdelete.exe),但 C# 中的 File I/O 也不能使用 SecureString。 读入一个预分配的字节数组。然后将该值转换为 SecureString 并擦除字节数组。此时无法从内存中读取。当你需要做 I/O 时,做相反的事情。将 SecureString 中的值放入字节数组中,传输并再次擦除。当然,在传输过程中,它位于内存和 I/O 通道上,因此可以从那里读取。理论上,至少。 一个建议 - 当您创建 m_chSensitiveData 对象时,请务必使用 GCHandle.Alloc(m_chSensitiveData, GCHandleType.Pinned) 将其固定,并将句柄存储为成员,然后在清除后在句柄上调用 .Free()。如果/当堆被压缩时,这将防止您的对象被移动,因为移动它将留下位直到被覆盖。 fixed 已经在其范围内执行此操作,但您可能不希望它在对象的生命周期内移动。但是,我很惊讶 Array.Clear() 对于 char 数组(值类型)来说还不够好。 【参考方案1】:

首先 - 感谢大家的想法和帮助!

我们无法使用 SecureString,因为我们正在向第三方发送数据。由于 System.Process() 仅允许流,因此无法使用字符数组。然后我们构建了一个 c++ MiddleLayer 来接收加密数据,使用 STARTUPINFO 解密和传输。我们最初的解决方案使用流进行 I/O,这是一个“几乎完美”的解决方案,因为我们只需要保留在内存中的 c++ 流。

我们联系了 Microsoft 寻求帮助,他们提出了几乎相同的解决方案,但使用 Handles/PIPES 而不是流,因此您可以关闭管道的两端。

这是示例,虚拟代码。希望这对其他人有帮助!

#include "stdafx.h"
#include <windows.h>
#include <stdio.h>
#include <tchar.h>
#include <WinBase.h>
#include <wincrypt.h>


int _tmain(int argc, _TCHAR* argv [])

HANDLE hFile = INVALID_HANDLE_VALUE;
STARTUPINFO StartInfo;
PROCESS_INFORMATION pi;
SECURITY_ATTRIBUTES sa;
BOOL fResult;
HANDLE hReadPipe = NULL;
HANDLE hWritePipe = NULL;
HANDLE hSensitiveReadPipe = NULL;
HANDLE hSensitiveWritePipe = NULL;
DWORD dwDataLength = 0;
DWORD dwWritten;
DWORD dwRead;
char responseData[1024];
char *sensitiveDataChar = NULL;
char *otherStuff = NULL;
char *sensitiveDataCharB = NULL;
DWORD dwSectorsPerCluster;
DWORD dwBytesPerSector;
DWORD dwNumberOfFreeClusters;
DWORD dwTotalNumberOfClusters;
SIZE_T SizeNeeded;

__try


    sensitiveDataChar = "YourSensitiveData";
    int lastLoc = 0;


    ZeroMemory(&sa, sizeof(sa));
    sa.nLength = sizeof(sa);
    sa.bInheritHandle = TRUE;

    // Create Pipe to send sensitive data
    fResult = CreatePipe(&hSensitiveReadPipe, &hSensitiveWritePipe, &sa, 0);
    if (!fResult)
    
        printf("CreatePipe failed with %d", GetLastError());
        __leave;
    

    // Create Pipe to read back response
    fResult = CreatePipe(&hReadPipe, &hWritePipe, &sa, 0);
    if (!fResult)
    
        printf("CreatePipe failed with %d", GetLastError());
        __leave;
    

    // Initialize STARTUPINFO structure
    ZeroMemory(&StartInfo, sizeof(StartInfo));
    StartInfo.cb = sizeof(StartInfo);
    StartInfo.dwFlags = STARTF_USESTDHANDLES;
    StartInfo.hStdInput = hSensitiveReadPipe;
    StartInfo.hStdOutput = hWritePipe;
    StartInfo.hStdError = GetStdHandle(STD_ERROR_HANDLE);

    ZeroMemory(&pi, sizeof(pi));

    // Launch third party app
    fResult = CreateProcess(_T("c:\\temp\\ThirdParty.exe"), NULL, NULL, NULL, TRUE, 0, NULL, _T("c:\\temp"), &StartInfo, &pi);
    if (!fResult)
    
        printf("CreateProcess failed with %d", GetLastError());
        __leave;
    

    dwDataLength = strlen(sensitiveDataChar);
    // Write to third party's standard input
    fResult = WriteFile(hSensitiveWritePipe, sensitiveDataChar, dwDataLength, &dwWritten, NULL);
    if (!fResult)
    
        printf("WriteFile failed with %d", GetLastError());
        __leave;
    
    FlushFileBuffers(hSensitiveWritePipe);
    CloseHandle(hSensitiveReadPipe);

    DWORD dwLength = 1024;
    printf("Waiting...\n");

    // Read from third party's standard out
    fResult = ReadFile(hReadPipe, responseData, dwLength, &dwRead, NULL);
    if (!fResult)
    
        printf("ReadFile failed with %d\n", GetLastError());
        __leave;
    
    responseData[dwRead] = '\0';

    printf("%s\n", responseData);       

__finally

    // Clean up
    ZeroMemory(responseData, sizeof(responseData));

    if (hFile != INVALID_HANDLE_VALUE)
    
        FlushFileBuffers(hFile);
        CloseHandle(hFile);
    

    if (hSensitiveWritePipe != NULL)
    
        FlushFileBuffers(hSensitiveWritePipe);
        CloseHandle(hSensitiveWritePipe);
    

    if (hSensitiveReadPipe != NULL)
    
        CloseHandle(hSensitiveReadPipe);
    

    if (hReadPipe != NULL) CloseHandle(hReadPipe);
    if (hWritePipe != NULL) CloseHandle(hWritePipe);



【讨论】:

【参考方案2】:

你应该看一下Microsoft .Net 中内置的SecureString,可以在here 找到信息。它允许您加密内存中的数据,以单个字符分隔,并且可以在您调用IDispose、文档here 时立即处理。

System.String 类的实例既是不可变的,当不可变时 不再需要,不能以编程方式安排垃圾 收藏;也就是说,实例在创建后是只读的,并且 无法预测实例何时会被删除 电脑内存。因此,如果 String 对象包含敏感 密码、信用卡号或个人数据等信息, 信息在使用后可能会被泄露 因为您的应用程序无法从计算机内存中删除数据。

重要 这种类型实现了 IDisposable 界面。当你使用完类型后,你应该处理 直接或间接的。要直接处理类型, 在 try/catch 块中调用其 Dispose 方法。处置它 间接地,使用语言结构,例如 using (in C#) 或 Using (在 Visual Basic 中)。有关详细信息,请参阅“使用对象 IDisposable 接口主题中的“实现 IDisposable”部分。

SecureString 对象与 String 对象的相似之处在于它具有 文本值。但是,SecureString 对象的值是 自动加密,可以修改,直到您的应用程序标记 它是只读的,可以通过以下任一方式从计算机内存中删除 您的应用程序或 .NET Framework 垃圾收集器

SecureString的前提如下:

没有要检查、比较和/或转换的成员。 完成后实施IDispose。 实质上将对象强制转换为加密的char []

这种设计是为了确保不会发生意外或恶意暴露

【讨论】:

@hatchet 我读到了,但为什么它不可行? SecureString 的全部意义在于将用户输入转换为加密的 char [],然后在完成后处理它。理论上,如果实施得当是正确的。 我明白你的意思。您必须不遗余力地从字符串创建 SecureString(它是为 char by char 构造设置的)。所以我看不出 OP 将其驳回的依据。 @hatchet 将string 强制转换为char [] 背后的理论是因为string 是纯文本。通过一次迭代一个char,它应该只存在于引入它的单个点。但是所有其他实例都不会被复制或移动,单个实例只是一个输入。无处可去。 见上文,但 SecureString 类不能用于与供应商或文件 I/O 通信。我宁愿流式传输到我们的第 3 方,以免写入物理磁盘,但我确实有这个选项。无论哪种情况,都需要 Stream 类型的对象,而 GC 倾向于将其复制到整个内存空间。 @BrianJenkins 你最好使用某种形式的加密或散列。这样您就可以毫无问题地发送它,然后用户在任何时候输入它都会被处理。您应该能够通过MemoryStream 编写您的char [],这样您就可以使用SecureString。真正的问题是,你想做什么以及对谁。【参考方案3】:

我建议要么加密敏感数据并且永远不要维护纯文本,要么可能使用 SecureString (http://msdn.microsoft.com/en-us/library/system.security.securestring%28v=vs.110%29.aspx)。

您可以使用空字符串初始化 SecureString,然后使用 AppendChar 构建字符串。这避免了保留字符串的纯文本的需要。

【讨论】:

@hatchet 仅仅因为 OP 表示他已经尝试过了,并不意味着使用 SecureString 是不正确的.. 尤其是当我们看不到任何代码实现或使用时.. 我想什么理查德声明关于 OP 的选项是有效的 @hatchet,请参阅修改后的答案。谢谢 MethodMan。 没问题.. 很高兴看到来自 OP 的编码工作,这样我们就可以理解 What, and Why 关于为什么某些东西不适合他们的原因.. 我正在尝试与我们的供应商就相互加密过程进行合作。如果他们可以处理解密,这是一个可能的解决方案。令我感到惊讶的是,除了 SecureString 之外,C# 没有更好的安全对象集来处理信用卡、社会保险号、密码等敏感数据,而且必须以几乎不安全的方式加载/卸载安全对象。这是一个鸡/蛋的问题 处理密码(等)的最佳方法是永远不要使用纯文本。通常只有服务之间传递的哈希 (SHA 256)。

以上是关于c# 并从内存中删除敏感数据和所有垃圾收集副本的主要内容,如果未能解决你的问题,请参考以下文章

从内存中删除敏感信息

前端周报:Vue 新语法糖引争议;VSCode 现内存泄漏 BUG,官方置若罔闻引不满;百度应用收集用户敏感数据被谷歌短暂下架

20171120

垃圾收集器二

安全牛学习笔记收集敏感数据隐藏痕迹

python敏感词过滤