Executing CSharp Assemblies from C code

Introduction 介绍

The integration of native C code with managed C# assemblies opens up a realm of possibilities, allowing malware developers to leverage the strengths of both worlds within a single application. Most of the modern C2 frameworks have the option for executing C# assemblies, no matter on which language they are built upon. I know it is a feature we all desire but have you ever wondered, how it works on the bottom level? How it is possible to invoke an assembly from a language like C?
本机 C 代码与托管 C# 程序集的集成开辟了一个可能性领域,使恶意软件开发人员能够在单个应用程序中利用这两个世界的优势。大多数现代 C2 框架都可以选择执行 C# 程序集,无论它们基于哪种语言构建。我知道这是我们都渴望的功能,但你有没有想过,它是如何在底层工作的?如何从像 C 这样的语言调用程序集?

At its core is the Common Language Runtime (CLR), a vital component of the .NET framework.
其核心是公共语言运行时 (CLR),它是 .NET 框架的重要组成部分。

The magic begins with the compilation process. The native C code is typically compiled into machine code specific to the target architecture, while the C# code is compiled into an intermediate language called Common Intermediate Language (CIL). This intermediate language serves as a bridge between different languages and platforms.
魔术从编译过程开始。本机 C 代码通常编译为特定于目标体系结构的机器代码,而 C# 代码则编译为称为通用中间语言 (CIL) 的中间语言。这种中间语言是不同语言和平台之间的桥梁。

During runtime, when your application is in action, the CLR comes into play. It takes the CIL from the managed C# assemblies and Just-In-Time (JIT) compiles it into machine code that can be executed by the underlying hardware. This JIT compilation ensures that the C# code is optimized for the specific environment it’s running on.
在运行时,当应用程序处于运行状态时,CLR 将发挥作用。它从托管 C# 程序集中获取 CIL,实时 (JIT) 将其编译为可由基础硬件执行的机器代码。此 JIT 编译可确保 C# 代码针对其运行的特定环境进行优化。

Before diving deeper into this topic, make sure to join my Discord where we share experience, knowledge and doing CTF together.
在深入研究这个话题之前,请务必加入我的 Discord,在那里我们一起分享经验、知识和做 CTF。

#What is CLR? 什么是 CLR?

The Common Language Runtime (CLR) stands as a foundational element within the Microsoft .NET framework, playing a crucial role in the execution of .NET applications. It creates a versatile runtime environment, allowing developers to code in various languages such as C#, VB.NET, and F#. This multilingual support is made possible by compiling code into a common intermediate language (CIL or IL), which CLR then translates into native machine code during runtime.
公共语言运行时 (CLR) 是 Microsoft .NET 框架中的基础元素,在 .NET 应用程序的执行中起着至关重要的作用。它创建了一个多功能的运行时环境,允许开发人员使用 C#、VB.NET 和 F# 等各种语言进行编码。这种多语言支持是通过将代码编译为通用中间语言(CIL 或 IL)来实现的,然后 CLR 在运行时将其转换为本机机器代码。

One of CLR’s notable features is its management of memory. It ensures efficient memory allocation and deallocation, mitigating the risk of memory leaks and enhancing overall program reliability.
CLR 的一个显著特点是它对内存的管理。它确保了高效的内存分配和释放,降低了内存泄漏的风险,并增强了整体程序的可靠性。

CLR also incorporates Just-In-Time Compilation (JIT), dynamically compiling IL code into native machine code at runtime. This adaptive process optimizes performance by tailoring the code to the specific characteristics of the underlying hardware.
CLR 还集成了实时编译 (JIT),在运行时将 IL 代码动态编译为本机机器代码。此自适应过程通过根据底层硬件的特定特征定制代码来优化性能。

The Common Language Runtime (CLR) is an integral part of the Windows operating system, making it available by default. This inclusion in the Windows environment streamlines the development process for .NET applications, as developers can leverage CLR’s features without the need for additional installations.
公共语言运行时 (CLR) 是 Windows 操作系统的一个组成部分,使其在默认情况下可用。Windows 环境中的这种包含简化了 .NET 应用程序的开发过程,因为开发人员可以利用 CLR 的功能,而无需进行其他安装。

