您的位置:首页 > 其它

Windows Vista IE 7保护模式开发者生存指南

2009-06-06 13:06 447 查看
Windows Vista IE 7保护模式开发者生存指南

保护模式之简介
Internet Explorer 7的保护模式是Vista中的一项新特色,隶属于用户账户控制(UAC)的一部分。保护模式通过对运行于IE进程内的代码进行限制,来达到保护电脑的目的,即便一个恶意网页利用了IE或IE插件中的某个代码注入bug,它也不能对系统造成损害。

完整性级别及UIPI
Windows Vista对需要保证其安全的对象引入了一个新的属性:强制完整性级别,由以下四个级别组成:

Ø 系统级:由系统组件使用,通常应用程序不应使用。
Ø 高级:运行于提升至管理员权限的进程所有。
Ø 中级:运行于正常状态的进程所有。
Ø 低级:由IE及Windows Mail使用,以提供保护模式。

Windows在运行某个进程时,通常也会包含其完整性级别,且一旦进程运行之后,级别不可更改,只能在进程创建的那一刻进行设置。进程的完整性级别会带来以下三种主要的影响:

1、 进程所创建的任何安全对象都具有同样的完整性级别。
2、 进程不能访问比自身完整性级别更高的资源。
3、 进程不能向比自身完整性级别更高的进程发送窗口消息。

以上列表并非完整,但这三个对IE插件的影响尤为显著,前两者防止低级别进程篡改 IPC资源,如包含敏感数据的共享内存、或影响程序正确执行的关键数据;后者则被称为用户界面进程隔离(User Interface Process Isolation UIPI),设计于防止某些破坏性的攻击,如攻击者通过发送消息,诱使某个进程运行不安全代码。

虚拟化
虚拟化(在微软的某些文档中也称为重定向)是一种防止进程向注册表及文件系统受保护区域写入的功能,且不会影响程序的正常功能。对完整性为中级的进程来说,受保护区域为系统关键区域,如HKLM、System32及Program Files目录,等等;对完整性为低级的进程来说,其受到的限制更严,只能对注册表及文件系统的特定低权限区域进行写入,任何对此区域之外的写请求都会被禁止。
当某个进程想要对其没有权限的区域进行写入时,虚拟化这时就起作用了,它把这些写操作重定向至当前用户个人配置文件(User Profile)下的目录(或注册表键),最终的写操作就写到这里。之后,当程序想要读取数据时,读操作也被重定向至此,正确读取了之前写入的数据。
虚拟化技术也会对IE插件有所影响,因为插件再也不能为了与其他进程共享数据,而把配置信息写入到注册表中了(甚至HKCU下也不行),同时,在那些插件可写数据之处,也是极受约束的,只有一些IE特定的目录,如Favorites及Cookies可写。

保护模式何时打开
在Vista默认状态下,IE均运行在保护模式中,状态栏(下图)显示了保护模式是否启用:



关闭UAC,就可以彻底关掉保护模式了,在Internet属性对话框的安全页中,不勾选“启用保护模式”;也可运行一个提升权限后的新IE实例,来临时绕过保护模式,但这样做之后,会使IE运行在高级完整性级别,而不像普通程序那样在中级。

示例程序及插件
示例代码中包含了两个工程,第一个工程是IEExtension,它是一个停靠在IE窗口底部的一个插件:



第二个工程是DemoAPP,其为一个用作通讯的exe文件,它本身代码并不多,关键代码都在 IEExtension中与其进行通讯之处。插件中成对出现的按钮代表了在保护模式中不能正常工作的方法(按钮1)及新的可工作于保护模式中的方法(按钮 2),列表控件用于显示状态信息,如Windows API的返回值。
以下将主要讲解插件需要怎么做才能正常工作于保护模式中,并在示例代码中介绍一些API,每个主题都相对应一个(或一对)按钮。

