Saturday, 8 February 2020

DLL Import Redirection in Windows 10 1909

While poking around in NTDLL the other day for some Chrome work I noticed an interesting sounding new feature, Import Redirection. As far as I can tell this was introduced in Windows 10 1809, although I'm testing this on 1909.

What piqued my interesting was during initialization I saw the following code being called:

NTSTATUS LdrpInitializeImportRedirection() {
    PUNICODE_STRING RedirectionDllName =     
          &NtCurrentPeb()->ProcessParameters->RedirectionDllName;
    if (RedirectionDllName->Length) {
        PVOID Dll;
        NTSTATUS status = LdrpLoadDll(RedirectionDllName, 0x1000001, &Dll);
        if (NT_SUCCESS(status)) {
            LdrpBuildImportRedirection(Dll);
        }
        // ...
    }

}

The code was extracting a UNICODE_STRING from the RTL_USER_PROCESS_PARAMETERS block then passing it to LdrpLoadDll to load it as a library. This looked very much like a supported mechanism to inject a DLL at startup time. Sounds like a bad idea to me. Based on the name it also sounds like it supports redirecting imports, which really sounds like a bad idea.

Of course it’s possible this feature is mediated by the kernel. Most of the time RTL_USER_PROCESS_PARAMETERS is passed verbatim during the call to NtCreateUserProcess, it’s possible that the kernel will sanitize the RedirectionDllName value and only allow its use from a privileged process. I went digging to try and find who was setting the value, the obvious candidate is CreateProcessInternal in KERNELBASE. There I found the following code:

BOOL CreateProcessInternalW(...) {
    LPWSTR RedirectionDllName = NULL;
    if (!PackageBreakaway) {
        BasepAppXExtension(PackageName, &RedirectionDllName, ...);
    }

    RTL_USER_PROCESS_PARAMETERS Params = {};
    BasepCreateProcessParameters(&Params, ...);
    if (RedirectionDllName) {
        RtlInitUnicodeString(&Params->RedirectionDllName, RedirectionDllName);
    }

    // ...

}

The value of RedirectionDllName is being retrieved from BasepAppXExtension which is used to get the configuration for packaged apps, such as those using Desktop Bridge. This made it likely it was a feature designed only for use with such applications. Every packaged application needs an XML manifest file, and the SDK comes with the full schema, therefore if it’s an exposed option it’ll be referenced in the schema.

Searching for related terms I found the following inside UapManifestSchema_v7.xsd:

<xs:element name="Properties">
  <xs:complexType>
    <xs:all>
      <xs:element name="ImportRedirectionTable" type="t:ST_DllFile" 
                  minOccurs="0"/>
    </xs:all>
  </xs:complexType>
</xs:element>

This fits exactly with what I was looking for. Specifically the Schema type is ST_DllFile which defined the allowed path component for a package relative DLL. Searching MSDN for the ImportRedirectionTable manifest value brought me to this link. Interestingly though this was the only documentation. At least on MSDN I couldn’t seem to find any further reference to it, maybe my Googlefu wasn’t working correctly. However I did find a Stack Overflow answer, from a Microsoft employee no less, documenting it *shrug*. If anyone knows where the real documentation is let me know.

With the SO answer I know how to implement it inside my own DLL. I need to define list of REDIRECTION_FUNCTION_DESCRIPTOR structures which define which function imports I want to redirect and the implementation of the forwarder function. The list is then exported from the DLL through a REDIRECTION_DESCRIPTOR structure as   __RedirectionInformation__. For example the following will redirect CreateProcessW and always return FALSE (while printing a passive aggressive statement):

BOOL WINAPI CreateProcessWForwarder(
    LPCWSTR lpApplicationName,
    LPWSTR lpCommandLine,
    LPSECURITY_ATTRIBUTES lpProcessAttributes,
    LPSECURITY_ATTRIBUTES lpThreadAttributes,
    BOOL bInheritHandles,
    DWORD dwCreationFlags,
    LPVOID lpEnvironment,
    LPCWSTR lpCurrentDirectory,
    LPSTARTUPINFOW lpStartupInfo,
    LPPROCESS_INFORMATION lpProcessInformation)
{
    printf("No, I'm not running %ls\n", lpCommandLine);
    return FALSE;
}

const REDIRECTION_FUNCTION_DESCRIPTOR RedirectedFunctions[] =
{
    { "api-ms-win-core-processthreads-l1-1-0.dll", "CreateProcessW"
                  &CreateProcessWForwarder },
};

extern "C" __declspec(dllexport) const REDIRECTION_DESCRIPTOR __RedirectionInformation__ =
{
    CURRENT_IMPORT_REDIRECTION_VERSION,
    ARRAYSIZE(RedirectedFunctions),
    RedirectedFunctions

};

I compiled the DLL, added it to a packaged application, added the ImportRedirectionTable Manifest value and tried it out. It worked! This seems a perfect feature for something like Chrome as it’s allows us to use a supported mechanism to hook imported functions without implementing hooks on NtMapViewOfSection and things like that. There are some limitations, it seems to not always redirect imports you think it should. This might be related to the mention in the SO answer that it only redirects imports directly in your applications dependency graph and doesn’t support GetProcAddress. But you could probably live with that,

