C++中调用Powershell浅析

0x01 Intro

工作的项目,主要是使用的C++开发,在Windows平台下还大量引入了Powershell脚本用于快速扩展一些功能。这篇笔记主要来总结下,在基于C/C++的Windows编程中,如何快速方便的调用Powershell脚本。

0x02 C#调用Powershell代码

为什么要从C#开始讲起呢?因为我们知道,Powershell实际上是属于C#的子集(System.Management.Automation),所以实际上我们在C#中调用Powershell就是调用 System.Management.Automation 对象。
新建一个C#工程,并且添加 System.Management.Automation 的引用,这部分在标准库中可能查找不到,需要自己手动指定DLL文件的位置,其一般位于 C:\Program Files (x86)\Reference Assemblies\Microsoft\WindowsPowerShell\3.0 (这里还是推荐Everything省去很多烦恼)。基本的代码如下:

1
2
using System.Management.Automation;
using System.Collections.ObjectModel;

引用增加如上两个类,后者主要用于存储脚本的输出对象,创建并调用的过程就更简单了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
using (PowerShell PowerShellInstance = PowerShell.Create())
{
PowerShellInstance.AddScript("Multi-Line Powershell Code;ETc...;");
PowerShellInstance.AddParameter("param1", "parameter 1 value!");
}
Collection<PSObject> PSOutput = PowerShellInstance.Invoke();
foreach (PSObject outputItem in PSOutput)
{
if (outputItem != null)
{
Console.WriteLine(outputItem.BaseObject.GetType().FullName);
Console.WriteLine(outputItem.BaseObject.ToString() + "\n");
}
}

0x03 在C++中调用C#代码

既然C#能够如此方便的调用Powershell,那么如果我们能在C++中直接调用C#代码,不就也解决了调用Powershell的问题了吗?
微软已经为我们想到了CLR(https://docs.microsoft.com/zh-cn/dotnet/standard/clr
),谈CLR之前也想谈谈COM,因为后面也会用到。COM 全称Component Object Mode 组件对象模型。其实CLR的本质是一个更好的COM,COM中间的一个重要的概念就是组件和接口,可以把一个个模块简单理解为组件,他们之间存在一定的约定,这些约定的表现就是一些简单函数入口(接口),不同组件中之间依照一定的约定进行调用。具体到微软的提现,有两种方式,其一是IDL(接口定义语言),其二是TLB(类型库)。我们在稍后就会使用到TLB这种实现。
我们新建一个CLR工程,然后主要代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include "stdafx.h"
using namespace System;
using namespace System::Collections::ObjectModel;
using namespace System::Management::Automation;
int _tmain(int argc, _TCHAR* argv[])
{
PowerShell^ _ps = PowerShell::Create();
_ps->AddScript("Get-ChildItem C:\\");
auto results = _ps->Invoke();
for (int i = 0; i < results->Count; i++)
{
String^ objectStr = results[i]->ToString();
Console::WriteLine(objectStr);
}
return 0;
}

当开启了CLR编译支持后,就可以引入C#代码的一些语法支持了,具体的C++互操作规则可以参考MSDN

0x04 使用CMake构建工程

因为工作的项目需要跨平台,为了方便各平台间的编译器调用,所以选择了CMake的工具链,那么在Windows下如何使用CMake构建支持CLR的项目呢?其实在Cmake中已经包含有支持 /clr 编译参数的例子了,我们来简单看一下CMakeFilelist.txt是如何编写的:

1
2
3
4
5
6
7
8
9
10
cmake_minimum_required(VERSION 3.9)
project (CLRAppProject CXX CSharp)
string(REPLACE "/EHsc" "" CMAKE_CXX_FLAGS ${CMAKE_CXX_FLAGS})
string(REPLACE "/RTC1" "" CMAKE_CXX_FLAGS_DEBUG ${CMAKE_CXX_FLAGS_DEBUG})
...
target_compile_options(CLIApp PRIVATE "/clr")
...

中间无关的代码已经省略,这里主要注意两点,其一是编译选项中加入”/clr”,另一边就是支持CLR编译选项的两个重要参数,这部分有个小技巧就是可以直接从MSVC工程的Command Line中将参数提取出来,再替换即可。

0x05 在C++中调用Powershell代码

我们首先来整理一下思路,C#调用Powershell(本质是使用 System.Management.Automation 这个命名空间)非常方便,添加一个Refenrence再显示的声明一下就可以调用Powershell对象了。那么我们能不能抛开CLR,通过C/C++代码去直接调用C#产生的代码呢?答案是肯定的,回顾前面所述,CLR算是COM的进化,所以我们可以使用类似动态调用COM的方式去调用它,微软已经给我们提供了CLR API了,具体可以参考MSDN
事实上,Github中也有比较完备的项目,作为一个脱胎于渗透师的伪开发,这里我选择直接抄 https://github.com/leechristensen/UnmanagedPowerShell 中的代码。这个代码可以算得上是,通过CLR提供的接口CLRRuntimeInfo,我们能够在c++中加载clr然后通过clr调用托管代码的普遍例子。

第一部分是 PowershellRunner ,这是一个C#工程,代码和我们 0x02 中的代码差不多,总得来说创建了一个 InvokePS 的静态方法用于调用PS指令,重载了一部分PSHost对象及PSHostUserInterface对象的接口(这里实际有点Bug,会导致输出不全,具体可以自己调试下)。

第二部分是 UnmanagedPowerShell,也是整个项目的关键,这是一个C/C++工程,并且是没有启用CLR支持的。PowerShellRunnerDll.h是我们刚才的C#项目编译出来的DLL的Hex。UnmanagedPowerShell.cpp则是我们C代码的主要逻辑。我们拆除比较重要的几个部分,分别学习一下:

首先是定义部分,主要代码如下 (去除了无关部分):

1
2
3
4
5
6
7
8
9
10
11
12
#pragma region Includes and Imports
#include <windows.h>
#include <comdef.h>
#include <mscoree.h>
#include <metahost.h>
#pragma comment(lib, "mscoree.lib")
#import "mscorlib.tlb" raw_interfaces_only \
high_property_prefixes("_get","_put","_putref") \
rename("ReportEvent", "InteropServices_ReportEvent")
using namespace mscorlib;
#pragma endregion

首先 #pragma region#pragma endregion成对出现起到折叠代码块的作用,可以参考MSDN
其次,分别谈论下这几个头文件的作用:Windows.h就不用说了用得很多;comdef.h主要是定义了COM必不可少的定义,比如错误信息啥的;mscoree.h是.NET的核心头文件,里面导出了调用CLR提供的功能必不可少的一些函数,除此之外非托管代码可以通过COM直接调用.NET的Assembly中的托管对象;metahost.h也导出了CLR相关的函数。这里也顺便科普下什么是托管代码和非托管代码,托管代码全部由.NET的CLR管理,提供了相关垃圾回收等函数,非托管代码如C++等对内存操作需要自己进行手动管理。
最后#import这段是必不可少的一部分定义,其作用是从COM中引入相关的.NET类型,具体可以参考MSDN

剩下的代码逻辑,我们简要概括下:

  1. 创建RuntimeHost,在.Net 2.0之前的版本,我们使用CorBindToRuntime将CLR加载到非托管代码过程(通过 ICorRuntimeHost 接口,在.NET 2.0及以后版本中已由 ICLRRuntimeHost 取代),也就是这里为什么提供了两个创建RuntimeHost的函数。
  2. 通过RuntimeHost指针调用Start方法,将CLR初始化到进程中。
  3. 通过GetDefaultDomain方法取得当前进程的默认操作域。
  4. 通过COM的QueryInterface方法查询某个组件是否支持某个特定的接口,OK的话就返回那个接口指针。
  5. 可选两种方式来载入C#编译的DLL,一种是直接从本地文件系统读取这个DLL文件,一种就是代码实际采用的Hex读入的方式。
  6. 通过assembly的方式直接从内存中取得C#编译的DLL的InvokePS方法,至此就可以传入参数调用Powershell代码了。

0x05 Summary

抛开工作上的项目来说,在渗透工具中,使用的最多的还是 0x05 中所示的方式,出名的项目诸如CobaltStrike的Powershell功能模块,以及Meterpreter的Powershell功能模块(https://github.com/rapid7/metasploit-payloads/tree/master/c/meterpreter/source/extensions/powershell)均有使用此种技术。最后再留个作业,如何实现CS中的Powershell-Import功能,且在往后的Powershell执行中,保留其前面导入的代码在同一个PS会话中呢?

Ref

https://stackoverflow.com/questions/19634220/c-and-powershell
https://www.codeproject.com/Articles/880154/MFC-PowerShells-Easily
https://stackoverflow.com/questions/40126734/set-clr-support-to-true-with-cmake
https://gitlab.kitware.com/cmake/cmake/tree/master/Tests/CSharpLinkToCxx
http://blog.csdn.net/xum2008/article/details/7268761
https://github.com/honeyful/powershellExecute
https://www.codeproject.com/Articles/18229/How-to-run-PowerShell-scripts-from-C
https://blogs.msdn.microsoft.com/saveenr/2010/03/08/how-to-create-a-powershell-2-0-module-and-cmdlet-with-visual-studio-2010-screencast-included/
https://www.codeproject.com/Articles/1210366/Compiling-your-C-code-to-NET-Part
https://www.codeproject.com/Articles/1128868/Compiling-Your-C-Code-to-NET-Part