Skip to content

Instantly share code, notes, and snippets.

@jborean93
Last active September 17, 2024 22:04
Show Gist options
  • Save jborean93/eb8e88547deb443de83e9f854f60e594 to your computer and use it in GitHub Desktop.
Save jborean93/eb8e88547deb443de83e9f854f60e594 to your computer and use it in GitHub Desktop.
WDAC Investigations for Ansible
Function Get-ValidatedScriptBlock {
[OutputType([ScriptBlock])]
param (
[Parameter(Mandatory)]
[string]
$Name,
[Parameter(Mandatory)]
[string]
$ScriptAsBase64
)
$tmpPathBase = [System.IO.Path]::Combine([System.IO.Path]::GetTempPath(), $Name)
$tmpPath = "$tmpPathBase-$([Guid]::NewGuid()).ps1"
$fs = $null
try {
$scriptBytes = [System.Convert]::FromBase64String($ScriptAsBase64)
$script = [System.Management.Automation.Language.Parser]::ParseInput(
[System.Text.Encoding]::UTF8.GetString($scriptBytes),
"$tmpPathBase.ps1",
[ref]$null,
[ref]$null).GetScriptBlock()
if ([System.Management.Automation.Security.SystemPolicy]::GetFilePolicyEnforcement) {
# New API added in 5.1 for Win 11/Server 2025 and 7+ which we can use
# to provide our own FileStream with only access for ourselves.
$fs = [System.IO.FileStream]::new(
$Name,
[System.IO.FileMode]::Create,
[System.IO.FileAccess]::ReadWrite,
[System.IO.FileShare]::None,
4096,
[System.IO.FileOptions]::DeleteOnClose)
$fs.Write($scriptBytes, 0, $scriptBytes.Length)
$policy = [System.Management.Automation.Security.SystemPolicy]::GetFilePolicyEnforcement(
$tmpPath, $fs)
if ($policy -in @(
[System.Management.Automation.Security.SystemScriptFileEnforcement]::Allow
# WDAC is not enabled, all scripts are allowed.
[System.Management.Automation.Security.SystemScriptFileEnforcement]::None
# WDAC is set in audit mode, still allow the script
[System.Management.Automation.Security.SystemScriptFileEnforcement]::AllowConstrainedAudit
)) {
$script
}
else {
throw "Script is marked as $policy, it is either not signed, has an invalid signature, or the signer was not trusted by WDAC."
}
}
else {
# If the new API is not available we rely on the known behaviour
# of Get-Command to fail with CommandNotFoundException if the
# script is not allowed to run.
[System.IO.File]::WriteAllBytes($tmpPath, $scriptBytes)
try {
$cmd = Get-Command -Name $tmpPath -CommandType ExternalScript -ErrorAction Stop
}
catch [System.Management.Automation.CommandNotFoundException] {
throw "Script is either not signed, has an invalid signature, or the signer was not trusted by WDAC."
}
# We cannot lock the file as pwsh does not read it with a read
# share access. Instead we verify that the cached script contents
# that was loaded matched our input to ensure nothing overwrote it
# with their own script.
$expectedScript = $cmd.OriginalEncoding.GetString($scriptBytes)
if ($expectedScript -ne $cmd.ScriptContents) {
throw "Script has been modified during signature check."
}
$script
}
}
catch {
$_.ErrorDetails = "Provided script for $Name cannot be run: $_"
$PSCmdlet.WriteError($_)
}
finally {
if ($fs) { $fs.Dispose() }
if (Test-Path -LiteralPath $tmpPath) {
Remove-Item -LiteralPath $tmpPath -Force
}
}
}

Thoughts on WDAC and Ansible

Current Behaviour

We execute the following bootstrap script:

try { [Console]::InputEncoding = [Console]::OutputEncoding = New-Object System.Text.UTF8Encoding } catch { $null = $_ }

if ($PSVersionTable.PSVersion -lt [Version]"3.0") {
    '{"failed":true,"msg":"Ansible requires PowerShell v3.0 or newer"}'
    exit 1
}

$exec_wrapper_str = $input | Out-String
$split_parts = $exec_wrapper_str.Split(@("`0`0`0`0"), 2, [StringSplitOptions]::RemoveEmptyEntries)
If (-not $split_parts.Length -eq 2) { throw "invalid payload" }
Set-Variable -Name json_raw -Value $split_parts[1]
& ([ScriptBlock]::Create($split_parts[0]))

