Wednesday, 17 April 2019

ProcessDebugObjectHandle Anti-Anti-Debug Trick

During my implementation of NT Debug Object support in NtObjectManager (see a related blog here) I added support to open the debug object for a process by using the ProcessDebugObjectHandle process information class. This immediately struck me as something which could be used for anti-debugging, so I did a few searches and sure enough I was right, it's already documented (for example this link).

With that out of the way I went back to doing whatever it was I should have really been doing. Well not really, instead I considered how you could bypass this anti-debug check. This information was harder to find, and typically you just hook NtQueryInformationProcess and change the return values. I wanted to do something more sneaky, so I looked at various examples to see how the check is done. In pretty much all cases the implementation is:

BOOL IsProcessBeingDebugged() { HANDLE hDebugObject; NTSTATUS status = NtQueryInformationProcess(GetCurrentProcess(), ProcessDebugObjectHandle, hDebugObject, sizeof(hDebugObject), NULL); if (NT_SUCCESS(status) && hDebugObject) { return TRUE; } return FALSE; }

The code checks if the query is successful and then whether a valid debug object handle has been returned, returning TRUE if that's the case. This would indicate the process is being debugged. If the an error occurs or the debug object handle is NULL, then it indicates the process is not being debugged.

To progress I'd now analyse the logic and find the failure conditions for the detection, fortunately the code isn't very big. We want the function to return FALSE even though the debugger is attached, this means we need to either:

  • Make the query return an error code even though a debugger is attached, or...
  • Let the query succeed but return a NULL handle.
We've reached the limit with what we can do staring at the anti-debug code. We'll dig into the other side, the kernel implementation of the information class. It boils down to a single function:

