简介
在渗透测试当中经常会使用到PowerShell
来执行脚本, 但是直接使用PowerShell.exe
是一个非常敏感的行为, EDR等产品对PowerShell.exe
进程的创建监控的很密切, 并且随着PowerShell
的渗透测试工具的普及, 越来越多的EDR会利用微软提供的AMSI
接口对PS脚本进行扫描, 但是对于低版本的PowerShell
并没有引入AMSI
;
如果我们可以自己实现一个PowerShell
, 而不是去调用系统的PowerShell.exe
来执行PS脚本, 就会使得我们的行为更加的隐蔽, 甚至我们可以将自实现的PowerShell
模块注入到一个第三方进程中去(例如svchost.exe
), 可能会使行为更加隐蔽;
通过C#实现PS调用
Powershell
实际上是属于C#的子集(System.Management.Automation
), 所以实际上我们在C#中调用Powershell就是调用 System.Management.Automation对象:
using System;
using System.Reflection;
using System.Text;
using System.Management.Automation;
using System.Collections.ObjectModel;
using System.Management.Automation.Runspaces;
namespace Test
{
class Program
{
public static void Main(string[] args)
{
InvokePS("PS> ");
}
public static int InvokePS(string ps)
{
Runspace runspace = RunspaceFactory.CreateRunspace();
runspace.Open();
while (true)
{
try
{
Console.Write(ps);
string cmd = Console.ReadLine();
if (cmd.Contains("exit"))
{
break;
}
Pipeline pipeline = runspace.CreatePipeline();
pipeline.Commands.AddScript(cmd);
pipeline.Commands.Add("Out-String");
Collection<PSObject> results = pipeline.Invoke();
StringBuilder stringBuilder = new StringBuilder();
foreach (PSObject obj in results)
{
foreach (string line in obj.ToString().Split(new[] { "\r\n", "\r", "\n" }, StringSplitOptions.None))
{
stringBuilder.AppendLine(line.TrimEnd());
}
}
Console.Write(stringBuilder.ToString());
}
catch (Exception e)
{
string errorText = e.Message + "\n";
Console.Write(errorText);
}
}
return 0;
}
}
}
需要注意的是在引用System.Management.Automation
时, 需要手动找System.Management.Automation.dll
的路径, 然后添加到引用中去:
有了Test.exe
我们就可以执行PowerShell
命令了, 同时我们可以以反射的形式调用InvokePS
函数, 我们把Test.exe
文件进行base64编码, 然后通过Assembly.Load
的形式反射加载调用InvokePS
函数:
using System;
using System.Reflection;
using System.Text;
using System.Management.Automation;
using System.Collections.ObjectModel;
using System.Management.Automation.Runspaces;
namespace Test
{
class Program
{
public static void Main(string[] args)
{
string base64str = "TVqQAAMAAAAEAAAA...."; // Test.exe文件的base64编码
byte[] buffer = Convert.FromBase64String(base64str);
Assembly assembly = Assembly.Load(buffer);
Type type = assembly.GetType("Test.Program");
MethodInfo method = type.GetMethod("InvokePS");
Object obj = assembly.CreateInstance(method.Name);
//object[] methodArgs = new object[] { new string[] { } };
string PS = "PS> ";
object[] methodArgs = new object[] { PS };
method.Invoke(obj, methodArgs);
}
}
}
C Native Call PowerShell
M1
上面我们通过C#代码加载了Test.exe然后调用了InvokePS
函数来执行PS命令, 同样我们可以利用C/C++来自己构造CLR
运行时, 然后通过"反射"的方式来执行InvokePS
函数:
#include <iostream>
#include <windows.h>
#include <mscoree.h>
#include <metahost.h>
#include <string>
#include <comdef.h>
#include <metahost.h>
#pragma comment(lib, "mscoree.lib")
using namespace std;
LPCWSTR NetVersion = L"v2.0.50727"; // L"v2.0.50727" "v4.0.30319"
int main()
{
// 初始化COM
HRESULT hr = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED);
if (FAILED(hr))
{
cout << "CoInitializeEx Error: " << std::hex << std::showbase << hr << endl;
return hr;
}
// 创建CLR运行时宿主
ICLRMetaHost* pMetaHost = NULL;
hr = CLRCreateInstance(CLSID_CLRMetaHost, IID_ICLRMetaHost, (LPVOID*)&pMetaHost);
if (FAILED(hr))
{
cout << "CLRCreateInstance Error: " << std::hex << std::showbase << hr << endl;
CoUninitialize();
return hr;
}
// 设置CLR版本
ICLRRuntimeInfo* pRuntimeInfo = NULL;
hr = pMetaHost->GetRuntime(NetVersion, IID_ICLRRuntimeInfo, (LPVOID*)&pRuntimeInfo);
if (FAILED(hr))
{
cout << "GetRuntime Error: " << std::hex << std::showbase << hr << endl;
pMetaHost->Release();
CoUninitialize();
return hr;
}
// GetInterface
ICLRRuntimeHost* pClrRuntimeHost = NULL;
hr = pRuntimeInfo->GetInterface(CLSID_CLRRuntimeHost, IID_ICLRRuntimeHost, (LPVOID*)&pClrRuntimeHost);
if (FAILED(hr))
{
cout << "GetInterface Error: " << std::hex << std::showbase << hr << endl;
pRuntimeInfo->Release();
pMetaHost->Release();
CoUninitialize();
return hr;
}
// 启动CLR
hr = pClrRuntimeHost->Start();
if (FAILED(hr))
{
cout << "pClrRuntimeHost Error: " << std::hex << std::showbase << hr << endl;
pClrRuntimeHost->Release();
pRuntimeInfo->Release();
pMetaHost->Release();
CoUninitialize();
return hr;
}
// 调用.NET程序的Main方法
DWORD dwRet = 0;
hr = pClrRuntimeHost->ExecuteInDefaultAppDomain(L"//The_Path_Of//Test.exe", L"Test.Program", L"InvokePS", L"PS> ", &dwRet);
if (FAILED(hr))
{
cout << "ExecuteInDefaultAppDomain Error: " << std::hex << std::showbase << hr << endl;
pClrRuntimeHost->Release();
pRuntimeInfo->Release();
pMetaHost->Release();
CoUninitialize();
return hr;
}
// 释放资源
pClrRuntimeHost->Release();
pRuntimeInfo->Release();
pMetaHost->Release();
CoUninitialize();
return 0;
}
这里需要注意的是GetRuntime
设置CLR版本时需要和我们的Test.exe
的版本一样, 这里都是v2.0.50727
;
还有一个问题是在上面C#代码中我们的InvokePS
函数的返回值是int
, 如果我们把返回值设置为string
等, 我们在执行ExecuteInDefaultAppDomain
时会报COR_E_MISSINGMETHOD
(0x80131513)错误;
M2
上面M1这种调用方式需要Test.exe
文件落盘, 不是很方便, 参考Metasploit等工具, 我们发现可以这样初始化并调用函数, 可以实现和上面C#调用相似的效果:
UnMangaedPS.h:
#pragma once
//------------------------------------------------------------
//----------- Created with 010 Editor -----------
//------ www.sweetscape.com/010editor/ ------
//
// File : Test.exe
// Address : 0 (0x0)
// Size : 5632 (0x1600)
//------------------------------------------------------------
unsigned char PowerShellRunner_dll[5632] = {
0x4D, 0x5A, 0x90, 0x00, 0x03
};
const unsigned int PowerShellRunner_dll_len = 5632;
UnMangaedPS.cpp
#include "UnMangaedPS.h"
#include <iostream>
#include <windows.h>
#include <mscoree.h>
#include <metahost.h>
#include <string>
#include <comutil.h>
#include <comdef.h>
#include <metahost.h>
#pragma comment(lib, "mscoree.lib")
// Import mscorlib.tlb (Microsoft Common Language Runtime Class Library).
#import "mscorlib.tlb" auto_rename raw_interfaces_only \
high_property_prefixes("_get","_put","_putref") \
rename("ReportEvent", "InteropServices_ReportEvent")
using namespace mscorlib;
using namespace std;
VOID Cleanup(ICorRuntimeHost *pCorRuntimeHost) {
if (pCorRuntimeHost)
{
pCorRuntimeHost->Release();
pCorRuntimeHost = NULL;
exit(-1);
}
}
typedef HRESULT(WINAPI *funcCLRCreateInstance)(
REFCLSID clsid,
REFIID riid,
LPVOID * ppInterface
);
typedef HRESULT(WINAPI *funcCorBindToRuntime)(
LPCWSTR pwszVersion,
LPCWSTR pwszBuildFlavor,
REFCLSID rclsid,
REFIID riid,
LPVOID* ppv);
HRESULT createHost(const wchar_t* version, ICorRuntimeHost** ppCorRuntimeHost)
{
bool hostCreated = false;
HMODULE hMscoree = LoadLibrary(L"mscoree.dll");
if (hMscoree)
{
HRESULT hr = NULL;
funcCLRCreateInstance pCLRCreateInstance = NULL;
ICLRMetaHost *pMetaHost = NULL;
ICLRRuntimeInfo *pRuntimeInfo = NULL;
bool hostCreated = false;
pCLRCreateInstance = (funcCLRCreateInstance)GetProcAddress(hMscoree, "CLRCreateInstance");
if (pCLRCreateInstance == NULL)
{
wprintf(L"Could not find .NET 4.0 API CLRCreateInstance");
goto Cleanup;
}
hr = pCLRCreateInstance(CLSID_CLRMetaHost, IID_PPV_ARGS(&pMetaHost));
if (FAILED(hr))
{
// Potentially fails on .NET 2.0/3.5 machines with E_NOTIMPL
wprintf(L"CLRCreateInstance failed w/hr 0x%08lx\n", hr);
goto Cleanup;
}
// v2.0.50727 v4.0.30319
hr = pMetaHost->GetRuntime(version, IID_PPV_ARGS(&pRuntimeInfo));
if (FAILED(hr))
{
wprintf(L"ICLRMetaHost::GetRuntime failed w/hr 0x%08lx\n", hr);
goto Cleanup;
}
// Check if the specified runtime can be loaded into the process.
BOOL loadable;
hr = pRuntimeInfo->IsLoadable(&loadable);
if (FAILED(hr))
{
wprintf(L"ICLRRuntimeInfo::IsLoadable failed w/hr 0x%08lx\n", hr);
goto Cleanup;
}
if (!loadable)
{
wprintf(L".NET runtime v2.0.50727 cannot be loaded\n");
goto Cleanup;
}
// Load the CLR into the current process and return a runtime interface
hr = pRuntimeInfo->GetInterface(CLSID_CorRuntimeHost, IID_PPV_ARGS(ppCorRuntimeHost));
if (FAILED(hr))
{
wprintf(L"ICLRRuntimeInfo::GetInterface failed w/hr 0x%08lx\n", hr);
goto Cleanup;
}
hostCreated = true;
Cleanup:
if (pMetaHost)
{
pMetaHost->Release();
pMetaHost = NULL;
}
if (pRuntimeInfo)
{
pRuntimeInfo->Release();
pRuntimeInfo = NULL;
}
return hostCreated;
}
return hostCreated;
}
void InvokeMethod(_TypePtr spType, PCWSTR method, PCWSTR command)
{
HRESULT hr;
bstr_t bstrStaticMethodName(method);
SAFEARRAY *psaStaticMethodArgs = NULL;
variant_t vtStringArg(command);
variant_t vtPSInvokeReturnVal;
variant_t vtEmpty;
psaStaticMethodArgs = SafeArrayCreateVector(VT_VARIANT, 0, 1);
LONG index = 0;
hr = SafeArrayPutElement(psaStaticMethodArgs, &index, &vtStringArg);
if (FAILED(hr))
{
wprintf(L"SafeArrayPutElement failed w/hr 0x%08lx\n", hr);
return;
}
// Invoke the method from the Type interface.
hr = spType->InvokeMember_3(
bstrStaticMethodName,
static_cast<BindingFlags>(BindingFlags_InvokeMethod | BindingFlags_Static | BindingFlags_Public), // BindingFlags_InvokeMethod
NULL,
vtEmpty,
psaStaticMethodArgs,
&vtPSInvokeReturnVal);
if (FAILED(hr))
{
wprintf(L"Failed to invoke InvokePS w/hr 0x%08lx\n", hr);
return;
}
else
{
// Print the output of the command
wprintf(vtPSInvokeReturnVal.bstrVal);
}
SafeArrayDestroy(psaStaticMethodArgs);
psaStaticMethodArgs = NULL;
}
HRESULT RuntimeHost(PCWSTR pszVersion, PCWSTR pszAssemblyName, PCWSTR pszClassName, PCWSTR pszMethodName, PCWSTR pszArgName) {
HRESULT hr;
ICorRuntimeHost *pCorRuntimeHost = NULL;
IUnknownPtr spAppDomainThunk = NULL;
_AppDomainPtr spDefaultAppDomain = NULL;
// The .NET assembly to load. PowerShellRunner
bstr_t bstrAssemblyName(pszAssemblyName); // Test
_AssemblyPtr spAssembly = NULL;
// The .NET class to instantiate. PowerShellRunner.PowerShellRunner TestCalc.Program
bstr_t bstrClassName(pszClassName); // Test.Program
_TypePtr spType = NULL;
// Create the runtime host v2.0.50727 v4.0.30319
if (!createHost(pszVersion, &pCorRuntimeHost))
{
wprintf(L"Failed to create the runtime host\n");
Cleanup(pCorRuntimeHost);
}
// Start the CLR
hr = pCorRuntimeHost->Start();
if (FAILED(hr))
{
wprintf(L"CLR failed to start w/hr 0x%08lx\n", hr);
Cleanup(pCorRuntimeHost);
}
DWORD appDomainId = NULL;
hr = pCorRuntimeHost->GetDefaultDomain(&spAppDomainThunk);
if (FAILED(hr))
{
wprintf(L"RuntimeClrHost::GetCurrentAppDomainId failed w/hr 0x%08lx\n", hr);
Cleanup(pCorRuntimeHost);
}
// Get a pointer to the default AppDomain in the CLR.
hr = pCorRuntimeHost->GetDefaultDomain(&spAppDomainThunk);
if (FAILED(hr))
{
wprintf(L"ICorRuntimeHost::GetDefaultDomain failed w/hr 0x%08lx\n", hr);
Cleanup(pCorRuntimeHost);
}
hr = spAppDomainThunk->QueryInterface(IID_PPV_ARGS(&spDefaultAppDomain));
if (FAILED(hr))
{
wprintf(L"Failed to get default AppDomain w/hr 0x%08lx\n", hr);
Cleanup(pCorRuntimeHost);
}
// Load the .NET assembly.
// (Option 1) Load it from disk - usefully when debugging the PowerShellRunner app (you'll have to copy the DLL into the same directory as the exe)
// hr = spDefaultAppDomain->Load_2(bstrAssemblyName, &spAssembly);
// (Option 2) Load the assembly from memory
SAFEARRAYBOUND bounds[1];
bounds[0].cElements = PowerShellRunner_dll_len;
bounds[0].lLbound = 0;
SAFEARRAY* arr = SafeArrayCreate(VT_UI1, 1, bounds);
SafeArrayLock(arr);
memcpy(arr->pvData, PowerShellRunner_dll, PowerShellRunner_dll_len);
SafeArrayUnlock(arr);
hr = spDefaultAppDomain->Load_3(arr, &spAssembly);
printf("Load the assembly from memory\n");
if (FAILED(hr))
{
wprintf(L"Failed to load the assembly w/hr 0x%08lx\n", hr);
Cleanup(pCorRuntimeHost);
}
// Get the Type of PowerShellRunner.
hr = spAssembly->GetType_2(bstrClassName, &spType);
if (FAILED(hr))
{
wprintf(L"Failed to get the Type interface w/hr 0x%08lx\n", hr);
Cleanup(pCorRuntimeHost);
}
// Call the static method of the class InvokePS
InvokeMethod(spType, pszMethodName, pszArgName);
return hr;
}
int main()
{
LPCWSTR NetVersion = L"v2.0.50727"; // L"v2.0.50727" "v4.0.30319"
RuntimeHost(NetVersion, L"Test", L"Test.Program", L"InvokePS", L"PS> ");
return 0;
}
通过这种方式调用就不需要Test.exe
落盘了, 然后可以反射加载的形式执行InvokePS
函数, 并且这种即使InvokePS
函数返回string
类型也不错报错了;
这里需要注意的是在#import "mscorlib.tlb"
的时候可能报错说找不到文件, 可以在属性 -> VC++目录 -> 附加目录中添加mscorlib.tlb
所在目录(例如C:\Windows\Microsoft.NET\Framework\v2.0.50727);
总结
这里编译的形式是exe
, 在实际使用中可以为DLL, 甚至shellcode; 更成熟的代码可以参考UnmanagedPowerShell
等项目;
参考:
- https://begtostudy-tech.blogspot.com/2011/03/use-clr4-hosting-api-to-invoke-net.html
- https://github.com/leechristensen/UnmanagedPowerShell
- https://3gstudent.github.io/backup-3gstudent.github.io/%E4%BB%8E%E5%86%85%E5%AD%98%E5%8A%A0%E8%BD%BD.NET%E7%A8%8B%E5%BA%8F%E9%9B%86(execute-assembly)%E7%9A%84%E5%88%A9%E7%94%A8%E5%88%86%E6%9E%90/