For the ssh and winrm connection plugin this is done through powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Unrestricted -EncodedCommand $base64 whereas the psrp connection plugin sends the script through the PowerShell pipeline API. When the process/script has started the connection plugin then sends data over stdin (ssh/winrm) or the input pipe (psrp) with the exec_wrapper.ps1 then a JSON string after \0\0\0\0 in a format similar to:

{
    "module_entry": "module code as base64 string",
    "powershell_modules": {
        "ModuleUtilName": "module util code as base64 string",
    },
    "csharp_utils": {
        "ModuleUtilName": "module util code as base64 string",
    },
    "csharp_utils_modules": ["List of csharp_utils required by the module only and not exec wrapper"],
    "module_args": {
        "key": "module arguments/options from task"
    },
    "actions": ["actions to perform - depends on become, async, etc"],
    "environment": {
        "env name": "env value"
    }
}

This JSON string is parsed by the exec_wrapper.ps1 and uses the PowerShell .NET API to setup the module state before invoking it. Any of the C# utils provided are compiled using a custom Add-Type implementation. This uses the same APIs as the builtin Add-Type cmdlet just with a few more options exposed.

What this means is Ansible builds up a payload to execute with the bootstrapping code and exec options as part of the input to the script. This allows us to invoke code without having to touch the disk or copy any codes/files beforehand to invoke the modules.

In terms of signing and validation we would need to validate the immediate stdin exec code (exec_wrapper.ps1) and the subsequence manifest JSON entries:

  • module_entry - this is the module code that is run
  • powershell_modules - any PowerShell module util code loaded in the session
  • csharp_utils - any C# code compiled and loaded in the session
  • actions - subsequence exec wrapper code (async, become, coverage, module runner, etc)

WDAC Restrictions

WDAC is integrated in PowerShell and is used to configure what code can and cannot be run. With WDAC you can apply a policy that restricts what PowerShell will allow to run. A script must be signed by a certificate that is trusted and explicitly added in the WDAC policy file.

PowerShell can run in either Full Language Mode (FLM) or Constrained Language Mode (CLM) with the latter being a severly locked down environment. With WDAC PowerShell will default to run in CLM and only use FLM when invoking a script or module cmdlet that has been signed by a publisher trusted in the WDAC policy. More information around CLM can be found in PowerShell Constrained Language Mode but some important details are:

  • CLM can run very little code unless what it is calling has been signed and trusted
  • The approved allowed types in CLM is limited and really not much use to us
  • Some cmdlets like Add-Type are blocked because it can define arbitrary types

By default PowerShell will run in Constrained Lanaguage Mode (CLM) which is highly restricted in what can be done. Essentially only the cmdlets provided by PowerShell and any modules that are signed and trusted in the WDAC policy is available. When a signed and trusted script/function is run it will run in FLM allowing it to work as it would without WDAC.

Validation Process

PowerShell can only run files that are allowed to run based on the WDAC policy. It uses the certificate on the authenticode signature to validate whether the script can run in the WDAC policy that is set. The script or module must be on the filesystem for PowerShell to validate the script and run it in FLM. You cannot dot source a signed script when in CLM but you can use & or just call it by the path. For example here is how you can run a signed script when in CLM mode. This script just runs $ExecutionContext.SessionState.LanguageMode to display the language mode it was run in.

# The interactive/default session we are running in will be CLM
$ExecutionContext.SessionState.LanguageMode
# ConstrainedLanguage

# This is not dot sourcing, this is how to invoke a script in the same pwd
.\signed.ps1
# FullLanguage

# This is the same as the above just with the explicit call operator &
& .\signed.ps1
# FullLanguage

# This is dot sourcing which will not work in CLM
. .\signed.ps1
# C:\Users\vagrant\dev\signed\signed.ps1 : Cannot dot-source this command because it was defined in a different language mode.
# To invoke this command without importing its contents, omit the '.' operator.
# At line:1 char:1
# + . .\signed.ps1
# + ~~~~~~~~~~~~~~
#     + CategoryInfo          : InvalidOperation: (:) [signed.ps1], NotSupportedException
#     + FullyQualifiedErrorId : DotSourceNotSupported,signed.ps1

# It is possible to invoke the script in a sub process in FLM
pwsh -File signed.ps1
# FullLanguage

# Same for PowerShell 5.1
powershell -File signed.ps1
# FullLanguage