NTSTATUS DbgkOpenProcessDebugPort(PEPROCESS Process, PHANDLE DebugObject) { if (!Process->DebugPort) return STATUS_PORT_NOT_SET; if (PsTestProtectedProcessIncompatibility(Process, KeGetCurrentProcess())) { return STATUS_PROCESS_IS_PROTECTED; } return ObOpenObjectByPointer(Process->DebugObject, MAXIMUM_ALLOWED, DbgkDebugObjectType, UserMode, DebugObject); 

There are three failure cases in this code:

  1. If there's no debug port attached then return STATUS_PORT_NOT_SET.
  2. If the process holding the debug port is at a higher protection level return STATUS_PROCESS_IS_PROTECTED.
  3. Finally open a handle to the debug object and return the status code from the open operation.
For our purposes case 1 is a non-starter as it means the process is not being debugged. Case 2 is interesting but as the Process object parameter (which comes from the handle passed in the query) will be the same as KeGetCurrentProcess that'd never fail. We're therefore all in on case 3. It turns out that the debug objects, like many kernel objects are securable resources. We can confirm that by using NtObjectManager by querying for the DebugObject type and checking its SecurityRequired flag.

PowerShell executing "Get-NtType DebugObject | Select SecurityRequired" and returning True.

If SecurityRequired is true then it means the object must have a security descriptor whether it has a name or not. Therefore we can cause the call to ObOpenObjectByPointer to fail by setting a security descriptor which prevents the process using the anti-debug check opening the debug object and therefore returning FALSE from the check.

To test that we need a debugger and a debuggee. As I do my best to avoid writing new C++ code I converted the anti-debug code to C# using my NtApiDotNet library:

using (var result = NtProcess.Current.OpenDebugObject(false)) { if (result.IsSuccess) { Console.ForegroundColor = ConsoleColor.Red; Console.WriteLine("[ERROR] We're being Debugged, stahp!"); } else { Console.ForegroundColor = ConsoleColor.Green; Console.WriteLine("[SUCCESS] Go ahead, we're cool!"); } }

I don't bother to check for a NULL handle as the kernel code indicates that can't happen, either you get an error, or you get a valid handle. Anyway it doesn't need to be robust, ..., for me ;-)

For the debugger, again we can write it in C#:

Win32ProcessConfig config = new Win32ProcessConfig(); config.ApplicationName = @"Path\To\Debuggee.exe"; config.CommandLine = "debuggee"; config.CreationFlags = CreateProcessFlags.DebugProcess; using (var p = Win32Process.CreateProcess(config)) { using (var dbg = p.Process.OpenDebugObject()) { SecurityDescriptor sd = new SecurityDescriptor(""); dbg.SetSecurityDescriptor(sd, SecurityInformation.Dacl); while (true) { var e = dbg.WaitForDebugEvent(); e.Continue(); if (e is ExitProcessDebugEvent) { break; } } } }

This code is pretty simple, we create the debuggee process with the DebugProcess flag. When CreateProcess is called the APIs will create a new debug object and attach it to the new process. We can then open the debug object and set an appropriate security descriptor to block the open call in the debuggee. Finally we can just poll the debug object which resumes the target, looping until completion.

What can we set as the security descriptor? The obvious choice would be to set an empty DACL which blocks all access. This is distinct from a NULL DACL which allows anyone access. We can specify an empty DACL in SDDL format using "D:". If you test with an empty DACL the debuggee can still open the debug object, this is because the kernel specified MAXIMUM_ALLOWED, as the current user is the owner of the object this allows for READ_CONTROL and WRITE_DAC access to be granted. If we're an administrator we can change the owner field (or by using a WontFix bug) however instead we'll just specify the OWNER_RIGHTS SID with no access. This will block all access to the owner. The SDDL for that is "D:(A;;0;;;OW)".

If you put this all together yourself you'll find it works as expected. We've successfully circumvented the anti-debug check. Of course this anti-debug technique is unlikely to be used in isolation, so it's not likely to be of much real use.

The anti-debug author is trying to model one state variable, whether a process is being debugged, by observing the state of something else, the success or failure from opening the debug object port. You might assume that as the anti-debug check is directly interacting with a debug artefact then there's a direct connection between the two states. However as I've shown that's not the case as there's multiple ways the failure case can manifest. The code could be corrected to check explicitly for STATUS_PORT_NOT_SET and only then indicate the process is not being debugged. Of course this behavior is not documented anywhere, and even it was could be subject to change.

The problem with the anti-debug code is not that you can set a security descriptor on the debug object and get it to fail but the code itself does take into accurately take into account the thing its trying to check. This problem demonstrates the fundamental difficulty in writing secure code, specifically:

Any non-trivial program has a state space too large to accurately model in finite time which leads to unexpected or undefined behavior.

Or put another way:

The time constrained programmer writes what works in testing, not what is correct.

While bypassing anti-debug is hardly a major security issue (well unless you write DRM code), the process I followed here is pretty much the same for any of my bugs. I thought it'd be interesting to see my approach to these sorts of problems.

Tuesday, 12 March 2019

Windows Object Case Sensitivity - Extended Edition

In my last blog post I discussed the changes going on in NTFS to improve case sensitivity support, specifically for WSL. What I glossed over was the impact of case sensitivity on object manager lookups. It turns out I'd not fully stated the facts of the case *ahem* so thought I should remedy that. Again this is based on the behavior of Windows 10 1809. No reason to believe it doesn't go back further.

Specifically let's look back at ObpLookupObjectName which is where the OBJ_CASE_INSENSITIVE flag is forced. To make it clearer I simplified the code in the original blog as it was relevant to the file objects, and thus for NTFS resource but in reality the code is more similar to the following:

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

I've highlighted the additional code I omitted last time.  Each kernel type has a flag CaseInsensitive inside its OBJECT_TYPE_INITIALIZER structure which indicates whether it's always case insensitive or not. Obviously for File objects this flag is set to TRUE which means as long as ObpCaseInsensitive is also set to TRUE then the lookup will always be case insensitive. However if the CaseInsensitive flag is false then the object name lookup is always case sensitive no matter what value ObpCaseInsensitive takes.

Of course maybe no types use this flag and so it's not relevant? There's various ways we could check this, the simplest in my mind is just run a kernel debugger (locally works) and dump the type list. I've put up a Javascript file (link), which is based on the DumpKnownTypes.js file written by @_hugsy_. Run the script in WinDBG with the .scriptrun command and you'll get a dump of types which can be case sensitive and can have a name. The following is a list of the types dumped from Windows 10 1809, I've highlighted the ones which in my opinion are most interesting:
  • Type
  • Job
  • Partition
  • ActivityReference
  • PsSiloContextPaged
  • PsSiloContextNonPaged
  • DebugObject
  • Event
  • Mutant
  • Callback
  • Semaphore
  • Timer
  • IRTimer
  • Profile
  • KeyedEvent
  • WindowStation
  • Desktop
  • TpWorkerFactory
  • Adapter
  • Controller
  • TmTx
  • TmRm
  • TmEn
  • Section
  • Session
  • RegistryTransaction
  • ALPC Port
  • EnergyTracker
  • PowerRequest
  • WmiGuid
  • EtwRegistration
  • EtwSessionDemuxEntry
  • EtwConsumer
  • CoverageSampler
  • DmaAdapter
  • FilterConnectionPort
  • FilterCommunicationPort
  • NdisCmState
  • VRegConfigurationContext
The reason I picked the 7 highlighted types (Event, Mutant, Semaphore, WindowsStation, Desktop, Section and ALPC Port) are these objects tend to have names. Other types could have names but are unlikely to and some, such as Callback, can only be created in the kernel. Notably neither Directory or SymbolicLink are in the list, they'd have been the more interesting objects to attack.

You might wonder what the security angle is here? Let's look at how an object is looked up by name. First here's the object directory structure:

0: kd> dt nt!_OBJECT_DIRECTORY
   +0x000 HashBuckets      : [37] Ptr32 _OBJECT_DIRECTORY_ENTRY
   +0x094 Lock             : _EX_PUSH_LOCK
   +0x098 DeviceMap        : Ptr32 _DEVICE_MAP
   +0x09c ShadowDirectory  : Ptr32 _OBJECT_DIRECTORY
   +0x0a0 NamespaceEntry   : Ptr32 Void
   +0x0a4 SessionObject    : Ptr32 Void
   +0x0a8 Flags            : Uint4B
   +0x0ac SessionId        : Uint4B

The highlighted line shows the object directory doesn't store a list of objects, instead it uses a simple hash table with 37 hash buckets. I've already written about abusing this in Poc||GTFO 13 where I abused the hash table to create a object lookup which took 19 minutes. But for the purposes of this discussion it limits what the kernel can do for a hash algorithm to select the initial bucket as it must support both case sensitive and insensitive lookup with the same code. The actual lookup code looks similar to the following:

POBJECT_DIRECTORY ObpLookupDirectoryEntryEx(POBJECT_DIRECTORY Directory, PUNICODE_STRING Name, ULONG AttributeFlags){ BOOLEAN CaseInSensitive = AttributeFlags & OBJ_CASE_INSENSITIVE; SIZE_T CharCount = Name−>Length / sizeof(WCHAR); PWCHAR Buffer = Name−>Buffer; ULONG Hash = 0; while(CharCount) { Hash = (Hash / 2) + 3 * Hash; Hash += RtlUpcaseUnicodeChar(*Buffer); Buffer++; CharCount−−; } POBJECT_DIRECTORY_ENTRY Entry = Directory−>HashBuckets[Hash % 37]; while(Entry) { if(Entry−>HashValue == Hash) { if(RtlEqualUnicodeString(Name, ObpGetObjectName(Entry−>Object), CaseInSensitive)){ ObReferenceObject(Entry−>Object); return Entry−>Object; } } Entry = Entry−>ChainLink; } return NULL; }

The hash algorithm is highlighted. In order to support different case sensitivities then the name is upper cased before being hashed. It's only when the name itself is checked does the OBJ_CASE_INSENSITIVE flag come into play. Also note that the object entries are stored in a linked list and will bail out when the first object with a matching name is encountered.

This, therefore, is the security related issue. Specifically the linked list is built head first, which makes the order of object name lookup LIFO. As in if there's already an object called 'ABC' inserted, then the object 'abc' will be inserted at the head of the list if the object creation is case sensitive. If a case insensitive search is then performed for the object, then looking up 'ABC' will actually return the 'abc' object as it comes first in the linked list. This results in the ability to resource plant over existing objects. Let's test this out with an Event object:

Hijacking lookup of a case insensitive path.

The screenshot shows using NtObjectManager to create two events. The first event is named 'ABC'. Next we can create the Event named 'abc' without the OBJ_CASE_INSENSITIVE flag specified which will succeed. Finally we open the event again case-insensitive, we get back the last event added, specifically the lower case form even though we requested the upper case name.

This demonstrates we can execute a resource planting attack, let's find a vulnerability and get to exploitation! Not so fast, if you go and lookup the documentation for CreateEvent you'll find the following text associated with the lpName parameter (I've highlighted the important part):

"The name of the event object. The name is limited to MAX_PATH characters. Name comparison is case sensitive."

It turns out that the Win32 APIs never specify OBJ_CASE_INSENSITIVE to the native APIs for objects such as Event, Mutex, Section etc. Therefore you might be able to execute this planting attack against users of the native APIs which set the flag (like NtObjectManager does by default) but it's unlikely you could use it against the Win32 APIs. Using case-sensitive behaviour isn't consistently documented of course, while CreateEvent has documented it, CreateFileMapping does not. I guess we could change that ourselves ;-)

What's the conclusions you can drawn from this? Probably just that Windows is always more complicated than you expect. It's possible that this could still be interesting when exploiting kernel code or user code which uses native APIs but in the vast majority of cases it's probably not a significant problem.

Sunday, 17 February 2019

NTFS Case Sensitivity on Windows

Back in February 2018 Microsoft released on interesting blog post (link) which introduced per-directory case-sensitive NTFS support. MS have been working on making support for WSL more robust and interop between the Linux and Windows side of things started off a bit rocky. Of special concern was the different semantics between traditional Unix-like file systems and Windows NTFS.

I always keep an eye out for new Windows features which might have security implications and per-directory case sensitivity certainly caught my attention. With 1903 not too far off I thought it was time I actual did a short blog post about per-directory case-sensitivity and mull over some of the security implications. While I'm at it why not go on a whistle-stop tour of case sensitivity in Windows NT over the years.

Disclaimer. I don't currently and have never previously worked for Microsoft so much of what I'm going to discuss is informed speculation.

The Early Years

The Windows NT operating system has had the ability to have case-sensitive files since the very first version. This is because of the OS's well known, but little used, POSIX subsystem. If you look at the documentation for CreateFile you'll notice a flag, FILE_FLAG_POSIX_SEMANTICS which is used for the following purposes:

"Access will occur according to POSIX rules. This includes allowing multiple files with names, differing only in case, for file systems that support that naming."

It's make sense therefore that all you'd need to do to get a case-sensitive file system is use this flag exclusively. Of course being an optional flag it's unlikely that the majority of Windows software will use it correctly. You might wonder what the flag is actually doing, as CreateFile is not a system call. If we dig into the code inside KERNEL32 we'll find the following:

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, ...); }

This code shows that if the FILE_FLAG_POSIX_SEMANTICS flag is set, the the Attributes member of the OBJECT_ATTRIBUTES structure passed to NtCreateFile is initialized to 0. Otherwise it's initialized with the flag OBJ_CASE_INSENSITIVE. The OBJ_CASE_INSENSITIVE instructs the Object Manager to do a case-insensitive lookup for a named kernel object. However files do not directly get parsed by the Object Manager, so the IO manager converts this flag to the IO_STACK_LOCATION flag SL_CASE_SENSITIVE before handing it off to the file system driver in an IRP_MJ_CREATE IRP. The file system driver can then honour that flag or not, in the case of NTFS it honours it and performs a case-sensitive file search instead of the default case-insensitive search.

Aside. Specifying FILE_FLAG_POSIX_SEMANTICS supports one other additional feature of CreateFile that I can see. By specifying FILE_FLAG_BACKUP_SEMANTICS, FILE_FLAG_POSIX_SEMANTICS  and FILE_ATTRIBUTE_DIRECTORY in the dwFlagsAndAttributes parameter and CREATE_NEW as the dwCreationDisposition parameter the API will create a new directory and return a handle to it. This would normally require calling CreateDirectory, then a second call to open or using the native NtCreateFile system call.

NTFS always supported case-preserving operations, so creating the file AbC.txt will leave the case intact. However when it does an initial check to make sure the file doesn't already exist if you request abc.TXT then NTFS would find it during a case-insensitive search. If the create is done case-sensitive then NTFS won't find the file and you can now create the second file. This allows NTFS to support full case-sensitivity. 

It seems too simple to create files in a case-sensitive manner, just use the FILE_FLAG_POSIX_SEMANTICS flag or don't pass OBJ_CASE_INSENSITIVE to NtCreateFile. Let's try that using PowerShell on a default installation on Windows 10 1809 to see if that's really the case.

Opening the file AbC.txt with OBJ_CASE_INSENSITIVE and without.

First we create a file with the name AbC.txt, as NTFS is case preserving this will be the name assigned to it in the file system. We then open the file first with the OBJ_CASE_INSENSITIVE attribute flag set and specifying the name all in lowercase. As expected we open the file and displaying the name shows the case-preserved form. Next we do the same operation without the OBJ_CASE_INSENSITIVE flag, however unexpectedly it still works. It seems the kernel is just ignoring the missing flag and doing the open case-insensitive. 

It turns out this is by design, as case-insensitive operation is defined as opt-in no one would ever correctly set the flag and the whole edifice of the Windows subsystem would probably quickly fall apart. Therefore honouring enabling support for case-sensitive operation is behind a Session Manager Kernel Registry valueObCaseInsensitive. This registry value is reflected in the global kernel variable, ObpCaseInsensitive which is set to TRUE by default. There's only one place this variable is used, ObpLookupObjectName, which looks like the following:

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

From this code we can see if ObpCaseInsensitive set to TRUE then regardless of the Attribute flags passed to the lookup operation OBJ_CASE_INSENSITIVE is always set. What this means is no matter what you do you can't perform a case-sensitive lookup operation on a default install of Windows. Of course if you installed the POSIX subsystem you'll typically find the kernel variable set to FALSE which would enable case-sensitive operation for everyone, at least if they forget to set the flags. 

