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.









Saturday 2 June 2018

Adding a Command Line to PowerShell's Process Listing

My NtObjectManager PowerShell module has plenty of useful functions and cmdlets, but it's not really designed for general use-cases such as system administration. I was reminded of this when I got a reply to a tweet I made announcing a new version of the module.

While the Get-NtProcess cmdlet does have a CommandLine property it's not really a good idea to use it just for that. Each process object returned from the cmdlet is an instance of the NtProcess class which maintains an open handle. While the garbage collector should eventually kick in and clean up for you it's still bad practice to leave open handles lying around.

Wouldn't it be useful if you could get the command line of a process without requiring a large third party module? As pointed out in the tweet you can use WMI but it's uglier than calling Get-Process. It also has a small flaw, it doesn't show the command lines for elevated processes on the same desktop which Get-NtProcess can do (at least on Windows 8 and above).

So I decided to investigate how I might add the command line to the existing Get-Process cmdlet in the simplest way possible. To do so I expose some functionality in PowerShell which I'm guessing few realise exists, or for that matter need to use. First let's see what type of object Get-Process is returning. We can call GetType on the returned objects and find that out.

PS C:\> $ps = Get-Process
PS C:\> $ps[0].GetType() | select Fullname

FullName
--------
System.Diagnostics.Process

We can see that it's just returning the list of normal framework Process objects. Nothing too surprising there, however one thing is interesting, the object has more properties available than the Process class in the framework. A good example is the Path property which returns the full path to the main executable:

PS C:\> $ps[0] | select Path

Path
----
C:\Program Files (x86)\Google\Chrome\Application\chrome.exe

Where does that come from? Using the Get-Member cmdlet gives us a clue.

PS C:\> $ps[0] | Get-Member Path

   TypeName: System.Diagnostics.Process

Name MemberType     Definition
---- ----------     ----------
Path ScriptProperty System.Object Path {get=$this.Mainmodule.FileName;}

Very interesting, it seems to a script property, after some digging it turns out this script is added in something called a Type Extension file which has been around since the early days of PowerShell. It allows you to add arbitrary properties and methods to existing types. The extensions for the Process class are in $PSHome\types.ps1xml, a snippet is shown below.

<Type>
 <Name>System.Diagnostics.Process</Name>
 <Members>
  <ScriptProperty>
   <Name>Path</Name>
    <GetScriptBlock>$this.Mainmodule.FileName</GetScriptBlock>
   </ScriptProperty>
...

Of course what I should have done first is just checked Lee Holmes blog where he wrote a description of these Type Extension files a mere 12 years ago! Anyway you can also get the help for this feature using running Get-Help about_Types.

This sounds ideal, we can add our create out own Type Extension file and add a scripted property to pull out the command line. We just need to write it, the easiest solution would be to use my NtApiDotNet .NET library, but if you're using that you might as well just use the NtObjectManager module to begin with as the library is what the module is built on. Therefore, we'll need to re-implement everything in C# using Add-Type then just invoke that to get the command line when necessary. This is not too hard, I just based it on the code in my library.

If you copy the gist into a file with a ps1xml extension you can then add the extension to the current session using the Update-TypeData cmdlet and passing the path to the file. If you want this to persist you can add the call to your profile, or save the session and reload it.

PS C:\> Update-TypeData .\command_line_type.ps1xml
PS C:\> Get-Process explorer | select CommandLine

CommandLine
-----------
C:\WINDOWS\Explorer.EXE

I've tried to make it as compact as possible, for example I don't enable SeDebugPrivilege which would be useful for administrators as it'd allow you to read the command line from almost any process on the system. You could add that feature if you like. One thing I had to do is also call OpenProcess on the PID, which is odd as the Process class actually has a SafeHandle property which returns a native handle for the process. Unfortunately the framework opens this handle with PROCESS_ALL_ACCESS rights, not the limited PROCESS_QUERY_LIMITED_INFORMATION access we require. This means that elevated processes can not be opened, removing the advantage this approach gives us.

This is also the reason WMI doesn't return all command lines. The WMI host process impersonates the caller when querying the command line for a process. WMI then uses an old method of extracting the command line by reading the command line directly from memory (using a technique similar to this StackOverflow post). As this requires PROCESS_VM_READ access this fails with elevated processes. Perhaps they should move to the NtQueryInformationProcess approach on modern versions of Windows ;-)

PS C:\> start -verb runas notepad test.txt
PS C:\> Get-WmiObject Win32_process | ? Name -eq "notepad.exe" | select CommandLine

CommandLine
-----------

PS C:\> Get-Process notepad | select CommandLine

CommandLine
-----------
"C:\WINDOWS\system32\notepad.exe" test.txt

Hope you find this information useful, there's loads of useful functionality in PowerShell which can make your life much easier. You just have to find it first :-)