However, to be useful in Chrome it obviously has to work outside of a packaged application. One obvious limitation is there doesn’t seem to be a way of specifying this redirection DLL if the application is not packaged. Microsoft could support this using a new Process Thread Attribute, however I’d expect the potential for abuse means they’d not be desperate to do so.

The initial code doesn’t seem to do any checking for the packaged application state, so at the very least we should be able to set the RedirectionDllName value and create the process manually using NtCreateUserProcess. The problem was when I did the process initialization failed with STATUS_INVALID_IMAGE_HASH. This would indicate a check was made to verify the signing level of the DLL and it failed to load.

Trying with any Microsoft signed binary instead I got STATUS_PROCEDURE_NOT_FOUND which would imply the DLL loaded but obviously the DLL I picked didn't export __RedirectionInformation__. Trying a final time with a non-Microsoft, but signed binary I got back to STATUS_INVALID_IMAGE_HASH again. It seems that outside of a packaged application we can only use Microsoft signed binaries. That’s a shame, but oh well, it was somewhat inconvenient to use anyway.

Before I go there are two further undocumented functions (AFAIK) the DLL can export.

BOOL __ShouldApplyRedirection__(LPWSTR DllName)

If this function is exported, you can disable redirection for individual DLLs based on the DllName parameter by returning FALSE.

BOOL __ShouldApplyRedirectionToFunction__(LPWSTR DllName, DWORD Index)

This function allows you to disable redirection for a specific import on a DLL. Index is the offset into the redirection table for the matched import, so you can disable redirection for certain imports for certain DLLs.

In conclusion, this is an interesting feature Microsoft added to Windows to support a niche edge case, and then seems to have not officially documented it. Nice! However, it doesn’t look like it’s useful for general purpose import redirection as normal applications require the file to be signed by Microsoft, presumably to prevent this being abused by malicious code. Also there's no trivial way to specify the option using CreateProcess and calling NtCreateUserProcess doesn't correctly initialize things like SxS and CSRSS connections.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

Now if you’ve bothered to read this far, I might as well admit you can bypass the signature check quite easily. Digging into where the DLL loading fails we find the following code inside LdrpMapDllNtFileName:

if ((LoadFlags & 0x1000000) && !NtCurrentPeb()->IsPackagedProcess)
{
  status = LdrpSetModuleSigningLevel(FileHandle, 8);
  if (!NT_SUCCESS(status))
    return status;

}

If you look back at the original call to LdrpLoadDll you'll notice that it was passing flag 0x1000000, which presumably means the DLL should be checked against a known signing level. The check is also disabled if the process is in a Packaged Process through a check on the PEB. This is why the load works in a Packaged Application, this check is just disabled. Therefore one way to get around the check would be to just use a Packaged App of some form, but that's not very convenient. You could try setting the flag manually by writing to the PEB, however that can result in the process not working too well afterwards (at least I couldn't get normal applications to run if I set the flag).

What is LdrpSetModuleSigningLevel actually doing? Perhaps we can just bypass the check?

NTSTATUS LdrpSetModuleSigningLevel(HANDLE FileHandle, BYTE SigningLevel) {
    DWORD Flags;
    BYTE CurrentLevel;
    NTSTATUS status = NtGetCachedSigningLevel(FileHandle, &Flags, &CurrentLevel);
    if (NT_SUCCESS(status))
        status = NtCompareSigningLevel(CurrentLevel, SigningLevel);
    if (!NT_SUCCESS(status))
        status = NtSetCachedSigningLevel(4, SigningLevel, &FileHandle);
    return status;

}

The code is using a the NtGetCachedSigningLevel and NtSetCachedSigningLevel system calls to use the kernel's Code Integrity module to checking the signing level. The signing level must be at least level 8, passing in from the earlier code, which corresponds to the "Microsoft" level. This ties in with everything we know, using a Microsoft signed DLL loads but a signed non-Microsoft one doesn't as it wouldn't be set to the Microsoft signing level.

The cached signature checks have had multiple flaws before now. For example watch my UMCI presentation from OffensiveCon. In theory everything has been fixed for now, but can we still bypass it?

The key to the bypass is noting that the process we want to load the DLL into isn't actually running with an elevated signing level, such as Microsoft only DLLs or Protected Process. This means the cached image section in the SECTION_OBJECT_POINTERS structure doesn't have to correspond to the file data on disk. This is effectively the same attack as the one in my blog on Virtual Box (see section "Exploiting Kernel-Mode Image Loading Behavior").

Therefore the attack we can perform is as follows:

1. Copy unsigned Import Redirection DLL to a temporary file.
2. Open the temporary file for RWX access.
3. Create an image section object for the file then map the section into memory.
4. Rewrite the file with the contents of a Microsoft signed DLL.
5. Close the file and section handles, but do not unmap the memory.
6. Start a process specifying the temporary file as the DLL to load in the RTL_USER_PROCESS_PARAMETERS structure.
7. Profit?

Copy of CMD running with the CreateProcess hook installed.

Of course if you're willing to write data to the new process you could just disable the check, but where's the fun in that :-)