Recently, Matt Graeber (@mattifestation) and I have been digging into methods to bypass User Mode Code Integrity (UMCI) in the context of Device Guard. As a result, many CVEs have been released and Microsoft has done a good job at mitigating the attack surface that PowerShell imposes on UMCI by way of continuing to improve Constrained Language Mode (CLM) – the primary PowerShell policy enforcement mechanism for Device Guard and AppLocker. The following mitigations significantly reduce the attack surface, and were a result of CVE-2017-0218.
The vast majority of these injection bugs abuse Microsoft-signed PowerShell scripts or modules that contain functionality to run unsigned code. This ranges all the way from calling Invoke-Expression on a parameter to custom implementations of PowerShell’s Add-Type cmdlet. Microsoft-signed PowerShell code is targeted because an AppLocker or Device Guard policy that permits Microsoft code to execute will execute in Full Language Mode – i.e. with no restrictions placed on what code can be executed.
To fully understand the fix, let’s examine the behavior before the patch. The gist of this bug is that an attacker can use functions within a Microsoft-signed PowerShell script to bypass UMCI. To demonstrate this, let’s look at the script “utils_SetupEnv.ps1”, a component of Windows Troubleshooting Packs contained within C:\Windows\diagnostics. This particular script has a function called “import-cs”. This function simply takes C# code and calls Add-Type on it (note: Add-Type is blocked in constrained language mode). Since the script is Microsoft-signed, it executes in Full Language mode, allowing an attacker to execute arbitrary C#.
As you can see above, we imported “utils_SetupEnv.ps1”, which exposed the “import-cs” function to us. Using that function, we can pass it our own C# which gets executed, bypassing constrained language mode. The above bug was issued CVE-2017-0218 and fixed.
In response to bugs like the one above, Microsoft imposed a few additional restrictions when running in PowerShell’s Constrained Language Mode. The first one is that you are no longer allowed to import PowerShell scripts (.PS1s) via Import-Module or other means. If you try to import a script in CLM, you will see something like this:
One possible workaround would be to rename a PowerShell script (.PS1) to a module file (.PSM1) and import it that way, right? Well, that is where the second mitigation comes in.
Microsoft introduced a restriction on what you can import and use via PowerShell modules (.PSM1s). This was done with “Export-ModuleMember”; if you aren’t familiar with Export-ModuleMember, it defines what functions in a module are able to be used once its imported. In Constrained Language Mode, a module’s function has to be exported via Export-ModuleMember in order for it to be available. This significantly reduces the attack surface of functions to abuse in Microsoft signed PowerShell modules. It’s also just good practice in general to explicitly define what functions you want to expose to users of your module.
If we rename “utils_SetupEnv.ps1” to “utils_SetupEnv.psm1” and try to import it, it will appear to import successfully. You will notice that the “import-cs” function we used earlier isn’t recognized. This is because “import-cs” is not exposed via Export-ModuleMember.
If we look at a valid PowerShell module file, the Export-ModuleMember definition will look something like this:
What that essentially means is that only “Export-ODataEndpointProxy” can be used when the module is imported. This severely limits the abuse potential for Microsoft-signed PowerShell scripts that contain functions that can be abused to run unsigned code as most of them won’t be exposed. Again, the PowerShell team is slowly but surely mitigating many of the bypass primitives we use to circumvent Constrained Language Mode.
After investigating that fix, I discovered and reported a way around the Export-ModuleMember addition, which was assigned CVE-2017-8715 and fixed in the October Patch Tuesday release. This bypass abuses a PowerShell Module Manifest (.PSD1). When investigating the implications of these files, I realized that you could configure module behavior via these files, and that they weren’t subject to the same signing requirements that many of the other PowerShell files are (likely since PSD1 files don’t contain executable code). Despite that, PSD1s can still be signed.
If we go back to the “utils_SetupEnv.ps1” script, the “import-cs” function is no longer available since no functions from that script are exposed via Export-ModuleMember. To get around this, we can rename “utils_SetupEnv.ps1” to “utils_SetupEnv.psm1” so we can import it. After doing so, we can drop a corresponding module manifest for “utils_SetupEnv.psm1” that exports the beloved “import-cs” function for us. This module manifest will look something like this:
As you can see, we have set “import-cs” as a function to export via “FunctionsToExport”. This will export the function just as Export-ModuleMember would have. Since PowerShell module manifest files were not constrained to the same code signing requirements as other PowerShell files, we can simply create our own for the actual Microsoft signed script we want to abuse. After dropping the above manifest for the “utils_SetupEnv” PowerShell module, we can now use the “import-cs” function to execute arbitrary C# code, despite the newly introduced .PS1 import and Export-ModuleMember mitigation.
As I mentioned above, this bypass was issued CVE-2017-8715. The fix involved subjecting PowerShell module manifest files (.PSD1s) to the same code signing requirements as all the other files in a module in that if a module is signed and permitted by Device Guard or AppLocker policy, then a module manifest will only be honored if it is also signed pursuant to a whitelist rule. Doing so will prevent an attacker from modifying an existing manifest or using their own.
As always, report any UMCI bypasses to MSRC for CVEs 🙂