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.