Chasing AMSI (Anti-Malware Scripting Interface)
If you've been doing work with commodity, offensive scripts, you're probably well aware that AMSI a.k.a. Anti-Malware Scripting Interface is kind of a pain in the butt because it does its job well. Evading it isn't the toughest task, but it can be tedious. If you step through the process logically and efficiently, it's actually quite easy.
This is a walk-through to show you how to bypass it and as an example, I'm using PowerSploit's "Invoke-ReflectivePEInjection" script/cmdlet. I chose this one because it's lengthy and has multiple functions, which makes it a good candidate to isolate each function and find the parts that are causing AMSI to trigger. Another note, this was only tested with Windows Defender and not any third-party AV vendors. So, results may vary. The take-away should be the process.
First, as a proof of concept that AMSI does its job, start by performing a web request (Invoke-WebRequest) to pull the contents of the script, isolate the contents in $content, then attempt to invoke it with Invoke-Expression.
Nothing happened? But if we check the last error (echo $error[0]) we'll see the above message showing that the content was blocked due to malicious contents.
First thing's first, copy the raw content of the script from here and or just copy it from your $content variable and place it into a new scripting pane.
Next, just for the sake of covering all angles, change the name of the function and try to run the script. As expected, it fails. Life's never that easy, but you gotta try right?
The next thing we can do it highlight each function and try to run them separately.
Get-Win32Types passes. We continue this process with each function. Highlight the function and click the right-side play button to run just the snippet.
Continuing on, whammy! We trigger AMSI at the Get-Win32Functions function. Open a new scripting pane and copy this function into it so we can dissect it without potentially screwing up our main code base. From here, we'll break the function up and see where we trip AMSI.
After the first two blocks of code, I add a "}" to close off the function. We don't care if the function code is valid as long as ISE accepts it when we play that snippet. Then, I highlight just the top portion and play it. AMSI didn't trigger so we know it's not that block. I continue on by removing that bracket and placing it after the next block of code.
Finally, we found the block of code where the purp is hiding!
So, what about this block of code is unique to the others and causing AMSI to trip? Most of the code has previously executed without being flagged. Good chance its the VirtualProtect string. So, how do we get around that? Pretty trivial actually. By replacing that with
$("V,i,r,t,u,a,l,P,r,o,t,e,c,t" -replace ",","")
All this does is take V,i,r,t,u,a,l,P,r,o,t,e,c,t and replace the commas with nothing, effectively giving us the same string "VirtualProtect", but since AMSI doesn't find that specific string, it won't flag.
Winner winner, chicken dinner!
Just to be sure, after that edit, I replace that whole function and no alerts. Saweeet. I continue this process until I clean up all of the alerts. The next function that alerts is Create-RemoteThread.
Yes, even though the term "CreateRemoteThread" is a property of a variable, you can still make it an interpreted value. Play that function snippet, and it passes.
Now, for a little bit of a tricky part. If you play each function snippet independently , you'll notice that each one passes, but if you play CmdLet as a whole, it still triggers. WTH, mate? Obviously, there is some AND'ing logic somewhere.
So the next thing we do is start from the top, executing groups of functions.
The easiest way to do this is first start by collapsing all of the functions by clicking the - sign under the function names. Unless you love scrolling, then skip this step.
Once all functions are collapsed, we add two brackets to close off the the collections of functions. Then, highlight the code snippet from the beginning to just after the two brackets and play just that code snippet. No alert, yet again. Continuing this process, we get another alert just after the Get-DelegateType function.
Examining the Get-DelegateType function, there doesn't seem to be anything fishy that would cause an alert. So, the next thing we can do is figure out where that function's being called, which is in Get-Win32Functions. Our little problem child from before.
At this point, my guess is that the AMSI logic is either AND'ing or waiting for a valid function to be instantiated? I didn't bother to look under the hood. What I mean by AND'ing: One piece of content on its own may not be alerted on, but if it's seen with another piece of code, then trigger an alert.
Now, this function DOES have some things you would expect any type of Anti-Virus to trip on. There's a lot to dissect here, but why bother unless you really want to know which ones are triggering? If you're not familiar with how process injection works, the technique typically relies on a few functions that are not commonly used in normal processes and even less common from a Powershell session.
Man, where to start here? That's a lot of code to go through. Well, instead of using the previous method of appending a bracket and running the above code snippet, lets just delete lines of code from that function and run some tests. Yes, you have to delete the lines. AMSI will still trip on commented out lines.
ProNoobTip: Ctrl+X (cut), Ctrl+Z (undo) and Ctrl+H (find and replace) are your friends here. Just make sure you revert back to the original code each time you remove chunks.
I decided to remove the the bottom 3/4th's of the function and play the snippet and wewty! First shot, we get execution. So we know the purp is somewhere in that remove 3/4th's of the function.
Cut to, it turns out AMSI is flagging on the variable names. ¯\_(ツ)_/¯
This is where we just highlight and replace. Of course, if you want to automate this, just grep through and replace all variables with whatever pattern you want. The two variable names that flagged were $VirtualFree* and $WriteProcessMemory*. So, I just did a search and replace on those and put "NotReally" in front of them. If you're doing a search and replace, make sure you include the $ or you may end up mangling some of the strings that are named similarly in the code, which will most likely corrupt the function.
If we continue the process, we get one more snag in the Main function. Not in the scriptblock, but the main Main.... mang.
Lastly, we continue the code cutting process and turns out it trips on this line (of all things)...
Just change this line to if(!$ComputerName) and we're done.
After successfully loading the functions without tipping AMSI, I was successfully able to inject a dll into a suspended svchost process :) One note, there is a line in the Invoke-ReflectivePEInjection CmdLet that needs to be corrected:
Change:
$GetProcAddress = $UnsafeNativeMethods.GetMethod('GetProcAddress')
To
$GetProcAddress = $UnsafeNativeMethods.GetMethod('GetProcAddress', [reflection.bindingflags] "Public,Static", $null, [System.Reflection.CallingConventions]::Any, @((New-Object System.Runtime.InteropServices.HandleRef).GetType(), [string]), $null);
More information can be found here: https://github.com/mitre/caldera/issues/38
As a quick recap of the methodology used. First, isolate each function. When a function trips, examine that function by cutting away code. Once that was done and AMSI still triggered, we grouped functions together and repeated the code-cutting process. Pretty simple, aye? Time-consuming, yup, but overall this isn't really a tough thing to do. And this process can be easily automated through grep, search, replace, etc.
AMSI does an awesome job of 1) preventing the use of these offensive, commodity scripts by people who don't know what they're doing with them and 2) can potentially make the evasion process a pain for someone who does know what they're doing. So the effect is essentially the barrier-to-entry for usage of these types of scripts is definitely raised. Which, overall, we could say reduces the threat footprint, but doesn't doesn't negate it.