Monday 25 June 2018

Disabling AMSI in JScript with One Simple Trick

This blog contains a very quick and dirty way to disable AMSI in the context of Windows Scripting Host which doesn't require admin privileges or modifying registry keys/system state which an AV such as Defender should pick up on. It's for information purposes only, I've tested this on an up-to-date Windows 10 1803 machine.

It's come to my attention that a default script file from DotNetToJScript no longer works because Windows Defender blocks it, thanks a lot everyone who contributed to getting my tools flagged as malware.

Dialog showing Windows defender blocks DotNetToJScript through AMSI.

If you look carefully in the screenshot you'll see it shows that the "Affected items:" is prefixed with amsi:. This is an indication that the detection wasn't based on the file but due to behavior through the Antimalware Scan Interface. Certainly the script could be reworked to get around this issue (it seems to work for example in scriptlets oddly enough) but I'll probably never need to bother as I never wrote it for the use cases "Sharpshooter" uses it for. Still I had an idea for a way of bypassing AMSI which I thought I'd test out.

I was in part inspired to dig this technique out again after seeing MDSec's recent work on newer bypasses for AMSI in PowerShell as well as a BlackHat Asia talk from Tal Liberman. However I've not seen this technique described anywhere else, but I'm sure someone can correct me if I'm wrong.


How AMSI Is Loaded in Windows Scripting Host

AMSI is implemented as an COM server which is used to communicate to the installed security product via an internal channel. A previous attack against AMSI was actually to hijack this COM registration as documented by Matt Nelson. The scripting host is not supposed to call the COM object directly, instead it calls methods via exported functions in AMSI.DLL, we can watch being loaded by setting an appropriate filter in Process Monitor. 

AMSI loading into WScript.exe

We can use the stack trace feature of Process Monitor to find the code responsible for loading AMSI.DLL. It's actually part of the scripting engine, such as JScript or VBScript rather than a core part of WSH. The basics of the code are below.

HRESULT COleScript::Initialize() { hAmsiModule = LoadLibraryExW(L"amsi.dll", nullptr, LOAD_LIBRARY_SEARCH_SYSTEM32); if (hAmsiModule) { ① // Get initialization functions. FARPROC pAmsiInit = GetProcAddress(hAmsiModule, "AmsiInitialize"); pAmsiScanString = GetProcAddress(hAmsiModule, "AmsiScanString"); if (pAmsiInit){ if (pAmsiScanString && FAILED(pAmsiInit(&hAmsiContext))) ② hAmsiContext = nullptr; } } bInit = TRUE; ③ return bInit; }

Based on this code we can see it loading the AMSI DLL  then calling AmsiInitialize to get a context handle. The interesting thing about this code is regardless of whether AMSI initializes or not it will always return success . This leads to three ways of causing this code to fail and therefore never initialize AMSI, block loading AMSI.DLL, make AMSI.DLL not contain methods such as AmsiInitialize, or cause AmsiInitialize to fail.

These are somewhat interconnected. For example Tal Liberman mentions in his presentation (slide 56) that you could copy an AMSI using application to another directory and it will try and load AMSI.DLL from that directory. As this AMSI can be some unrelated DLL which doesn't export AmsiInitialize then the load will succeed but the rest will fail. Unfortunately this trick won't work here as the flag LOAD_LIBRARY_SEARCH_SYSTEM32 is being passed which means LoadLibraryEx will always try and load from SYSTEM32 first. Getting AmsiInitialize to fail will be a pain, and we can't trivially prevent this code load AMSI.DLL from SYSTEM32, so what do we do? We preload an alternative AMSI.DLL of course.

Hijacking AMSI.DLL

How do we go about loading an alternative AMSI.DLL with the least amount of effort possible? Something which perhaps not everyone realizes is that LoadLibrary will try and find an existing loaded DLL with the requested name so that it doesn't load the same DLL twice. This works not just for the name of the DLL but also the main executable. Therefore if we can convince the library loader that our main executable is actually called AMSI.DLL then it'll return that instead. Unable to find AmsiInitialize exported this should result in AMSI failing to initialize but continuing to execute a script without inspecting it. 

How can we change the name of our main executable to AMSI.DLL without modifying process memory? Simple, we copy WSCRIPT.EXE to another directory but call it AMSI.DLL then run it. Wait, what? How can we run AMSI.DLL, doesn't it need to SOMETHING.EXE? On Windows there's two main ways of executing a process, ShellExecute or CreateProcess. When calling ShellExecute the API looks up the handler for the extension, such as .EXE or .DLL and performs particular actions based on the result. Generally .EXE will redirect to just calling CreateProcess, where as for .DLL it'll try and load it in a registered viewer, on a default system there probably isn't one. However, CreateProcess doesn't care what extension the file has as long as it's an Executable File based on its PE header. [Aside, you can actually execute a DLL using the native APIs, but we don't have access to that from WSH]. Therefore, as long as we can call CreateProcess on AMSI.DLL which is actually a copy of WSCRIPT.EXE it will execute. To do this we can just use WScript.Shell's Exec method which just calls CreateProcess directly.

var obj = new ActiveXObject("WScript.Shell"); obj.Exec("amsi.dll dotnettojscript.js");

This results in a process called AMSI.DLL running. When JScript or VBScript tries to load AMSI.DLL it now gets back a reference to main executable and AMSI no longer works. From what I can tell this short script doesn't get detected by AMSI itself, so it's safe to run to bootstrap the "real" code you want to run.

WScript.Exe running as AMSI.DLL

To summarise the attack:

  1. Start a stub script which copies WSCRIPT.EXE to a known location but with the name AMSI.DLL. This is still the same catalog signed executable just in a different location so would likely bypass detection based purely on signatures.
  2. In the stub script execute the newly created AMSI.DLL with the "real" script.
  3. Err, that's about it.

AFAIK this doesn't work with PowerShell because it seems to break something important inside the code which renders PS inoperable. Whether this is by design or not I've no idea. Anyway, I know this is a silly way of bypassing AMSI but it just shows that this sort of self-checking feature rarely works out very well when malware could very easily modify the platform which is doing the detection.