Introduction and Goals

AMSI offers a fantastic interface for endpoint security vendors to gain insight into in-memory buffers from components that choose have their content scanned. Microsoft documents the following list of components that opt in to AMSI:

User Account Control, or UAC (elevation of EXE, COM, MSI, or ActiveX installation)

PowerShell (scripts, interactive use, and dynamic code evaluation)

Windows Script Host (wscript.exe and cscript.exe)

JavaScript and VBScript

Office VBA macros

As a defender engaged in detection engineering and as an attack researcher interested in maturing evasion techniques, I was left with these questions:

What are the actual PE files in which these components opt in to AMSI? Is the documentation accurate or are there components that are missing from the list above? Is it possible to tap in to AMSI optics without needing to register an AMSI provider as an endpoint security vendor?

AMSI Component Enumeration

In order to address the first two questions, we need to identify a way to automate the discovery of AMSI components. The strategy involved will consist of listing any EXE or DLL file that has the ASCII or Unicode string “amsi.dll” in it. Why search for the term “amsi.dll”?

amsi.dll is the DLL that exports the functions required to scan buffers, namely: AmsiScanBuffer, AmsiScanString, and AmsiUacScan. The inclusion of this term will imply that the EXE or DLL will load the amsi.dll either as a static import (i.e. implying it will be stored in the PE as an ASCII string) or loaded dynamically at runtime (i.e. implying it will be stored in the PE as a Unicode string).

Some crude PowerShell will supply us with the answers:

$UserPEs = Get-CimInstance -ClassName CIM_DataFile -Filter 'Drive = "C:" and (Extension = "exe" or Extension = "dll")' -Property 'Name' | Select -ExpandProperty Name $AMSIReferences1 = $UserPEs | % { Select-String -Encoding ascii -LiteralPath $_ -Pattern 'amsi\.dll' }

$AMSIReferences2 = $UserPEs | % { Select-String -Encoding unicode -LiteralPath $_ -Pattern 'amsi\.dll' } $AMSIReferences1.Path

$AMSIReferences2.Path

The PowerShell snippet above uses WMI to enumerate all EXEs and DLLs. This method was chosen over Get-ChildItem since it has a tendency to throw terminating exceptions when attempting to access a file it does not have access to. Next, it scans each file for the literal string “amsi.dll” (as a regular expression) in both ASCII and Unicode using Select-String (i.e. the equivalent of grep for PowerShell).

Upon weeding out the results, the following AMSI components surfaced:

%windir%\System32\consent.exe %windir%\System32\jscript.dll %windir%\System32\vbscript.dll %windir%\System32\wbem\fastprox.dll %windir%\Microsoft.NET\assembly\GAC_MSIL\System.Management.Automation\v4.0_3.0.0.0__31bf3856ad364e35\System.Management.Automation.dll %windir%\Microsoft.NET\Framework64\v4.0.30319\clr.dll %ProgramFiles%\WindowsApps\Microsoft.Office.Desktop_16051.11929.20300.0_x86__8wekyb3d8bbwe\VFS\ProgramFilesCommonX86\Microsoft Shared\VBA\VBA7.1\VBE7.DLL

Using a little inference and file investigation, here are the above components categorized by the AMSI component groups documented by Microsoft:

User Account Control: consent.exe

PowerShell: System.Management.Automation.dll

JavaScript and VBScript: jscript.dll, vbscript.dll

Office VBA macros: VBE7.dll

Uncategorized: clr.dll, fastprox.dll

So what about those uncategorized AMSI components? As for clr.dll, the Common Language Runtime, Microsoft did mention that as of .NET 4.8, they scan in-memory assembly loads. At this point, several researchers have already investigated bypasses and their work is highlighted here:

So that leaves fastprox.dll. The analysis of this component follows.

AMSI for WMI

Considering its presence in the System32\wbem directory and that the file description for fastprox.dll is “WMI Custom Marshaller”, it is self-evident that this is related to WMI. Just for further verification, we can use PowerShell to identify the processes in which fastprox.dll is loaded:

