Flier's Sky

天空,蓝色的天空,眼睛看不到的东西,眼睛看得到的东西

导航

http://www.blogcn.com/user8/flier_lu/index.html?id=2309231&run=.0C9B086

随着安全性编程逐渐受到重视,我们需要面对一些以前容易忽视的安全隐患。例如在一个系统字符串中保存当前用户密码或其他敏感信息,则具备权限的其他进程可以很轻松的通过系统提供的 ReadProcessMemory 函数或调试接口,搜索并读取这个字符串的内容,进而了解对此字符串的维护逻辑。在破解软件时一个很常见的方法,就是提供一个特殊的注册码,然后用调试器找到注册算法保存注册码的位置,再通过设置数据断点跟踪注册码的验证算法。要避免这种安全隐患,一个简单的办法是将字符串加密再保存,只在使用的时候解密使用,这样可以一定程度避免敏感信息泄露。CLR 中系统提供的 System.String 虽然功能强大,但因为系统封装的不透明性以及设计上的一些硬伤,使之不适用于保存敏感数据。Shawn Farkas 在其 BLog 的一篇文章(Making Strings More Secure)中讨论了 Whidbey 中为什么要用一个新的 System.Security.SecureString 替换现有 System.String 来实现类似功能。

    首先看看 System.String 为什么不适合保存敏感数据的需求

    1.字符串的内存是在堆中分配的,也就是说其内存完全由 GC 来管理。GC 在进行垃圾收集时,根据具体使用算法不同,完全可能将保存明文密码的字符串在内存中多次拷贝,并留下多个副本,造成安全隐患。
    2.字符串的内容是不加密的,其他进程可以很容易被其他进程通过读进程内存的方式访问。如果进程数据页被交互到硬盘,则还会在硬盘的交换文件中保存敏感数据的内容。
    3.字符串是不可变的,因此一旦要修改一个字符串,则会在内存中留下新旧两份字符串。具体原因和 CLR 内部优化策略请参见我另外一篇文章《CLR中字符串不变性的优化》
    4.因为字符串不可变,所以没有什么好的办法能够显式清除一个字符串的内容。

    在 Whidbey 之前,一般推荐使用字节数组来保存敏感数据,因为字节数组可以被 pin 到一个固定内存位置,并能够显式加密和清除内容。而从 Whidbey 开始,将引入一个新的 System.Security.SecureString 类型专门处理自动加密的需求。
    SecureString 将使用 DPAPI (Data Protection Application-Programming Interface) 完成字符串内容的加密工作,并确保 GC 不会自动处理字符串内容,而是通过 IDisposable 接口和 finalizer 完成加密字符串资源的生命期维护工作。同时 SecureString 还支持将自己设置为只读,避免其他代码修改其内容。
    SecureString 可以从一个数组构造而来,也可以建立空字符串后一个字符一个字符地添加。可以通过 AppendChar()、InsertAt()、RemoveAt() 和 SetAt() 函数对字符串内容进行字符粒度的维护;MakeReadOnly() 和 IsReadOnly() 函数可以确保字符串的只读性;Clear()、Dispose()和 finalizer 可以对字符串的生命期进行维护。
    下面是一个使用 SecureString 的简单例子:
以下为引用:

public static SecureString GetPassword()
{
    SecureString password = new SecureString();

    // get the first character of the password
    ConsoleKeyInfo nextKey = Console.ReadKey(true);

    while(nextKey.Key != ConsoleKey.Enter)
    {
        if(nextKey.Key == ConsoleKey.BackSpace)
        {
            password.RemoveAt(password.Length - 1);

            // erase the last * as well
            Console.Write(nextKey.KeyChar);
            Console.Write(" ");
            Console.Write(nextKey.KeyChar);
        }
        else
        {
            password.AppendChar(nextKey.KeyChar);
            Console.Write("*");
        }

        nextKey = Console.ReadKey(true)
    }

    Console.WriteLine();

    // lock the password down
    password.MakeReadOnly();
    return password;
}


    而在使用 SecureString 的时候也需要注意不要通过 System.String 操作,否则就白忙了,呵呵。可以用类似下面代码的方法,直接操作其内容,如
以下为引用:

IntPtr bstr = Marshal.SecureStringToBSTR(password);

try
{
    // ...
    // use the bstr
    // ...
}
finally
{
    Marshal.ZeroFreeBSTR(bstr);
}



    然后,来看看 SecureString 使用的 DPAPI 是如何对数据进行保护的。Shawn Farkas 在其 Managed DPAPI Part I: ProtectedData 和 Managed DPAPI Part II: ProtectedMemory 两篇文章里面简要的介绍了 DPAPI 的功能和使用方法。

    简单说来常用的就是两对函数,CryptProtectData 和 CryptUnprotectData 使用给定的密钥对指定数据块进行加/解密;CryptProtectMemory 和 CryptUnprotectMemory 则通过 LSA 提供的在一定范围内有效的密钥对数据进行加/解密。前者适用于给定密钥情况下;后者则根据指定范围不同,支持进程内、跨进程和同一登陆帐号等不同范围内的透明加/解密。对面向运行时需求的 SecureString 来说,使用后者足以;而如果需要将加密后内容序列化到磁盘文件或数据库,则需要使用前者。此外还可以通过 CryptAPI 相关函数,提供跨网络的加/解密支持,这是上述两者无法提供的。
以下为引用:

BOOL WINAPI CryptProtectData(
  DATA_BLOB* pDataIn,
  LPCWSTR szDataDescr,
  DATA_BLOB* pOptionalEntropy,
  PVOID pvReserved,
  CRYPTPROTECT_PROMPTSTRUCT* pPromptStruct,
  DWORD dwFlags,
  DATA_BLOB* pDataOut
);

BOOL WINAPI CryptUnprotectData(
  DATA_BLOB* pDataIn,
  LPWSTR* ppszDataDescr,
  DATA_BLOB* pOptionalEntropy,
  PVOID pvReserved,
  CRYPTPROTECT_PROMPTSTRUCT* pPromptStruct,
  DWORD dwFlags,
  DATA_BLOB* pDataOut
);

BOOL CryptProtectMemory(
  LPVOID pData,
  DWORD cbData,
  DWORD dwFlags
);

BOOL CryptUnprotectMemory(
  LPVOID pData,
  DWORD cbData,
  DWORD dwFlags
);



    最后,我根据 Whidbey 的 SecureString 实现,移植了一个版本到 v1.1 下,但因为缺少 CER (Constrained Execution Regions) 和 CF (Critical Finalization) 的支持,其安全性还是无法完全保障,权且作为 v2.0 之前的过渡品吧,呵呵
    
to be continue...