Windows NTFS文件系统目录大小写敏感导致的安全问题

41yf1sh 系统安全 2019年3月13日发布
Favorite收藏

导语:早在2018年2月,微软就发表过一篇有趣的博客文章,该文章介绍了NTFS文件系统中每个目录的大小写敏感问题。微软一直致力于为WSL提供更加强大的支持,并且开始打通Linux与Windows之间的边界。

概述

早在2018年2月,微软就发表过一篇有趣的博客文章,该文章介绍了NTFS文件系统中每个目录的大小写敏感问题。微软一直致力于为WSL提供更加强大的支持,并且开始打通Linux与Windows之间的边界。特别值得注意的是,传统的类Unix文件系统和Windows NTFS之间具有不同的语义。

我一直密切关注着新的Windows功能,并寻找是否可能存在安全风险。显然,目录大小写敏感的问题引起了我的注意。现在,我觉得是时机写一篇关于目录大小写敏感的简短文章,并列举一下其中的安全隐患。并且,既然我要分析这一方面,那么我觉得可以从Windows NT开始,分析一下多年以来的大小写敏感性问题。

早期版本

从Windows NT的第一个版本开始,文件系统就具有大小写敏感的特性。这是因为在操作系统中存在众所周知但很少使用到的POSIX子系统。如果查看CreateFile的文档,我们可以看到其中存在一个标志:FILE_FLAG_POSIX_SEMANTICS,该标志用于:

根据POSIX规则进行访问。其中包括允许多个文件具有字母相同但大小写不同的名称,适用于支持该命名方式的文件系统。

因此,为了让文件系统能够具有大小写敏感的特性,我们需要做的就是使用此标志。但是,由于该标志是一个可选的标志,因此大多数Windows软件可能都不太会正确使用它。我们希望了解这一标志实际上完成的工作,因为CreateFile并不是系统调用。如果我们深入研究KERNEL32中的代码,可以发现如下部分:

BOOL CreateFileInternal(LPCWSTR lpFileName, ..., DWORD dwFlagsAndAttributes) {
    // ...
    OBJECT_ATTRIBUTES ObjectAttributes;
    if (dwFlagsAndAttributes & FILE_FLAG_POSIX_SEMANTICS){
      ObjectAttributes.Attributes = 0;
    } else {
      ObjectAttributes.Attributes = OBJ_CASE_INSENSITIVE;
    }
   
    NtCreateFile(..., &ObjectAttributes, ...);
}

这段代码说明,如果设置了FILE_FLAG_POSIX_SEMANTICS标志,那么传递给NtCreateFile的OBJECT_ATTRIBUTES结构的Attributes成员将初始化为0。否则,将会使用标志OBJ_CASE_INSENSITIVE来初始化。OBJ_CASE_INSENSITIVE指示对象管理器(Object Manager)命名的内核对象执行不区分大小写的查找过程。但是,对象管理器不会直接解析文件,因此IO管理器会将此标志转化为IO_STACK_LOCATION标志SL_CASE_SENSITIVE,然后再将其传递给IRP_MJ_CREATE IRP中的文件系统驱动程序。然后,文件系统驱动程序可以选择是否遵循该标志。在NTFS文件系统中,将会遵循这一标志,并执行区分大小写的文件搜索过程,而不再进行默认的不区分大小写的搜索。

此外我还发现,FILE_FLAG_POSIX_SEMANTICS支持CreateFile的另外一个附加功能。通过在dwCreationDisposition参数中指定FILE_FLAG_BACKUP_SEMANTICS、FILE_FLAG_POSIX_SEMANTICS和FILE_ATTRIBUTE_DIRECTORY,以及作为dwCreationDisposition参数的CREATE_NEW,API将创建一个新目录,并返回一个句柄。这通常需要调用CreateDirectory,然后第二次调用打开或使用本地NtCreateFile系统调用。

NTFS始终支持大小写敏感操作,因此如果我们创建一个名为ABC.txt的文件,实际的大小写将保持不变。但是,在其确认文件是否存在的初始检查过程中是不区分大小写的,如果我们请求的是abc.TXT,那么NTFS将会找到该文件。如果创建的是大小写敏感的文件,那么NTFS将无法找到文件,也就意味着我们可以创建第二个文件。这样一来,就允许NTFS完全支持大小写敏感。

以大小写敏感的方式创建文件非常简单,只需要使用FILE_FLAG_POSIX_SEMANTICS标志,或者不将OBJ_CASE_INSENSITIVE传递给NtCreateFile即可。接下来,让我们在Windows 10 1809的默认版本中尝试是否真的可以使用PowerShell实现这一点。

1.PNG