保护模式之下的解决办法
IE 7中增加了几个新的API,它们位于ieframe.dll中,插件可以使用它们来执行那些在保护模式中受限的功能,既可以直接通过iepmapi.lib链接到这些API,也可以通过 LoadLibrary()/GetProcAddress()获取运行时的函数指针,如果想要插件在Vista之前的Windows平台上也能正常加载,就必须使用后一种方法了。
特权提升的大多数功能是通过一个代理进程(ieuser.exe)来实现的,只要IE进程运行于低完整性级别中,它就不能执行更高权限的任务,这也是ieuser.exe这个角色的目的所在。

在运行时探知保护模式
如果想知道插件是否运行在IE进程的保护模式中,可使用IEIsProtectedModeProcess():

HRESULT IEIsProtectedModeProcess(BOOL* pbResult);

如果返回值是一个成功的HRESULT且*pbResult为TRUE,则可以确定为保护模式;另外,基于*pbResult的返回值,也可决定代码下一步该怎么办:

HRESULT hr;
BOOL bProtectedMode = FALSE;

hr = IEIsProtectedModeProcess ( &bProtectedMode );

if ( SUCCEEDED(hr) && bProtectedMode )
// IE运行于保护模式中
else
// IE 未运行于保护模式中

文件系统的写入
当处于保护模式时,插件只能写到用户配置文件下的某些目录中,且只有Temp、 Temporary Internet Files、Cookies、Favorites这些低完整性级别目录为可写。但IE 7也照顾到了某些兼容性问题,对某些常用的目录进行虚拟化,对这些目录的写入会被重定向至Temporary Internet Files的子目录中;如果插件想要对敏感位置进行写入,如Windows目录,那么写操作会失败。
回到话题上来,当插件想要对文件系统进行写入时,应使用 IEGetWriteableFolderPath() API而不是GetSpecialFolderPath()、GetFolderPath()、SHGetKnownFolderPath()。 IEGetWriteableFolderPath()可探知保护模式,如果插件请求的目录不允许写入,IEGetWriteableFolderPath()会返回E_ACCESSDENIED,IEGetWriteableFolderPath() 的原型如下:

HRESULT IEGetWriteableFolderPath(GUID clsidFolderID, LPWSTR* lppwstrPath);

GUID为定义在knownfolders.h头文件中的 FOLDERID_InternetCache、FOLDERID_Cookies及FOLDERID_History,另外,似乎没有与Temp目录对应的GUID,所以当需要写临时文件时,推荐使用FOLDERID_InternetCache。以下为在缓存中创建临时文件的一段代码:

HRESULT hr;
LPWSTR pwszCacheDir = NULL;
TCHAR szTempFile[MAX_PATH] = {0};

hr = IEGetWriteableFolderPath(FOLDERID_InternetCache, &pwszCacheDir);

if ( SUCCEEDED(hr) )
{
GetTempFileName(CW2CT(pwszCacheDir), _T("bob"), 0, szTempFile);
CoTaskMemFree(pwszCacheDir);

// szTempFile中现在为临时文件的完整路径
}

如果IEGetWriteableFolderPath()成功,它会分配一块缓冲区并在pwszCacheDir中返回其地址,我们只须把这个目录传递给GetTempFileName(),接着用CoTaskMemFree()释放缓冲区就行了。
IEGetWriteableFolderPath()不仅是用于写临时文件,插件也可在保护模式的另存为对话框中用到它,这将在下面的“提示用户保存文件”一节中说到。

注册表的写入
因为注册表是系统的关键区域,所以,不应允许运行在浏览器中的代码修改其中的任何部分,以防止运行恶意代码。为此,只有一个键值对插件来说可写,与文件系统一样,这个键也是在当前用户配置文件下的低权限区域,可通过调用 IEGetWriteableHKCU()来获取此键的句柄:

HRESULT IEGetWriteableHKCU(HKEY* phKey);

如果函数成功,就可在其他的注册表API中使用这个返回的HKEY来写入数据了。

