技术背景
继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程序中的潜在恶意内容进行静态检测。
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 |
int, signed int, long, 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技术实现的一种范例,蓝军研究人员应不局限于该项目中的实现,探索更加灵活与隐蔽的构造方案。
绿盟科技天元实验室专注于新型实战化攻防对抗技术研究。
研究目标包括:漏洞利用技术、防御绕过技术、攻击隐匿技术、攻击持久化技术等蓝军技术,以及攻击技战术、攻击框架的研究。涵盖Web安全、终端安全、AD安全、云安全等多个技术领域的攻击技术研究,以及工业互联网、车联网等业务场景的攻击技术研究。通过研究攻击对抗技术,从攻击视角提供识别风险的方法和手段,为威胁对抗提供决策支撑。
M01N Team
聚焦高级攻防对抗热点技术
绿盟科技蓝军技术研究战队
原文始发于微信公众号(M01N Team):破局P/Invoke,D/Invoke隐匿技术与武器化实现剖析