看到这儿,我们已经可以使用运行在IE进程中的代码来处理所有文件系统和注册表了。下面我们看一个更复杂的话题,它也需要一个更复杂的解决方案:运行在一个更高完整性级别的另一个进程的IPC。我们将介绍两种不同形式的IPC:内核对象和windows消息。
1.创建一个IPC对象
当一个插件和一个单独的进程希望通信的时候,这种通信发生在两个代码之间,而不用经过IE封装。NT安全API和强制性完整级别现在可以发挥它们的作用了,默认情况下从一个插件到一个单独的应用程序的通信是被阻挡的,因为这个应用程序运行在比IE更高的完整性级别。
如果这个单独的应用程序创建了一个插件需要使用的内核对象(例如,一个事件或互斥对象),在插件存取它们之前,这个应用程序必须降低这个对象的完整性级别。这个应用程序可以使用安全API来修改对象的ACL来降低它的完整性级别。下面的代码是从MSDN的文章“Understanding and Working in Protected Mode Internet Explorer”摘取的,将一个HANDLE赋给一个内核对象,并设置它的完整性级别为低。
// The LABEL_SECURITY_INFORMATION SDDL SACL to be set for low integrity
LPCWSTR LOW_INTEGRITY_SDDL_SACL_W = L"S:(ML;;NW;;;LW)";
bool SetObjectToLowIntegrity(
HANDLE hObject, SE_OBJECT_TYPE type = SE_KERNEL_OBJECT)
{
bool bRet = false;
DWORD dwErr = ERROR_SUCCESS;
PSECURITY_DESCRIPTOR pSD = NULL;
PACL pSacl = NULL;
BOOL fSaclPresent = FALSE;
BOOL fSaclDefaulted = FALSE;
if ( ConvertStringSecurityDescriptorToSecurityDescriptorW (
LOW_INTEGRITY_SDDL_SACL_W, SDDL_REVISION_1, &pSD, NULL ) )
{
if ( GetSecurityDescriptorSacl (
pSD, &fSaclPresent, &pSacl, &fSaclDefaulted ) )
{
dwErr = SetSecurityInfo (
hObject, type, LABEL_SECURITY_INFORMATION,
NULL, NULL, NULL, pSacl );
bRet = (ERROR_SUCCESS == dwErr);
}
LocalFree ( pSD );
}
return bRet;
}
这个示例代码使用了两个mutex,其目的是让这个插件告诉什么时候这个应用程序在运行。当这个进程启动的时候DemoAPP EXE创建了它们,当你点击其中一个Open Mutex按钮的时候这个插件尝试去打开它们。Mutex 1具有默认的完整性级别,而mutex 2则通过上面所说的SetObjectToLowIntgrity()函数设置到低完整性级别。这意味着当保护模式启用的时候,这个插件将只能存取mutex 2。以下是当你点击两个Open Mutex按钮时所看到的结果:
保护模式的另一个影响是,一个插件不能让一个单独的应用程序继承一个内核对象的句柄。举个例子来说,当保护模式被启用的时候,我们的插件不能创建一个文件映射对象,运行这个单独的应用程序(传递bInheritHandles的参数TRUE给CreateProcess()),并且让这个应用程序继承文件映射对象的句柄。
HANDLE hMapping;
SECURITY_ATTRIBUTES sa = { sizeof(SECURITY_ATTRIBUTES) };
sa.bInheritHandle = TRUE;
hMapping = CreateFileMapping( INVALID_HANDLE_VALUE, &sa,
PAGE_READWRITE, 0, cbyData, NULL );
// Omitted: Put data in the shared memory block...
// Run the EXE and pass it the shared memory handle.
CString sCommandLine;
BOOL bSuccess;
STARTUPINFO si = { sizeof(STARTUPINFO) };
PROCESS_INFORMATION pi = {0};
sCommandLine.Format( _T("/"C://path//to//DemoApp.exe/" /h:%p"),
hMapping );
bSuccess = CreateProcess(
NULL, sCommandLine.GetBuffer(0), NULL, NULL,
TRUE, // TRUE => the new process should inherit handles
NORMAL_PRIORITY_CLASS, NULL, NULL, &si, &pi );
DemoApp然后将从/h交换读取句柄的值,并使用这个值来调用MapViewOfFile()来读取数据。这是让新的进程自动接受一个内核对象句柄的标准技术,但是当保护模式被启用的时候,新的进程实际上是由代理进程发起的。因为IE进程没有直接启动这个新的进程,句柄继承不再起作用。
为了解决这个限制,插件可以为一个IPC对象使用预先定义好的名称,那么这个单独的应用程序将能偶访问这个对象,因为这个对象具有低完整性级别。如果你不想使用一个预先定义好的名称,你可以在运行的时候产生一个名称(例如使用一个GUID作为名字),并将这个名称传递给这个单独的应用程序:
// Get a GUID that we'll use as the name of the shared memory object.
GUID guid = {0};
WCHAR wszGuid[64] = {0};
HRESULT hr;
CoCreateGuid( &guid );
StringFromGUID2( guid, wszGuid, _countof(wszGuid) );
// Create the file mapping object, we don't need a SECURITY_ATTRIBUTES
// struct since the handle won't be inherited.
HANDLE hMapping;
hMapping = CreateFileMapping( INVALID_HANDLE_VALUE, NULL,
PAGE_READWRITE, 0, cbyData, CW2CT(wszGuid) );
// Omitted: Put data in the shared memory block...
// Run the EXE and pass it the name of the shared memory object.
// Note that the bInheritHandles param to CreateProcess() is FALSE.
CString sCommandLine;
BOOL bSuccess;
STARTUPINFO si = { sizeof(STARTUPINFO) };
PROCESS_INFORMATION pi = {0};
sCommandLine.Format( _T("/"C://path//to//DemoApp.exe/" /n:%ls"),
wszGuid );
bSuccess = CreateProcess(
NULL, sCommandLine.GetBuffer(0), NULL, NULL,
FALSE, // FALSE => the new process does not inherit handles
NORMAL_PRIORITY_CLASS, NULL, NULL, &si, &pi );
使用这个方法,这个EXE程序在命令行中接受到这个IPC对象的名称。它然后能调用OpenFileMapping()来访问这个对象。但是,这个方法有些复杂,因为你需要对于对象生命周期管理非常谨慎。当使用句柄继承的时候,事件的顺序是:
1、插件创建IPC对象,并将其引用计数设为1。
2、插件起动继承了句柄的进程。这增加对象的引用计数到2。
3、插件现在可以立即关闭它的句柄,因为它不再需要这个对象了。引用计数降到0。
4、这个新的进程完成任何它需要与这个IPC对象的东西。因为它依然有一个开放的句柄,这个对象在这个新的进程关闭它的句柄之前一直存在。
当我们只把对象名传递给EXE的时候如果我们使用以上步骤,我们将产生一个竞态条件,在这个EXE程序有机会打开其上的一个句柄前,这个插件可能会关闭它的句柄(因此会删除这个IPC对象),过程如下:
1、该插件创建IPC对象,将其引用计数设为1。
2、插件起动新进程,将IPC对象的名字传递给它。引用计数依然是1。
3、插件不能立即关闭它的句柄,它需要等到新进程打开一个到对象的句柄。要实现这个任务需要某些类别的同步。
4、新的进程打开一个到对象的句柄,然后读取数据。这个时候,它可以发信号给插件以唤醒插件线程。现在插件可以安全的关闭它的句柄了。
在示例项目中我选择的做法是,在DemoApp创建主对话框前,让它从共享内存中读数据。插件在CreateProcess()后调用WaitForInputIdle(),这将使该线程被阻挡,直到DemoApp的主对话框已经被创建和显示。一旦DemoApp的线程空闲后,它已经结束了对共享内存的使用,对插件来说,关闭它的句柄已经安全。
band演示了通过共享内存传递数据的两种方法。当你点击Run EXE 1按钮的时候,band写当前的日期和时间到共享内存中,然后传递一个句柄给DemoApp。当保护模式被启用的时候,这个方法将失败,DemoApp将报告非法的句柄错误。点击Run EXE 2按钮,传递文件映射对象的名称给DemoApp,它将显示它从共享内存中读取的数据:
2.接收Windows消息
UIPI阻止特定的windows消息被从一个低完整性级别进程发送到一个高完整性级别进程。如果你的应用程序需要接收来自你的插件的消息,你可以调用ChangeWindowMessageFilter()来允许一个特定的消息通过:
BOOL ChangeWindowMessageFilter(UINT message, DWORD dwFlag);
参数message是消息的值,而dwFlag则指明这个消息是被允许还是阻挡。传递MSGFLT_ADD则是允许消息传递,而MSGFLT_REMOVE则是阻挡它。当处理来自其他进程的windows消息时一定要非常小心——如果通过消息来接收数据,你必须将它看作非信任的,你应该在根据它行动前对它进行验证确认。(Remember that inter-process messages can be sent from anywhere, and such messages can be used for attacks, as mentioned earlier.)
本文中的演示项目显示了如何通过registered windows消息进行通信。对于mutex示例,有两个消息。DemoApp通过OnInitDialog()中的代码允许第二个消息通过过滤器:
m_uRegisteredMsg1 = RegisterWindowMessage( REGISTERED_MSG1_NAME );
m_uRegisteredMsg2 = RegisterWindowMessage( REGISTERED_MSG2_NAME );
ChangeWindowMessageFilter( m_uRegisteredMsg2, MSGFLT_ADD );
当你点击band中的两个Send Message按钮后,你将看到如下结果:
第一个消息没有被允许通过过滤器,SendMessage()返回了0。
为了兼容xp, 应该动态加载user32.dll
BOOL CancleMessageFilter()
{
BOOL bRet = FALSE;
HMODULE hUserMod = LoadLibrary(TEXT("user32.dll"));
if( NULL == hUserMod )
{
return FALSE;
}
_ChangeWindowMessageFilter pChangeWindowMessageFilter =
(_ChangeWindowMessageFilter)GetProcAddress( hUserMod, "ChangeWindowMessageFilter" );
if( NULL == pChangeWindowMessageFilter )
{
return FALSE;
}
//MSGFLT_ADD: 1, MSGFLT_REMOVE: 2
bRet = pChangeWindowMessageFilter(XOLI_BEGIN, MSGFLT_ADD);
bRet &= pChangeWindowMessageFilter(XOLI_EXIT_PROCESS, MSGFLT_ADD);
bRet &= pChangeWindowMessageFilter(XOLI_RUN_AS_ADMIN, MSGFLT_ADD);
if( NULL != hUserMod )
{
FreeLibrary( hUserMod );
}
return bRet;
}