提示用户保存文件
当IE运行于保护模式时,插件还有一个办法(非直接的)可写入到文件系统中,而且是在低权限区域之外,这是通过调用IEShowSaveFileDialog()显示一个保存文件对话框来完成的。如果用户输入了一个文件名,插件就能通过调用 IESaveFile()让IE来写这个文件。要注意的是,这个操作总是会显示保存文件对话框,以确保用户知道将要写入一个文件。

保存文件的步骤如下:
1、 调用IEShowSaveFileDialog()显示保存文件对话框。
2、 调用IEGetWriteableFolderPath()获取IE缓存目录。
3、 把数据写到缓存目录的一个临时文件中。
4、 调用IESaveFile()把数据复制到用户选择的文件中。
5、 清理临时文件

IEShowSaveFileDialog()是对通用文件保存对话框的一个包装函数:

HRESULT IEShowSaveFileDialog(
HWND hwnd,
LPCWSTR lpwstrInitialFileName,
LPCWSTR lpwstrInitialDir,
LPCWSTR lpwstrFilter,
LPCWSTR lpwstrDefExt,
DWORD dwFilterIndex,
DWORD dwFlags,
LPWSTR* lppwstrDestinationFilePath,
HANDLE* phState
);

hwnd是一个插件所有的窗口句柄,IE将会使用最顶层的所有者窗口作为对话框的父窗口;lppwstrDestinationFilePath是一个指向LPWSTR的指针,其为用户选择的文件路径;phState是一个指向 HANDLE的指针,其为用户所选择文件的句柄,在调用其他API时,也会用到这个句柄。其他参数与OPENFILENAME结构中的对应成员用法类似。
IEShowSaveFileDialog()返回S_OK代表用户选择了某个文件名,S_FALSE代表取消了对话框,而失败的HRESULT代表API未成功。
下面示例代码中首先调用IEShowSaveFileDialog()提示用户输入文件路径:

void CBandDialog::OnSaveLog(UINT uCode, int nID, HWND hwndCtrl)
{
HRESULT hr;
HANDLE hState;
LPWSTR pwszSelectedFilename = NULL;
const DWORD dwSaveFlags =
OFN_ENABLESIZING | OFN_HIDEREADONLY | OFN_PATHMUSTEXIST |
OFN_OVERWRITEPROMPT;

// Get a filename from the user.
hr = IEShowSaveFileDialog (
m_hWnd, L"Saved log.txt", NULL,
L"Text files|*.txt|All files|*.*|",
L"txt", 1, dwSaveFlags, &pwszSelectedFilename,
&hState );

if ( S_OK != hr )
return;

接下来,调用IEGetWriteableFolderPath()获取可写的缓存目录路径:

LPWSTR pwszCacheDir = NULL;
TCHAR szTempFile[MAX_PATH] = {0};

// 获取IE缓存目录路径,其为保护模式下可写的目录
hr = IEGetWriteableFolderPath ( FOLDERID_InternetCache, &pwszCacheDir );

if ( SUCCEEDED(hr) )
{
// 得到目录中的一个临时文件名
GetTempFileName ( CW2CT(pwszCacheDir), _T("bob"), 0, szTempFile );
CoTaskMemFree ( pwszCacheDir );

// 把数据写到临时文件中
hr = WriteLogFile ( szTempFile );
}

如果一切正常,接着会调用IESaveFile(),IESaveFile()接受 IEShowSaveFileDialog()返回的状态句柄及临时文件路径作为参数,要注意的是,这个HANDLE不是标准的句柄,其无需关闭,在调用完IESaveFile()之后,HANDLE会自动释放。
如果因为某些原因IESaveFile()的调用未完成,例如在写临时文件时发生错误,那就必须清理HANDLE及IEShowSaveFileDialog()中分配的临时空间,这由IECancelSaveFile()来完成。

if ( SUCCEEDED(hr) )
{
// 如果写文件成功,IE就会把数据保存在用户选择的路径中
hr = IESaveFile ( hState, T2CW(szTempFile) );

// 清理临时文件
DeleteFile ( szTempFile );
}
else
{
// 未完成保存操作,那就只有取消它了
IECancelSaveFile ( hState );
}

在插件与其他程序间通讯
前面介绍的内容都是与文件系统及注册表相关的,其限制IE调用某些API对系统造成损害,下面涉及到一些更复杂的内容,与其他运行在更高完整性级别中程序的进程间通讯(IPC),这又分为两种类型:内核对象及窗口消息。

一、创建IPC对象
插件与单独进程的进程间通讯,涉及到NT安全API及强制完整性级别检查,默认状态下,会阻止插件到单独进程的通讯,因为外部程序运行在比IE更高的完整性级别中。
如果外部程序要创建一个插件可使用的内核对象(如event或mutex),那就必须降低这个对象的完整性级别,以便插件可以访问它。外部程序可使用安全API,通过修改对象的ACL来降低其完整性级别,下面的代码来自MSDN,接受一个内核对象的 HANDLE作为参数,并设置其完整性级别为低:

// LABEL_SECURITY_INFORMATION SDDL SACL被设为低完整性级别
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程序启动时会创建这两个mutex,当点击其中一个Open Mutex按钮时,插件会试图打开它们。Mutex 1具有默认完整性级别,而Mutex 2被SetObjectToLowIntegrity()设为低完整性级别,这意味着一旦处于保护模式,插件将只能访问Mutex 2,下面是点击Open Mutex按钮之后的程序输出:



保护模式的另一个作用是插件不能启动一个继承自内核对象句柄的单独进程,例如,处于保护模式时,示例中的插件不能通过创建一个文件映射对象,来启动一个单独的进程(CreateProcess()中的参数bInheritHandles为 TRUE),且使这个进程继承了文件映射对象的句柄。

HANDLE hMapping;
SECURITY_ATTRIBUTES sa = { sizeof(SECURITY_ATTRIBUTES) };

sa.bInheritHandle = TRUE;

hMapping = CreateFileMapping( INVALID_HANDLE_VALUE, &sa,
PAGE_READWRITE, 0, cbyData, NULL );

//在此可把数据放在共享内存块中

//运行EXE并传递给它共享内存句柄
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代表新的进程应该继承句柄
NORMAL_PRIORITY_CLASS, NULL, NULL, &si, &pi );