> Get-Process | Where-Object { $_.Modules.ModuleName -contains 'fastprox.dll' } Handles NPM(K) PM(K) WS(K) CPU(s) Id SI ProcessName

------- ------ ----- ----- ------ -- -- -----------

2196 274 219988 232044 14,573.92 1192 5 chrome

1162 47 85544 38524 803.86 14580 5 mmc

692 42 129920 55564 1,081.20 2408 5 powershell

874 47 77144 87852 73.48 4040 5 powershell

686 39 71132 42608 42.78 12620 5 powershell

229 13 2596 10072 0.13 2956 0 svchost

480 20 3840 6728 69.66 3376 0 svchost

613 34 26776 17356 4,370.64 3648 0 svchost

217 43 2572 4148 18.64 6728 0 svchost

564 33 11276 16544 4.34 11520 0 svchost

129 7 1496 2196 0.77 5232 0 unsecapp

1650 67 318004 256536 99.28 16576 5 vmconnect

898 29 62664 23660 1,267.36 4776 0 vmms

386 16 8492 13408 21.77 14220 0 WmiPrvSE

176 10 2684 8592 1.36 15772 0 WmiPrvSE

The svchost.exe processes can be resolved to their respective services using the following PowerShell one-liner:

> Get-Process | Where-Object { $_.Modules.ModuleName -contains 'fastprox.dll' -and $_.ProcessName -eq 'svchost' } | ForEach-Object { Get-CimInstance -ClassName Win32_Service -Filter "ProcessId = $($_.Id)" } | Format-Table -AutoSize ProcessId Name StartMode State Status ExitCode

--------- ---- --------- ----- ------ --------

2956 Netman Manual Running OK 0

3376 iphlpsvc Auto Running OK 0

3648 Winmgmt Auto Running OK 0

6728 SharedAccess Manual Running OK 0

11520 BITS Auto Running OK 0

So it would appear as though any process possibly wanting to interact with WMI might need to interface with this DLL. Let’s jump straight into the code now to identify how fastprox.dll interacts with amsi.dll.

The only reference to “amsi.dll” is in the JAmsi::JAmsiInitialize function. Here is a portion of the relevant disassembly:

First off, this function only initializes AMSI if the current process is not %windir%\System32\wbem\wmiprvse.exe. I imagine the rationale for this to be noise reduction and perhaps to intend to capture primarily remote WMI operations. This is unscientific speculation, however.

What follows is the call to LoadLibrary on amsi.dll and resolution of the relevant export functions needed like AmsiScanBuffer. The only cross reference that subsequently calls AmsiScanBuffer is the JAmsi::JAmsiRunScanner function:

JAmsiRunScanner is called by JAmsi::JAmsiProcessor which is called by the following functions:

CWbemSvcWrapper::XWbemServices::ExecNotificationQueryAsync CWbemSvcWrapper::XWbemServices::CreateInstanceEnum CWbemSvcWrapper::XWbemServices::ExecQueryAsync CWbemSvcWrapper::XWbemServices::ExecQuery CWbemSvcWrapper::XWbemServices::CreateInstanceEnumAsync CWbemSvcWrapper::XWbemServices::GetObjectW CWbemSvcWrapper::XWbemServices::ExecMethod CWbemSvcWrapper::XWbemServices::ExecMethodAsync CWbemSvcWrapper::XWbemServices::ExecNotificationQuery CWbemSvcWrapper::XWbemServices::GetObjectAsync JAmsi::JAmsiProcessor (called by CWbemInstance::SetPropValue)

Aside from the last function, these all correspond to methods implemented in the IWbemServices interface. The last function most likely corresponds to the IWbemClassObject::Put method.

Now, how can we get a sense of what WMI event data an endpoint security product might have insight into? Well, in my Palantir post on ETW, I described how AmsiScanBuffer is instrumented to log all events to the Microsoft-Antimalware-Scan-Interface provider. What’s great about implementing the instrumentation in AmsiScanBuffer is that regardless of the Windows component that opts in to AMSI, we have an opportunity to trace all AMSI buffers from any process that calls AmsiScanBuffer. If you want to learn more about ETW mechanics and tracing, I encourage you to read the post. For now, let’s run logman to capture all AMSI events in an attempt to capture relevant WMI events:

