破局P/Invoke,D/Invoke隐匿技术与武器化实现剖析

渗透技巧 2年前 (2022) admin
874 0 0
破局P/Invoke,D/Invoke隐匿技术与武器化实现剖析

技术背景

继PowerShell之后,.NET接过蓝军武器化开发的大旗。在Windows平台的终端对抗中,对系统API的调用是不可缺少的,传统的P/Invoke方式虽然便捷,却带来了额外的暴露风险。D/Invoke的提出旨在解决P/Invoke存在的问题,并配合各种现代终端对抗手段对Hook等检测手法进行有效反制。


01 P/Invoke的功能与不足

P/Invoke(全称Platform Invoke)是.NET中提供的特性,帮助用户从托管代码中访问非托管代码,调用原生Win32 API是武器化场景中最常用的操作。

//DLLImport关键字指示了对于非托管DLL的导入引用
//以Managed Type对被调用函数进行函数声明,指明参数类型与数量、返回值类型
[DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Auto)]
public static extern int MessageBox(IntPtr hWnd, String text, String caption, uint type);


如下图,P/Invoke方式调用的外部API会在程序集导入表中留下记录。针对API的检测是终端对抗防御方的常规检测手段。基于此安全产品可对.NET程序中的潜在恶意内容进行静态检测。

破局P/Invoke,D/Invoke隐匿技术与武器化实现剖析


02 D/Invoke-改进的调用方式

借助Delegates声明函数并动态调用

D/Invoke技术灵活运用了C# Delegates的特性,以Delegate类型完成Unmanaged API的函数声明,并通过DynamicInvoke函数进行动态调用。在P/Invoke中这两个过程是绑定的,但D/Invoke中则划分为独立的两部分,增加了复杂度与灵活度。


更隐蔽的函数声明方式

与P/Invoke相同,D/Invoke需要使用Managed类型对被调用的Native的函数声明进行重新定义,指定参数数量、类型和返回类型。

C++类型

.NET类型

void

System.Void

bool

System.Boolean

signed char

System.SByte

unsigned char

System.Byte

wchar_t

System.Char

short, signed short

System.Int16

unsigned short

System.UInt16

intsigned intlong, signed long

System.Int32

unsigned int, unsigned long

System.UInt32

__int64, signed __int64

System.Int64

unsigned __int64

System.UInt64

float

System.Single

double, long double

System.Double

void*

System.IntPtr

NULL

System.IntPtr.Zero


上文出现的函数声明在D/Invoke下的形式如下:

[UnmanagedFunctionPointer(CallingConvention.StdCall, SetLastError = true)]
public delegate int MessageBox(IntPtr hWnd, String text, String caption, uint type);


其中函数体声明的部分与P/Invoke下区别不大,故而可以参考已经积累得较多的P/Invoke函数声明进行改写(例如pinvoke.net的内容)。D/Invoke的函数声明只需保证参数数量与类型以及返回值类型的准确,函数名与变量名均可任意命名,相比P/Invoke具有更隐蔽的函数声明方式。


利用Delegate机制实现调用

相较于P/Invoke中的直接调用,D/Invoke技术的调用方式更加复杂,且必须手动获取对应函数的内存地址,这也是为何要配合一个庞大的代码库作为支撑共同发布。


核心调用过程实现如下,利用Marshal.GetDelegateForFunctionPointer函数将指针转化为可调用的Delegate。只要函数地址与参数数量/类型明确,便可跳转到指针指向的内存地址,完成函数调用。

public static object DynamicFunctionInvoke(IntPtr FunctionPointer, Type FunctionDelegateType, ref object[] Parameters)
{
    Delegate funcDelegate = Marshal.GetDelegateForFunctionPointer(FunctionPointer, FunctionDelegateType);
    return funcDelegate.DynamicInvoke(Parameters);
}


此处使用的DynamicInvoke调用函数无需了解Delegate的详细类型,因而可以通过Parameters参数获取不同类型与数量的参数。虽然这种灵活性以损失最多80%的性能为代价,但在武器化场景中并不非常紧要。明确参数数量与类型的前提下可使用Invoke方式作为代替,例如前面MessageBox函数可通过如下的方式进行调用。

 ((MessageBox)funcDelegate).Invoke((IntPtr) Parameters[0],(String) Parameters[1],(String)Parameters[2], (uint) Parameters[3]);


D/Invoke武器化项目中给出了一种灵活且通用的实现方式,但并非唯一。在性能需求较高的场景下可使用Invoke代替DynamicInvoke方式,从OPSEC角度考量,也可能规避以DynamicInvoke调用为特征的检测手段。


03 灵活的加载方式带来Hook对抗优势

各种Hook技术是安全产品检测恶意操作的有效手段,P/Invoke调用方式天然不具备Hook对抗的能力。为避免敏感函数的调用,D/Invoke实现了若干种模块加载与函数地址解析的方式。在尽量避免敏感API调用的前提下,配合手动加载DLL、Module Overloading和Syscall等现代终端对抗技术实现了Hook对抗效果。


手动基址获取与地址解析,减少敏感API调用

获取已加载到内存的DLL基址有两种方式。第一种是通过Process.GetCurrentProcess().Modules找到需要检索的DLL,而后通过BaseAddress属性获取DLL基地址。该方式调用的系统库底层利用P/Invoke调用了Kernel32.dll中的EnumProcessModules函数来获取所有的模块信息,若安全产品针对该API进行Hook则有效检测。


另外一种更符合OPSEC实践的方式则是解析进程PEB的LDR_DATA_TABLE_ENTRY结构,获取对应DLL的基地址,该方式无需调用EnumProcessModules等敏感API,具有更高的隐蔽性和安全性。

typedef struct _LDR_DATA_TABLE_ENTRY {
    PVOID Reserved1[2];
    LIST_ENTRY InMemoryOrderLinks;
    PVOID Reserved2[2];
    PVOID DllBase;
    PVOID EntryPoint;
    PVOID Reserved3;
    UNICODE_STRING FullDllName;
    BYTE Reserved4[8];
    PVOID Reserved5[3];
    union {
        ULONG CheckSum;
        PVOID Reserved6;
    };
    ULONG TimeDateStamp;
} LDR_DATA_TABLE_ENTRY, *PLDR_DATA_TABLE_ENTRY;


而在获取DLL基址之后,D/Invoke项目通过自行实现的函数地址解析函数,解析DLL导入表获取函数对应的地址。这种方法避免了敏感函数GetProcAddress的调用,对于已存在的inline Hook依然无能为力,因而需要配合其他的对抗手段进行反制。


结合模块手动加载与模块重载,对抗inline Hook

D/Invoke项目中实现了手动DLL加载模块,在替代LoadLibrary的功能之外,还提供了更大的灵活性,例如支持从内存中加载模块而不局限于文件。在此DLL加载模块的基础上,作者提供了两种Hook对抗思路。从磁盘上文件副本读取未修改的DLL,手动加载到内存中的新模块,并解析函数地址进行调用;读取文件副本后,覆盖到被修改的DLL内存中,实现破坏安全产品inline Hook的目的。


两类方式各有其局限性,第一种方式会产生新的模块加载行为,例如Sysmon的” Image loaded”事件;而第二种方式需要调用NtProtectVirtualMemory这个敏感API以修改内存权限,部分安全产品也可能检测Hook代码是否被覆写。


此外,为了降低开发难度,DLL加载模块底层依靠了NtWriteVirtualMemory、NtProtectVirtualMemory这两个用户态函数来完成内存写入和权限修改,针对这两个函数的监测可有效检测手动DLL加载行为。


Syscall直接系统调用规避用户态Hook

Syscall直接系统调用是绕过用户态的DLL直接和内核通讯的技术,该方法可绕过各种用户态Hook手段,一经提出便在蓝军武器化项目中得到了广泛的应用。常见的syscall基础武器化项目例如syswhispers2的执行流程包括:从ntdll.dll获取系统调用号/使用内置的系统调用号表,通过内联汇编的方式执行”syscall”指令,执行系统调用。


而在D/Invoke武器化项目中采用了一种折中的syscall调用方式。从磁盘读取ntdll.dll文件,利用函数地址解析代码获取待调用函数的地址,从中拷贝包含系统调用号的整个函数到新的内存空间,最终利用Delegate的方式调用包含syscall的代码片段。


04 总结

D/Invoke是为了解决P/Invoke调用不够灵活、额外的暴露指标等问题而产生的攻击技术,从设计之初便具备隐匿对抗的意图。Delegate等技术单个来看都不算是新技术,但组合起来后便能够化腐朽为神奇,将各种现代终端对抗技术集成起来。在DLL加载、函数解析方面提供了若干个成熟的功能模块供用户自由组合,带来了D/Invoke在Hook对抗的优势。单nuget包的形式,大大降低了武器化开发中的成本。但D/Invoke武器化项目也只是D/Invoke技术实现的一种范例,蓝军研究人员应不局限于该项目中的实现,探索更加灵活与隐蔽的构造方案。


破局P/Invoke,D/Invoke隐匿技术与武器化实现剖析

绿盟科技天元实验室专注于新型实战化攻防对抗技术研究。

研究目标包括:漏洞利用技术、防御绕过技术、攻击隐匿技术、攻击持久化技术等蓝军技术,以及攻击技战术、攻击框架的研究。涵盖Web安全、终端安全、AD安全、云安全等多个技术领域的攻击技术研究,以及工业互联网、车联网等业务场景的攻击技术研究。通过研究攻击对抗技术,从攻击视角提供识别风险的方法和手段,为威胁对抗提供决策支撑。


破局P/Invoke,D/Invoke隐匿技术与武器化实现剖析

M01N Team

聚焦高级攻防对抗热点技术

绿盟科技蓝军技术研究战队


原文始发于微信公众号(M01N Team):破局P/Invoke,D/Invoke隐匿技术与武器化实现剖析

版权声明:admin 发表于 2022年2月16日 上午10:00。
转载请注明:破局P/Invoke,D/Invoke隐匿技术与武器化实现剖析 | CTF导航

相关文章

暂无评论

您必须登录才能参与评论!
立即登录
暂无评论...