Note that PowerShell has an issue with -File when the signed script has the [CmdletBinding()]param() attribute at the start. While I think this is a bug the issue is closed and probably won't be fixed, or will only be fixed for new PowerShell versions which don't help us PowerShell/PowerShell#20508.

It is not possible to get the content of a signed script and invoke it in memory in FLM from CLM. You cannot create a ScriptBlock from a string normally as [ScriptBlock] is not a core type. You also can't hack it in by defining it through the provider to do the string conversion for you as it will still run in CLM.

$script = Get-Content signed.ps1 -Raw
& ([ScriptBlock]::Create($script))

# Cannot invoke method. Method invocation is supported only on core types in this language mode.
# At line:1 char:1
# + & ([ScriptBlock]::Create($script))
# + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
#     + CategoryInfo          : InvalidOperation: (:) [], RuntimeException
#     + FullyQualifiedErrorId : MethodInvocationNotSupportedInConstrainedLanguage

# While the script string contains the signature, it is not trusted as it isn't
# loaded from a file.
${function:Test-Function} = $script
Test-Function
# ConstrainedLanguage

Running a new Runspace inside FLM will also run in FLM. This is the same if the PowerShell class is set to run in the current runspace or even with a new runspace:

"Parent Mode: $($ExecutionContext.SessionState.LanguageMode)"

# Same as no mode set
$ps = [PowerShell]::Create([System.Management.Automation.RunspaceMode]::NewRunspace)
$ps.AddScript('"PowerShell with implicit Runspace: $($ExecutionContext.SessionState.LanguageMode)"').Invoke()

$ps = [PowerShell]::Create([System.Management.Automation.RunspaceMode]::CurrentRunspace)
$ps.AddScript('"PowerShell in current Runspace: $($ExecutionContext.SessionState.LanguageMode)"').Invoke()

$rs = [RunspaceFactory]::CreateRunspace()
$rs.Open()
$ps = [PowerShell]::Create()
$ps.Runspace = $rs
$ps.AddScript('"PowerShell with explicit Runspace: $($ExecutionContext.SessionState.LanguageMode)"').Invoke()
$rs.Dispose()

# Parent Mode: FullLanguage
# PowerShell with implicit Runspace: FullLanguage
# PowerShell in current Runspace: FullLanguage
# PowerShell with explicit Runspace: FullLanguage

PowerShell's validation is done through the Wldp* set of Win32 APIs. PowerShell will use WldpGetLockdownPolicy to determine whether WDAC is enforced on the system. Here is an example of it being run through the Ctypes module.

$WLDP_HOST_INFORMATION_REVISION = 1
$WLDP_HOST_ID_POWERSHELL = 4

[Flags()] enum LockdownState {
    WLDP_LOCKDOWN_UNDEFINED = 0
    WLDP_LOCKDOWN_SECUREBOOT_FLAG = 1
    WLDP_LOCKDOWN_DEBUGPOLICY_FLAG = 2
    WLDP_LOCKDOWN_UMCIENFORCE_FLAG = 4
    WLDP_LOCKDOWN_UMCIAUDIT_FLAG = 8
    WLDP_LOCKDOWN_EXCLUSION_FLAG = 16
    WLDP_LOCKDOWN_DEFINED_FLAG = 0x80000000
}

ctypes_struct WLDP_HOST_INFORMATION {
    [int]$dwRevision;
    [int]$dwHostId;
    [MarshalAs('LPWStr')][string]$szSource
    [Microsoft.Win32.SafeHandles.SafeFileHandle]$hSource
}
$wldp = New-CtypesLib wldp.dll

$hostInfo = [WLDP_HOST_INFORMATION]::new()
$hostInfo.dwRevision = $WLDP_HOST_INFORMATION_REVISION
$hostInfo.dwHostId = $WLDP_HOST_ID_POWERSHELL
$hostInfo.hSource = [Microsoft.Win32.SafeHandles.SafeFileHandle]::new([IntPtr]::Zero, $false)

$state = 0
$result = $wldp.WldpGetLockdownPolicy([ref]$hostInfo, [ref]$state, 0)

"Result 0x{0:X8}`nState 0x{1:X8} {2}" -f $result, $state, ([LockdownState]$state)
# Result 0x00000000
# State 0x80000005 WLDP_LOCKDOWN_SECUREBOOT_FLAG, WLDP_LOCKDOWN_UMCIENFORCE_FLAG, WLDP_LOCKDOWN_DEFINED_FLAG

The WLDP_LOCKDOWN_UMCIENFORCE_FLAG flag (4) is the important flag here that tells us the system has enabled User-mode Code Integrity (UMCI). When in this mode the default language mode will be ConstrainedLanguage. The code as used by PowerShell 7 can be found in SystemPolicy.GetWldpPolicy where the system policy has path and handle as null.

When PowerShell is in CLM and goes to invoke a script from the file system it will try two different APIs to validate whether it's allowed in the WDAC policy:

Build 22621 is Windows 11 22H2 and the first server release that has this API will be the upcoming Server 2025. If that API is not available then PowerShell falls back to WldpGetLockdownPolicy. The API that PowerShell uses to call this is publicly exposed:

$fs = [System.IO.FileStream]::OpenRead("signed.ps1")
[System.Management.Automation.Security.SystemPolicy]::GetFilePolicyEnforcement("signed.ps1", $fs)
$fs.Dispose()

It looks like this is only present on PowerShell 5.1 in Server 2025 or newer. I'm unsure if it will ever be backported but since it's not there in box in our supported versions we cannot rely on it. To manually call the Wldp APIs you can use the following PowerShell code:

$WLDP_EXECUTION_EVALUATION_OPTION_NONE = 0

enum WLDP_EXECUTION_POLICY {
    WLDP_CAN_EXECUTE_BLOCKED = 0
    WLDP_CAN_EXECUTE_ALLOWED = 1
    WLDP_CAN_EXECUTE_REQUIRE_SANDBOX = 2
}

$wldp = New-CtypesLib wldp.dll

$hostId = [Guid]'8E9AAA7C-198B-4879-AE41-A50D47AD6458'
$fs = [System.IO.File]::OpenRead("$pwd\signed.ps1")
try {
    $policy = 0
    $res = $wldp.WldpCanExecuteFile(
        [ref]$hostId,
        $WLDP_EXECUTION_EVALUATION_OPTION_NONE,
        $fs.SafeFileHandle,
        $wldp.MarshalAs("Test WldpCanExecuteFile", "LPWStr"),
        [ref]$policy)
}
finally {
    $fs.Dispose()
}

"Result 0x{0:X8}`nPolicy 0x{1:X8} {2}" -f $res, $policy, ([WLDP_EXECUTION_POLICY]$policy)

# When targeting a signed file
# Result 0x00000000
# Policy 0x00000001 WLDP_CAN_EXECUTE_ALLOWED

# When targeting an unsigned file
# Result 0x00000000
# Policy 0x00000002 WLDP_CAN_EXECUTE_REQUIRE_SANDBOX

And an example of WldpGetLockdownPolicy:

$WLDP_HOST_INFORMATION_REVISION = 1
$WLDP_HOST_ID_POWERSHELL = 4

[Flags()] enum LockdownState {
    WLDP_LOCKDOWN_UNDEFINED = 0
    WLDP_LOCKDOWN_SECUREBOOT_FLAG = 1
    WLDP_LOCKDOWN_DEBUGPOLICY_FLAG = 2
    WLDP_LOCKDOWN_UMCIENFORCE_FLAG = 4
    WLDP_LOCKDOWN_UMCIAUDIT_FLAG = 8
    WLDP_LOCKDOWN_EXCLUSION_FLAG = 16
    WLDP_LOCKDOWN_DEFINED_FLAG = 0x80000000
}

ctypes_struct WLDP_HOST_INFORMATION {
    [int]$dwRevision;
    [int]$dwHostId;
    [MarshalAs('LPWStr')][string]$szSource
    [Microsoft.Win32.SafeHandles.SafeFileHandle]$hSource
}
$wldp = New-CtypesLib wldp.dll

$hostInfo = [WLDP_HOST_INFORMATION]::new()
$hostInfo.dwRevision = $WLDP_HOST_INFORMATION_REVISION
$hostInfo.dwHostId = $WLDP_HOST_ID_POWERSHELL

$fs = [System.IO.File]::OpenRead("$pwd\signed.ps1")
try {
    $hostInfo.szSource = "$pwd\signed.ps1"
    $hostInfo.hSource = $fs.SafeFileHandle

    $state = 0
    $result = $wldp.WldpGetLockdownPolicy([ref]$hostInfo, [ref]$state, 0)
}
finally {
    $fs.Dispose()
}

