概述
本文深入介绍了Windows内核中一个有趣的逻辑漏洞,以及我与Microsoft的合作伙伴共同修复的过程。如果内核和驱动程序的开发人员在访问设备对象时未考虑IO管理器的操作方式,那么漏洞所产生的最大影响将会是本地权限提升。本篇文章重点说明了我发现漏洞的过程,并详细分析了技术背景。关于进一步调查的更多信息、修复方式和如何避免使用漏洞类编写新代码,可以参考MSRC的博客文章。
技术背景
我在尝试对Issue#779进行漏洞利用时,偶然发现了这个存在漏洞的类。该问题是一个文件TOCTOU绕过了自定义字体加载的缓解策略。在Windows 10中,引入了缓解策略,以限制可利用字体内存损坏漏洞的影响。通常,可以轻松使用文件和对象管理器符号链接的组合,来利用文件TOCTOU问题。使用符号链接的漏洞利用过程可以以普通用户的身份进行,而不会位于沙箱中。我并没有在这个影响较小的问题上花费太多的时间,并没有使用符号链接,而是使用了Shadow Object Directories来实现漏洞利用。Microsoft将该漏洞编号为CVE-2016-3219。我在我的待办清单中添加了一个意外行为的标注,以便日后跟进。
时间过去了一年,我决定回过头去研究一下是否存在更加深入的意外行为。失败的代码类似于以下内容:
HANDLE OpenFilePath(LPCWSTR pwzPath) {
UNICODE_STRING Path;
OBJECT_ATTRIBUTES ObjectAttributes;
HANDLE FileHandle;
NTSTATUS status;
RtlInitUnicodeString(&Path, pwzPath);
InitializeObjectAttributes(&ObjectAttributes,
&Path,
OBJ_KERNEL_HANDLE | OBJ_CASE_INSENSITIVE);
status = IoCreateFile(
&FileHandle,
GENERIC_READ,
&ObjectAttributes,
// ...
FILE_OPEN,
FILE_NON_DIRECTORY_FILE,
// ...
IO_NO_PARAMETER_CHECKING | IO_FORCE_ACCESS_CHECK);
if (NT_ERROR(status))
return NULL;
return FileHandle;
}
当该代码尝试在路径中打开包含对象管理器符号链接的文件时,对IoCreateFile的调用因STATUS_OBJECT_NAME_NOT_FOUND而失败。通过进一步的挖掘,我发现了ObpParseSymbolicLink中错误的来源,如下所示:
NTSTATUS ObpParseSymbolicLink(POBJECT_SYMBOLIC_LINK Object,
PACCESS_STATE AccessState,
KPROCESSOR_MODE AccessMode) {
if (Object->Flags & SANDBOX_FLAG
&& !RtlIsSandboxedToken(AccessState->SubjectSecurityContext, AccessMode))
return STATUS_OBJECT_NAME_NOT_FOUND;
// ...
}
在这里,失败的检查是Microsoft在Windows 10中引入的符号链接缓解的一部分。我在沙箱中创建了符号链接,它将在对象的结构中设置SANDBOX_FLAG。在打开字体文件时,解析符号链接的过程将会进行此项检查。在设置沙箱标志后,内核还会调用RtlIsSandboxedToken来确定调用者是否仍然位于沙箱中。由于打开字体文件的调用是在沙箱进程中,因此线程RtlIsSandboxedToken应该会返回TRUE,函数将会继续执行。相反,实际上它返回的是FALSE,这使得内核认为调用是来自于权限更高的进程,并返回STATUS_OBJECT_NAME_NOT_FOUND,从而缓解任何可能的漏洞利用。
在这时,我明白了我的漏洞利用是在哪里失败的,但并不清楚为什么会失败。具体而言,我不清楚为什么RtlIsSandboxToken会返回FALSE。深入研究这个函数后,我得到了一个重要的想法:
BOOLEAN RtlIsSandboxedToken(PSECURITY_SUBJECT_CONTEXT SubjectSecurityContext,
KPROCESSOR_MODE AccessMode) {
NTSTATUS AccessStatus;
ACCESS_MASK GrantedAccess;
if (AccessMode == KernelMode)
return FALSE;
if (SeAccessCheck(
SeMediumDaclSd,
SubjectSecurityContext,
FALSE,
READ_CONTROL,
0,
NULL,
&RtlpRestrictedMapping,
AccessMode,
&GrantedAccess,
&AccessStatus)) {
return FALSE;
}
return TRUE;
}
其中,有一个重要的参数是AccessMode,它的类型为KPROCESSOR_MODE,可以设置为UserMode或KernelMode这两个值中的其中一个。如果AccessMode参数设置为值KernelMode,则该函数将自动返回FALSE,表示当前调用者不在沙箱中。在内核调试器中,我们在这个函数中设置断点,以进行确认。当从我的漏洞利用中调用时,AccessMode被设置为KernelMode。那么,RtlIsSandboxToken为什么会返回TRUE呢?为了理解内核的运行方式,我们还需要更加深入地了解AccessMode参数所代表的内容。
先前访问模式
Windows中的每个线程都具有与之关联的先前访问模式(Previous Access Mode)。这一先前访问模式存储在KTHREAD结构的PreviousMode成员之中。第三方使用ExGetPreviousMode访问该成员,并返回KPROCESSOR_MODE类型。如果用户模式线程由于系统调用转换而正在运行内核代码,则会将先前访问模式设置为UserMode。作为示例,下图展示了通过NTDLL中的系统调用调度存根(System Call Dispatch Stub)从用户模式应用程序到系统调用NtOpenFile的调用。