Thursday, September 8, 2016

Using Device Guard to Mitigate Against Device Guard Bypasses

In my last post, I presented an introduction to Device Guard and described how to go about developing a fairly locked down code integrity policy - a policy that consisted entirely of implicit allow rules. In this post, I’m going to describe how to deny execution of code that would otherwise be whitelisted according to policy. Why would you want to do this? Well, as I blogged about previously, one of the easiest methods of circumventing user-mode code integrity (UMCI) is to take advantage of signed applications that can be used to execute arbitrary, unsigned code. In the blog post, I achieved this using one of Microsoft’s debuggers, cdb.exe. Unfortunately, cdb.exe isn’t the only signed Microsoft binary that can circumvent a locked down code integrity policy. In the coming months, Casey Smith (@subtee) and I will gradually unveil additional signed binaries that circumvent UMCI. In the spirit of transparency, Casey and I will release bypasses as we find them but we will only publicize bypasses for which we can produce an effective mitigation. Any other bypass would be reported to Microsoft through the process of coordinated disclosure.

While the existence of bypasses may cause some to question the effectiveness of Device Guard, consider that the technique I will describe will block all previous, current, and future versions of binaries that circumvent UMCI. The only requirement being that the binaries be signed with a code signing certificate that is in the same chain as the PCA certificate used when we created a deny rule - a realistic scenario. What I’m describing is the FilePublisher file rule level.

In the example that follows, I will create a new code integrity policy with explicit deny rules for all signed versions of the binaries I’m targeting up to an arbitrary version (99.0.0.0) – cdb.exe, windbg.exe, and kd.exe – three user-mode and kernel-mode debuggers signed by Microsoft. You can then merge the denial CI policy with that of your reference policy. I confirmed with the Device Guard team at Microsoft that what I’m about to describe is most likely the ideal method (at time of writing) of blocking the execution of individual binaries that bypass your code integrity policy.

# The directory that contains the binaries that circumvent our Device Guard policy
$Scanpath = 'C:\Program Files\Windows Kits\10\Debuggers\x64'

# The binaries that circumvent our Device Guard policy
$DeviceGuardBypassApps = 'cdb.exe', 'windbg.exe', 'kd.exe'

$DenialPolicyFilePath = 'BypassMitigationPolicy.xml'

# Get file and signature information for every file in the scan directory
$Files = Get-SystemDriver -ScanPath $Scanpath -UserPEs -NoShadowCopy

# We'll use this to filter out the binaries we want to block
$TargetFilePaths = $DeviceGuardBypassApps | ForEach-Object { Join-Path $Scanpath $_ }

# Filter out the user-mode binaries we want to block
# This would just as easily apply to drivers. Just change UserMode to $False
# If you’re wanting this to apply to drivers though, you might consider using
# the WHQLFilePublisher rule.
$FilesToBlock = $Files | Where-Object {
    $TargetFilePaths -contains $_.FriendlyName -and $_.UserMode -eq $True
}

# Generate a dedicated device guard bypass policy that contains explicit deny rules for the binaries we want to block.
New-CIPolicy -FilePath $DenialPolicyFilePath -DriverFiles $FilesToBlock -Level FilePublisher -Deny -UserPEs

# Set the MinimumFileVersion to 99.0.0.0 - an arbitrarily high number.
# Setting this value to an arbitraily high version number will ensure that any signed bypass binary prior to version 99.0.0.0
# will be blocked. This logic allows us to theoretically block all previous, current, and future versions of binaries assuming
# they were signed with a certificate signed by the specified PCA certificate
$DenyPolicyRules = Get-CIPolicy -FilePath $DenialPolicyFilePath
$DenyPolicyRules | Where-Object { $_.TypeId -eq 'FileAttrib' } | ForEach-Object {
    # For some reason, the docs for Edit-CIPolicyRule say not to use it...
    Edit-CIPolicyRule -FilePath $DenialPolicyFilePath -Id $_.Id -Version '99.0.0.0'

}


# The remaining portion is optional. They are here to demonstrate
# policy merging with a reference policy and deployment.


<#
$ReferencePolicyFilePath = 'FinalPolicy.xml'
$MergedPolicyFilePath = 'Merged.xml'
$DeployedPolicyPath = 'C:\DGPolicyFiles\SIPolicy.bin'
#>

# Extract just the file rules from the denial policy. We do this because I don't want to merge
# and possibly overwrite any policy rules from the reference policy.
<#
$Rules = Get-CIPolicy -FilePath $DenialPolicyFilePath
Merge-CIPolicy -OutputFilePath $MergedPolicyFilePath -PolicyPaths $ReferencePolicyFilePath -Rules $Rules
#>

# Deploy the new policy and reboot.
<#
ConvertFrom-CIPolicy -XmlFilePath $MergedPolicyFilePath -BinaryFilePath $DeployedPolicyPath
#>

So in the code above, to generate the policy, we specified the location where the offending binaries were installed. In reality, they can be in any directory and you can generate this deny policy on any machine. In other words, you’re not required to generate it on the machine that will have the code integrity policy deployed. That directory is then scanned. You need to filter out the specific binaries that you want to deny and merge the deny policy with a reference policy and redeploy. Once you’ve redeployed the policy, you will want to validate its efficacy. To validate it, I would ensure the following:
  1. Both the x86 and x64 version of the binary are blocked.
  2. At least two versions of each binary (for each architecture) are blocked.

So, for example, to validate that the signed cdb.exe can no longer execute, be sure to obtain two versions of cdb.exe and have a 32-bit and 64-bit build of each version.

It is unfortunately kind of a hack to have to manually modify the policy XML to specify an arbitrarily large version number. Ideally, in a future version of Device Guard, Microsoft would allow you to specify a wildcard that would imply that the deny rule would apply to all versions of the binary. In the meantime, this hack seems to get the job done. What’s great about this simple workflow is that as new bypasses come out, you can just keep adding deny rules to an all-encompassing Device Guard bypass code integrity policy! In fact, I plan on maintaining such a bypass-specific CI policy on GitHub in the near future.

Now, I’ve done a decent amount of testing of this mitigation, which I consider to be effective and not difficult to implement. I encourage everyone out there to poke holes in my theory, though. And if you discover a bypass for my mitigation, please be a good citizen and let the world know! I hope these posts are continuing to pique your interest in this important technology!

For reference, here is the policy that was generated based on the code above. Note that while there are explicit file paths in the generated policy, the deny rules apply regardless of where the binaries are located on disk.


<?xml version="1.0" encoding="utf-8"?>
<SiPolicy xmlns="urn:schemas-microsoft-com:sipolicy">
  <VersionEx>10.0.0.0</VersionEx>
  <PolicyTypeID>{A244370E-44C9-4C06-B551-F6016E563076}</PolicyTypeID>
  <PlatformID>{2E07F7E4-194C-4D20-B7C9-6F44A6C5A234}</PlatformID>
  <Rules>
    <Rule>
      <Option>Enabled:Unsigned System Integrity Policy</Option>
    </Rule>
    <Rule>
      <Option>Enabled:Audit Mode</Option>
    </Rule>
    <Rule>
      <Option>Enabled:Advanced Boot Options Menu</Option>
    </Rule>
    <Rule>
      <Option>Required:Enforce Store Applications</Option>
    </Rule>
    <Rule>
      <Option>Enabled:UMCI</Option>
    </Rule>
  </Rules>
  <!--EKUS-->
  <EKUs />
  <!--File Rules-->
  <FileRules>
    <FileAttrib ID="ID_FILEATTRIB_F_1" FriendlyName="C:\Program Files\Windows Kits\10\Debuggers\x64\cdb.exe FileAttribute" FileName="CDB.Exe" MinimumFileVersion="99.0.0.0" />
    <FileAttrib ID="ID_FILEATTRIB_F_2" FriendlyName="C:\Program Files\Windows Kits\10\Debuggers\x64\kd.exe FileAttribute" FileName="kd.exe" MinimumFileVersion="99.0.0.0" />
    <FileAttrib ID="ID_FILEATTRIB_F_3" FriendlyName="C:\Program Files\Windows Kits\10\Debuggers\x64\windbg.exe FileAttribute" FileName="windbg.exe" MinimumFileVersion="99.0.0.0" />
  </FileRules>
  <!--Signers-->
  <Signers>
    <Signer ID="ID_SIGNER_F_1" Name="Microsoft Code Signing PCA">
      <CertRoot Type="TBS" Value="27543A3F7612DE2261C7228321722402F63A07DE" />
      <CertPublisher Value="Microsoft Corporation" />
      <FileAttribRef RuleID="ID_FILEATTRIB_F_1" />
      <FileAttribRef RuleID="ID_FILEATTRIB_F_2" />
      <FileAttribRef RuleID="ID_FILEATTRIB_F_3" />
    </Signer>
    <Signer ID="ID_SIGNER_F_2" Name="Microsoft Code Signing PCA 2010">
      <CertRoot Type="TBS" Value="121AF4B922A74247EA49DF50DE37609CC1451A1FE06B2CB7E1E079B492BD8195" />
      <CertPublisher Value="Microsoft Corporation" />
      <FileAttribRef RuleID="ID_FILEATTRIB_F_1" />
      <FileAttribRef RuleID="ID_FILEATTRIB_F_2" />
      <FileAttribRef RuleID="ID_FILEATTRIB_F_3" />
    </Signer>
  </Signers>
  <!--Driver Signing Scenarios-->
  <SigningScenarios>
    <SigningScenario Value="131" ID="ID_SIGNINGSCENARIO_DRIVERS_1" FriendlyName="Auto generated policy on 09-07-2016">
      <ProductSigners />
    </SigningScenario>
    <SigningScenario Value="12" ID="ID_SIGNINGSCENARIO_WINDOWS" FriendlyName="Auto generated policy on 09-07-2016">
      <ProductSigners>
        <DeniedSigners>
          <DeniedSigner SignerId="ID_SIGNER_F_1" />
          <DeniedSigner SignerId="ID_SIGNER_F_2" />
        </DeniedSigners>
      </ProductSigners>
    </SigningScenario>
  </SigningScenarios>
  <UpdatePolicySigners />
  <CiSigners>
    <CiSigner SignerId="ID_SIGNER_F_1" />
    <CiSigner SignerId="ID_SIGNER_F_2" />
  </CiSigners>
  <HvciOptions>0</HvciOptions>
</SiPolicy>

2 comments: