• 编写高质量代码改善C#程序的157个建议——建议118:使用SecureString保存密钥等机密字符串


    建议118:使用SecureString保存密钥等机密字符串

    托管代码中的字符串是一类特殊的对象,它们不可用被改变。每次使用System.String类张的方法之一时,或者使用此类型进行运算时(如赋值、拼接等),都要在内存中创建新的字符串对象,也就是为该新对象分配新的空间。这就带来了两个问题:

    1. 原来的字符串是不是还在内存当中?
    2. 如果在内存当中,那么机密数据(如密码)该如何保存才足够安全?

    针对第一个问题,我们来看一段代码:

            static void Method1()
            {
                string str = "liming";
                Console.WriteLine(str);
            }
    
            static void Main(string[] args)
            {
                Method1(); //在此处打上断点
                Console.ReadKey();
            }

    在Method1方法处打上断点。在VS中让程序执行到此处,在即时窗口中相继运行命令:

    .load sos.dll

    !dso

    运行结果:

    .load sos.dll
    已加载扩展 G:WindowsMicrosoft.NETFrameworkv4.0.30319sos.dll
    !dso
    PDB symbol for clr.dll not loaded
    OS Thread Id: 0x9806c (622700)
    ESP/REG  Object   Name
    003EE700 027022a8 System.Object[]    (System.String[])
    003EE9F4 027022a8 System.Object[]    (System.String[])
    003EEDA0 027022a8 System.Object[]    (System.String[])
    003EEDB8 027022a8 System.Object[]    (System.String[])
    003EEDC4 027022a8 System.Object[]    (System.String[])
    003EEE40 027022a8 System.Object[]    (System.String[])
    003EEF9C 027022a8 System.Object[]    (System.String[])
    003EEFD4 027022a8 System.Object[]    (System.String[])
    003EF510 02701238 System.SharedStatics

    打开“调试”->“窗口”->“内存”->“内存1”窗口,找到对应Object列的内存地址"027022a8",然后在内存窗口中输入。

    由于此时还没有进入到Method1中,所以内存当中不存在字符串“liming”。接着让程序运行到方法内部,可以看到内存中应经存在“liming”了。

    这就出现了一个问题,如果有人恶意扫描你的内存,程序中所保存的机密信息将无处可逃。幸好FCL提供了System.Security.SecureString,SecureString表示一个应保密的文本,它在初始化时就已经被加密了。使用SecureString的示例如下:

            static System.Security.SecureString secureString = new System.Security.SecureString();
    
            static void Method2()
            {
                secureString.AppendChar('l');
                secureString.AppendChar('i');
                secureString.AppendChar('m');
                secureString.AppendChar('i');
                secureString.AppendChar('n');
                secureString.AppendChar('g');
            }
    
            static void Main(string[] args)
            {
                Method2(); 
                Console.ReadKey();
            }

    使用相同的调试手法可以发现,再次进入Method2后,已经找不到对应的字符串“liming”了。但是,核心数据保存问题已经解决了,可是文本总是要取出来的,只要取出来不是就会被发现吗?这个问题没法避免,但是我们可以做到文本使用完毕就释放掉,代码如下:

            static void Method3()
            {
                secureString.AppendChar('l');
                secureString.AppendChar('i');
                secureString.AppendChar('m');
                secureString.AppendChar('i');
                secureString.AppendChar('n');
                secureString.AppendChar('g');
                IntPtr addr = Marshal.SecureStringToBSTR(secureString);
                string temp = Marshal.PtrToStringBSTR(addr);
                //使用该机密文本做一些事情
                ///=======开始清理内存
                //清理掉非托管代码中对应的内存的值
                Marshal.ZeroFreeBSTR(addr);
                //清理托管代码对应的内存的值(采用重写的方法)
                int id = GetProcessID();
                byte[] writeBytes = Encoding.Unicode.GetBytes("xxxxxx");
                IntPtr intPtr = Open(id);
                unsafe
                {
                    fixed (char* c = temp)
                    {
                        WriteMemory((IntPtr)c, writeBytes, writeBytes.Length);
                    }
                }
                ///=======清理完毕
            }
    
            static PROCESS_INFORMATION processInfo = new PROCESS_INFORMATION();
    
            public static int GetProcessID()
            {
                Process p = Process.GetCurrentProcess();
                return p.Id;
            }
    
            public static IntPtr Open(int processId)
            {
                IntPtr hProcess = IntPtr.Zero;
                hProcess = ProcessAPIHelper.OpenProcess(ProcessAccessFlags.All, false, processId);
                if (hProcess == IntPtr.Zero)
                    throw new Exception("OpenProcess失败");
                processInfo.hProcess = hProcess;
                processInfo.dwProcessId = processId;
                return hProcess;
            }
    
            static int WriteMemory(IntPtr addressBase, byte[] writeBytes, int writeLength)
            {
                int reallyWriteLength = 0;
                if (!ProcessAPIHelper.WriteProcessMemory(processInfo.hProcess, addressBase, writeBytes, writeLength, out reallyWriteLength))
                {
                    throw new Exception();
                }
                return reallyWriteLength;
            }
    
            [StructLayout(LayoutKind.Sequential)]
            internal struct PROCESS_INFORMATION
            {
                public IntPtr hProcess;
                public IntPtr hThread;
                public int dwProcessId;
                public int dwThreadId;
            }
    
            [Flags]
            enum ProcessAccessFlags : uint
            {
                All = 0x001F0FFF,
                Terminate = 0x00000001,
                CreateThread = 0x00000002,
                VMOperation = 0x00000008,
                VMRead = 0x00000010,
                VMWrite = 0x00000020,
                DupHandle = 0x00000040,
                SetInformation = 0x00000200,
                QueryInformation = 0x00000400,
                Synchronize = 0x00100000
            }
    
            static class ProcessAPIHelper
            {
                [DllImport("kernel32.dll")]
                public static extern IntPtr OpenProcess(ProcessAccessFlags dwDesiredAccess, [MarshalAs(UnmanagedType.Bool)] bool bInheritHandle, int dwProcessId);
    
                [DllImport("kernel32.dll", SetLastError = true)]
                public static extern bool WriteProcessMemory(IntPtr hProcess, IntPtr lpBaseAddress, byte[] lpBuffer, int nSize, out int lpNumberOfBytesWritten);
    
                [DllImport("kernel32.dll", SetLastError = true)]
                public static extern bool ReadProcessMemory(IntPtr hProcess, IntPtr lpBaseAddress, [Out] byte[] lpBuffer, int dwSize, out uint lpNumberOfBytesRead);
    
                [DllImport("kernel32.dll", SetLastError = true)]
                [return: MarshalAs(UnmanagedType.Bool)]
                public static extern bool CloseHandle(IntPtr hObject);
            }

    注意查看上文中的代码:

    IntPtr addr = Marshal.SecureStringToBSTR(secureString);
    string temp = Marshal.PtrToStringBSTR(addr);

    这两行代码表示的就是把机密文本从SecureString取出来,临时赋值给字符串temp。这里存在两个问题:第一行实际调用的是非托管代码,它在内存中也会存储一个“liming”;第二行代码会在托管内存中存储一个“liming”。这两段文本的释放方式是不一样的。前者可以通过使用下面代码释放:

    Marshal.ZeroFreeBSTR(addr);

    而托管内存中的文本,只能通过重写来完成(如上文中,就是重写成无意义的“xxxxxx”了)。当然,没有绝对的安全,因为即便如此,让关键字符串在内存中像流星一样一闪而过,它也存在被捕获的可能性。但是我们通过这种方法降低了数据被破解的概率。

    转自:《编写高质量代码改善C#程序的157个建议》陆敏技

  • 相关阅读:
    2019天梯赛训练1
    Python课程设计 搭建博客
    最容易理解的贪吃蛇小游戏
    数据结构-队列
    数据结构-堆栈(2)
    数据结构-堆栈(1)
    数据结构-线性表(3)
    数据结构-线性表(1)
    linux知识积累
    Maven学习笔记
  • 原文地址:https://www.cnblogs.com/jesselzj/p/4751198.html
Copyright © 2020-2023  润新知