"Result 0x{0:X8}`nState 0x{1:X8} {2}" -f $result, $state, ([LockdownState]$state)

# When targeting a signed file
# Result 0x00000000
# State 0x80000000 WLDP_LOCKDOWN_DEFINED_FLAG

# When targeting an unsigned file
# Result 0x00000000
# State 0x80000004 WLDP_LOCKDOWN_UMCIENFORCE_FLAG, WLDP_LOCKDOWN_DEFINED_FLAG

All of these APIs require a file handle hence the need for the script to be loaded from the filesystem. There does exist new APIs added with WldpCanExecuteFile called WldpCanExecuteBuffer and WldpCanExecuteStream which seem to support validating the script contents from a byte buffer or stream respectively. Unfortunately when decompiling the code for these functions it shows that they set WLDP_CAN_EXECUTE_BLOCKED or WLDP_CAN_EXECUTE_REQUIRE_SANDBOX if UCMI protection is enabled and do nothing to validate the buffer/stream is trusted. This makes them unfit for purpose and effectively a no-op. Maybe in the future this functionality might be added to the function but for now it cannot be used. See the below for a way to call it in PowerShell for future investigation:

$WLDP_EXECUTION_EVALUATION_OPTION_NONE = 0

enum WLDP_EXECUTION_POLICY {
    WLDP_CAN_EXECUTE_BLOCKED = 0
    WLDP_CAN_EXECUTE_ALLOWED = 1
    WLDP_CAN_EXECUTE_REQUIRE_SANDBOX = 2
}

$wldp = New-CtypesLib wldp.dll

$hostId = [Guid]'8E9AAA7C-198B-4879-AE41-A50D47AD6458'

# Should be raw bytes but potentially it might want the UTF-16-LE encoding of
# the script due to how the pwsh Authenticode SIP provider works.
$fileBytes = [System.IO.File]::ReadAllBytes("$pwd\signed.ps1")
# $fileBytes = [System.Text.Encoding]::Unicode.GetBytes((Get-Content signed.ps1 -Raw))

$buffer = [System.Runtime.InteropServices.Marshal]::AllocHGlobal($fileBytes.Length)
try {
    [System.Runtime.InteropServices.Marshal]::Copy($fileBytes, 0, $buffer, $fileBytes.Length)

    $policy = 0
    $res = $wldp.WldpCanExecuteBuffer(
        [ref]$hostId,
        $WLDP_EXECUTION_EVALUATION_OPTION_NONE,
        $buffer,
        $fileBytes.Length,
        $wldp.MarshalAs("Test WldpCanExecuteBuffer", "LPWStr"),
        [ref]$policy)
}
finally {
    [System.Runtime.InteropServices.Marshal]::FreeHGlobal($buffer)
}

"Result 0x{0:X8}`nPolicy 0x{1:X8} {2}" -f $res, $policy, ([WLDP_EXECUTION_POLICY]$policy)

Add-Type

One common scenario used in Ansible is to use Add-Type to compile C# code at runtime. For example the default Ansible.Basic.AnsibleModule type is sent across as a C# module util and compiled using the custom Add-CSharpType cmdlet. In Windows PowerShell 5.1, this mechanism will spawn csc.exe to build a temporary .dll on the disk which is then loaded in the PowerShell process. On PowerShell 7+ this instead builds the assembly in memory in the powershell process itself. As csc.exe is signed by Microsoft this is not blocked by default with the base WDAC policy so Add-Type does work.

A lot of the online docs state that Add-Type will not work in WDAC when compiling from a string rather than loading a .dll directly as the DLL emitted by csc.exe won't be signed. It seems like that is either no longer the case or potentially has been fixed in specific Windows versions (more investigation is needed). This https://posts.specterops.io/documenting-and-attacking-a-windows-defender-application-control-feature-the-hard-way-a-case-73dd1e11be3a blog post is an excellent and thorough breakdown as to what is happening in this case but to sum it up:

  • PowerShell writes a temporary .cs file to $env:TEMP
  • PowerShell calls WldpSetDynamicCodeTrust which has the Kernel set an NTFS extended attribute $KERNEL.PURGE.TRUSTCLAIM on the .cs file
    • Only the kernel can set $KERNEL. EAs and the .PURGE. section means the kernel will remove it if the file is changed in any way
  • PowerShell calls csc.exe with the argument /EnforceCodeIntegrity
  • csc.exe will compile the code, check if the input .cs had the $KERNEL.PURGE.TRUSTCLAIM EA and get the kernel to also add it to the output .dll
  • PowerShell then tries to load that dll checking if the EA is present