首先,我们创建一个名为AbC.txt的文件,在NTFS中是保留大小写的,因此这就将作为文件系统中分配给它的名称。然后,我们打开文件,设置OBJ_CASE_INSENSITIVE属性标志,并以小写形式指定全部名称。正如预期的那样,我们打开文件,显示了保留大小写形式的文件名。接下来,我们在没有OBJ_CASE_INSENSITIVE标志的情况下执行相同的操作,但出乎意料的是,它仍然有效。看起来内核只是忽略了丢失的标志,并且仍然保持大小写不敏感。

事实证明,这是在设计中被定义的,因为大小写不敏感的操作被定义为可选项,没有人会正确设置此标志,并且这可能会造成Windows子系统的崩溃。因此,为会话管理器内核注册表ObCaseInsensitive提供了大小写敏感的支持,默认情况下设置为TRUE。这个变量只在一个地方使用,就是ObpLookupObjectName,如下所示:

NTSTATUS ObpLookupObjectName(POBJECT_ATTRIBUTES ObjectAttributes, ...) {
  // ...
  DWORD Attributes = ObjectAttributes->Attributes;
 
  if (ObpCaseInsensitive) {
    Attributes |= OBJ_CASE_INSENSITIVE;
  }
 
  // Continue lookup.
}

从这段代码中我们可以看到,无论ObpCaseInsensitive是否设置为TRUE,无论传递给查找操作的属性标志是什么,总会设置上OBJ_CASE_INSENSITIVE标志。这意味着,无论我们做什么,都无法在默认的Windows安装上执行区分大小写的查找操作。当然,如果我们安装了POSIX子系统,我们通常会发现内核变量设置为FALSE,这将会为每个人启动大小写敏感的操作,为了避免他们忘记设置标志的情况。

让我们再次使用PowerShell进行相同的测试,但这一次保证ObpCaseInsensitive为FALSE,现在看看是否获得了预期的操作。

2.PNG

在设置了OBJ_CASE_INSENSITIVE标志后,我们仍然可以使用小写名称打开AbC.txt。但是,如果没有指定标志,我们将得到STATUS_OBJECT_NAME_NOT_FOUND,表示查找操作失败。

适用于Linux的Windows子系统

接下来,让我们快速介绍一下Windows 10 1607中的WSLL。WSL以某种方式来表示典型的区分大小写的Linux文件系统。从理论上讲,开发人员可以在不区分大小写的文件系统上实现它,但这可能会引入太多的兼容性问题。然而,全局禁用ObCaseInsensitive可能会在Windows端引入自己的一组兼容性问题。需要进行妥协,以支持现有卷上的区分大小写的文件。