#Loading Assemblies from C#
从 C 加载程序集#

Loading assemblies from C# is a trivial process mainly because the CLR is already present in the memory of the parent process. The most easy way to load C# assembly from another C# program is by using Assembly.Load method.
从 C# 加载程序集是一个微不足道的过程,主要是因为 CLR 已存在于父进程的内存中。从另一个 C# 程序加载 C# 程序集的最简单方法是使用 Assembly.Load 方法。

The Assembly.Load method is part of the System.Reflection namespace and allows you to load an assembly by providing its name or path. There are a few overloads of this method, but a common one takes a string parameter representing the name of the assembly to load.
该 Assembly.Load 方法是命名空间的一部分, System.Reflection 允许您通过提供程序集的名称或路径来加载程序集。此方法有一些重载,但常见的重载采用表示要加载的程序集名称的字符串参数。

public static Assembly Load(string assemblyString);

The assemblyString parameter can be the full name of the assembly, which includes the assembly’s simple name, version, culture, and public key token.
该 assemblyString 参数可以是程序集的全名,其中包括程序集的简单名称、版本、区域性和公钥标记。

Another key component we need to discuss is AppDomain. In C#, an AppDomain (Application Domain) is a lightweight, isolated, and self-contained environment within a process where .NET applications run. It provides a way to isolate and unload applications independently within a single process. By default, each C# application operates under at least 1 AppDomain. In most cases, for simple applications, you don’t explicitly interact with the default AppDomain because it is automatically created for you. However, in more complex scenarios or when dealing with advanced features like application domain isolation and unloading, you might create additional AppDomains.
我们需要讨论的另一个关键组件是 AppDomain 。在 C# 中, AppDomain (应用程序域)是运行 .NET 应用程序的进程中的轻量级、隔离且独立的环境。它提供了一种在单个进程中独立隔离和卸载应用程序的方法。默认情况下,每个 C# 应用程序至少在 1 下运行 AppDomain 。在大多数情况下,对于简单应用程序,您不会显式地与默认 AppDomain 值交互,因为它是自动为您创建的。但是,在更复杂的方案中,或者在处理应用程序域隔离和卸载等高级功能时,可能会创建其他 AppDomains .

Let’s say you have a scenario where the assembly name is known and it is in the same working directory as the custom loader below. Here is basic example of loading Rubeus, which is in the same directory.
假设您有一个方案,其中程序集名称是已知的,并且它与下面的自定义加载程序位于同一工作目录中。这是加载 Rubeus 的基本示例,它位于同一目录中。

using System;
using System.Reflection;

class Program
{
static void Main()
{
string exePath = "Rubeus.exe";

// Create a new AppDomain
AppDomain domain = AppDomain.CreateDomain("MyAppDomain");

// Load the assembly into the new AppDomain
Assembly otherAssembly = domain.Load(AssemblyName.GetAssemblyName(exePath));

// Find and execute the entry point method (usually Main)
MethodInfo entryPoint = otherAssembly.EntryPoint;

if (entryPoint != null)
{
ParameterInfo[] parameters = entryPoint.GetParameters();

string[] arguments = new string[parameters.Length];

for (int i = 0; i < parameters.Length; i++)
{
arguments[i] = parameters[i].ParameterType.IsValueType
?
Activator.CreateInstance(parameters[i].ParameterType).ToString()
: null;
}

// Execute the entry point method in the new AppDomain
domain.ExecuteAssembly(exePath, null, arguments);
}
else
{
Console.WriteLine("No entry point found in the specified assembly.");
}

// Unload the AppDomain
AppDomain.Unload(domain);
}
}

After compilation and execution, we can indeed observe that the Rubeus.exe is executed.
经过编译和执行,我们确实可以观察到Rubeus.exe被执行了。

Executing CSharp Assemblies from C code
Executing Rubeus from custom C# application
从自定义 C# 应用程序执行 Rubeus

