应用容器(AppContainers) 通常是用于运行通用 Windows 平台(UWP)进程(也被称为 Metro、应用商店或现代应用进程等)的沙箱环境。在应用容器内运行的进程具有低 完整性级别(Integrity Level) ,这意味着它几乎无法访问任何资源,因为大多数对象(例如文件)的默认完整性级别为中。因此,在应用容器内运行的代码由于这种访问权限的限制,无法造成实质性的损害。此外,从对象管理器的角度来看,应用容器创建的命名对象会存储在其自身的对象管理器目录下,该目录基于一个称为应用容器安全标识符(AppContainer SID)的标识符。这意味着一个应用容器无法干扰另一个应用容器的对象。
例如,如果一个不在应用容器(AppContainer)内的进程创建了一个名为“abc”的互斥量(mutex),那么它的完整名称实际上是“\Sessions\1\BaseNamedObjects\abc”(假设该进程在会话1中运行)。另一方面,如果应用容器A创建了一个同样名为“abc”的互斥量,那么它的完整名称则类似于“\Sessions\1\AppContainerNamedObjects\S-1-15-2-466767348-3739614953-2700836392-1801644223-4227750657-1087833535-2488631167\abc”。这意味着它永远不会干扰其他应用容器或任何在应用容器外运行的进程。
尽管应用容器(AppContainers)最初是专为应用商店(Store)应用而设计的,但它们也可用于执行“常规”应用程序,同时提供相同级别的安全性和隔离性。让我们来看看如何实现这一点。
首先,我们需要创建应用容器并获取其应用容器安全标识符(AppContainer SID)。这个SID是基于容器名称的哈希值生成的。在通用 Windows 平台(UWP)环境中,这个名称由应用程序包和签名者的13位哈希值组成。对于常规应用程序,我们可以选择任意字符串作为容器名称;如果选择相同的字符串,将会生成相同的SID——这意味着我们实际上可以使用它来将多个进程“捆绑”到同一个应用容器中。
第一步是创建一个应用容器(AppContainer)配置文件(此处省略错误处理):
PSID appContainerSid;
::CreateAppContainerProfile(containerName, containerName, containerName, nullptr, 0, &appContainerSid);
复制代码
containerName 参数是至关重要的。如果该函数执行失败,很可能意味着该应用容器配置文件已经存在。在这种情况下,我们需要从现有的配置文件中提取安全标识符(SID):
::DeriveAppContainerSidFromAppContainerName(containerName, &appContainerSid); 复制代码
下一步是为进程创建做好准备。最基本的操作是初始化一个进程属性列表,其中包含一个 SECURITY_CAPABILITIES 结构体,以表明我们希望在应用容器(AppContainer)内创建该进程。在此过程中,我们可以指定该应用容器应具备的能力,例如互联网访问权限、访问文档库的权限,以及 Windows 运行时所定义的其他任何能力:
STARTUPINFOEX si = { sizeof(si) };
PROCESS_INFORMATION pi;
SIZE_T size;
SECURITY_CAPABILITIES sc = { 0 };
sc.AppContainerSid = appContainerSid;
::InitializeProcThreadAttributeList(nullptr, 1, 0, &size);
auto buffer = std::make_unique<BYTE[]>(size);
si.lpAttributeList = reinterpret_cast<LPPROC_THREAD_ATTRIBUTE_LIST>(buffer.get());
::InitializeProcThreadAttributeList(si.lpAttributeList, 1, 0, &size));
::UpdateProcThreadAttribute(si.lpAttributeList, 0, PROC_THREAD_ATTRIBUTE_SECURITY_CAPABILITIES, &sc, sizeof(sc), nullptr, nullptr));
复制代码
目前,我们尚未指定任何能力。现在,我们已经准备好创建进程了:
::CreateProcess(nullptr, exePath, nullptr, nullptr, FALSE,
EXTENDED_STARTUPINFO_PRESENT, nullptr, nullptr,
(LPSTARTUPINFO)&si, &pi);
复制代码
我们可以使用通常的第一个“试验对象”——记事本(Notepad)来尝试这一操作。记事本成功启动,一切看起来都正常。然而,如果我们尝试通过记事本的“文件/打开”菜单项来打开几乎任何文件,我们会发现记事本无法访问常规位置的文件,比如“我的文档”或“我的图片”。这是因为记事本是以低完整性级别(Low Integrity Level)运行的,而文件的默认完整性级别是中(Medium)完整性级别:
“在进程资源管理器(Process Explorer)中,进程标记为‘AppContainer’意味着它是以低完整性级别(Low Integrity Level)运行的。”
如果我们希望记事本能够访问用户的文件,例如文档和图片,我们就需要在这些对象上显式设置权限,以允许应用容器安全标识符(AppContainer SID)进行访问。可以使用诸如
SetNamedSecurityInfo 这样的函数来实现(完整的代码可以在 GitHub 上的项目中查看)。
我已经创建了一个简单的应用程序来测试这些功能。我们可以指定一个容器名称、一个可执行文件的路径,然后点击“运行”按钮,使其在应用容器中执行。我们还可以添加需要获得完全权限的文件夹/文件:
现在,让我们尝试一个更有趣的应用程序——Windows 媒体播放器(是的,我知道,现在谁还用老版的媒体播放器呢?但它确实是个有趣的例子)。Windows 媒体播放器有一个(可能让人觉得烦人的?)特性,即你在任何给定时间只能运行它的一个实例。其工作原理是,当媒体播放器启动时,它会创建一个具有特定名称的互斥体(mutex),名为“Microsoft_WMP_70_CheckForOtherInstanceMutex”。如果这个互斥体已经存在,媒体播放器就会向它的“伙伴”(即之前已经运行的媒体播放器实例)发送一条消息,然后终止自身。我们可以使用进程资源管理器(Process Explorer)来做一个简单的技巧,即关闭这个互斥体句柄,然后再启动另一个媒体播放器实例。
让我们尝试点不一样的:让我们在一个应用容器(AppContainer)中运行 Windows 媒体播放器(WMP)。然后,我们再在另一个不同的应用容器中运行另一个 WMP 实例。这样我们是否能得到两个独立的 WMP 实例呢?
以这种方式运行 WMP 时,会弹出它的辅助程序 setup_wm.exe,该程序会询问 WMP 的初始设置。点击“快速设置(Express Settings)”会关闭对话框,但随后它又会再次弹出!而且会不断重复弹出!你无法摆脱它,除非你关闭对话框,但这样一来 WMP 也就无法启动了。你能猜到这是为什么吗?
如果你猜是“权限”问题——你猜对了。当这个对话框弹出时,运行进程监视器(Process Monitor)并过滤出“访问被拒绝(ACCESS DENIED)”的事件,会看到类似这样的结果:
显然,某些注册表项需要访问权限,以便设置能够被保存。该工具允许我们添加这些注册表项,并为它们设置完全权限:
现在,我们可以在两个不同的容器中运行 WMP(只需更改容器名称并重新运行),它们都能正常运行。这是因为现在每个互斥体(mutex)都有一个唯一名称,该名称以相关应用容器(AppContainer)的应用容器安全标识符(AppContainer SID)作为前缀:
相关代码可以在以下网址找到:https://github.com/zodiacon/RunAppContainer.
原文链接