Adventures in the Wonderful World of AMSI.

Sep 5 · 17 min read


So before the reader signs up to this adventure, I’d like to make them aware of a few things. First of all, the blog is quite lengthy and the first sections are made up of high-level reviews of existing research. The last part of the blog is my contribution to AMSI research, which is “new” (I wouldn’t be surprised if other people are already doing this). I also don’t provide a POC, but an adept reader should be able to reproduce the method I allude to.

I also reference a number of authors that have also done research in this area. If you feel you’ve been unfairly portrayed, or I’ve been inaccurate, let me know so I can amend this post. Also I’m far from an expert, so if you think I’ve said something that isn’t factual let me know!


A quick thanks to:

@woolfordphilip for reading my drafts and providing advice.

@0xbc for being a binary ninja and helping with random QU’s.

@vortexau for listening to me ramble and telling me to try harder.

The bloodhound slack for being an awesome resource and community.

TLDR of the “new”;

Rather then use “LoadLibrary” and “GetProcAddress” to locate the memory addresses of “amsi.dll” and “amsiScanBuffer”, walk the “PEB” of the process and the export directory of “amsi.dll”.


Narrative: A quick anecdote about what led me down the garden path.

An Introduction to AMSI: A description of AMSI and the basics of how it functions.

Reviving Dead Mice: Refactoring Matt Graeber’s PowerShell one-liner to bypass current signatures.

A Brief History: High-level review of previous PowerShell/C# bypasses by various authors.

Hard Mode: High-Level review of existing VBA AMSI bypasses.

Another Page in the Book: A theoretical overview of an AMSI bypass that doesn’t utilise “LoadLibrary”, “GetProcAddress” or strings containing “amsi”. This is the “new” stuff.


Recently I gave a presentation at a local security meetup where I wanted to demonstrate an attack on an emulated organisation (MegaCorp), which resides in my home lab. The plan was to kick off the attack chain with a malicious Excel document sent through to the finance department. So, I went ahead fired up my beloved Empire (forever in our hearts) console and proceeded to generate a launcher which I could embed in my “invoice”. This was my first time playing with office macros and what I failed to take into consideration is that the VBA engine includes the Anti Malware Scan Interface (AMSI for here on out) just like PowerShell. I’m all too familiar with the PowerShell implementation of AMSI after working through RastaLabs (By the way, if you’re interested in learning AD exploitation techniques, I can’t recommend RastaLabs enough) with Empire.

Defender isn’t a fan.

To get over the hurdle I went ahead and utilised the most exercised and valueable skill of any security professional: googling. Luckily enough, google turned up the shoulders of giants with which I could stand.





Reading these articles, it looked like I had two choices:

1. Indulge in the cat and mouse game, modify the existing AMSI bypasses to slip past defender.


2. Take the path of least resistance and make use of Excel 4.0 Macros.

Given that the malicious office document was a small part of what I wanted to present, I proceeded with option 2 which slipped past defender with little effort. I was still interested in the AMSI bypass option, it was just a matter of waiting for spare time.

Along came the spare time and soon after this article with the theoretical overview of an AMSI bypass for VBA macros and a brain dump of my knowledge of AMSI.

An Introduction to AMSI

AMSI is described by Microsoft as

“a versatile interface standard that allows your applications and services to integrate with any antimalware product that’s present on a machine. AMSI provides enhanced malware protection for your end-users and their data, applications, and workloads.” —

Cool description, but what does this actually mean? — The AMSI allows Anti-Malware solutions to register their own AMSI provider to give themselves visibility of potentially malicious content before it runs. If the scripting engine detects a behaviour it deems as suspicious, it will publish the associated content to the AMSI for the providers to review and make a decision on whether the provided content is malicious.

This allows Anti-Malware solutions to look at individual “behaviours” of potentially malicious scripts rather than having to make a decision based on a giant blob of obfuscated code. The visibility provided includes what suspicious Windows APIs and COM objects are being utilised, and the strings that are passed as arguments to them.

Let’s look at the following example, where obfuscation has been attempted in pseudo code by splitting a string into multiple variables before passing it to a Windows API call:

$s1 = “Lo”; $s2 = “ad”; $s3 = “Pay”;suspiciousWindowsAPI($s3 + $s1 + $s2)

The scripting engine should recognise a Windows API that is often used for malicious ends has been called and as such it should report the API and arguments to the AMSI providers:


Based on Microsoft’s documentation we expect that if the Anti-Malware provider has signatured the string and its association with the Windows API call, it will flag this behaviour as being malicious and terminate execution of the script.

One thing to note, is the following paragraph from Microsoft’s documentation on AMSI:

“In response, antimalware software starts to do basic language emulation. For example, if we see two strings being concatenated, then we emulate the concatenation of those two strings, and then run our signatures on the result. Unfortunately, this is a fairly fragile approach, because languages tend to have a lot of ways to represent and concatenate strings.” -

This seems to indicate that there is a potential weakness in the way scripting engines process potentially malicious strings before passing them to the AMSI provider.

To get an idea of what Microsoft is signaturing in PowerShell as suspicious behaviour, you can have a look at the open sourced version or fire up a disassembler like Gidhra. The open source version of PowerShell includes a class, “SuspiciousContentChecker”, which at the time of writing contains a function with a case statement built of precomputed hashes generated from various strings associated with malicious behaviour. If you’d like to take a peek yourself, you can find the file in question here:

A look at the signatures from the PowerShell repo.

At its bare bones, AMSI is only providing Anti-Malware solutions more content to check against their “signatures”. As a result, we can expect playing the usual cat and mouse game of avoiding blacklisted strings and behaviours, will allow us to slip payloads past Anti-Malware solutions.

As of Windows 10, Microsoft have proceeded to implement AMSI into:

  • UAC
  • PowerShell
  • The Windows Script Host
  • JavaScript
  • VBScript
  • Office VBA Macros
  • .NET 4.8

As we head along this journey, you will soon see AMSI’s effectiveness varies from implementation to implementation.

If you are interested in understanding AMSI in more detail I recommend checking out the following Official resources:




Reviving Dead Mice

Now we’ve come to the realisation that AMSI just provides more content for Anti-Malware solutions to check signatures against, and that content is only provided if the scripting engine signatures the behaviour, we can proceed to studying the core components of previous AMSI bypasses to see if a new implantation can be crafted which avoids detection.

The first AMSI bypass we are going to look at is the earliest I’m familiar with. This PowerShell AMSI bypass was published by Matt Graeber who jested how it could fit into a single tweet.

Matt Graeber’s timeless bypass in a single tweet. —

The mechanics of this bypass have been detailed in the following blog post by MDSec:

Reading this article, we find out the bypass sets a variable which is later used in some logic that determines whether content handled by AMSI should be scanned. By setting this variable to “True”, the logic directs down a path which reports back that the content scanned isn’t malicious without actually scanning it. Sounds good to me!

Let’s throw this at the wall and see if it still sticks:

Matt’s bypass is now detected by Defender.

…and it slides right off.

Remember the quote from Microsoft I included above, the one about basic language emulation and string concatenation? Let’s make some adjustments and see what happens…

Who else uses Invoke-Mimikatz to verify if Defender is working?

We see with some slight obfuscation this bypass sneaks past Defender’s signatures. It’s quite clear the PowerShell implementation of AMSI isn’t overly robust.

A Brief History

If you do some further research, you’ll find a number of AMSI bypasses which have been blogged about by various authors:

Turns out my google-foo isn’t as great as I expected, and I managed to miss this article while developing content:

After reading these articles, you’ll realise the cat and mouse game has been played and that the research of these authors, appears to build off of the other authors, or build off of the original research ( & published by Avi Gimpel and Zeev Ben Porat of CyberArk.

Based on my testing of the vanilla implantation of these bypasses, most of them no longer work without some love.

Let’s take a look at the refactored CyberArk version which is written in C#:

public static string run(){IntPtr dllHandle = LoadLibrary("amsi.dll"); //load the amsi.dllif (dllHandle == null) return "error";//Get the AmsiScanBuffer function addressIntPtr AmsiScanbufferAddr = GetProcAddress(dllHandle, "AmsiScanBuffer");if (AmsiScanbufferAddr == null) return "error";IntPtr OldProtection = Marshal.AllocHGlobal(4); //pointer to store the current AmsiScanBuffer memory protection//Pointer changing the AmsiScanBuffer memory protection from readable only to writeable (0x40)bool VirtualProtectRc = VirtualProtect(AmsiScanbufferAddr, 0x0015, 0x40, OldProtection);if (VirtualProtectRc == false) return "error";//The new patch opcodevar patch = new byte[] {0x31,0xff,0x90};//Setting a pointer to the patch opcode array (unmanagedPointer)IntPtr unmanagedPointer = Marshal.AllocHGlobal(3);Marshal.Copy(patch, 0, unmanagedPointer,3);//Patching the relevant line (the line which submits the rd8 to the edi register) with the xor edi,edi opcodeMoveMemory(AmsiScanbufferAddr + 0x001b, unmanagedPointer, 3);return "OK";}

After reading the article and analysing the code we come to the understanding that the code is doing the following:

· Locating the amsi.dll library in memory

· Locating the AmsiScanBuffer function in memory

· Changing the memory protection of this function so we can modify the logic

· Writing 3 new bytes into the machine instructions of the function

Specifically, these new instructions when placed at the correct offset cause the AmsiScanBuffer function to return a value which indicates that nothing malicious was detected. Once this payload is run, anything after the fact won’t be scanned — effectively “bypassing” AMSI.

If we take the C# and drop it into a PowerShell script, we find this particular version no longer works either:

Note: I copy-pasted the C# from zc00l’s blog referenced above — I just included it inline rather loading it from disk.

Result of running the vanilla CyberArk bypass

But if we strip out all the Console.WriteLine calls, change the variable names and split any strings which contain “amsi”….


Critical Hit!

Just like with Matt Graebers implementation we’ve avoided signaturing and successfully disabled AMSI for any on-going execution.

Now we’ve had a look at a few payloads, we can safely say these examples aren’t really “bypass” techniques but rather techniques to disable AMSI itself. It is only the code that disables AMSI which is actually doing the “bypassing”.

Hard Mode

We are feeling quite confident after modifying some existing AMSI “bypasses” to the point they slip right past Defender’s signatures. However, these payloads are specifically for PowerShell and if you recall the original goal was a “bypass” for Office documents. How hard could it be to port the same bypass to VBA in combination with an Empire payload and still sneak past Defender?

Well it turns out this has already been blogged about by at least two people:

1. — Iliya Dafchev

2. — Richard Davy and Gary Nield

We can see research 2 is an iteration of research 1 and research 1 is an iteration of some of the research discussed in the previous section. A researcher publishes a bypass, Microsoft signatures it, so on and so on goes the game.

In both cases, the authors discuss how the usual PowerShell shenanigans of trying to break up suspicious strings won’t fly in the world of VBA.

Research 1:

In research 1 the author is able to port an iteration of Rasta Mouse’s PowerShell implementation and with some slight adjustments get it past Defender.

The author notes how during their experimentation they were able to narrow down “AmsiScanBuffer”, “amsi.dll” and “RtlMoveMemory” as the strings likely responsible for the payload getting caught before it could patch the “AmsiScanBuffer” function.

To avoid using the “AmsiScanBuffer” string, the author ends up using “GetProcAddress” with the “AmsiCloseSession” string instead. This provides the memory address of the “AmsiCloseSession” function which can be used to calculate the address of the function we’d really like to target, “AmsiScanBuffer”. This is just a matter of firing up Windbg to discover the difference between the two function’s addresses.

To get over the hurdle of using the “RtlMoveMemory” string, the author realises they can use another function which achieves the same ends of copying memory from one location to another.

This weakness of utilising alternate Window’s APIs to bypass detection is also called out in some research ( by Pieter Ceelen of OutFlank.

The author closes their article by mentioning that they’d disclosed their findings to Microsoft and made some recommendations to tighten the signaturing just that bit more.

Research 2:

The authors of this article start their journey to an ASMI bypass by trying to see if the payload published in research 1 would still work. They quickly discover that since the recommendations of the author of research 1, Microsoft have indeed been back to the drawing board.

Understanding all the previous research, they were well aware to get a working AMSI bypass using the original CyberArk method, they would simply need to find the following core components:

  • A method to locate the memory address of the “AmsiScanBuffer” function without using suspicious strings.
  • A function to modify the memory of the “AmsiScanBuffer” which isn’t already signatured.

The author proceeds to walk through the process of creating a new bypass which slightly adjusts what is prensented in research 1. They simply find a new function to patch memory with and another function within the “amsi.dll” with which they can use to calculate the offset of “AmsiScanBuffer”.

After getting this working they note that after publishing they’d expect Microsoft will simply signature their findings and the game will continue. So rather then stop there, they continue to explore another mechanism with which to discover the location of the “AmsiScanBuffer”.

They explain that you can use the base address of “amsi.dll” and then search its entire address space for the unique pattern of bytes which make up the machine code used by the “AmsiScanBuffer” function. Using this mechanism instead of calculating the “AmsiScanBuffer” address from another function within the “amsi.dll” prevents Microsoft from simply adding all the other functions to their signatures.

Shortly after this VBA research came out, a researcher at Context security used a similar method to create a bypass for the PowerShell implementation of AMSI:

Another Page in the Book

After reading all this research and some experimentation of my own I had a pretty good idea what I would need to craft a bypass which would sail right past Defender’s current signatures. My hope was, that I could produce a working bypass which would remove the use of any string containing “amsi” in the final product. I also wanted to remove the use of the Windows APIs: “LoadLibrary” and “GetProcAddress”.

To achieve this, I would require the following:

1. A way to dynamically locate the base address of “amsi.dll

2. A way to dynamically locate the address of the “AmsiScanBuffer” function

During the experimentation with VBA AMSI bypasses, I frequently had Windbg hooked into my Excel process. While using WinDbg, I had started wondering how it knew the memory location of “amsi.dll” and all of its associated functions?

At this point I went down a bit of rabbit hole, looking in the import directory of Excel. Turns out dynamically loaded dlls are tracked elsewhere in a process.

Locating the Base Address

The answer for the memory location of “asmi.dll”, is the Process Environment Block or PEB. The PEB is a data structure used by the Windows Operating System to keep track of various attributes about a process. On a side note, you will find some interesting articles just by googling “Process Environment Block” 😉. This seemed to be an indicator I was likely on the right track.

To look at the attributes of a PEB for a particular process, we can attach WinDbg.

Process Environment Block in WinDbg

As can be seen above, the actual layout of this structure in memory is quite complicated. It is a collection of various datatypes, some of which are pointers to additional data structures. I will leave it to the reader to explore this structure and other structures discussed themselves. Specifically of interest to us though, is a pointer at an offset of 0x18 from the base of the PEB structure.

Process Environment Block in WinDbg

This pointer, points to another data structure, _PEB_LDR_DATA. This structure contains another three data structures which our located one after the other starting at an offset of 0x10 from the base of the _PEB_LDR_DATA structure. These data structures are _LIST_ENTRY structures. All three _LIST_ENTRY structures represent the head of a doubly-linked list which contains pointers to another data structure, _LDR_DATA_TABLE_ENTRYs.

_PEB_LDR_DATA struct

If we follow the second pointer of the InLoadOrderModuleList, it will take us to a _LDR_DATA_TABLE_ENTRY, which holds metadata of the last DLL which was loaded. This metadata includes more list entries which point to the metadata of other DLLs, the base address of the DLL, as well as a pointer to a Unicode string which contains the name of the loaded DLL.

An entry from the InLoadOrderLinks doubley-linked list

If we keep following the pointers of the doubly-linked list, eventually we will find ourselves at the metadata of “amsi.dll”, and will be able to retrieve the base address of the DLL. This is a tick in the box for the first required component on our list.

Locating the Function Address

Using the base address we’ve obtained, we can start parsing “amsi.dll” to find the function address we are after. For our starting point, we are going to take a look at the “DOS header” of the DLL, which is a _IMAGE_DOS_HEADER structure. For the rest of explanation I’m going to stop using the term DLL and swap to Portable Excutable (PE from here on out), as the structures we are about to look at are generic to all PE types, not just DLLs. The very last attribute of the DOS header, “e_lfanew”, is the particular attribute we need. The value it holds can be added to the base address of the PE to derive the “Relative Virtual Address” (RVA from here on out) of the PE.

This address is important because the various “pointers” that keep track of the different data structures of a PE don’t contain the actual memory address of the data structures, rather they contain an offset. This offset is the actual location of the data structure in memory, relative to the RVA. If a pointer contains an offset of 0x60, we can expect the data structure to be located at the actual memory address:

PE Base Address + e_lfanew + 0x60

DOS Header

As well as the RVA being used to calculate actual memory addresses, it also points to the IMAGE_NT_HEADERS data structure which contains the offsets of additional data structures. The data structure we are after is the _IMAGE_OPTIONAL_HEADER64. For a x64 portable executable, the _IMAGE_OPTIONAL_HEADER64 is always located at an offset of 0x18 from the RVA, which means you don’t actually have to parse the IMAGE_NT_HEADERS to calculate its location.


The _IMAGE_OPTIONAL_HEADER64 structure is also made up of various attributes and pointers to additional structures. In this case what we are looking for is the location of the DataDirectory. The DataDirectory is an array of _IMAGE_DATA_DIRECTORY structures, which is located at an offset of 0x70 from the base of the _IMAGE_OPTIONAL_HEADER64 structure. This offset is also always the same for x64 PEs, so doesn’t need to be calculated.

Optional Header
Data Directory

As can be seen above, each _IMAGE_DATA_DIRECTORY structure is made up of two attributes, a virtual address (this is an offset) which points to an array of entries for the data directory, and it’s total size in bytes. The data directory which concerns us, is the first, the Export Directory. The Export Directory is a structure of type _IMAGE_EXPORT_DIRECTORY, which contains metadata about the PE and the functions it “exports”. We are interested in the attributes which are used to map function names to the base addresses of functions in memory:

Export Directory Entry


An array of offsets, which when added to the base address point to the function (machine instructions of the function) in memory.

AddressOfFunctions Array


An array of offsets, which when added to the base address point to a string containing the name of a function.

AddressOfNames Array


An array of integers, which provide an index of the AddressOfFunctions array for which a particular function name is associated with. In some cases there might be functions that don’t have names associated with them. However, every function name has a name ordinal, (AddressOfFunctions array index) associated with it. The table below with pseudo data may help with visualising how the arrays all fit together.

From the table above, we can see the function located at the offset 0xD200 doesn’t have a name associated with it because its index isn’t included in the AddressOfNameOrdinals array. However, we can see the index (3) for 0xD350 appears in the AddressOfNameOrdinals array and its name is located in a string at offset 0xC45

AddressOfNameOrdinals Array

These three arrays provide the second piece of the puzzle, a way to dynamically obtain the function address of AmsiScanBuffer.


You made it this far, well done!

To weaponise this information into a working bypass I used the previous VBA research as a Skeleton and then produced code to:

  • Locate the PEB
  • Parse the PEB for the start of the InLoadOrderModuleList doubly-linked list
  • Walk the doubly-linked list looking for the target DLL
  • Parse the PE Header of the target DLL for the Export Directory
  • Walk the Export Directory for the target function
  • Set the memory protections of the function
  • Use the CyberArk path to disable “AMSI

As I mentioned above the bad news is I’m not going to be providing POC code. That being said I hope the exploration of previous AMSI bypasses, my explanation of parsing a process and the high-level overview of my implementation that follows, should be enough for an adept reader to recreate this bypass for themselves.

You’ll just have to take my word for it that this worked (at the time of writing).

Pseudo Evidence:

Snippet 1 of my Implementation
Snippet 2 of my Implementation
Powershell to check the status of defender’s real time monitoring and send it as a GET request.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade