Author: Clément Labro
This post is a sequel to Bypassing LSA Protection in Userland and The End of PPLdump. Here, I will discuss how I was able to bypass the latest mitigation implemented by Microsoft and develop a new Userland exploit for injecting arbitrary code in a PPL with the highest signer type.
My previous work on protected processes (see Bypassing LSA Protection in Userland) yielded a tool called PPLdump showcasing the possibility for a user with administrator privileges to inject arbitrary code in such processes in Userland, thus effectively bypassing LSA protection without the need for a Kernel driver.
In July 2022 though, Microsoft put an end to this exploit by preventing PPLs from loading “Known DLLs”. To do so, they simply modified an if statement in the process initialization routine to make sure that the \KnownDlls directory handle is not initialized if the process is protected (i.e. PPL or PP), whereas previously this behavior was only effective for PPs. For more details, I would encourage you to read this blog post: The End of PPLdump.
However, the \KnownDlls directory handle initialization is only one part of the problem. The fundamental issue still remains, that is, a DLL’s signature is not verified when it is mapped from a Section object. What this means for us is that, if we manage to write a valid object directory handle right where the \KnownDlls handle is normally initialized, we can still use the same kind of DLL hijacking exploit and thus inject unsigned code in a PPL.
There is nothing new here. This was already explained several years ago by Alex Ionescu and James Forshaw when they discussed the various techniques they found for injecting code in both PPLs and PPs. As such, the exploit chain I am going to discuss here mainly relies on things that were already described in the blog post series “Injecting Code into Windows Protected Processes using COM” (Part 1, Part 2) by James Forshaw.
Our objective is to inject arbitrary code in a PPL at the highest protection level (i.e. WinTcb). To do so, we will adopt the following strategy:
What we need to achieve this scenario is a write-what-where condition. The “where” part is trivial. The \KnownDlls handle is stored in the global variable ntdll!LdrpKnownDllDirectoryHandle and is therefore located at the same address for all processes.
If we attach to explorer.exe with WinDbg for instance, we can see that ntdll!LdrpKnownDllDirectoryHandle is located at 0x7ffafdc5c030 and has the value 0x3c.
And if we attach to spoolsv.exe (Print Spooler service), we can see that ntdll!LdrpKnownDllDirectoryHandle indeed has the same address 0x7ffafdc5c030, but a different value.
As for the “what” part, it is a bit more complicated because the handle value we need to write must reference a valid Object Directory in the target PPL, and we cannot open the process with the access rights that would allow us to determine its value.
The solution to these two problems can be found with a tool such as System Informer, by inspecting the opened handles in a PPL versus a normal process.
A “normal” process such as explorer.exe has at least two opened Directory handles (\KnownDlls and \Sessions\1\BaseNamedObjects in this example), whereas a PPL such as wininit.exe only has one. In the case of a PPL, System Informer does not show the Directory’s name, because it would have to open the process with PROCESS_DUP_HANDLE in order to duplicate the handle and query its properties, which it cannot do precisely because the process is protected.
One way to work around this issue is to use a Kernel Debugger. Here, we can see that the handle references \BaseNamedObjects, which we could have guessed from our previous observation.
However, although System Informer did not show the Directory’s name, it was still able to acquire a list of handles opened in the protected process. This is made possible by the NtQuerySystemInformation system call and the SystemHandleInformation information class. When calling this function, the system generously provides a list of all opened handles in all processes. Each handle entry is returned in the form of a SYSTEM_HANDLE_TABLE_ENTRY_INFO structure that contains 3 interesting members: UniqueProcessId, ObjectTypeIndex and HandleValue.
Thanks to the UniqueProcessId member, we will be able to list all the handles belonging to the PPL we target. The ObjectTypeIndex member will allow us to find only handles associated with an object of type “Directory”. This way, we can determine the value of the handle to \BaseNamedObjects in virtually any protected process.
We now have both the “what” and “where” of our hypothetical write-what-where condition. We still have to find the “write“.
We need to find an arbitrary memory write primitive. However, relying on a 0-day vulnerability in a service or any other executable that can run as a PPL is not an option. What we can do though, is induce a type confusion in a protected process that exposes a COM object such as described in Injecting Code into Windows Protected Processes using COM – Part 1.
Although (D)COM is built on top of DCE/RPC, there are fundamental differences between the two. With DCE/RPC, the process of marshaling and unmarshaling data is always static in the sense that it is predetermined at build time according to an IDL file. For example, the IDL of the MS-EFSR interface describes how to marshal the data sent in a call to the procedure EfsRpcOpenFileRaw as follows.
With (D)COM, however, this process may rely on a Type Library, in which case marshaling is determined at runtime. Let us consider the following dummy example. We have a Type Library that describes the interface ICounter. This interface has one method, GetCounterValue, which takes a CounterName as an input value, and returns a CounterValue.
In this configuration, the out parameter Value is not marshaled by the client. It will be marshaled by the server when the server-side GetCounterValue routine returns.
However, if we somehow manage to force the server to load a Type Library we control, we could change the interface definition like this, and thus induce a type confusion.
In this new configuration, the parameter Value becomes an attacker-controlled input that will be marshaled as is when calling the server’s Stub. However, on server side, the GetCounterValue routine will still treat it as a pointer, resulting in a type confusion. In this example, a zero would be written at an arbitrary address.
If we can find a protected process that exposes such a COM object, we could use this trick to achieve our write-what-where condition.
Before working on this project, I had already worked on the Windows Update Medic Service, so I knew it was an interesting target for that purpose.
This service runs inside a PPL with the Signer type Windows. This is not the maximum value (WinTcb), but we will get to that a bit later.
This service exposes two COM objects: WaaSProtectedSettingsProvider and WaaSRemediation. The latter implements several interfaces, one of which is IWaaSRemediationEx, which has an associated Type Library.
If we create an instance of WaaSRemediation from OleViewDotNet, we can indeed see a call to LoadRegTypeLib with Process Monitor, resulting in the file C:\Windows\System32\WaaSMedicPS.dll being loaded.
The class WaaSRemediation has the CLSID 72566E27-1ABB-4EB3-B4F0-EB431CB1CB32, so we can find its registration information at the following location in the registry: HKLM\SOFTWARE\Classes\CLSID\{72566e27-1abb-4eb3-b4f0-eb431cb1cb32}.
The Type Library has the ID 3ff1aab8-f3d8-11d4-825d-00104b3646c0, so we can find it at the following location: HKLM\SOFTWARE\Classes\TypeLib\{3ff1aab8-f3d8-11d4-825d-00104b3646c0}. The TypeLib path is stored in the key 1.0\0\Win64.
The target file is a DLL, so we should not be able to hijack it since the process is protected, right? Well, it turns out Type Libraries can be either stored as standalone .tlb files or embedded in an EXE/DLL. Even in the latter case, this is not a problem, as explained by J.F.:
If we want to hijack this Type Library, we just have to edit the registry key ...\1.0\0\Win64 and set the path of a Type Library file under our control before creating an instance of the WaaSRemediation class.
Now that we know how we can hijack the Type Library, we should focus on the interface(s) and method(s) we can override. To do so, we can first inspect the content of the original TypeLib with OleViewDotNet or OleView (which comes with the Windows SDK).
The interface has two procedures, LaunchDetectionOnly and LaunchRemediationOnly. Each of them has an out return value we can override so that the server writes arbitrary data at an address under our control.
With a bit of static reverse engineering, we can see that, ultimately, they both call the internal function LaunchRemediationHelper.
In the following corresponding assembly, we control RSI. So, this is rather straightforward, we could have the value returned by SysAllocString being written at an arbitrary location.
The value returned by LaunchRemediationOnly is a VARIANT. The type of the VARIANT is VT_UINT (i.e. 0x17) and its value is the result of LaunchRemediationHelper.
In the following corresponding assembly, we control RDI. So, we could have the WORD 0x17 being written at an arbitrary location and the result of LaunchRemediationHelper being written 8 bytes after this address. In addition, as far as I can say, LaunchRemediationHelper always returns S_OK (i.e. 0x00000000).
Therefore, our potential write primitive could be summarized as follows, where xx represents an unknown value being written, and ?? represents a value in memory that would be left unmodified.
These two primitives are not great, to say the least, but is there any way we could leverage them for achieving our goal?
Our objective is to write the handle value of the object directory \BaseNamedObjects at the address of ntdll!LdrpKnownDllDirectoryHandle. There are some characteristics about handles that are worth mentioning here.
In our case, the \BaseNamedObjects handle is opened in the early stages of the process creation, so its value should not exceed 0xfc and should therefore fit in a single byte. In addition, if the handle value is 0x54 for instance, the three next values 0x55, 0x56 and 0x57, are also perfectly valid.
In the previous part, we saw that we can force LaunchDetectionOnly to write a heap address returned by SysAllocString at an arbitrary address. Such an address could be 0x1fade7354b8 for instance, or b8 54 73 de fa 01 00 00, following the little-endian representation.
If we consider this address as a simple series of bytes, we can see that it contains the value we want – 0x54 – assuming the value of the \BaseNamedObjects handle is 0x54. Thanks to our type confusion trick, we can force the service to write the returned heap address at ntdll!LdrpKnownDllDirectoryHandle-1, which would yield something like this in memory.
Of course, we want the value 54 00 00 00 00 00 00 00, not 54 73 de fa 01 00 00 00 so we need to set the 4 extra bytes to zero. This is where LaunchRemediationOnly comes in handy. We know that this method can be used to write the pattern 17 00 ?? ?? ?? ?? ?? ?? 00 00 00 00 ?? ?? ?? ??, which conveniently contains 4 consecutive zeroes. Writing this pattern at ntdll!LdrpKnownDllDirectoryHandle-7 will yield something like this in memory.
And we finally get the expected handle value! Of course, this is a trivial example as the address returned by LaunchDetectionOnly contained the byte we needed. In an actual exploit, we would have no way to know the value of the address returned by SysAllocString. We would not know at which offset from ntdll!LdrpKnownDllDirectoryHandle we should write either.
That being said, although heap addresses are random, they follow some alignment rules we might be able to exploit. So, I compiled a dataset of 2000 addresses returned by LaunchDetectionOnly and I used a simple Excel spreadsheet to determine the best strategy to adopt depending on the handle value we want to write. Remember that this value is something we can determine even though the target process is protected.
I will spare you the boring details, but essentially, the most efficient strategy would be:
Then, it is theoretically just a matter of repeating this until we hit the appropriate value.
Now that we have an exploit strategy, we should implement it and test it. The first step is to create the Type Library. As explained earlier, I simply transformed the two out parameters into [in] ULONGLONG input values.
Then, we can use the following code to check whether everything is working as expected. Please note that, for testing purposes, the address of ntdll!LdrpKnownDllDirectoryHandle is simply hardcoded here.
Unfortunately, this is not that simple. The initial call to LaunchDetectionOnly works fine, but then, any subsequent call to either of the two methods results in a crash, as shown in the below WinDbg output.
The function LdrpFindKnownDll, which originates from LoadLibraryExW, raises the exception 0xC0000008, i.e. EXCEPTION_INVALID_HANDLE. At this point in the execution, the value of the \KnownDlls directory handle is indeed something like 0x00000001c026de8b, and LoadLibraryExW does not like it. Who would have thought?…
To figure out why LoadLibraryExW is called, we need to better understand how LaunchDetectionOnly and LaunchRemediationOnly work. First of all, as we saw earlier, these two methods call the same helper function – LaunchRemediationHelper – but with slightly different input parameters. The LaunchRemediationHelper method itself creates an instance of the CWaasRemediation class and uses it to invoke the method RunEx. Only then, things start getting more interesting as RunEx calls the evocative method LoadPluginLibrary.