安全 BSTR 和其他数据包装器






3.80/5 (4投票s)
2003年10月16日
9分钟阅读

47843

543
本文介绍了一个适用于任何内存中数据类的安全包装器框架,并讨论了其在 Microsoft bstr_t 和 CComBSTR 包装器(用于 COM 数据类型 BSTR)中的应用。
引言
一个近期项目中,安全性很重要,并且需要使用 COM BSTR 字符串,这促使了我开发这个安全包装器框架。安全编码规则要求:- 敏感数据应尽可能短时间地以明文形式保存在内存中
- 当变量超出作用域时,应立即用特定模式覆盖其内容
- 当敏感数据通过 COM 传递时,必须进行加密
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_t
。Data_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
可以相互构造、相互比较以及相互赋值。
抽象基类
为了实现所需的功能,我创建了两个抽象基类,它们包含新的共享功能,并为新的包装器类强制要求一些额外的纯虚方法。这些抽象基类是DataWrapper
和 DataHolder
。DataWrapper
是外部使用的包装器类(如 _bstr_t
),而 DataHolder
则保存数据(如 Data_t
)。DataWrapper
新版本的_bstr_t
和 CComBSTR
继承自此类,因为它们是包装器。它实现了与控制暴露监控相关的其他必需功能,并强制要求一个方法来返回 DataWrapper
的 DataHolder
对象。DataHolder
新版本的Data_t
和 CCOMBSTR
继承自此类,因为数据就保存在这里。它实现了与数据隐藏、显示以及暴露监控相关的大部分功能。它强制要求一些方法来确定要隐藏和显示哪些内部数据。整体类结构
因此,总体的类结构如下:
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
结构体有助于暴露监控线程、其控制方法以及 Hide
和 Reveal
方法。包含的 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);
};
继承类必须实现 HideMe
和 RevealMe
方法,因为只有它们知道数据的位置。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_t
,DataHolder
类保存在类变量 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
.
.
.
}