DemoApp会从/h选项中读取句柄值,并用它来调用MapViewOfFile()完成数据读取,这也是使新进程自动接收到某个内核对象句柄的标准方法,但处于保护模式时,新进程实际上由代理进程启动,也正是因为IE进程不会直接启动新进程,所以句柄继承失效。
要突破这个限制,插件可为IPC对象使用一个预定义名,这样其他进程就能访问此对象了(因为这个对象为低完整性级别)。如果不想使用预定义名,也可在运行时生成一个“名字”(如为名字使用一个GUID),并把它传递给其他的单独进程。

//得到一个用作共享内存对象名的GUID
GUID guid = {0};
WCHAR wszGuid[64] = {0};
HRESULT hr;

CoCreateGuid( &guid );
StringFromGUID2( guid, wszGuid, _countof(wszGuid) );

//创建文件映射对象,因为句柄不可被继承,所以无需SECURITY_ATTRIBUTES结构
HANDLE hMapping;

hMapping = CreateFileMapping( INVALID_HANDLE_VALUE, NULL,
PAGE_READWRITE, 0, cbyData, CW2CT(wszGuid) );

//在此可把数据放在共享内存块中

//运行EXE并传递给它共享内存对象名
//注意CreateProcess()中的参数bInheritHandles为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代表新的进程不继承句柄
NORMAL_PRIORITY_CLASS, NULL, NULL, &si, &pi );

这样,EXE就可在命令行中接收到IPC对象名,它接着会调用OpenFileMapping()来访问这个对象,另外,这个方法中,最重要的一点是要非常留意对象生命期。以下是使用句柄继承的步骤:

1、 插件创建IPC对象,并置引用计数为1。
2、 插件启动新进程,其继承了句柄。这个操作把对象引用计数增加为2。
3、 插件能立即关闭它的句柄,因为它不再需要此对象。引用计数降为1。
4、 新进程可通过IPC对象来进行所需的操作了。因为它拥有一个打开的句柄,对象直到新进程关闭句柄后才会结束。