The end result is a .dll that is not signed but still trusted to be loaded into the process because the caller of Add-Type was trusted and running in FLM. It is also possible to use Add-Type to output a permanent dll that is unsigned and load that in after:

Add-Type -TypeDefinition @'
using System;

public class Test
{
    public static string TestMethod()
    {
        return "foo";
    }
}
'@ -OutputAssembly test.dll

fsutil file queryea test.dll

# Extended Attributes (EA) information for file C:\Users\vagrant\dev\signed\test.dll:

# Total Ea Size: 0x2d

# Ea Buffer Offset: 0
# Ea Name: $KERNEL.PURGE.TRUSTCLAIM
# Ea Value Length: c
# 0000:  01 00 08 00 00 00 00 00  00 00 00 00              ............

This dll is treated as trusted until either

  • The file is modified in some way
  • The WDAC policy is refreshed which may have invalidated the trust (needs validation)

If the WDAC policy has explicitly disabled csc.exe then Add-Type will no longer work with PowerShell 5.1 and it will have to instead load pre-compiled .dll that has been signed and trusted in the WDAC policy.

As PowerShell 7 doesn't use csc.exe to compile the dll but rather compiles it in process there is no special validation that occurs and just being in trusted mode is enough to get this working.

FLM Gotchas

Some gotchas I've encountered which may be problematic when running a script in FLM:

  • Using Invoke-Expression will run the expression in CLM even when in FLM
$ExecutionContext.SessionState.LanguageMode
# FullLanguage

Invoke-Expression '$ExecutionContext.SessionState.LanguageMode'
# ConstrainedLanguage
  • You cannot call powershell.exe -File script.ps1 when the script has [CmdletBinding()]param() - PowerShell/PowerShell#20508

    • You can hack it in with powershell.exe -Command "& .\script.ps1" (dealing with paths + args may be tricky)
  • Trying to invoke a script that is unsigned or has an invalid signature errors with an misleading error

    • It states the file/command doesn't exist rather than it saying it is not signed or has an invalid signature
./unsigned.ps1 : The term './unsigned.ps1' is not recognized as the name of a cmdlet, function, script file, or
operable program. Check the spelling of the name, or if a path was included, verify that the path is correct and try
again.
At C:\Users\vagrant\dev\signed\test.ps1:1 char:1
+ ./unsigned.ps1
+ ~~~~~~~~~~~~~~
    + CategoryInfo          : ObjectNotFound: (./unsigned.ps1:String) [], CommandNotFoundException
    + FullyQualifiedErrorId : CommandNotFoundException

Problems for Ansible

  • How do we get the bootstrap wrapper running
    • We might be able to slim it down further by just using builtin cmdlets/operators and move more logic to the exec wrapper
    • How do we also achieve this with become and async which has a limited command line length
  • Is it at all possible to get this working without touching the disk
    • I don't think so short of finding a security flaw in pwsh
  • How will we verify the content provided to us
    • We cannot just trust the exec wrapper or even the manifest code as it is arbitrary data from stdin, need some way to validate the
      • module/become/async/coverage wrapper actions
      • C# utils from a string (or pre-compiled dll)
      • pwsh utils from a string
      • module code from a string
    • Only way WDAC can check something is through a file
      • Can we utilise some sort of temp unnamed file so nothing else can open it
    • Could look at another signing mechanism outside of WDAC
  • How should we adapt to C# utils or even inline Add-Type code
    • We can ignore Add-Type code in the modules and just state csc.exe is needed
    • C# module utils are more difficult as we would need a way to validate the content is trusted or precompile them
  • What does this look like with win_shell/win_powershell/raw
    • New processes will run in CLM
    • Providing a way to bypass this is going to open a security hole
    • Need some way of validating inline pwsh scripts or just not supporting this at all
  • How will we test this in CI
  • How can we ensure that this doesn't impact non-WDAC execution
    • Can we try and keep both paths the same for less complexity
    • Will the extra sigs checks be done in both, is the authenticode CRL checks going to slow us down
    • Will writing the content to the disk going to add any slowness
    • Will pre-compiling C# code improve the speed or slow it down due to extra data being sent across the wire
  • Should we pre-build dlls and ship them across as signed and avoid the pre-compile problem
  • How can we deal with _low_level_execute_commands or shell actions
    • This is important for a connection copy and fetch functionality
    • Rebooting requires explicit commands (which callers can override)
    • Shell actions like mkdtemp will run inline PowerShell code
