根据之前的Execute-Assembly的实现原理,可以预测到Execute-Assembly主要有3个检测点。第一个检测点是加载CLR环境,第二个检测点是加载程序集,第三个检测点在于执行入口点的地方。在我看来,第一第二个检测点是比较好实现的。
ETW使用前置知识
根据XPN在他的博文Hiding your .NET - ETW一文中指出利用ETW(Event Trace for Windows)检测CLR的加载。而ProcessHacker或者ProcessExplorer这两款工具都能从进程角度查看进程是否加载了CLR环境。
使用logman query providers
命令查看所有的提供者。如图,执行结果的第一项是提供者名称,第二项是提供者对应的GUID。
也可以通过设置指定得provider name
或者GUID
来获取具体的提供者的详细信息。即使用logman query providers <provider name>
或者logman query providers <GUID>
。
通过执行logman query providers ".NET Common Language Runtime"
语句返回的结果如下。除了具有第一部分提供程序的名称和GUID之外,第二部分是一些关键字的信息,也就是筛选事件的标志。通过设置这些标志来筛选我们所需要的事件。第三部分是安全级别,而第四部分对应的是事件对应的进程ID和进程路径。
PS C:\Users\14349> logman query providers ".NET Common Language Runtime"
提供程序 GUID
-------------------------------------------------------------------------------
.NET Common Language Runtime {E13C0D23-CCBC-4E12-931B-D9CC2EEE27E4}
值 关键字 描述
-------------------------------------------------------------------------------
0x0000000000000001 GCKeyword GC
0x0000000000000002 GCHandleKeyword GCHandle
0x0000000000000004 FusionKeyword Binder
0x0000000000000008 LoaderKeyword Loader
0x0000000000000010 JitKeyword Jit
0x0000000000000020 NGenKeyword NGen
0x0000000000000040 StartEnumerationKeyword StartEnumeration
0x0000000000000080 EndEnumerationKeyword StopEnumeration
0x0000000000000400 SecurityKeyword Security
0x0000000000000800 AppDomainResourceManagementKeyword AppDomainResourceManagement
0x0000000000001000 JitTracingKeyword JitTracing
0x0000000000002000 InteropKeyword Interop
0x0000000000004000 ContentionKeyword Contention
0x0000000000008000 ExceptionKeyword Exception
0x0000000000010000 ThreadingKeyword Threading
0x0000000000020000 JittedMethodILToNativeMapKeyword JittedMethodILToNativeMap
0x0000000000040000 OverrideAndSuppressNGenEventsKeyword OverrideAndSuppressNGenEvents
0x0000000000080000 TypeKeyword Type
0x0000000000100000 GCHeapDumpKeyword GCHeapDump
0x0000000000200000 GCSampledObjectAllocationHighKeyword GCSampledObjectAllocationHigh
0x0000000000400000 GCHeapSurvivalAndMovementKeyword GCHeapSurvivalAndMovement
0x0000000000800000 GCHeapCollectKeyword GCHeapCollect
0x0000000001000000 GCHeapAndTypeNamesKeyword GCHeapAndTypeNames
0x0000000002000000 GCSampledObjectAllocationLowKeyword GCSampledObjectAllocationLow
0x0000000020000000 PerfTrackKeyword PerfTrack
0x0000000040000000 StackKeyword Stack
0x0000000080000000 ThreadTransferKeyword ThreadTransfer
0x0000000100000000 DebuggerKeyword Debugger
0x0000000200000000 MonitoringKeyword Monitoring
值 级别 描述
-------------------------------------------------------------------------------
0x00 win:LogAlways Log Always
0x02 win:Error Error
0x04 win:Informational Information
0x05 win:Verbose Verbose
PID 映像
-------------------------------------------------------------------------------
0x000035a8 C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe
0x000022dc F:\users\MPic 2.2.1.3\MPic.exe
0x000033c8 F:\users\markdownpad2-portable\MarkdownPad2.exe
0x00001b3c C:\Program Files\CONEXANT\SAII\SmartAudio.exe
0x00001818
0x00000e34
命令成功结束。
XPN在他的博文Hiding your .NET - ETW中,也给出了验证测试代码,代码的功能简而言之就是通过ETW实时的
捕获.NET Common Language Runtime
提供者的AssemblyDCStart_V1
事件。但是这个验证代码有一个缺陷就是,只有当Assembly Loader进程退出后才能捕获对应的AssemblyDCStart_V1
事件。但是,这对我来说是致命的。所以我尝试使用krabsetw库来实现。
#define AssemblyDCStart_V1 155
#define LoaderKeyword 0x08
#include <windows.h>
#include <stdio.h>
#include <wbemidl.h>
#include <wmistr.h>
#include <evntrace.h>
#include <Evntcons.h>
static GUID ClrRuntimeProviderGuid = { 0xe13c0d23, 0xccbc, 0x4e12, { 0x93, 0x1b, 0xd9, 0xcc, 0x2e, 0xee, 0x27, 0xe4 } };
// Can be stopped with 'logman stop "dotnet trace" -etw'
const char name[] = "dotnet trace\0";
#pragma pack(1)
typedef struct _AssemblyLoadUnloadRundown_V1
{
ULONG64 AssemblyID;
ULONG64 AppDomainID;
ULONG64 BindingID;
ULONG AssemblyFlags;
WCHAR FullyQualifiedAssemblyName[1];
} AssemblyLoadUnloadRundown_V1, *PAssemblyLoadUnloadRundown_V1;
#pragma pack()
static void NTAPI ProcessEvent(PEVENT_RECORD EventRecord) {
PEVENT_HEADER eventHeader = &EventRecord->EventHeader;
PEVENT_DESCRIPTOR eventDescriptor = &eventHeader->EventDescriptor;
AssemblyLoadUnloadRundown_V1* assemblyUserData;
switch (eventDescriptor->Id) {
case AssemblyDCStart_V1:
assemblyUserData = (AssemblyLoadUnloadRundown_V1*)EventRecord->UserData;
wprintf(L"[%d] - Assembly: %s\n", eventHeader->ProcessId, assemblyUserData->FullyQualifiedAssemblyName);
break;
}
}
int main(void)
{
TRACEHANDLE hTrace = 0;
ULONG result, bufferSize;
EVENT_TRACE_LOGFILEA trace;
EVENT_TRACE_PROPERTIES *traceProp;
printf("ETW .NET Trace example - @_xpn_\n\n");
memset(&trace, 0, sizeof(EVENT_TRACE_LOGFILEA));
trace.ProcessTraceMode = PROCESS_TRACE_MODE_REAL_TIME | PROCESS_TRACE_MODE_EVENT_RECORD;
trace.LoggerName = (LPSTR)name;
trace.EventRecordCallback = (PEVENT_RECORD_CALLBACK)ProcessEvent;
bufferSize = sizeof(EVENT_TRACE_PROPERTIES) + sizeof(name) + sizeof(WCHAR);
traceProp = (EVENT_TRACE_PROPERTIES*)LocalAlloc(LPTR, bufferSize);
traceProp->Wnode.BufferSize = bufferSize;
traceProp->Wnode.ClientContext = 2;
traceProp->Wnode.Flags = WNODE_FLAG_TRACED_GUID;
traceProp->LogFileMode = EVENT_TRACE_REAL_TIME_MODE | EVENT_TRACE_USE_PAGED_MEMORY;
traceProp->LogFileNameOffset = 0;
traceProp->LoggerNameOffset = sizeof(EVENT_TRACE_PROPERTIES);
if ((result = StartTraceA(&hTrace, (LPCSTR)name, traceProp)) != ERROR_SUCCESS) {
printf("[!] Error starting trace: %d\n", result);
return 1;
}
if ((result = EnableTraceEx(
&ClrRuntimeProviderGuid,
NULL,
hTrace,
1,
TRACE_LEVEL_VERBOSE,
LoaderKeyword
0,
0,
NULL
)) != ERROR_SUCCESS) {
printf("[!] Error EnableTraceEx\n");
return 2;
}
hTrace = OpenTrace(&trace);
if (hTrace == INVALID_PROCESSTRACE_HANDLE) {
printf("[!] Error OpenTrace\n");
return 3;
}
result = ProcessTrace(&hTrace, 1, NULL, NULL);
if (result != ERROR_SUCCESS) {
printf("[!] Error ProcessTrace\n");
return 4;
}
return 0;
}
krabsetw安装与使用
krabsetw是微软开发的一个C++库,其主要目的在于简化ETW的交互。krabsetw目前只支持x64的操作系统,而且编译环境最好是VS2017及以上。
本文也并不使用推荐的NuGet安装krabsetw。而是使用vcpkg进行包管理。具体的关于NuGet的使用可以参考这篇文章。
当编译完成vcpkg.exe
之后,使用.\vcpkg.exe list
查看已经安装的开源库,然后使用.\vcpkg.exe install krabsetw:x64-windows
安装krabsetw库。并且一定要将项目的预处理器设置为UNICODE
。至于NDEBUG
和TYPEASSERT
任选其一进行设置。这是krabsetw项目所规定的。具体参见项目说明:https://github.com/microsoft/krabsetw/blob/master/krabs/README.md
使用krabsetw捕获CLR加载事件代码如下,具体的使用例子可以参考krabsetw例子说明。值得注意的是这个设置的关键字我设置的是MonitoringKeyword
是可以实时监控的。而不是设置LoaderKeyword
。
#define MonitoringKeyword 0x0000000200000000
void DetectByETW()
{
//回调函数
auto assembly_callback = [](const EVENT_RECORD& record, const krabs::trace_context& trace_context)
{
krabs::schema schema(record, trace_context.schema_locator);
krabs::parser parser(schema);
pids.push_back(record.EventHeader.ProcessId);
//获取ProcessId
DWORD dwPid = record.EventHeader.ProcessId;
WCHAR szExeFile[MAX_PATH] = { 0 };
DWORD dwSize = MAX_PATH;
HANDLE hProcess = OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, FALSE, dwPid);
QueryFullProcessImageNameW(hProcess, 0, szExeFile, &dwSize);
//检测内存信息
BOOL bIsExecuteAssembly = DetectByMemory(hProcess);
if (bIsExecuteAssembly == TRUE)
{
SetConsoleColor(FOREGROUND_RED | FOREGROUND_INTENSITY | BACKGROUND_BLUE);
printf("[%d] : %ls is execute-Assembly(.Net Load Memory)\n", dwPid, szExeFile);
SetConsoleColor(FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE);
}
else
{
printf("[%d] : %ls\n", dwPid, szExeFile);
}
return TRUE;
};
//设置跟踪会话
krabs::user_trace trace(L"Assembly Load Monitor");
//设置Provider
krabs::provider<> dotnet_rundown_provider(L".NET Common Language Runtime"); //L".NET Common Language Runtime"
//设置筛选事件关键字,逻辑为any模式
dotnet_rundown_provider.any(MonitoringKeyword);
//设置回调函数
dotnet_rundown_provider.add_on_event_callback(assembly_callback);
//开始
trace.enable(dotnet_rundown_provider);
trace.start();
}
加载程序集
第二个检测点位于加载程序集之后。在memcpy处打一个断点。
并在memcpy函数执行之后的目的地址下一个执行断点,并执行。这一步是为了定位需要加载的程序集在Assembly Loader进程中的位置。因为Assembly内存加载,程序集必然在进程的内存空间中。只是需要定位在哪里?且那块内存的内存属性和类型。
可以看到程序集保存在内存类型为MEM_COMMIT
和MEM_PRIVATE
以及保护类型为PAGE_READWRITE
的内存块
整个扫描逻辑就很简单了,只需要调用VirtualQueryEx获取内存信息,只需要选择内存类型为MEM_COMMIT
和MEM_PRIVATE
以及保护类型为PAGE_READWRITE
的内存块。然后扫描PE头信息即可。
BOOL DetectByMemory(HANDLE hProcess)
{
UCHAR SignMemory[] = { 0x54,0x68,0x69,0x73,0x20,0x70,0x72,0x6F,0x67,0x72,0x61,0x6D,0x20,0x63,0x61,0x6E,0x6E,0x6F,0x74,0x20,0x62,0x65,0x20,0x72,0x75,0x6E,0x20,0x69,0x6E,0x20,0x44,0x4F,0x53,0x20,0x6D,0x6F,0x64,0x65 };
BOOL bIsExecuteFile = FALSE;
if (NULL == hProcess)
return bIsExecuteFile;
SYSTEM_INFO sysInfo = { 0 };
GetSystemInfo(&sysInfo);
MEMORY_BASIC_INFORMATION pMemInfo = { 0 };
DWORD dwErrorCode;
for (DWORD64 MemoryAddress = (DWORD64)sysInfo.lpMinimumApplicationAddress; MemoryAddress < (DWORD64)0x700000000000; MemoryAddress += pMemInfo.RegionSize) //0x7ff4e85d0000 0x70000000
{
if (bIsExecuteFile == TRUE)
break;
if (VirtualQueryEx(hProcess, (LPVOID)MemoryAddress, &pMemInfo, sizeof(MEMORY_BASIC_INFORMATION)) == 0)
break;
if ((pMemInfo.Type == MEM_COMMIT || pMemInfo.Type == MEM_PRIVATE) && pMemInfo.Protect == PAGE_READWRITE) //
{
PVOID pMemoryBuffer = malloc(pMemInfo.RegionSize + 1);
memset(pMemoryBuffer, 0, pMemInfo.RegionSize + 1);
SIZE_T dwReturnNumber = 0;
if (ReadProcessMemory(hProcess, pMemInfo.BaseAddress, pMemoryBuffer, pMemInfo.RegionSize, &dwReturnNumber) == FALSE)
{
printf("[!] ReadProcessMemory Failed\n");
free(pMemoryBuffer);
pMemoryBuffer = NULL;
continue;
}
for (DWORD64 dwIndex = 0; dwIndex < pMemInfo.RegionSize + 1; dwIndex++)
{
if ((memcmp((PVOID)((DWORD64)pMemoryBuffer + dwIndex), SignMemory, sizeof(SignMemory)) == 0) &&
(memcmp((PVOID)((DWORD64)pMemoryBuffer + dwIndex - 0x4E), "MZ", 2) == 0))
{
bIsExecuteFile = TRUE;
break;
}
}
free(pMemoryBuffer);
pMemoryBuffer = NULL;
}
}
return bIsExecuteFile;
}