Let's try the same test again with PowerShell but make sure ObpCaseInsensitive is FALSE to see if we now get the expected operation.

Running the same tests but with ObpCaseInsensitive set to FALSE. With OBJ_CASE_INSENSITIVE the file open succeeds, without the flag it fails with an error.

With the OBJ_CASE_INSENSITIVE flag set we can still open the file AbC.txt with the lower case name. However without specifying the flag we we get STATUS_OBJECT_NAME_NOT_FOUND which indicates the lookup operation failed.

Windows Subsystem for Linux

Let's fast forward to the introduction of WSL in Windows 10 1607. WSL needed some way of representing a typical case-sensitive Linux file system. In theory the developers could have implemented it on top of a case-insensitive file system but that'd likely introduce too many compatibility issues. However just disabling ObCaseInsensitive globally would likely introduce their own set of compatibility issues on the Windows side. A compromise was needed to support case-sensitive files on an existing volume.

AsideIt could be argued that Unix-like operating systems (including Linux) don't have a case-sensitive file system at all, but a case-blind file system. Most Unix-like file systems just treat file names on disk as strings of opaque bytes, either the file name matches a sequence of bytes or it doesn't. The file system doesn't really care whether any particular byte is a lower or upper case character. This of course leads to interesting problems such as where two file names which look identical to a user can have different byte representations resulting in unexpected failures to open files. Some file systems such macOS's HFS+ use Unicode Normalization Forms to make file names have a canonical byte representation to make this easier but leads to massive additional complexity, and was infamously removed in the successor APFS. UPDATE: It's been pointed out that Apple actually reversed the APFS change in iOS 11/macOS 10.13.

This compromise can be found back in ObpLookupObjectName as shown below:

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

In the code we now find that the existing check for ObpCaseInsensitive is augmented with an additional check on the current thread's CrossThreadFlags for the ExplicitCaseSensitivity bit flag. Only if the flag is not set will case-insensitive lookup be forced. This looks like a quick hack to get case-sensitive files without having to change the global behavior. We can find the code which sets this flag in 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; } // ... }

Notice in the code to set the the ExplicitCaseSensitivity flag we need to have both SeDebugPrivilege and be a protected process at level 0x51 which is PPL at Windows signing level. This code is from Windows 10 1809, I'm not sure it was this restrictive previously. However for the purposes of WSL it doesn't matter as all processes are gated by a system service and kernel driver so these checks can be easily bypassed. As any new thread for a WSL process must go via the Pico process driver this flag could be automatically set and everything would just work.

Per-Directory Case-Sensitivity

A per-thread opt-out from case-insensitivity solved the immediate problem, allowing WSL to create case-sensitive files on an existing volume, but it didn't help Windows applications inter-operating with files created by WSL. I'm guessing NTFS makes no guarantees on what file will get opened if performing a case-insensitive lookup when there's multiple files with the same name but with different case. A Windows application could easily get into difficultly trying to open a file and always getting the wrong one. Further work was clearly needed, so introduced in 1803 was the topic at the start of this blog, Per-Directory Case Sensitivity. 

