概述
AppContainer是通常用于UWP进程(也称为Metro、Store、Modern)的沙箱。AppContainer中的进程以低完整性级别(Intergrity Level)运行,这实际上意味着它几乎无法访问所有内容,因为对象(例如:文件)的默认完整性级别为中。这意味着,在AppContainer内运行的代码由于缺乏访问权限,而无法对系统产生任何重大的损害。此外,从对象管理器的角度来看,AppContainer创建的命名对象基于称为AppContainer SID的标识符,存储在其自身的对象管理器目录下。这意味着,一个AppContainer不能干扰另一个对象。
例如,如果不在AppContainer中的进程,创建了名为“abc”的互斥锁,那么其全名实际上为“\Sessions\1\BaseNamedObjects\abc”(假设进程在会话1中运行)。另一方面,如果AppContainer A创建名为“abc”的互斥锁,则其全名类似于“\Sessions\1\AppContainerNamedObjects\S-1-15-2-466767348-3739614953-2700836392-1801644223-4227750657-1087833535-2488631167\abc”,也意味着它可能会干扰另一个AppContainer,或者是在AppContainer外部运行的任何进程。
尽管AppContainers是专门为商店的应用创建的,但它也可以用于执行“普通”应用程序,并提供相同级别的安全性和隔离性。接下来,让我们看看如何来实现这一点。
准备工作
首先,我们需要创建AppContainer,并获取AppContainer SID。这个SID基于容器名称的哈希值。在UWP的世界中,该名称由应用程序包和13位签名者哈希值组成。对于普通应用程序来说,我们可以选择任何字符串。如果选择了相同的字符串,就将会产生相同的SID,这也就意味着我们实际上可以使用它将几个进程“捆绑”到同一个AppContainer中。
第一步是创建一个AppContainer配置文件(忽略其中的错误):
PSID appContainerSid; ::CreateAppContainerProfile(containerName, containerName, containerName, nullptr, 0, &appContainerSid);
其中,containerName参数是一个重要的参数。如果该函数出现失败,可能意味着容器配置文件已经存在。在这种情况下,我们需要从现有的配置文件中提取SID:
::DeriveAppContainerSidFromAppContainerName(containerName, &appContainerSid);
下一步是为进程创建做准备。绝对最小值是使用SECURITY_CAPABILITIES结构初始化进程属性列表,从而指示我们希望在AppContainer中创建的流程。作为其中的一部分,我们可以指定此AppContainer中应该具有的功能,例如网络访问、对文档库的访问以及Windows Runtime定义的任何其他功能:
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
通常,我们可以利用记事本来进行尝试。经过测试,记事本的运行过程似乎没有问题。但是,如果我们尝试使用记事本的文件->打开菜单,来打开其他文件(几乎是任何文件),我们会发现记事本无法访问常用的位置(例如:我的文档或我的图片)。这是因为该进程正在以低完整性级别来运行,而文件默认为中完整性级别。
进程管理器(Process Explorer)中的“AppContainer”,使用的是低完整性级别。
如果我们希望记事本能够访问用户的文件(例如:文档和图片),那么就必须在这些对象中设置明确的权限,允许访问AppContainer SID。要使用的函数包括SetNamedSecurityInfo,关于完整代码请参阅GitHub上的项目。
我创建了一个简单的应用程序来测试上述内容。我们可以指定容器名称、可执行路径,然后单击“运行”以在AppContainer中运行。我们可以添加获得完整权限的文件夹或文件:
第二次尝试:Windows Media Player
接下来,让我们尝试一个更加有趣的应用程序,Windows Media Player。尽管我们知道,现在已经很少有用户使用老版本的Windows Media Player,但这确实是一个有趣的例子。Windows Media Player具有一些特定的功能,用户只能在特定的时间内运行它的单个实例。之所以具有这样的限制,其原理是在于WMP创建了一个具有非常独特的名称的互斥锁“Microsoft_WMP_70_CheckForOtherInstanceMutex”,如果它已经存在,那么将会向它的同伴(也就是之前存在的WMP实例)发送一条消息,然后终止。我们可以使用进程管理器执行的另一个简单技巧是关闭该句柄,然后启动另一个WMP。
我们来尝试一些不同的内容:试着在AppContainer中运行WMP,然后在另一个AppContainer中运行另外一个。我们想知道,这样会得到两个实例吗?
以这种方式运行WMP,将会弹出其帮助程序setup_wm.exe,它会询问WMP的初始设置。我们单击“快速设置”关闭对话框,然后,它会再次出现!依此重复。我们无法摆脱这一循环,除非关闭对话框后WMP没有启动。
经过分析,我们认为其原因在于权限的设置。在该对话框出现时,我们运行进程管理器,并过滤“权限拒绝”(ACCESS DENIED),其结果如下所示:
显然,需要访问某些键才能够保存设置。该工具允许添加这些键,并为它们设置完整权限:
现在,我们就可以在两个不同的容器中运行WMP(更改容器名称并重新运行),并且两个程序都能正常运行。这是因为,每个互斥锁现在都有一个以相关AppContainer的AppContainer SID为前缀的唯一名称:
源代码
本文相关代码,可以在GitHub(https://github.com/zodiacon/RunAppContainer)找到。