可以说,类Unix操作系统(包括Linux)根本没有区分大小写的文件系统,只是一个忽略大小写的文件系统。大多数类Unix文件系统只会将磁盘上的文件名视为不透明字节(Opaque Bytes)的字符串,判断文件名与字节序列是否匹配。文件系统并不关心任何特定字节是小写字母还是大写字母。这会导致一些有趣的问题,例如,用户看起来相同的两个文件名可能会有不同的字节表示,从而导致打开文件时出现意外的故障。某些文件系统(例如:macOS的HFS+,https://en.wikipedia.org/wiki/HFS_Plus)使用Unicode规范化形式(http://unicode.org/reports/tr15/)来使文件名具有规范化的字节表示,但这样又会产生额外的复杂性,并在后续APFS(https://en.wikipedia.org/wiki/Apple_File_System)中被迅速移除。

更新:有人指出,Apple实际上在iOS 11和macOS 10.13中改变了APFS。

我们可以在ObpLookupObjectName中找到,如下所示:

NTSTATUS ObpLookupObjectName(POBJECT_ATTRIBUTES ObjectAttributes, ...) {
  // ...
  DWORD Attributes = ObjectAttributes->Attributes;
 
  if (ObpCaseInsensitive &&
      KeGetCurrentThread()->CrossThreadFlags.ExplicitCaseSensitivity == FALSE) {
      Attributes |= OBJ_CASE_INSENSITIVE;
  }
 
  // 继续查找
}

在代码中,我们发现现有对ObpCaseInsensitive的检查是通过对ExplicitCaseSensitivity位标志当前线程的CrossThreadFlags额外检查进行了补充。仅当未设置标志时,才会强制进行不区分大小写的查找。这看起来存在一些问题,无需更改全局行为即可获取区分大小写的文件。我们可以在NtSetInformationThread中找到设置此标志的代码。

NTSTATUS NtSetInformationThread(HANDLE ThreadHandle,
                                THREADINFOCLASS ThreadInformationClass,
                                PVOID ThreadInformation,
                                ULONG ThreadInformationLength) {
  switch(ThreadInformationClass) {
 
    case ThreadExplicitCaseSensitivity:
      if (ThreadInformationLength != sizeof(DWORD))
        return STATUS_INFO_LENGTH_MISMATCH;
      DWORD value = *((DWORD*)ThreadInformation);
      if (value) {
        if (!SeSinglePrivilegeCheck(SeDebugPrivilege, PreviousMode))
          return STATUS_PRIVILEGE_NOT_HELD;
        if (!RtlTestProtectedAccess(Process, 0x51) )
          return STATUS_ACCESS_DENIED;
      }
   
      if (value)
        Thread->CrossThreadFlags.ExplicitCaseSensitivity = TRUE;
      else
        Thread->CrossThreadFlags.ExplicitCaseSensitivity = FALSE;
      break;
  }
  // ...
}

请注意,在设置ExplicitCaseSensitivity标志的代码时,我们需要同时拥有SeDebugPrivilege以及级别为0x51的受保护进程,也就是Windows签名PPL级别。这段代码来源于Windows 10 1809,我不清楚此前是否存在这样的限制。但是,我们的目标是WSL,因此这并不重要,因为所有的进程都由系统服务和内核驱动程序进行控制,所以实际上可以轻松绕过这些检查。由于WSL进程的任何新线程都必须通过Pico进程驱动程序,因此可以自动设置此标志,一切都会正常进行。

每个目录下的大小写敏感

逐线程地从大小写不敏感中退出解决了当前存在的问题,允许WSL在现有卷上创建大小写敏感的文件,但这种方式无法帮助Windows应用程序与WSL创建的文件实现交互操作。我猜测,当存在多个具有相同字母、不同大小写形式名称的文件时,如果执行大小写不敏感的查找,NTFS将不能保证会打开哪一个文件。由于试图打开一个文件的过程总会出现问题,所以Windows应用程序很可能经常出错。显然,我们需要进行进一步的工作,因此就会到了本文开头所提到的主题,每个目录下的大小写敏感性。

NTFS驱动程序已经处理了大小写敏感的查找操作,因此为什么不将这一重任交给NTFS,来启用大小写敏感的操作呢?一个简单的位标志就足够用了。我在本文开篇时引用的博客文章中,建议使用fsutil命令来设置区分大小写,但是我希望知道它是如何实现的。因此,我使用IDA在Windows Insider中查看fsutil,从而找出其原理。幸运的是,我们找到了关于大小写敏感的文档,我们将带有FILE_CS_FLAG_CASE_SENSITIVE_DIR设置的FILE_CASE_SENSITIVE_INFORMATION结构通过NtSetInformationFile传递到目录。使用FileCaseSensitiveInformation information类。我们可以在NTFS驱动程序中看到其实现。

NTSTATUS NtfsSetCaseSensitiveInfo(PIRP Irp, PNTFS_FILE_OBJECT FileObject) {
 
  if (FileObject->Type != FILE_DIRECTORY) {
    return STATUS_INVALID_PARAMETER;
  }
 
  NSTATUS status = NtfsCaseSensitiveInfoAccessCheck(Irp, FileObject);
  if (NT_ERROR(status))
    return status;
 
  PFILE_CASE_SENSITIVE_INFORMATION info =
        (PFILE_CASE_SENSITIVE_INFORMATION)Irp->AssociatedIrp.SystemBuffer; 
  if (info->Flags & FILE_CS_FLAG_CASE_SENSITIVE_DIR) {
    if ((g_NtfsEnableDirCaseSensitivity & 1) == 0)
      return STATUS_NOT_SUPPORTED;
    if ((g_NtfsEnableDirCaseSensitivity & 2)
        && !NtfsIsFileDeleteable(FileObject))
    {
      return STATUS_DIRECTORY_NOT_EMPTY;
    }
    FileObject->Flags |= 0x400;
  } else {
    if (NtfsDoesDirHaveCaseDifferingNames(FileObject)) {
      return STATUS_CASE_DIFFERING_NAMES_IN_DIR;
    }
    FileObject->Flags &= ~0x400;
  }
 
  return STATUS_SUCCESS;
}

在这里,还有一点需要进行说明。首先,我们只能将其应用于目录。此外,还需要通过调用NtfsCaseSensitiveInfoAccessCheck来传递访问权限检查。我们会暂时忽略这一点。

接下来,我们进入到实际设置/取消设置标志的阶段。除非在全局g_NtfsEnableDirCaseSensitivity变量中设置了位0,否则不会启用对目录大小写敏感的支持。此值从HKLM\SYSTEM\CurrentControlSet\Control\FileSystem中NtfsEnableDirCaseSensitivity的值加载,默认情况下该值设置为0。这意味着,在全新安装的Windows 10中无法使用此功能。几乎可以肯定,在安装WSL时会设置此值,但我也在Microsoft应用程序开发虚拟机上发现设置了此值,而我觉得虚拟机上没有安装WSL。因此,我们可能会发现,该功能会在一些意想不到的情况下启用。g_NtfsEnableDirCaseSensitivity变量也可以设置为位1,这表示在更改大小写敏感标志之前该目录必须为空(使用NtfsIsFileDeleteable检查),但我没有看到启用。如果这些检查通过,那么会在NTFS文件对象中设置标志0x400。

如果未设置该标志,那么唯一会进行的就是检查该目录是否包含任何冲突的文件名。这似乎是最近添加的功能,因为我最初在Insider Preview中测试此功能时,发现可疑禁用具有冲突的文件名的标志,这不一定是明智的行为。

回到访问检查上来,NtfsCaseSensitiveInfoAccessCheck的代码如下所示:

NTSTATUS NtfsCaseSensitiveInfoAccessCheck(PIRP Irp, PNTFS_FILE_OBJECT FileObject) {
 
  if (NtfsEffectiveMode(Irp) || FileObject->Access & FILE_WRITE_ATTRIBUTES) {
    PSECURITY_DESCRIPTOR SecurityDescriptor;
    SECURITY_SUBJECT_CONTEXT SubjectContext;
    SeCaptureSubjectContext(&SubjectContext);
    NtfsLoadSecurityDescriptor(FileObject, &SecurityDescriptor);
    if (SeAccessCheck(SecurityDescriptor,
                      &SubjectContext
                      FILE_ADD_FILE | FILE_ADD_SUBDIRECTORY | FILE_DELETE_CHILD)) {
      return STATUS_SUCCESS;
    }
  } 
 
  return STATUS_ACCESS_DENIED;
}

第一次检查确保使用FILE_WRITE_ATTRIBUTES访问打开文件句柄,但这还没有启动此标志。该检查还会确保如果对目录的安全描述符执行访问检查,那么调用者会被授予FILE_ADD_FILE、FILE_ADD_SUBDIRECTORY和FILE_DELETE_CHILD访问权限。根据推测,这种二次检查是为了防止文件句柄被共享到具有较少权限但具有FILE_WRITE_ATTRIBUTES权限的另一个进程的情况。

如果通过了安全检查,并且启用了此功能,那么现在就可以更改大小写敏感的行为,甚至可以通过任意Windows应用程序(例如:PowerShell或记事本)进行更改。另外,需要注意的是,大小写敏感会在原始目录下创建的任何新目录中继承。

3.PNG

每个目录下大小写敏感的安全问题

对于这一功能特性,我们最感兴趣的问题就是,这一功能是否存在一些安全问题?我们可能不会立即看到这一行为导致的问题。在这里,所进行的是颠覆了正常Windows应用程序的期望,当涉及到文件名查找的行为发生时,无法检测是否使用了这一设置,也无法进行缓解。使用FILE_FLAG_POSIX_SEMANTICS标志,我们会引入预期之外的大小写敏感功能,但这一功能也意味着NTFS驱动程序在进行查找决策时不会关注OBJ_CASE_INSENSITIVE的状态。从互相操作的角度来看,这有利的,但从安全性来看,就不是那么好了。

我们可以发现这一特性的潜在安全问题如下:

1、TOCTOU用于打开文件的文件名在进行安全检查和最终操作之间,进行了大小写的修改,可能会导致检查的与实际打开的是不同的文件。

2、如果创建请求的大小写与磁盘上文件的实际名称不匹配,那么会在共享位置覆盖文件查找。如果默认情况下启用了“禁止在空目录上设置大小写敏感”标志,可以缓解这一问题。

3、我们可以根据大小写敏感标志的状态,替换路径中早期目录的查找。这一点可以通过检查目录中冲突的文件名来实现部分缓解,但我无法预估这一问题的潜在风险有多严重。

有趣的是,此功能不会使用RtlIsSandboxToken来检查调用方是否位于沙箱中。只要满足访问检查的要求,就可以在AppContainer执行此操作。从好的方面来说,至少在默认情况下不会启用此功能。但是,我仍然可以想象,一些企业使用的镜像中意外启用了这一功能,或者某些应用程序(例如:Visual Studio)会将其启用。从安全角度来看,在全局范围内不开启大小写敏感较为安全。尽管这只是我初步的分析,还没有真正找到严重的漏洞利用方法,但这只是一个时间问题。

本文翻译自:https://tyranidslair.blogspot.com/2019/02/ntfs-case-sensitivity-on-windows.html如若转载,请注明原文地址: https://www.4hou.com/system/16271.html
点赞 0
  • 分享至
取消

感谢您的支持,我会继续努力的!

扫码支持

打开微信扫一扫后点击右上角即可分享哟

发表评论