The NTFS driver already handled the case-sensitive lookup operation, therefore why not move the responsibility to enable case sensitive operation to NTFS? There's plenty of spare capacity for a simple bit flag. The blog post I reference at the start suggests using the fsutil command to set case-sensitivity, however of course I want to know how it's done under the hood so I put fsutil from a Windows Insider build into IDA to find out what it was doing. Fortunately changing case-sensitivity is now documented. You pass the FILE_CASE_SENSITIVE_INFORMATION structure with the FILE_CS_FLAG_CASE_SENSITIVE_DIR set via NtSetInformationFile to a directory. with the FileCaseSensitiveInformation information class. We can see the implementation for this in the NTFS driver.

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; }

There's a bit to unpack here. Firstly you can only apply this to a directory, which makes some sense based on the description of the feature. You also need to pass an access check with the call NtfsCaseSensitiveInfoAccessCheck. We'll skip over that for a second. 

Next we go into the actual setting or unsetting of the flag. Support for Per-Directory Case-Sensitivity is not enabled unless bit 0 is set in the global g_NtfsEnableDirCaseSensitivity variable. This value is loaded from the value NtfsEnableDirCaseSensitivity in HKLM\SYSTEM\CurrentControlSet\Control\FileSystem, the value is set to 0 by default. This means that this feature is not available on a fresh install of Windows 10, almost certainly this value is set when WSL is installed, but I've also found it on the Microsoft app-development VM which I don't believe has WSL installed, so you might find it enabled in unexpected places. The g_NtfsEnableDirCaseSensitivity variable can also have bit 1 set, which indicates that the directory must be empty before changing the case-sensitivity flag (checked with NtfsIsFileDeleteable) however I've not seen that enabled. If those checks pass then the flag 0x400 is set in the NTFS file object.

If the flag is being unset the only check made is whether the directory contains any existing colliding file names. This seems to have been added recently as when I originally tested this feature in an Insider Preview you could disable the flag with conflicting filenames which isn't necessarily sensible behavior.

Going back to the access check, the code for NtfsCaseSensitiveInfoAccessCheck looks like the following:

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; }

The first check ensures the file handle is opened with FILE_WRITE_ATTRIBUTES access, however that isn't sufficient to enable the flag. The check also ensures that if an access check is performed on the directory's security descriptor that the caller would be granted FILE_ADD_FILE, FILE_ADD_SUBDIRECTORY and FILE_DELETE_CHILD access rights. Presumably this secondary check is to prevent situations where a file handle was shared to another process with less privileges but with FILE_WRITE_ATTRIBUTES rights. 

If the security check is passed and the feature is enabled you can now change the case-sensitivity behavior, and it's even honored by arbitrary Windows applications such as PowerShell or notepad without any changes. Also note that the case-sensitivity flag is inherited by any new directory created under the original.

Showing setting case sensitive on a directory then using Set-Content and Get-Content to interact with the files.

Security Implications of Per-Directory Case-Sensitivity

Let's get on to the thing which interests me most, what's the security implications on this feature? You might not immediately see a problem with this behavior. What it does do is subvert the expectations of normal Windows applications when it comes to the behavior of file name lookup with no way of of detecting its use or mitigating against it. At least with the FILE_FLAG_POSIX_SEMANTICS flag you were only introducing unexpected case-sensitivity if you opted in, but this feature means the NTFS driver doesn't pay any attention to the state of OBJ_CASE_INSENSITIVE when making its lookup decisions. That's great from an interop perspective, but less great from a correctness perspective.

Some of the use cases I could see this being are problem are as follows:
  • TOCTOU where the file name used to open a file has its case modified between a security check and the final operation resulting in the check opening a different file to the final one. 
  • Overriding file lookup in a shared location if the create request's case doesn't match the actual case of the file on disk. This would be mitigated if the flag to disable setting case-sensitivity on empty directories was enabled by default.
  • Directory tee'ing, where you replace lookup of an earlier directory in a path based on the state of the case-sensitive flag. This at least is partially mitigated by the check for conflicting file names in a directory, however I've no idea how robust that is.
I found it interesting that this feature also doesn't use RtlIsSandboxToken to check the caller's not in a sandbox. As long as you meet the access check requirements it looks like you can do this from an AppContainer, but its possible I missed something.  On the plus side this feature isn't enabled by default, but I could imagine it getting set accidentally through enterprise imaging or some future application decides it must be on, such as Visual Studio. It's a lot better from a security perspective to not turn on case-sensitivity globally. Also despite my initial interest I've yet to actual find a good use for this behavior, but IMO it's only a matter of time :-)