/* WARNING: Function: __security_push_cookie replaced with injection: security_push_cookie */
/* WARNING: Function: __security_pop_cookie replaced with injection: security_pop_cookie */
HRESULT WldpCanExecuteBuffer
(GUID *host,WLDP_EXECUTION_EVALUATION_OPTIONS options,BYTE *buffer,
ULONG bufferSize,PCWSTR auditInfo,WLDP_EXECUTION_POLICY *result)
{
uint uVar1;
bool bVar2;
int isPowerShellIfZero;
HRESULT returnResult;
int iVar3;
HRESULT local_d0;
uint local_cc;
WLDP_EXECUTION_POLICY *local_c8 [2];
undefined8 local_b8;
undefined8 *puStack_b0;
undefined4 *local_a8;
undefined4 *local_a0;
char telemetryFlag;
ulonglong systemState;
undefined4 local_88;
GUID *local_80;
WLDP_EXECUTION_POLICY **ppWStack_78;
uint *local_70;
HRESULT *pHStack_68;
undefined8 local_60;
GUID eventActivityId;
pHStack_68 = &local_d0;
local_60 = 0xfffffffffffffffe;
local_cc = (uint)options;
local_88 = 0;
local_d0 = 0;
eventActivityId.Data4[0] = '\0';
eventActivityId.Data4[1] = '\0';
eventActivityId.Data4[2] = '\0';
eventActivityId.Data4[3] = '\0';
eventActivityId.Data4[4] = '\0';
eventActivityId.Data4[5] = '\0';
eventActivityId.Data4[6] = '\0';
eventActivityId.Data4[7] = '\0';
eventActivityId.Data1 = 0;
eventActivityId.Data2 = 0;
eventActivityId.Data3 = 0;
systemState = 0;
ppWStack_78 = local_c8;
local_70 = &local_cc;
local_c8[0] = result;
local_80 = host;
wil::ScopeExit<>(&local_b8,&local_80);
if (((buffer == (BYTE *)0x0) || (bufferSize == 0)) || (bVar2 = IsKnownHost(host), !bVar2)) {
returnResult = -0x7ff8ffa9;
local_d0 = -0x7ff8ffa9;
if (telemetryFlag != '\0') {
telemetryFlag = 0;
EmitTelemetry(local_b8,(undefined *)0x0,&DAT_00000001,*(undefined4 *)*puStack_b0,*local_a8,
*local_a0);
}
}
else {
GetSystemStateForHost(host,&systemState);
returnResult = local_d0;
uVar1 = (uint)systemState;
if (((uint)systemState >> 2 & 1) == 0) {
*(int *)local_c8[0] = 1;
if (telemetryFlag != '\0') {
telemetryFlag = 0;
EmitTelemetry(local_b8,(undefined *)0x0,&DAT_00000001,*(undefined4 *)*puStack_b0,*local_a8,
*local_a0);
}
}
else {
EventActivityIdControl(3,&eventActivityId);
if ((uVar1 >> 3 & 1) == 0) {
isPowerShellIfZero = memcmp(host,&WLDP_HOST_POWERSHELL,0x10);
iVar3 = 2; // WLDP_EXECUTION_POLICY_REQUIRE_SANDBOX
if (isPowerShellIfZero != 0) {
iVar3 = 0; // WLDP_EXECUTION_POLICY_BLOCKED
}
*(int *)local_c8[0] = iVar3;
}
else {
*(int *)local_c8[0] = 1; // WLDP_EXECUTION_POLICY_ALLOWED
iVar3 = 1;
}
WldpLogCanExecute(host,(WLDP_EXECUTION_EVALUATION_OPTIONS)local_cc,auditInfo,1,iVar3,
&eventActivityId);
returnResult = local_d0;
if (telemetryFlag != '\0') {
telemetryFlag = 0;
EmitTelemetry(local_b8,(undefined *)0x0,&DAT_00000001,*(undefined4 *)*puStack_b0,*local_a8,
*local_a0);
}
}
}
return returnResult;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment