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 the highest supported version number (65535.65535.65535.65535) – 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 65535.65535.65535 - an arbitrarily high
number.
# Setting this value to an arbitraily high version number will
ensure that any signed bypass binary prior to version 65535.65535.65535.65535
# 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 '65535.65535.65535.65535'
}
# 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:
- Both the x86 and x64 version of the binary are blocked.
- 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="65535.65535.65535.65535" />
<FileAttrib
ID="ID_FILEATTRIB_F_2" FriendlyName="C:\Program Files\Windows
Kits\10\Debuggers\x64\kd.exe FileAttribute" FileName="kd.exe"
MinimumFileVersion="65535.65535.65535.65535" />
<FileAttrib
ID="ID_FILEATTRIB_F_3" FriendlyName="C:\Program Files\Windows
Kits\10\Debuggers\x64\windbg.exe FileAttribute"
FileName="windbg.exe" MinimumFileVersion="65535.65535.65535.65535" />
</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>
thanks for post
ReplyDeleteThank you. Very helpful !
ReplyDelete