Additionally, if we observe the process with ProcessHacker2, the Rubeus.exe will be present in the loaded modules:
此外,如果我们使用 ProcessHacker2 观察该过程,则Rubeus.exe将出现在加载的模块中:

Executing CSharp Assemblies from C code
Loaded DLLs, viewed from Process Hacker 2
加载的 DLL,从 Process Hacker 2 查看

#Loading Assemblies with C
使用 C 加载程序集

#What is the problem? 问题是什么?

While loading and executing assemblies is easy in C#, we should not be dependant of the language and its pros and cons.
虽然在 C# 中加载和执行程序集很容易,但我们不应该依赖于语言及其优缺点。

Executing C# assemblies with C can be a bit tricky due to the differences in how these languages work and the runtime environments they rely on.
使用 C 执行 C# 程序集可能有点棘手,因为这些语言的工作方式和它们所依赖的运行时环境存在差异。

Firstly, C# is designed to run on the .NET framework, which provides a managed runtime environment. This means that C# code is compiled into an intermediate language (IL) that is executed by the Common Language Runtime (CLR). On the other hand, C is a low-level language that doesn’t have built-in support for the features provided by the .NET framework.
首先,C# 设计为在 .NET 框架上运行,该框架提供了托管运行时环境。这意味着 C# 代码被编译为由公共语言运行时 (CLR) 执行的中间语言 (IL)。另一方面,C 是一种低级语言,不内置对 .NET Framework 提供的功能的支持。

One major challenge is that C doesn’t have a built-in understanding of the .NET runtime and its features, such as garbage collection, type safety, and reflection. C# relies heavily on these features for its execution, and trying to replicate them in C can be quite complex and error-prone.
一个主要挑战是 C 对 .NET 运行时及其功能(如垃圾回收、类型安全和反射)没有内置的理解。C# 的执行在很大程度上依赖于这些功能,尝试在 C 中复制它们可能非常复杂且容易出错。

Additionally, C# assemblies are typically packaged with metadata and other information that the CLR uses for execution. Replicating this functionality in C would require a deep understanding of the .NET runtime internals, which is a complex task.
此外,C# 程序集通常与 CLR 用于执行的元数据和其他信息打包在一起。在 C 语言中复制此功能需要深入了解 .NET 运行时内部结构,这是一项复杂的任务。

Another issue is that C# code often relies on libraries and dependencies that are part of the .NET framework. These libraries may not have direct equivalents in C, making it challenging to provide the same functionality.
另一个问题是 C# 代码通常依赖于作为 .NET Framework 一部分的库和依赖项。这些库在 C 语言中可能没有直接的等价物,因此提供相同的功能具有挑战性。

#The Solution? 解决方案是什么?

Executing C# assemblies from C involves a process called hosting. The idea is to create a host application in C that loads the CLR and runs the C# assembly.
从 C 执行 C# 程序集涉及一个名为 hosting 的过程。这个想法是在 C 中创建一个主机应用程序,用于加载 CLR 并运行 C# 程序集。

This involves initializing the CLR using functions such as CorBindToRuntimeEx or CLRCreateInstance. The CLR becomes the bridge, providing the necessary runtime environment for managed C# code.
这涉及使用 CorBindToRuntimeEx 或 CLRCreateInstance 等函数初始化 CLR。CLR 成为桥梁,为托管 C# 代码提供必要的运行时环境。

Once the CLR is hosted, the next step is loading the C# assembly. Functions like Assembly.Load or Assembly.LoadFrom facilitate this process, allowing the C program to bring the compiled C# code into the CLR environment. The CLR’s just-in-time compilation then translates the Intermediate Language (IL) code into native machine code. Loading the CLR empowers the C program to interact with the C# code flexibly, adapting to the dynamic nature of the managed environment.
承载 CLR 后,下一步是加载 C# 程序集。函数类似于 Assembly.Load 或 Assembly.LoadFrom 促进此过程,允许 C 程序将编译的 C# 代码引入 CLR 环境。然后,CLR 的实时编译将中间语言 (IL) 代码转换为本机机器代码。加载 CLR 使 C 程序能够灵活地与 C# 代码进行交互,从而适应托管环境的动态特性。

However, even though its possible to invoke C# assemblies from C-like languages, there is a specific limitation. As mentioned before, each and every C# application operates under at least 1 AppDomain which is created and present by default. In order for our C program to execute an assembly, the AppDomain must be explicitly accessed, since its not there by default. After accessing the AppDomain, we face another limitation. In order for a method to get called from ExecuteInDefaultAppDomain, it must inherit the following signature:
但是,即使可以从类 C 语言调用 C# 程序集,也存在特定的限制。如前所述,每个 C# 应用程序都至少在默认创建和存在的 1 AppDomain 下运行。为了让我们的 C 程序执行程序集,必须显式访问 , AppDomain 因为它默认不存在。访问后 AppDomain ,我们面临另一个限制。为了使方法从 ExecuteInDefaultAppDomain 中调用,它必须继承以下签名:

static int Method(String args)

Having that in mind, if you want to execute a C# assembly like Rubeus, one of the options can be adding such method that will forward the execution flow to the Main method.
考虑到这一点,如果要执行像 Rubeus 这样的 C# 程序集,其中一个选项可以添加这样的方法,该方法将执行流转发到 Main 方法。

static int LoadC(string arg)
{
Main(new string[]{ arg });
return 1;
}

Additionally, as stated in this nice reference,
此外,正如这个很好的参考资料中所述,

“If you want to be able to bind both languages you should use ICLRRuntimeHost::SetHostControl and create your own implementation of IHostControl that exposes an interface that can be used in managed code, create a managed AppDomainManager that also implements such interface, then obtain the ICLRControl and set the AppDomainManager managed to back your unmanaged interface. Theres a tutorial you can follow here: https://www.mode19.net/posts/clrhostingright/
“如果您希望能够绑定两种语言,则应使用 ICLRRuntimeHost::SetHostControl 并创建自己的 IHostControl 实现,该实现公开了可在托管代码中使用的接口,创建也实现了此类接口的托管 AppDomainManager,然后获取 ICLRControl 并将 AppDomainManager 托管设置为支持您的非托管接口。您可以在此处遵循教程:https://www.mode19.net/posts/clrhostingright/

This may sound a bit complicated but it works. If youre just looking to comunicate between managed and unmanaged code, check out UnamanagedExports nuget package, wich allows you to generate native dll libraries from managed code, wich lowers the complexity of this process by a magnitude.”
这听起来可能有点复杂,但它确实有效。如果您只是想在托管代码和非托管代码之间进行通信,请查看 UnamanagedExports nuget 包,它允许您从托管代码生成本机 dll 库,从而大大降低了此过程的复杂性。

Keeping the things simple for this demo, the following POC can be used to seamlessly execute C# assemblies, as soon as they have a method with the above signature:
为了简化此演示的内容,只要 C# 程序集具有具有上述签名的方法,就可以使用以下 POC 来无缝执行 C# 程序集:

#include <stdio.h>
#include <mscoree.h>
#include <windows.h>
#include <metahost.h>
#include <corerror.h>

#pragma comment(lib, "mscoree.lib")

int main() {
ICLRMetaHost* pMetaHost = NULL;
ICLRRuntimeInfo* pRuntimeInfo = NULL;
ICLRRuntimeHost* pClrHost = NULL;

// Initialize CLR MetaHost
HRESULT hr = CLRCreateInstance(CLSID_CLRMetaHost, IID_ICLRMetaHost, (LPVOID*)&pMetaHost);
if (FAILED(hr)) {
return -99;
}

hr = pMetaHost->GetRuntime(L"v4.0.30319", IID_ICLRRuntimeInfo, (LPVOID*)&pRuntimeInfo);
if (FAILED(hr)) {
pMetaHost->Release();
return -98;
}

BOOL loadable;
hr = pRuntimeInfo->IsLoadable(&loadable);
if (FAILED(hr) || !loadable) {
pRuntimeInfo->Release();
pMetaHost->Release();
return -97;
}

// Load the CLR into the current process
hr = pRuntimeInfo->GetInterface(CLSID_CLRRuntimeHost, IID_ICLRRuntimeHost, (LPVOID*)&pClrHost);
if (FAILED(hr)) {
pRuntimeInfo->Release();
pMetaHost->Release();
return -96;
}

// Start the CLR
pClrHost->Start();

// Load C# assembly and its arguments
const wchar_t* assemblyPath = L"C:\\Users\\Lsec\\Desktop\\Rubeus\\Rubeus\\bin\\x64\\Release\\Rubeus.exe";
const wchar_t* typeName = L"Rubeus.Program";
const wchar_t* methodName = L"LoadC";
const wchar_t* parameters = L"asktgt";

// Execution
DWORD dwRet;
HRESULT rez = pClrHost->ExecuteInDefaultAppDomain(assemblyPath, typeName, methodName, parameters, &dwRet);
// Execution check if needed
if (rez == S_OK)
{
printf("OKAY");
}
// Cleanup
pClrHost->Stop();
pClrHost->Release();
pRuntimeInfo->Release();
pMetaHost->Release();

return 0;
}

After executing the code against the modified Rubeus.exe, we can confirm that it is successfully executed.
针对修改后的Rubeus.exe执行代码后,我们可以确认代码已成功执行。

Executing CSharp Assemblies from C code
Asktgt module invoked from successfully passing the arguments
Asktgt 从成功传递参数中调用的模块

Lets analyze the code more in depth:
让我们更深入地分析代码:

#include <stdio.h>
#include <mscoree.h>
#include <windows.h>
#include <metahost.h>
#include <corerror.h>

#pragma comment(lib, "mscoree.lib")

In the preamble, necessary headers are included. mscoree.hwindows.hmetahost.h, and corerror.h provide declarations and definitions required for interacting with the Common Language Runtime (CLR) and handling errors. Additionally, #pragma comment(lib, "mscoree.lib") directs the linker to include the mscoree.lib library, essential for linking against the CLR.
在序言中,包括必要的标题。 mscoree.h 、 windows.h 、 metahost.h 和 corerror.h 提供与公共语言运行时 (CLR) 交互和处理错误所需的声明和定义。此外, #pragma comment(lib, "mscoree.lib") 指示链接器包含 mscoree.lib 库,这对于链接到 CLR 至关重要。

int main() {
ICLRMetaHost* pMetaHost = NULL;
ICLRRuntimeInfo* pRuntimeInfo = NULL;
ICLRRuntimeHost* pClrHost = NULL;

The main function serves as the entry point of the program. Here, we declare pointers to the ICLRMetaHostICLRRuntimeInfo, and ICLRRuntimeHost interfaces, which are crucial for CLR interaction.
该 main 函数用作程序的入口点。在这里,我们声明指向 ICLRMetaHost 、 ICLRRuntimeInfo 和 ICLRRuntimeHost 接口的指针,这对于 CLR 交互至关重要。

HRESULT hr = CLRCreateInstance(CLSID_CLRMetaHost, IID_ICLRMetaHost, (LPVOID*)&pMetaHost);
if (FAILED(hr)) {
return -99;
}

This line initializes the CLR MetaHost by calling CLRCreateInstance with the CLSID_CLRMetaHost identifier and obtaining the ICLRMetaHost interface. If the operation fails, the program exits with an error code.
此行通过使用 CLSID_CLRMetaHost 标识符调用 CLRCreateInstance 并获取 ICLRMetaHost 接口来初始化 CLR MetaHost。如果操作失败,程序将退出并显示错误代码。

hr = pMetaHost->GetRuntime(L"v4.0.30319", IID_ICLRRuntimeInfo, (LPVOID*)&pRuntimeInfo);
if (FAILED(hr)) {
pMetaHost->Release();
return -98;
}

The code queries the MetaHost for information about the desired CLR version (“v4.0.30319”). This version is a nice choice since it is shipped by default in each Windows 10. The obtained ICLRRuntimeInfo interface is stored in pRuntimeInfo. If unsuccessful, the previously acquired resources are released, and the program exits with an error code.
该代码向 MetaHost 查询有关所需 CLR 版本 (“v4.0.30319”) 的信息。这个版本是一个不错的选择,因为它默认在每个 Windows 10 中提供。获取 ICLRRuntimeInfo 的接口存储在 pRuntimeInfo 中。如果不成功,将释放以前获取的资源,并且程序将退出并显示错误代码。

BOOL loadable;
hr = pRuntimeInfo->IsLoadable(&loadable);
if (FAILED(hr) || !loadable) {
pRuntimeInfo->Release();
pMetaHost->Release();
return -97;
}

The IsLoadable method checks if the specified runtime version is loadable. If not, resources are released, and the program exits with an error code.
该 IsLoadable 方法检查指定的运行时版本是否可加载。否则,将释放资源,程序将退出并显示错误代码。

hr = pRuntimeInfo->GetInterface(CLSID_CLRRuntimeHost, IID_ICLRRuntimeHost, (LPVOID*)&pClrHost);
if (FAILED(hr)) {
pRuntimeInfo->Release();
pMetaHost->Release();
return -96;
}

Having confirmed compatibility and loadability, the code fetches the ICLRRuntimeHost interface, enabling direct interaction with the CLR runtime.
在确认兼容性和可加载性后,代码将提取 ICLRRuntimeHost 接口,从而实现与 CLR 运行时的直接交互。

pClrHost->Start();

The Start method initiates the CLR runtime within the current process.
该 Start 方法在当前进程中启动 CLR 运行时。

const wchar_t* assemblyPath = L"C:\\Path\\To\\Your\\Assembly.exe";
const wchar_t* typeName = L"Namespace.ClassName";
const wchar_t* methodName = L"MethodName";
const wchar_t* parameters = L"ParameterValues";

Here, paths and names are specified for the C# assembly, type, method, and parameters.
此处指定了 C# 程序集、类型、方法和参数的路径和名称。

DWORD dwRet;
HRESULT rez = pClrHost->ExecuteInDefaultAppDomain(assemblyPath, typeName, methodName, parameters, &dwRet);
if (rez == S_OK)
{
printf("OKAY");
}

The ExecuteInDefaultAppDomain method triggers the execution of the specified C# assembly within the default application domain. The result is stored in dwRet. If successful (result code is S_OK), “OKAY” is printed to the console just for a dummy result check syntax.
该 ExecuteInDefaultAppDomain 方法触发在默认应用程序域中执行指定的 C# 程序集。结果存储在 dwRet 中。如果成功(结果代码为 S_OK ),则将“OKAY”打印到控制台,仅用于虚拟结果检查语法。

pClrHost->Stop();
pClrHost->Release();
pRuntimeInfo->Release();
pMetaHost->Release();

The cleanup phase involves stopping the CLR runtime and releasing the acquired resources in the reverse order of acquisition.
清理阶段包括停止 CLR 运行时,并按与获取顺序相反的方式释放获取的资源。

#Conclusion 结论

While this code is far from practical and advanced, I believe it can still give you an idea of what is the process of executing assemblies from a low level language like C.
虽然这段代码远非实用和先进,但我相信它仍然可以让你了解从像 C 这样的低级语言执行程序集的过程。

I believe that having the ability to be flexible on the programming languages is a crucial skill for every malware developer. Being able to trigger or execute managed code from any environment can help you with both enumeration and exploitation during engagements.
我相信,能够灵活地使用编程语言是每个恶意软件开发人员的一项关键技能。能够从任何环境触发或执行托管代码可以帮助您在参与期间进行枚举和利用。

I am not aware of how other low level languages are treating the CLR, but my bet is that it always has to be explicitly loaded.
我不知道其他低级语言是如何处理 CLR 的,但我敢打赌,它总是必须显式加载。

Thank you so much for you time, and I hope you learned something new!
非常感谢您抽出宝贵时间,希望您能学到新东西!

原文始发于lsecqt:Executing CSharp Assemblies from C code

版权声明:admin 发表于 2024年4月11日 下午9:52。
转载请注明:Executing CSharp Assemblies from C code | CTF导航

相关文章