如果依照上述步骤,且仅是把对象名传递给EXE,那么实际上是创建了一个“竞跑”状态,插件可能在EXE有机会打开句柄之前就把它关闭了(IPC对象也被删除了)。以下是修改后的步骤:

1、 插件创建IPC对象,并置引用计数为1。
2、 插件启动新进程,并传递给它IPC对象名。此时引用计数仍为1。
3、 插件不能马上关闭它的句柄,它需要等待直到新进程已打开此对象的一个句柄。此时需要做一些同步。
4、 新进程打开对象句柄并读取数据,此时,它可以发信号给插件以唤醒其线程,插件现在可以安全地关闭它的句柄了。

在示例程序中,我们让DemoApp在创建主窗口之前,先从共享内存中读取数据。插件在调用完CreateProcess()之后,接下来就可调用WaitForInputIdle(),这个函数使线程阻塞,直到DemoApp的主窗口创建并显示出来。一旦DemoApp线程处于空闲状态,它将不再使用共享内存,此时插件就可以安全地关闭其句柄了。
当点击“Run EXE 1”按钮时,程序把当前日期及时间写到共享内存中,并传递给DemoApp一个句柄,如果此时处于保护模式,这个方法将会失败,DemoApp会返回一个非法句柄错误;而点击“Run EXE 2”按钮时,将传递文件映射对象名给DemoApp,程序此时就会显示读取自共享内存的数据了。



二、接收窗口消息
UIPI可防止特定窗口消息(即消息值大于或等于WM_USER的所有消息)从低完整性级别程序中发送到比其更高级的程序中。如果你的程序需要接收来自插件的消息,可调用ChangeWindowMessageFilter()来允许特定消息通过:

BOOL ChangeWindowMessageFilter(UINT message, DWORD dwFlag);

Message为消息值,dwFlag表明是否允许或阻止此消息,MSGFLT_ADD允许消息,而MSGFLT_REMOVE阻止消息。对待其他进程的消息需小心谨慎,如果通过消息来接收数据,对接收到的数据必须仔细确认及验证,因为进程间的消息来无定所,很可能被恶意利用。
示例程序演示了如何通过注册窗口消息来进行通讯,使用mutex的例子中有两种消息,DemoApp将在OnInitDialog()中允许第二种消息通过:

m_uRegisteredMsg1 = RegisterWindowMessage( REGISTERED_MSG1_NAME );
m_uRegisteredMsg2 = RegisterWindowMessage( REGISTERED_MSG2_NAME );
ChangeWindowMessageFilter( m_uRegisteredMsg2, MSGFLT_ADD );

当点击程序中“Send Message”按钮时,将会看到以下输出:



第一种消息不允许通过,所以SendMessage()返回0。

保护模式中的其他限制
一、运行其他程序
IE还有一种机制用于防止恶意代码进行通讯或启动其他进程,如果某个插件试图启动另一个进程,IE在启动进程之前,会请求用户的许可,例如,点击“查看源文件”命令会看到以下提示:



如果插件需要运行某个单独的EXE,可添加一个注册表键值以告之IE此EXE是受信任程序,可无须提示就运行,控制此行为的键值是:HKLM\Software\Microsoft\Internet Explorer\Low Rights\ElevationPolicy;创建一个新的GUID,再在ElevationPolicy下添加一个新键,名称就为刚才的GUID,在新键中,创建下面三个值:

Ø AppName:可执行文件名,如:DemoApp.exe。
Ø AppPath:EXE位于的目录。
Ø Policy:设为3的DWORD值。

如果你勾选了前面提示中的“不再对此程序显示此警告”,IE自己也会创建这个键。

二、拖放到其他程序
如果从网页中把某些内容拖放到其他程序中,也会显示一个类似的提示:



同样,也能通过在注册表中创建一个键值而不显示这个提示,格式与前面的一样,只不过这次键值位于DragDrop,而不是ElevationPolicy下。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: