Execute-Assembly(2)

根据之前的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。至于NDEBUGTYPEASSERT任选其一进行设置。这是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_COMMITMEM_PRIVATE以及保护类型为PAGE_READWRITE的内存块

  整个扫描逻辑就很简单了,只需要调用VirtualQueryEx获取内存信息,只需要选择内存类型为MEM_COMMITMEM_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;

}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值