65.9K
CodeProject 正在变化。 阅读更多。
Home

安全 BSTR 和其他数据包装器

starIconstarIconstarIcon
emptyStarIcon
starIcon
emptyStarIcon

3.80/5 (4投票s)

2003年10月16日

9分钟阅读

viewsIcon

47843

downloadIcon

543

本文介绍了一个适用于任何内存中数据类的安全包装器框架,并讨论了其在 Microsoft bstr_t 和 CComBSTR 包装器(用于 COM 数据类型 BSTR)中的应用。

引言

一个近期项目中,安全性很重要,并且需要使用 COM BSTR 字符串,这促使了我开发这个安全包装器框架。安全编码规则要求:
  • 敏感数据应尽可能短时间地以明文形式保存在内存中
  • 当变量超出作用域时,应立即用特定模式覆盖其内容
  • 当敏感数据通过 COM 传递时,必须进行加密
如您所知,Microsoft 提供了两种主要的 BSTR 数据类型的包装器类:CComBSTR_bstr_t。它们各有优缺点,项目中的开发人员都使用过。我决定不编写第三个包装器,而是基于这两个众所周知、可靠的类进行扩展,赋予它们所需的额外安全功能。

所需功能

为了满足项目需求,并使开发尽可能简单,并与使用常规包装器尽可能相似,我制定了以下功能列表:

  • 数据应隐藏在内存中
  • 数据只应在需要时才在内存中显示
  • 应监控数据显示的时长
  • 不再需要的数据应被彻底清除
  • 用于 COM 传输的数据应被加密和解密
  • 新的包装器应能与现有包装器交互
  • 性能不应受到明显影响
  • 新功能应尽可能自动化

隐藏数据

内存中的安全数据经过混淆处理,以至于黑客通过内存扫描或将内存转储到文件无法轻易看到。所遵循的规则是:数据通常是隐藏的,只在需要时显示,并且显示时间越短越好。此规则在 BSTR 包装器的各种方法中得以实现,通过确保数据在需要暴露(例如复制到 BSTR)之前一直处于隐藏状态。

我选择使用一种基于滚动异或(rolling exclusive OR)的高效混淆技术来隐藏数据。此功能包含在一个 Obfuscator 类中,如果需要更强大的方法,可以独立于其他代码进行更改。请注意,此功能与通信所需的加密不同。

显示数据

数据应在内存中以明文形式显示(即可用),时间越短越好。您可以通过使用提取器或强制转换函数之一来显示数据,例如:
Secure::_bstr_t 
strData(aBSTR,true);  // hidden
const char *szData;
.
.
.
szData = (const char *)strData; // revealed
.
.
.
strData.HideData();  // hidden – so is szData
这会显示数据,因为 szData 是直接指向底层成员变量的指针,因此必须是明文。隐藏数据会同时隐藏 szData 指向的数据。显然,在此类情况下,必须小心不要过早隐藏数据。

暴露监控

当数据在内存中可用(即未被混淆)时,即为显示或暴露。暴露监控的作用是检测安全数据暴露时间超过可编程时间的情况,并对其采取行动。

暴露监控存在一些明显的局限性。例如,如果您在数据暴露时将其复制到不安全区域,然后迅速隐藏原始数据,则暴露监控将无法检测到这一点,但内存中仍然存在一个已暴露的数据副本。您应该按照下一节的描述来处理此问题。

为了监控过度暴露的数据,我选择实现一个单独的线程和一个简单的当前暴露数据映射。显示和隐藏安全数据的方法会更新此映射。您可以声明式地或以编程方式按包装器类型启动和停止。抽象基类(稍后介绍)实现了此线程。

当数据暴露时间过长时,您可以让线程调用您的回调例程,或者简单地重新隐藏数据。

您的回调例程应具有以下签名:

typedef void (*pfnExposedDataCallback)(const DataHolder * pData);

清除数据

当数据超出作用域或被删除时,它会被一个定义的模式彻底覆盖。一个单一的非类 Wipe 函数将模式 FE EE FE EE… 写入内存。持有数据的类的析构函数会调用此函数,该函数也可用于在不再需要安全数据后立即用于不安全的数据副本。

加密用于 COM 传输

我选择了强 AES 对称加密,使用 128 位密钥和随机生成的初始化向量。加密数据包括原始数据的长度以及数据的 SHA1 哈希。数据首先用所有软件组件可用的密钥进行加密,然后用两个 COM 伙伴之间协商的 Diffie-Hellman 临时密钥再次加密。我使用整个 blob 的 base64 编码,以便通过 COM 将加密数据作为字符串轻松传输。

这可能是单独知识库(KB)的主题。由于性能、安全性和密钥管理方面的问题,这些功能与包装器类分开,此处不作介绍。

现有包装器

我仔细研究了现有包装器类的实现,以确定需要进行的更改。

在这两个包装器中,_bstr_t 更复杂,它有一个包含的类 Data_tData_t 是引用计数的,可以为多个具有相同值的 _bstr_t 对象保存数据。

例如,考虑以下代码片段:

_bstr_t strA(TEXT(“secret”);
_bstr_t strB(strA);
_bstr_t strC;

strC = strB;
上面的代码会生成 **三个** _bstr_t 对象,但只有一个 Data_t 对象,该对象在三个 _bstr_t 之间共享。我决定保留包含的类,但要取消 Data_t 的共享实现,因为当它们共享同一个 Data_t 对象时,会在一个 _bstr_t 对象中显示数据,而在另一个对象中隐藏数据时出现冲突问题。CComBSTR 包装器是一个更直接、更轻量级的类,它自己保存数据。

由于新的包装器类模仿了原始内置类,我决定保留相同的名称,但将它们以及其余实现放在 Secure 命名空间中。新的包装器内置了互操作性。例如,安全 _bstr_t 和原始 _bstr_t 可以相互构造、相互比较以及相互赋值。

抽象基类

为了实现所需的功能,我创建了两个抽象基类,它们包含新的共享功能,并为新的包装器类强制要求一些额外的纯虚方法。这些抽象基类是 DataWrapperDataHolderDataWrapper 是外部使用的包装器类(如 _bstr_t),而 DataHolder 则保存数据(如 Data_t)。

DataWrapper

新版本的 _bstr_tCComBSTR 继承自此类,因为它们是包装器。它实现了与控制暴露监控相关的其他必需功能,并强制要求一个方法来返回 DataWrapperDataHolder 对象。

DataHolder

新版本的 Data_tCCOMBSTR 继承自此类,因为数据就保存在这里。它实现了与数据隐藏、显示以及暴露监控相关的大部分功能。它强制要求一些方法来确定要隐藏和显示哪些内部数据。

整体类结构

因此,总体的类结构如下:

Namespace Secure
{
    inline void Wipe(void * pData, size_t size);

    class Obfuscator { };

    template<class T>class Checker{};

    class DataHolder{};

    class DataWrapper{};

    class _bstr_t : public DataWrapper
    {
        class Data_t : public DataHolder{};
    };

    class CComBSTR : public DataWrapper, public DataHolder{};
};
我们来详细了解一下每个类。

Checker

Checker 是一个便利类,用于声明式地启动和停止暴露监控。
template<class T
class Checker
{
public:
    inline Checker<T>(bool bStart = true,
                            pfnExposedDataCallback callback = NULL);
    inline ~Checker<T>();
};
您可以通过声明一个 Checker 对象来简单地控制暴露监控:
Secure::Checker<CComBSTR> BSTRMonitor(true, MyCallBack);
当对象超出作用域时,暴露监控线程会停止。

DataHolder

DataHolder 是最大的类,包含处理数据隐藏、显示以及暴露监控的方法。

EXPOSEDATA 结构体有助于暴露监控线程、其控制方法以及 HideReveal 方法。包含的 ExposeMap 是一个标准模板库映射,保存当前显示数据的详细信息。Reveal 方法向映射中添加条目,Hide 方法则从映射中删除条目。

class DataHolder
{
public:
    typedef std::map<const DataHolder *,_timeb>ExposeMap;
    typedef struct EXPOSEDATA
    {
        ExposeMap Map;
        CRITICAL_SECTION Crit
        HANDLE hThread;
        HANDLE hWake;
        bool bThreadActive;
        unsigned long lWaitTime;
        double dblMaxExposeTime;
        pfnExposedDataCallback callback;
    };

    DataHolder()
    inline void Hide() const;
    inline void Reveal() const;
    bool IsHidden() const;
    inline static void StartExposeThread(pfnExposedDataCallback callback,
                       double maxExposeTime, unsigned long waitTime);
    inline static void CloseExposeThread();
    inline void Obfuscate(void * pData, size_t size, unsigned char seed);
    inline void Deobfuscate(void * pData, size_t size, unsigned char seed);

protected:
    virtual void HideMe() const = 0;
    virtual void RevealMe() const = 0;
    void SeedAndHide() const;

    mutable bool m_hidden;
    mutable unsigned char m_seed;
    Obfuscator *m_obfuscator;

private:
    inline static EXPOSEDATA * EMap();
    inline static DWORD WINAPI ExposeThread(LPVOID param);
};
继承类必须实现 HideMeRevealMe 方法,因为只有它们知道数据的位置。

Data_t 的实现非常小,m_wstr 是 Unicode 字符串,m_str 是 ASCII 字符串。

inline void _bstr_t::Data_t::HideMe() const
{
    Obfuscate(m_wstr, ::SysStringByteLen(m_wstr),m_seed);
    Obfuscate(m_str, ::SysStringLen(m_wstr), m_seed);
}

inline void _bstr_t::Data_t::RevealMe() const
{
    Deobfuscate(m_wstr, ::SysStringByteLen(m_wstr), m_seed);
    Deobfuscate(m_str, ::SysStringLen(m_wstr), m_seed);
}
对于 CComBSTR,只有一个可选的 Unicode 字符串。
inline void CComBSTR::HideMe() const
{
    if (m_str != NULL)
    {
        Obfuscate(m_str, ::SysStringByteLen(m_str),m_seed);
    }
}

inline void CComBSTR::RevealMe() const
{
    if (m_str != NULL)
    {
        Deobfuscate(m_str, ::SysStringByteLen(m_str),m_seed);
    }
}

DataWrapper

DataWrapper 实现了暴露监控的控制方法,并强制要求继承类实现一个关于 DataHolder 数据对象位置的纯虚方法 Data
class DataWrapper
{
public:
    virtual const DataHolder * Data() const=0;
    void HideData() const;
    void RevealData() const;
    virtual bool IsDataHidden() const;
    inline static StartExposeChecking(pfnExposedDataCallback callback,
                       double maxExposeTime = MAXEXPOSETIME,
                       unsigned long waitTime = EXPOSEHEARTBEATMILLISECONDS);
    inline static void StopExposeChecking();
};
我们需要由派生类实现 Data 方法,因为基类不知道派生类将如何引用 DataHolder 类。

同样,此方法的实现也很小。

对于 _bstr_tDataHolder 类保存在类变量 m_Data 中。

inline const DataHolder * _bstr_t::Data() const
{
    return m_Data;
}
对于 CComBSTR,它自己也实现了 DataHolder
inline const DataHolder *CComBSTR::Data() const
{
    return this;
}

Obfuscator

Obfuscator 包含隐藏和显示内存数据的代码。我目前使用一种滚动的、带种子的异或技术,这种技术非常快。它不是一种非常强大的加密方法,但对于保护内存中瞬时数据来说是可接受的。稍后可能会包含更强大的方法。
class Obfuscator
{
public:
    void Obfuscate (void * pData, size_t size, unsigned char seed) const;
    void Deobfuscate (void * pData, size_t size, unsigned char seed) const;

private:
    void Xor(void * pData, size_t size, unsigned char seed) const
};

修改现有包装器

许多方法需要以微小但相似的方式进行修改。几乎所有的修改都与数据隐藏有关。我遵循了以下规则:
  • 所有构造函数和赋值方法在接收数据时都会隐藏数据。
  • 数据保持隐藏状态,除了短暂的内部显示/隐藏更改外,直到外部需要为止。
  • 当一个方法操作另一个包装器对象时,它会使该对象保持与找到它时相同的隐藏状态。
例如,比较运算符 `!`
inline bool _bstr_t::operator!() const throw()
{
    return (m_Data != NULL) ? !m_Data->GetWString() : true;
}
变成
inline bool _bstr_t::operator!() const throw()
{
    bool Rtn;
    bool bHidden = IsDataHidden();

    RevealData();
    Rtn = (m_Data != NULL) ? !m_Data->GetWString() : true;

    if (bHidden)
        HideData();
}
这里的修改是确定数据当前是否隐藏,然后显示它。在确定返回值的主要工作之后,如果数据在方法开始时是隐藏的,则将其隐藏。

包含的 Data_t 类中的另一个构造函数的例子

inline _bstr_t::Data_t::Data_t(const char* s) throw(_com_error)
            : m_str(NULL), m_RefCount(1)
{
    m_wstr = _com_util::ConvertStringToBSTR(s);

    if (m_wstr == NULL && s != NULL)
    {
        _com_issue_error(E_OUTOFMEMORY);
    }
}
变成
inline _bstr_t::Data_t::Data_t(const char* s) throw(_com_error)
            : m_str(NULL), m_RefCount(1)
{
    m_wstr = _com_util::ConvertStringToBSTR(s);

    if (m_wstr == NULL && s != NULL)
    {
        _com_issue_error(E_OUTOFMEMORY);
    }

    SeedAndHide();
}
在方法结束时,会调用 DataHolder 的 SeedAndHide 方法来计算混淆的种子值并隐藏数据。

使用安全包装器

提取数据后,您可以将数据复制到其他地方,然后调用 HideData 方法,或者让暴露监控线程来处理。您也可以选择使用 Secure::Wipe 方法来擦除不再需要的包含敏感数据的其他内存区域。
STDMETHODIMP CLASS:Method(BSTR bstrInput,…)
{
    // Acquire the input parameter into a secure string
    //
    Secure::_bstr_t strSensitiveData(bstrInput);

    // Extract as a plain const char *
    //
    const char * pExtract = (const char *)strSensitiveData;

    // Copy to another char *
    //
    size_t uiSize = strlen(pExtract) + 1;
    char * pCopy = new char[uiSize];
    strcpy(pCopy, pExtract);

    // Hide the secure string
    //
    strSensitiveData.HideData();
    .
    .
    .
    // Wipe the insecure copy
    //
    Secure::Wipe(pCopy, uiSize);
    delete[] pCopy;
    .
    .
    .
} 
对于需要主要使用安全包装器的整个 C++ 文件或项目,您可以默认使用安全包装器,但要继续使用原始包装器,可以使用 _ibstr_t 原始包装器。此示例还展示了暴露监控回调函数的用法。
#define SECURE_BSTR_T
#include “SecureBstrT.h”

void MyCallback(const DataHolder * pData)
{
    const char * szData = (const char *)(*pData);

    // Record the insecure exposure somewhere
    .
    .
    .
    // Hide the data
    pData->Hide();
}

Secure::Checker<_bstr_t> BstrCheck(true, MyCallback);


HRESULT CDemo::Method(BSTR bstrInput, BSTR * pbstrOutput)
{
    _bstr_t strSensitiveData(bstrInput);  // Secure
    _ibstrt strNormalData;                // Insecure
    .
    .
    .
    strNormalData = strSensitiveData;     // Exposed
    .
    .
    .
}

摘要

已开发出一个有效、易于使用、可扩展且增强了安全性的类框架。
© . All rights reserved.