logman start trace AMSITrace -p Microsoft-Antimalware-Scan-Interface (Event1) -o amsi.etl -ets

After starting the trace, interface with WMI a bit and see if any events are generated. I ran the following as a test:

$CimSession = New-CimSession -ComputerName .

Invoke-CimMethod -ClassName Win32_Process -MethodName Create -Arguments @{CommandLine = 'notepad.exe'} -CimSession $CIMSession

$CIMSession | Remove-CimSession

In the example above, I created a local CIM session to emulate a remote WMI connection where in the process, I would likely avoid tracing in the context of wmiprvse.exe which as described above, is excluded from AMSI introspection.

Finally, upon completion of you interacting with WMI, stop the trace:

logman stop AMSITrace -ets

I then used PowerShell to attempt to identify any WMI events and sure enough, I found one!

> $AMSIEvents = Get-WinEvent -Path .\amsi.etl -Oldest

> $AMSIEvents[5] | Format-List * Message : AmsiScanBuffer

Id : 1101

Version : 0

Qualifiers :

Level : 4

Task : 0

Opcode : 0

Keywords : -9223372036854775807

RecordId : 5

ProviderName : Microsoft-Antimalware-Scan-Interface

ProviderId : 2a576b87-09a7-520e-c21a-4942f0271d67

LogName :

ProcessId : 7184

ThreadId : 8708

MachineName : COMPY486

UserId :

TimeCreated : 10/3/2019 12:14:51 PM

ActivityId : 95823c06-72e6-0000-a133-8395e672d501

RelatedActivityId :

ContainerLog : c:\users\testuser\desktop\amsi.etl

MatchedQueryIds : {}

Bookmark : System.Diagnostics.Eventing.Reader.EventBookmark

LevelDisplayName : Information

OpcodeDisplayName : Info

TaskDisplayName :

KeywordsDisplayNames : {}

Properties : {System.Diagnostics.Eventing.Reader.EventProperty, System.Diagnostics.Eventing.Reader.EventProperty...} > $AMSIEvents[5].Properties Value

-----

0

1

1

WMI 300

300

{67, 0, 73, 0...}

{131, 136, 119, 209...}

False > [Text.Encoding]::Unicode.GetString($AMSIEvents[5].Properties[7].Value)

CIM_RegisteredSpecification.CreateInstanceEnum();

Win32_Process.GetObjectAsync();

Win32_Process.GetObject();

SetPropValue.CommandLine("notepad.exe"); > Get-CimInstance -ClassName Win32_Service -Filter "ProcessId = $($AMSIEvents[5].ProcessId)" ProcessId Name StartMode State Status ExitCode

--------- ---- --------- ----- ------ --------

7184 WinRM Auto Running OK 0

Alright, so how do we make sense of all this? First, upon going through each traced event, the sixth event (index 5) was the only event where the fourth Properties value had “WMI” in it. Also, the eighth Properties value contained what looked like binary data consisting of a Unicode string. Upon decoding it, it reflected the execution of Win32_Process Create in my example above. It was also worth noting the logged process ID where the AMSI event originated — 7184 which was an svchost.exe process. Obtaining the corresponding service using the Win32_Service WMI class resolved the actual process to the WinRM service, which makes sense considering the CIM cmdlets ride over WSMan, the protocol used by the WinRM service.

So this is really cool that as defenders and endpoint security vendors we get this context into suspicious WMI operations! Now, the WMI service is typically very noisy and the OS utilizes WMI on a regular basis for legitimate operations. Would this not inundate the consumers of AMSI buffers? Upon experimenting, I noticed that many WMI operations that I explicitly launched were not logged. Upon doing a little more reversing, the reason behind this became evident — JAmsi::JAmsiRunScanner is only ever conditionally executed upon JAmsi::JAmsiIsScannerNeeded returning TRUE:

Upon briefly digging into the implementation of JAmsi::JAmsiIsScannerNeeded, it became clear that WMI operation context strings have a CRC checksum calculated for them and will only be logged if their checksums match a whitelist of values:

The following CRC values are present in the whitelist as of the time of this writing: 0x788C9917, 0x96B23E8A, 0xB8DA804E, 0xC0B29B3D, 0xD16F4088, 0xD61D2EA7, 0xEF726924, 0x46B9D093, 0xF837EFC3.

I never did spend the time to brute force recover these checksum values but considering the Win32_Process Create operation was logged, it is likely a safe assumption that these checksums refer to common attacker WMI tradecraft. To calculate the checksum of these strings is likely a security through obscurity measure which is also performed in vbscript.dll and jscript.dll.

So to conclude this section, it is cool and reassuring to find that Microsoft continues to supply security optics to security vendors and defenders that they would otherwise be blind to! I’m sure there are plenty of bypasses to this logging once the whitelist is recovered, there is never a silver bullet when it comes to security and this is a huge step in the right direction!

One unanswered question that you may have is how the event fields in the Properties value returned from Get-WinEvent are interpreted. The next section will walk through the methodology used to answer that question.

AMSI Event Field Name Recovery

The easy way to associate event field names to the .ETL trace is to load them into Event Viewer and it will convert the trace to an EVTX file and under the hood will parse out the AMSI ETW provider event manifest and apply the field names to the generated .evtx accordingly which you can see in the Details view:

Additionally, you can use perfview.exe to recover ETW provider schemas back to XML. The order in which field names in the recovered schema are defined corresponds to the order of the values present in the Properties value returned by Get-WinEvent.

Based on limited reversing of the AmsiScanBuffer function, here are descriptions for the fields in event ID 1101 (AMSI scan events):

session: The session identifier. This will be populated if an AMSI scan session was established by calling AmsiOpenSession. This value is obtained by calling the IAmsiStream::GetAttribute method, specifying the AMSI_ATTRIBUTE_SESSION value. If this value was populated, you could use this to correlate multiple scans from the same session. For example, if an obfuscated PowerShell script executed, all scriptblocks associated with its execution in theory would have the same session identifer.

scanStatus: This appears to be a boolean value that I have only ever seen set to 1. I didn’t investigate further.

scanResult: This is the AMSI_RESULT value returned upon the endpoint product completing its scanning of the buffer. In this case, 1 refers to AMSI_RESULT_NOT_DETECTED.

appname: The application name that was passed via the AmsiInitialize appName parameter. In the case of fastprox.dll, appName is hardcoded to “WMI”.

contentname: If the scanned content originated from a file, this field consists of the full file path.

contentsize: Presumably, if the content is filtered, this would reflect the size of the filtered content which you would expect to be less than that of originalsize. In practice, I have yet to see filtered content.

originalsize: The size of the unfiltered content. This value is obtained by calling the IAmsiStream::GetAttribute method, specifying the AMSI_ATTRIBUTE_CONTENT_SIZE value.

content: The raw bytes of the payload that was scanned. Depending upon the component implementation, this will be either a Unicode string or raw bytes. For example, in the case of clr.dll, in-memory assembly load optics content consists of the entire PE that was scanned!

hash: The SHA256 hash of the scanned buffer.

contentFiltered: Indicates if the content was truncated/filtered. As of this writing, AmsiScanBuffer sets this to false. There is currently no logic to set this value to true.

Conclusion

The goal of this post was to establish a methodology for identifying AMSI components, investigating their implementation (using WMI optics as a use case), and to extrapolate context from AMSI ETW events. Such a methodology is required for attackers seeking to evade AMSI optics. It is also useful for defenders who are able to benefit from consuming ETW events. While it is unclear if Microsoft will formally document all components of AMSI, this methodology was used to uncover the evidence of Microsoft’s continued investment in AMSI and security optics moving forward!

Lastly, I will certainly be adding AMSI ETW tracing to my malware analysis repertoire as it will offer an outstanding opportunity to capture script, WMI, and .NET assembly content from otherwise heavily obfuscated malware samples.