高权限可能对您的应用程序不利:如何在安装结束时启动非提升进程






4.74/5 (22投票s)
2007年5月25日
10分钟阅读

214910

2497
一个可重用的 DLL,使用代码注入从 InnoSetup 脚本启动非提升的应用程序。
- 下载 SampleApp-setup.zip - 290.5 KB
- 下载 SampleApp-setup-src.zip - 53.6 KB
- 下载 SampleApp-src.zip - 9.0 KB
- 下载 VistaLib-src.zip - 13.9 KB
- 下载 Redist.zip - 53.7 KB
引言
许多安装包都提供了一个选项,允许用户在安装结束时启动应用程序
这很好用,除了一个小问题:如果安装在 Vista 计算机上,以这种方式启动的应用程序将以高权限级别执行,具有完整的管理员权限。本文讨论了为什么这不好,并提供了一个可重用的 DLL,可以将其包含在现有安装包中,以便在安装结束时以非高权限运行应用程序。还提供了一个流行的 InnoSetup 软件的示例安装脚本。随附的源代码还包含一些其他函数,在为 Windows Vista 编程时可能会有用。
背景
您遵循了 Microsoft 的指导方针,并仔细更新了您的应用程序,使其在受限用户(非管理员)的上下文中运行良好。您已在应用程序清单中添加了 asInvoker
值,以确保当用户启动您的应用程序时,它在没有管理权限的情况下启动。您已经测试过,它运行良好,您认为您的新 Vista 兼容版本已准备好发布。因此,您创建了安装包并进行了测试,现在您面临一个令人不快的意外:如果您选择在安装结束时启动应用程序的选项,该应用程序将以管理员身份启动,具有完整的管理权限。
为什么以高权限运行您的应用程序不好?因为事情可能会意外地中断。例如
- 以高权限启动时,您的应用程序可以访问通常不应访问的文件夹。除了明显的安全隐患之外,它还可能给您的用户带来问题:如果用户将文档保存到此类文件夹中,那么她或他下次运行您的应用程序时将无法打开该文档(因为当用户下次启动应用程序时,它将在没有管理权限的情况下执行!)
- 如果您的应用程序是由“真实”标准用户(不是 Administrators 组的成员)安装的,并且安装已由实际管理员授权(通过在标准用户的“肩上”的 UAC 提示中输入其密码),那么当您的应用程序由安装程序启动时,它将访问管理员的个人文件夹(我的文档等),与标准用户的不同。管理员不喜欢其他用户无限制地访问他们的个人文件夹
- 如果您的应用程序需要与 shell 交互(例如,响应 shell 广播的通知消息等),这种交互可能会中断:默认情况下,Vista UAC 会阻止从非高权限应用程序(例如 shell)发送的大多数消息到达高权限进程。
- 如果您的应用程序创建辅助进程(例如,显示任务栏通知图标,或运行热键监视器等),此类进程也将以高权限启动。如果它们需要与 shell 交互,这种交互也可能会中断。
- 最后但并非最不重要的一点:您永远不知道您的应用程序中可能会发现什么样的漏洞,这可能会为恶意软件开辟通往高权限的道路。病毒可能潜伏在用户的计算机上,悄悄地等待像您这样的应用程序以高权限启动,这样它就可以劫持您的高权限进程来提升自身并以完整的管理权限开始做坏事。
为什么您的应用程序在安装程序自动启动时以高权限运行?因为安装过程以完整的管理权限运行,并且当它创建子进程(例如您的应用程序的进程)时,此类进程将以与安装程序本身相同的高权限级别执行。
如何解决这个问题?
如果您已经在 Microsoft SDK 文档中搜索解决方案,您无疑会遇到另一个惊喜,比第一个更糟糕:Microsoft 没有提供从高权限进程启动非高权限进程的方法。没错,没有 API 调用,没有在清单中指定特殊值,甚至没有 ShellExecute()
API 的标志来允许这样做。如果您发现自己陷入这种境地,请继续阅读,本文是为您准备的。
让我们考虑我们拥有的选项
- 我们可以创建一个单独的辅助可执行文件,在必要时帮助我们的主应用程序启动非高权限任务。也就是说,它可以按如下方式工作
- 当用户想要安装应用程序(通过运行 setup.exe)时,她会首先启动辅助可执行文件(helper.exe)。
- 辅助进程将以非高权限启动,但它将通过 setup.exe 清单中的
requireAdministrator
值启动 setup.exe,后者将以高权限启动。 - 安装完成后,setup.exe 将向 helper.exe 发回信号,表明用户想要启动应用程序(app.exe)。收到信号后,helper.exe 将代表 setup.exe 启动 app.exe。由于 helper.exe 以非高权限启动,因此它也将以非高权限启动 app.exe。
这种方法可行,但很麻烦,因为它需要创建一个单独的辅助可执行文件,以及设计一个安装程序实用程序和辅助程序之间的通信协议,这不是一项简单的任务。
- 当用户想要安装应用程序(通过运行 setup.exe)时,她会首先启动辅助可执行文件(helper.exe)。
- 一个更简单的方法是利用 Windows Vista 内置任务计划程序的功能:我们的高权限进程可以向任务计划程序注册一个任务,使其在注册后立即以非高权限级别启动。(我已经在我的上一篇文章 Vista Elevator 中详细描述了这种方法)。这种方法比前一种方法更容易实现,并且当由管理员执行安装时,它运行得相当好。但是,如果应用程序是由标准用户安装的(管理员在标准用户的“肩上”授权安装),该过程不会按预期工作:任务计划程序将应用程序计划在管理员的上下文中启动,而不是在原始标准用户的上下文中启动。应用程序不会在安装结束时启动,而是在以后,当管理员登录到系统时启动,这与人们的预期相去甚远。
第二种方法的另一个问题是目标机器可能已禁用任务计划程序。在这种情况下,此方法将完全无法启动应用程序。
- 与其创建辅助可执行文件来启动我们的应用程序,不如找到一个已经在目标计算机上运行的现有非高权限进程,并让它通过将我们的代码注入到该进程中来代表我们启动一个非高权限进程。代码注入的完美候选者是 Windows shell 进程:它以非高权限运行,我们可以确定它始终存在于运行 Windows Vista 的计算机上(您上次看到没有运行 shell 的 Windows 计算机是什么时候?)。
解决方案:shell 进程中的代码注入
具体来说,这种方法可以按如下方式工作
- 高权限进程将找到一个属于 shell 的窗口,并且该窗口保证随时可用。一个很好的窗口是“Progman”:它负责显示用户桌面。我们可以调用
FindWindow()
API 来获取此窗口的句柄HWND hwndShell = ::FindWindow( _T("Progman"), NULL);
- 我们的高权限进程将调用
RegisterWindowsMessage()
API 来注册一个唯一的 বার্তা,我们将用它与 shell 的窗口通信uVEMsg = ::RegisterWindowMessage( _T("VistaElevatorMsg") );
- 我们的高权限进程将调用
SetWindowsHookEx()
API 来安装一个全局钩子,当系统上运行的任何进程处理 Windows 消息时,该钩子将被调用hVEHook = ::SetWindowsHookEx( WH_CALLWNDPROCRET, (HOOKPROC)VistaElevator_HookProc_MsgRet, hModule, 0);
- 安装钩子后,我们将向 shell 的窗口发送我们的唯一消息,这将使我们的钩子过程被调用。(这就是我们将代码注入 shell 进程的方式!)
::SendMessage( hwndShell, uVEMsg, 0, 0 );
- 当钩子过程被调用时(在 shell 进程的上下文中),它将调用
ShellExecute()
API 来启动我们需要的非高权限进程。由于 shell 进程未提升,我们的进程将继承 shell 的提升级别,因此该进程将以非高权限启动LRESULT CALLBACK VistaElevator_HookProc_MsgRet( int code, WPARAM wParam, LPARAM lParam ) { if ( code >= 0 && lParam ) { CWPRETSTRUCT * pwrs = (CWPRETSTRUCT *)lParam; if (pwrs->message == uVEMsg ) { bVESuccess = VistaTools::MyShellExec( pwrs->hwnd, NULL, szVE_Path, szVE_Parameters, szVE_Directory, bVE_NeedProcessHandle ? &hVE_Process : NULL ); } } return ::CallNextHookEx( hVEHook, code, wParam, lParam ); }
- 最后,我们将删除钩子,因为我们不再需要它
::UnhookWindowsHookEx( hVEHook );
在 C++ 项目中使用代码
上面描述的方法在 VistaTools.cxx 文件中实现为 RunNonElevated()
函数。如果您需要从自己的应用程序启动非高权限进程,您可以将此文件添加到您的 C++ 项目并直接调用此函数。有关如何使用文件和此函数的详细说明在 VistaTools.cxx 文件本身中提供。
但是请注意,为了使 RunNonElevated()
函数工作,您必须将其编译到 DLL 项目中。原因是全局钩子代码需要驻留在 DLL 中,它不能在可执行文件中。另请注意,如果您计划在 x64 版本的 Windows 下运行代码,则需要编译一个单独的 64 位版本的 DLL,以使其按预期工作。原因是,在 x64 版本的 Windows 上,shell 是一个原生的 64 位进程。为了将我们的代码注入其中,您的 DLL 也必须包含原生的 64 位代码。
另请注意,VistaTools.cxx 包含其他几个函数,您在为 Windows Vista 编程时可能会发现它们很有用,例如 IsVista()
、GetElevationType()
等。它们在文件本身中进行了描述。
在安装脚本中使用代码
如果您不使用 C++,或者如果您只需要从安装脚本调用 RunNonElevated()
函数,那么您可以使用我包含在 Redist.zip 文件中的预编译 DLL。此包包含 VistaLib32.dll 和 VistaLib64.dll 文件,它们以您可以运行 RunDll32.exe 实用程序(它是标准 Windows 发行版的一部分)来调用它的方式导出 RunNonElevated()
函数。
例如,如果您使用 InnoSetup(一个流行的软件安装包),那么通常当您想在安装结束时运行您的应用程序时,您会在安装脚本中包含以下行
[Run]
Filename: "{app}\SampleApp32.EXE"; Description: "Launch application";
上述命令启动 SampleApp32.EXE,它将以高权限级别启动。要以非高权限级别启动相同的应用程序,您需要将上述行更改为
[Run]
Filename: "RunDll32.exe"; Parameters:
"{code:AddQuotes|{app}\VistaLib32.dll},RunNonElevated
{code:AddQuotes|{app}\SampleApp32.EXE}";
Description: "Launch application";
这样的命令将使安装程序实用程序在安装结束时启动 RunDll32.exe。反过来,它将加载 VistaLib32.dll 并调用其 RunNonElevated
入口点,将我们的应用程序 SampleApp32.EXE 的路径(用双引号括起来)传递给它。(应用程序的可选命令行参数也可以在那里传递。)SampleApp32.exe 是一个非常简单的应用程序,除了调用 VistaLib32.dll 导出的一些函数并在消息框中显示结果之外,它什么也不做
要查看所有实际操作,请下载文件 SampleApp-setup.zip(请参阅本文顶部的链接)。它包含一个预编译的安装实用程序,该实用程序安装示例应用程序并在安装结束时以非高权限运行它。
向后兼容性如何?
VistaLib32/64.dll 中的代码向后兼容 Windows 2000(我没有用更早的 Windows 版本测试它)。如果您在 Windows XP 或 2000 下调用 RunNonElevated()
,它将简单地使用常规的 ShellExecute()
API 启动应用程序,就像直接调用 ShellExecute()
一样。这意味着您可以在 Vista 和预 Vista 版本的 Windows 上使用相同的安装脚本。
但是请注意,上面显示的安装脚本适用于 32 位版本的 Windows。要使其与 64 位版本一起使用,您需要修改脚本以使用文件 VistaLib64.dll 而不是 VistaLib32.dll。