概述
Windows环境上的Chromium沙箱已经经受了时间的考验,目前,人们普遍认为这是在大规模部署的沙箱机制中最好的一个,不需要特权提升即可运行。然而,优点和缺点往往都是相对的。沙箱的实现主要取决于Windows操作系统的安全性,而对Windows的更改行为不会受到Chromium开发团队的控制。如果我们在Windows的安全实施机制中发现错误,那么就有可能攻破沙箱。
本文主要分析了Windows 10 1903版本中引入的一个漏洞,该漏洞打破了CHromium用来保证沙箱安全的某些安全假设。我们将详细分析如何利用该漏洞来开发利用链,以在Chrome/Edge的GPU进程或Firefox的默认内容沙箱中实现沙箱逃逸。我们的漏洞利用过程,实际上是对Windows上一些小缺陷的仔细观察,这些小缺陷本身并没有越过安全边界,但却导致成功的沙箱逃逸。该漏洞在2020年4月修复,编号为CVE-2020-0981。
漏洞背景
在描述漏洞本身之前,我们首先快速浏览一下Chromium沙箱在Windows上的工作方式。借助受限令牌(Restricted Token),沙箱可以按照最小特权的概念来工作。受限令牌是在Windows 2000中添加的一项功能,可以通过以下操作来修改进程的访问令牌,从而减少授予该进程的访问权限:
1、永久禁用组;
2、删除特权;
3、添加受限制的SID。
禁用组将会删除访问令牌的成员,从而导致无法访问由这些组进行保护的资源。删除特权可以防止进程执行任何不必要的特权操作。最后,添加受限制的SID会更改安全访问检查过程。如果要被授予对资源的访问权限,我们需要匹配主列表以及“受限SID”列表中的组的安全描述符条目。如果SID列表中的成员没有被授予对资源的访问权限,那么访问就会被拒绝。
Chromium还使用从Vista开始添加的完整性级别(IL)功能来进一步限制资源访问。通过设置较低的IL,无论访问检查的结果如何,都可以阻止对更高完整性资源的写入访问。
通过这种方式,将受限令牌与IL结合使用,沙箱可以限制受威胁进程能够访问哪些资源,从而限制远程代码执行可能造成的潜在影响。阻止写访问尤为重要,因为这通常会使攻击者可以通过写文件或注册表项,来破坏系统的其他部分。
Windows上的任何进程都可以使用其他令牌创建新进程,例如:通过调用CreateProcessAsUser
。那么,是什么阻止了沙箱进程使用不受限制的令牌创建新进程呢?Windows和Chromium实施了一些安全缓解措施,以使得在沙箱外部创建新进程变得困难:
1、内核限制了非特权用户可以向新进程分配的令牌;
2、沙箱限制限制了用于新进程的适用访问令牌的可用性;
3、Chromium在Job对象内运行一个沙箱进程,该进程可以被任何硬进程配额限制为1的子进程继承。
4、在Windows 10中,Chromium使用子进程缓解策略来阻止子进程创建。除了来自3的Job对象之外,还应用了该对象。
所有这些缓解措施最终都依赖于Windows来确保安全。但是,到目前为止,最关键的还在于1。即使2、3、4都失败了,从理论上来看,我们也不能为新的进程分配更多特权的访问令牌。那么,在分配新令牌时,内核需要检查哪些内容呢?
我们假设调用过程中没有SeAssignPrimaryTokenPrivilege
(我们实际的测试中也没有),那么新的令牌必须满足两个条件之一,这两个条件已经在内核函数SeIsTokenAssignableToProcess
中进行了检查。检查的条件基于内核TOKEN对象结构中的指定值,如下所示:
总而言之,令牌必须满足以下条件:
1、当前进程令牌的子级。要求新令牌的父令牌ID等于进程令牌的ID。
2、当前进程令牌的同级。要求父令牌ID和身份验证ID相同。
此外,还进行了其他检查,以确保新令牌不是身份验证级别的模拟令牌,并且新令牌的IL必须小于或等于当前进程令牌。这些同样重要,但正如我们即将看到的,在实际中用处不大。
令牌分配的过程中,不会检查父令牌或子令牌是否受到限制。如果位于受限令牌沙箱中,我们能否通过所有检查,并将其分配给可以逃逸的沙箱的子级不受限制的令牌呢?显然是不能的,系统会在分配受限令牌时进行“同级令牌”检查,而这些环节将无法通过,这时父级/子级检查将强制执行。如果我们查看内核函数SepFilterToken
,我们将会了解其具体的实现方式。将现有属性从父令牌复制到新的受限令牌时,将会执行以下代码:
NewToken->ParentTokenId = OldToken->TokenId;
通过设置新的受限令牌的父令牌ID,可以确保只有创建受限令牌的进程才能将其用于子级,因为令牌ID对于TOKEN对象的每个实例都是唯一的。同时,通过修改父令牌ID,将会破坏同级检查。
但是,当我在Windows 10 1909环境上进行测试以验证令牌分配行为时,我发现有些奇怪,因为无论我创建了什么令牌,都无法导致分配失败。再次查看SepFilterToken
,我发现代码已经更改。
NewToken->ParentTokenId = OldToken->ParentTokenId;
现在,内核代码只会从旧令牌中复制父令牌ID。这完全打破了原有的检查,因为新的沙箱进程具有一个令牌,而这个令牌被视为桌面上任何其他令牌的同级。
假设我可以绕过其他三个已有的子进程缓解措施,那么这一行更改就足以让我们突破“受限令牌”沙箱。为此,我们还要进行一系列的尝试。
沙箱逃逸
最终,我想到的沙箱逃逸方式非常复杂,不一定是最佳方案。但是,由于Windows的复杂性,我们很难在漏洞利用链中找到可以利用的替代原语。
首先,我们尝试获取合适的访问令牌,并将其分配给新的进程。令牌需要满足以下条件:
1、令牌是主令牌,或者可以转换为主令牌;
2、令牌的IL等于沙箱的IL,或者允许写入,从而可以降低IL级别;
3、令牌符合同级令牌的标准,可以进行分配;
4、令牌用于当前的控制台会话;
5、令牌未沙箱化,或者比当前令牌沙箱化的程序要小。
访问令牌是可保护的对象,因此,如果我们具有足够的访问权限,就可以打开令牌的句柄。但是,访问令牌不是用名称来引用的,而是需要打开令牌,这就意味着需要我们有权访问进程或模拟线程。我们可以使用Get-AccessibleToken
命令使用PowerShell的NtObjectManager
模块查找可访问的令牌。
PS> $ps = Get-NtProcess -Name "chrome.exe" `
-FilterScript { $_.IsSandboxToken } `
-IgnoreDeadProcess
PS> $ts = Get-AccessibleToken -Processes $ps -CurrentSession `
-AccessRights Duplicate
PS> $ts.Count
101
这个脚本获取了计算机上运行的每个沙箱Chrome进程的句柄,然后使用每个进程中的访问令牌来确定我们可以为TOKEN_DUPLICATE
访问打开哪些其他的令牌。之所以要检查TOKEN_DUPLICATE
在新进程中使用的令牌的原因是,由于两个进程不能使用同一访问令牌对象,因此我们需要对令牌进行复制。访问检查需要判断调用进程是否对目标进程具有PROCESS_QUERY_LIMITED_INFORMATION
访问权限,这是打开令牌的先决条件。我们得到了很多结果,大概超过100个条目。
但是,这个数字是有迷惑性的。一开始,几乎可以确定,我们可以访问的某些令牌在沙箱中的数量要比当前令牌在沙箱中的数量更多。确实,我们只需要未沙箱化的可访问令牌。其次,尽管有许多可访问的令牌,但这很可能是代表着只有少数进程能够访问大量的令牌。为此,我们需要将其过滤为仅可以访问非沙箱标记的Chrome进程的命令行。
PS> $ts | ? Sandbox -ne $true | `
Sort {$_.TokenInfo.ProcessCommandLine} -Unique | `
Select {$_.TokenInfo.ProcessId},{$_.TokenInfo.ProcessCommandLine}
ProcessId ProcessCommandLine
--------- ----------------------------------
6840 chrome.exe --type=gpu-process ...
13920 chrome.exe --type=utility --service-sandbox-type=audio ...
在所有可能的Chrome进程中,只有GPU进程和Audio utility进程有权访问非沙箱令牌。这并不意外。渲染器进程比GPU或Audio沙箱具有更多的锁定功能,这是由于调用系统服务以使这些进程正常运行的限制。这确实意味着,由于大多数远程代码执行发生在呈现的HTML或JS内容中,因此大大降低了远程代码执行出现沙箱逃逸的可能性。也就是说,确实存在GPU漏洞,例如Lokihardt曾在Pwn2Own 2016上使用过的一个漏洞。
接下来,让我们集中讨论逃逸GPU进程沙箱。由于我现在没有GPU远程代码执行,因此我将DLL注入到进程中以实现逃逸。这并不像听起来那么简单,一旦GPU进程启动,该进程就被锁定为仅加载Microsoft签名的DLL。我使用了KnownDlls的技巧将DLL加载到内存中。
为了实现沙箱逃逸,我们需要执行以下操作:
1、打开一个不受限制的令牌;
2、复制令牌,以创建新的主令牌,并使令牌可写;
3、删除令牌的IL,以匹配当前令牌(对于GPU来说,这是低IL);
4、使用新令牌调用CreateProcessAsUser
;
5、逃逸低IL沙箱。
即使是第一步,我们也存在问题。获取不受限制令牌的最简单方法是为父进程(即主要的Chrome浏览器进程)打开令牌。但是,如果查看令牌列表,我们发现GPU进程可以访问,但其中不包括Chrome主浏览器进程。原因在于,GPU进程沙箱可以打开浏览器进程的令牌。使用这个令牌,可以创建一个新的受限令牌,该令牌将通过同级检查,以创建具有更多访问权限的新进程,并逃逸沙箱。为了缓解这种情况,我修改了对进程令牌的访问权限,以阻止较低IL的进程为TOKEN_DUPLICATE
访问打开令牌。大家可以关注HardenTokenIntegerityLevelPolicy
。在进行这一修复之前,我们不需要内核中的漏洞,即可逃逸Chrome GPU沙箱,至少不需要正常的低级别IL令牌。
因此,我们无法使用简单的方法,但我们应该可以简单地枚举进程,并找到符合标准的一个进程。我们可以通过使用NtGetNextProcess
系统调用来实现这一点,正如我在上一篇文章中所描述的一样。我们打开所有进程,以进行PROCESS_QUERY_LIMITED_INFORMATION
访问,然后打开令牌,进行TOKEN_DUPLICATE
和TOKEN_QUERY
访问。然后,我们可以检查令牌,以确保其不受限制,然后再继续执行第二步。
要复制令牌,我们可以调用DuplicateTokenEx
并请求传递TOKEN_ALL_ACCESS
作为所需访问的主令牌。但是,有一个新问题,当我们尝试降低IL时,会从SetTokenInformation
中得到ERROR_ACCESS_DENIED
。这是由于Microsoft在WIndows 10中添加了沙箱缓解措施,并向后移植到了所有受支持的操作系统,包括Windows 7。下面的代码是NtDuplicateToken
的摘要,其中已经引入了缓解措施:
ObReferenceObjectByHandle(TokenHandle, TOKEN_DUPLICATE,
SeTokenObjectType, &Token, &Info);
DWORD RealDesiredAccess = 0;
if (DesiredAccess) {
SeCaptureSubjectContext(&Subject);
if (RtlIsSandboxedToken(Subject.PrimaryToken)
&& RtlIsSandboxedToken(Subject.ClientToken)) {
BOOLEAN IsRestricted;
SepNewTokenAsRestrictedAsProcessToken(Token,
Subject.PrimaryToken, &IsRestricted);
if (Token == Subject.PrimaryToken || IsRestricted)
RealDesiredAccess = DesiredAccess;
else
RealDesiredAccess = DesiredAccess
& (Info.GrantedAccess | TOKEN_READ | TOKEN_EXECUTE);
}
} else {
RealDesiredAccess = Info.GrantedAccess;
}
SepDuplicateToken(Token, &DuplicatedToken, ...)
ObInsertObject(DuplicatedToken, RealDesiredAccess, &Handle);
当我们复制令牌时,内核会检查调用方是否已经沙箱化。如果将其沙箱化,则内核将检查要复制的令牌的限制是否小于调用方。如果限制较少,则代码会将所需的访问权限限制为TOKEN_READ
和TOKEN_EXECUTE
。这意味着,如果我们请求类似于TOKEN_ADJUST_DEFAULT
这样的写访问权限,它将被从赋值调用返回给我们的句柄删除。反过来,这会阻止我们减少IL,以便可以将其分配给新进程。
这似乎导致我们的漏洞利用链彻底终结。如果我们无法写入令牌,就无法降低令牌的IL,从而无法对其进行分配。但是,这个实现有一个小缺陷,重复操作将会继续完成,并仅返回具有受限访问权限的句柄。当我们创建新的令牌对象时,默认安全性将授予调用方对令牌对象的完全访问权限。这意味着,一旦我们获得了新令牌的句柄,就可以调用普通的DuplicateHandle
API将其转换为完全可写的句柄。目前我们还不清楚这是不是有意的,但应该注意的是,如果新令牌的限制没那么严格,那么CreateRestrictedToken
中的类似检查会返回错误。无论如何,我们都可以滥用这个功能,以获得可写的、不受限制的令牌,以将其分配给具有正确IL的新进程。
现在,我们可以获得一个不受限制的令牌,可以调用CreateProcessAsUser
来创建我们的新进程。但它的速度并不快,因为GPU进程仍然在受限Job对象中运行,这会阻止创建新进程。在将近5年前的一篇名为“In-Console-Able”的文章中,我详细介绍了Job对象如何阻止新进程的创建。我们不能在控制台驱动程序中使用相同的漏洞来逃逸Job对象吗?在Windows 8.1上,我们似乎可以,但是在Windows 10上,有两件事阻止我们对其的利用:
1、Microsoft更改了Job对象以支持辅助进程计数器。如果我们拥有SeTcbPrivilege
,我们可以将一个标志传递给NtCreateUserProcess
,以在Job内部创建一个新进程,该进程不计入进程总数。控制台驱动程序使用它来清除了逃逸Job的前置条件。由于我们在沙箱中没有SeTcbPrivilege
,因此无法使用这个功能。
2、Microsoft为令牌添加了一个新的标志,以防止将其用于新的进程。Chrome会在所有沙箱进程中设置这个标志,以限制新的子进程。即使没有1,该标志也将阻止滥用控制台驱动程序以产生新的进程。
通过将这两个功能块进行组合,我们借助滥用控制台驱动程序的方式,在当前Job之外产生了一个新的进程。我们需要想出一种方式,既可以避免Job对象限制,也可以绕过子进程限制的标志。
Job对象是从父对象继承到子对象的,因此,如果我们可以在Job对象之外找到GPU进程可以控制的基础讷航,则可以将该进程用作新的父对象并逃逸Job。遗憾的是,在默认情况下,如果我们检查GPU进程可以访问哪些进程,会发现它只能自行打开。
PS> Get-AccessibleProcess -ProcessIds 6804 -AccessRights GenericAll `
| Select-Object ProcessId, Name
ProcessId Name
--------- ----
6804 chrome.exe
打开其自身可能不会很有帮助,但我们不能依靠运气来实现漏洞利用,我们需要尝试别的方式。
我注意到一件事,在一个很小的竞争条件下,设置一个新的Chrome沙箱程序。首先创建进程,然后应用Job对象。如果我们可以让Chrome浏览器生成新的GPU进程,就可以在应用Job对象之前将其用作父进程。GPU进程的处理甚至支持崩溃时重新生成该进程。但是,我们找不到在不导致当前GPU进程终止的情况下,启动新GPU进程的方法,因此无法运行足够长的代码来利用竞争条件。
相反,我决定专注于寻找一个RPC服务,该服务将在Job之外创建一个新进程。有很多RPC服务将进程创建作为主要目标,而其他服务则将进程创建作为一个附加功能。例如,我们以前说过Secondary Logon服务,其中RPC服务的全部目的是产生新进程。
但是,这个想法有一个小缺陷,令牌中的子进程缓解标志是跨模拟边界继承的。由于通常使用模拟令牌作为新进程的基础,因此任何新的进程都将被阻止。但是,我们有一个未设置标志的非受限令牌,可以使用非受限令牌创建一个可以在RPC调用期间模拟的受限令牌,并且可以绕过子进程缓解标志。
我尝试列出可以通过这种方式利用的已知服务,如下所示:
Secondary Logon Service 不可访问 不可逃逸Job
WMI Win32_Process 不可访问 可以逃逸Job
用户帐户控制(UAC) 可访问 不可逃逸Job
后台智能传输服务(BITS) 不可访问 可以逃逸Job
DCOM Activator 可访问 可以逃逸Job
上表并不全面,可能还会有其他RPC服务允许创建进程。但是,正如我们在上面所看到的那样,我们无法从沙箱级别访问这些派生出Secondary Logon、WMI、BITS的已知RPC服务。UAC服务是可以访问的,存在一种通过滥用调试对象来滥用服务,以运行任意特权代码的方法。但遗憾的是,当创建一个新的UAC进程时,该服务会将父进程设置为调用方进程。继承Job对象后,新进程将会被阻止。
列表中的最后一个服务是DCOM Activator,该系统服务负责启动进程外COM服务器,可以从我们的沙箱级别访问该服务。它还将所有COM服务器作为服务进程的子级启动,这意味着Job对象不会被继承。看起来很理想,但还存在一个小问题,为了使DCOM Activator有效,我们需要沙箱可以创建的进程外COM服务器。该对象必须满足以下条件:
1、服务器的Launch Security授予沙箱本地激活权限;
2、服务器不能以交互用户身份运行(该用户会从沙箱中派生),也不能在服务进程中运行;
3、服务器可执行文件必须可以访问受限令牌。
在这里,我们不需要担心第3条,GPU进程可以访问系统可执行文件,因此我们使用预先安装的COM服务器。创建后,是否无法访问COM服务器也并不重要,我们所需要的只是在Job外部启动COM服务器进程的权限,然后就可以实现劫持。我们可以使用OleViewDotNet
和Select-ComAccess
命令,找到可访问的COM服务器。
PS> Get-ComDatabase -SetCurrent
PS> Get-ComClass -ServerType LocalServer32 | `
Where-Object RunAs -eq "" | `
Where-Object {$_.AppIdEntry.ServiceName -eq ""} | `
Select-ComAccess -ProcessId 6804 `
-LaunchAccess ActivateLocal -Access 0 | `
Select-Object Clsid, DefaultServerName
Clsid DefaultServerName
----- -----------------
3d5fea35-6973-4d5b-9937-dd8e53482a56 coredpussvr.exe
417976b7-917d-4f1e-8f14-c18fccb0b3a8 coredpussvr.exe
46cb32fa-b5ca-8a3a-62ca-a7023c0496c5 ieframe.dll
4b360c3c-d284-4384-abcc-ef133e1445da ieframe.dll
5bbd58bb-993e-4c17-8af6-3af8e908fca8 ieproxy.dll
d63c23c5-53e6-48d5-adda-a385b6bb9c7b ieframe.dll
在Windows 10的默认安装中,我们有6个备选的软件。请注意,其中4个都在DLL中,但是这些类已经注册为在DLL Surrogate中运行,因此仍然可以在进程外使用。我们决定选择COREDPUSSVR中的服务器,因为它是唯一的可执行文件,而不是通用的DLLHOST,因此更易于查找。这个COM服务器的启动安全性授予每个人和所有AppContainer程序包本地激活权限,如下所示:
顺便提一句,即使为COREDPUSSVR
注册了两个类,这个可执行文件实际上也只注册了一个以417976b7
开头的类。创建另一个类,将启动服务器可执行文件,但是类的创建将会挂起,以等待一个永远不会出现的类。
要启动服务器,我们需要在模拟子进程无标志受限令牌的同时调用CoCreateInstance
。我们还需要传递CLSCTX_ENABLE_CLOAKING
,以模拟令牌激活服务器,默认值将使用已设置子进程缓解标志的进程令牌,因此将会阻止进程的创建。这样一来,我们就可以发现一个COREDPUSSVR
实例在相同的沙箱级别运行,但是在Job对象之外,并且也没有子进程的缓解。我们似乎接近成功了。
但是,还没有那么快。通常,新进程的默认安全性基于用于创建新进程的访问令牌中的默认DACL。但遗憾的是,由于某些未知原因,DCOM Activator在进程上设置了一个明确的DACL,它仅授予对用户、SYSTEM和当前登录SID的访问权限。即使GPU进程实际上以相同的安全级别运行,也不允许GPU进程打开新的COM服务器进程。我们如此接近成功,但又离成功如此遥远。我尝试了几种方法在COM服务器内部执行代码,例如Windows Hooks,但没有效果。
幸运的是,进程启动后创建的所有线程仍然将使用默认的DACL。我们可以打开其中一个线程,进行完全访问,并使用SetThreadContext
更改线程上下文以重定向执行。我们需要对这些新线程的线程ID进行暴力破解,因为进一步的沙箱缓解措施将阻止我们使用CreateToolhelp32Snapshot
来枚举无法直接打开的进程,而NtGetNextThread
需要我们现在还没有的父进程句柄。
滥用线程会非常痛苦,特别是在我们没有办法直接将任何内容写入进程的时候,但这至少可以有效。为了简便起见,我决定调用WinExec,它将生成一个新进程,并且只需要执行命令行即可。新进程将具有基于默认DACL的安全性,因此我们可以将其打开。
我可以选择其他类似LoadLibrary
的方式来加载DLL。但是,在比较混乱的线程上下文中,这可能会导致进程崩溃。我认为,最好的方法是尽快逃逸这个进程,以避免这种情况。
那么,用什么作为WinExec的命令行呢?我们无法在COM服务器进程中直接写入或分配内存,但是我们可以轻松地重新利用二进制文件中现有的字符串来执行。为了避免寻找字符串地址或处理ASLR,我们选择在DLL的开头使用PE签名,该签名为我们提供了字符串“PE”。当传递给WinExec时,当前的PATH环境变量将会用于查找要启动的可执行文件。我们可以将PATH设置为COM服务器中所需的任何内容,因为当以相同的安全级别启动进程时,DCOM Activator将使用调用方的环境。我们唯一要做的就是找到一个可以写入的目录,这次我们可以使用Get-AccessibleFile
找到一个候选对象,如下所示。
PS> Get-AccessibleFile -Win32Path "C:" -Recurse -ProcessIds 6804 `
-DirectoryAccessRights AddFile -CheckMode DirectoriesOnly `
-FormatWin32Path | Select-Object Name
Name
----
C:ProgramDataMicrosoftDeviceSync
通过设置PATH环境变量,在其中包含DeviceSync
路径,并将名为PE.exe的可执行文件复制到该目录,我们可以设置线程上下文,并生成一个新的进程,该进程不在Job对象中,并且可以由GPU进程打开。
现在,我们可以利用内核漏洞,以低IL运行不受限制的令牌,从新的进程中调用CreateProcessAsUser
。这样一来,将会删除所有除低级别IL以外的沙箱。最后一步,就是突破低IL。也有很多方法可以做到这一点,但我选择了滥用UAC服务。通过滥用相同的令牌访问权限,我们得以在利用链中滥用权限,打开不受限制的令牌,从而获得UI访问权限。这样一来,我们可以自动化特权用户界面,以在低级别沙箱外部执行任意代码。这不一定有效,但可以尝试。
最终,我们总结的利用链如下:
1、打开一个不受限制的令牌。
a、暴力破解进程,直到找到合适的进程令牌。
2、复制令牌,以创建新的主令牌,并使令牌可写。
a、重复令牌为只读;
b、复制句柄,以获得写访问权限。
3、投放令牌的IL,以匹配当前令牌。
4、使用新令牌调用CreateProcessAsUser
。
a、创建一个新的受限令牌,以删除子进程缓解标志。
b、将环境块的PATH设置为包含DeviceSync
的文件夹,然后投放PE.exe文件。
c、模拟受限令牌,并创建OOP COM服务器。
d、暴力破解COM服务器进程中的线程ID。
e、修改线程上下文,以调用WinExec,在内存中传递已知PE签名的地址。
f、等待创建PE进程。
5、逃逸低IL沙箱。
a、生成屏幕键盘的副本,并打开其令牌。
b、根据打开的令牌,创建具有UI访问权限的新进程。
c、自动运行对话框,以退出低IL沙箱。
或者,以图表形式表示如下:
总结
我希望通过这篇文章,能让大家熟悉Windows内核中的一个微小更改是如何严重影响沙箱环境安全性的。此外,还演示了缓解沙箱行为的漏洞利用的价值。因为缓解措施的存在,很多时候都让漏洞利用过程不再那么便捷。
研究引入漏洞的场景和过程会非常有趣。我们推断,也许是有人在更新代码的过程中,认为这是一个Bug,并且对其进行了“修复”。或者,是随着时间的推移才失去了单线的安全性。无论如何,这并不是有意的调整,而且也在现在提供了修复的方案。
由于操作系统中的一些“特性”,即使漏洞利用方法需要花费很多精力才能发现,但通常也可以通过一些缓解措施来达成目标。这些功能本身并非安全问题,但对于构建漏洞利用链来说非常有帮助。