execute-assembly实现原理(非托管C++代码调用C#)

作者: 裤衩哥的小屋-8sec  更新时间:2020-05-16 03:00:54  原文链接


execute-assembly实现原理(非托管C++代码调用C#)

cs中新加了一个功能,直接加载C#编译的PE文件: execute-assembly 命令

看了下3gstudent大佬的文章,然后简单学习了下这个的原理,三好学生使用ICLRMetaHost::GetRuntime获取有效的ICLRRuntimeInfo指针。这里我找到了另一个方法就是ExecuteInDefaultAppDomain来调用Assembly方法

  • execute-assembly实现原理(非托管C++代码调用C#)
      • ICLRRuntimeHost::ExecuteInDefaultAppDomain 方法
      • ICLRMetaHost::GetRuntime方法

实现原理

ICLRRuntimeHost::ExecuteInDefaultAppDomain 方法

演示代码其实就是非托管 C++ 代码调用 C# dll

实现的功能和上一个 Assembly Load 学习中实现的差不多。

在非托管代码中手动启动 CLR 加载应用程序域来运行托管的 dll,从而调用其中的方法。

利用 Unmanaged API 中的 ICLRRuntimeHost 接口,啟用 Unmanaged 主應用程式,將 Common Language Runtime 載入處理序 (Process)。

#include <Windows.h>
#include <MSCorEE.h>
#include <stdio.h>
#include <metahost.h>
using namespace std;
#pragma comment(lib, "mscoree.lib")
int main(int argc)
{
    ICLRMetaHost *pMetaHost = nullptr;
    ICLRMetaHostPolicy *pMetaHostPolicy = nullptr;
    ICLRRuntimeHost *pRuntimeHost = nullptr;
    ICLRRuntimeInfo *pRuntimeInfo = nullptr;

    HRESULT hr = CLRCreateInstance(CLSID_CLRMetaHost, IID_ICLRMetaHost, (LPVOID*)&pMetaHost);
    hr = pMetaHost->GetRuntime(L"v4.0.30319", IID_PPV_ARGS(&pRuntimeInfo));

    if (FAILED(hr)) {
        wprintf(L"failed to call csharp dll.\n");
    }
    hr = pRuntimeInfo->GetInterface(CLSID_CLRRuntimeHost, IID_PPV_ARGS(&pRuntimeHost));
    hr = pRuntimeHost->Start();
    DWORD dwRet = 0;
    hr = pRuntimeHost->ExecuteInDefaultAppDomain(L"ClassLibrary1.dll", //不会产生新的进程
        L"DllDemo1.Aaaaa1",
        L"Bbbbb",
        L"aaa",
        &dwRet);
    hr = pRuntimeHost->Stop();
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace DllDemo1
{
    public class Aaaaa1
    {
        public static int Bbbbb(string str)
        {
            System.Diagnostics.Process p = new System.Diagnostics.Process();
            p.StartInfo.FileName = "c:\\windows\\system32\\calc.exe";
            p.Start();
            return 0;
        }
    }
}

测试可以加载exe运行。

以下是3gstudent文章演示代码:

ICLRMetaHost::GetRuntime方法

使用ICLRMetaHost::GetRuntime获取有效的ICLRRuntimeInfo指针。

#include "stdafx.h"
#include <metahost.h>
#include <windows.h>
#pragma comment(lib, "MSCorEE.lib")

HRESULT RuntimeHost_GetRuntime_ICLRRuntimeInfo(PCWSTR pszVersion, PCWSTR pszAssemblyName, PCWSTR pszClassName, PCWSTR pszMethodName, PCWSTR pszArgName)
{
    HRESULT hr;
    ICLRMetaHost *pMetaHost = NULL;
    ICLRRuntimeInfo *pRuntimeInfo = NULL;
    ICLRRuntimeHost *pClrRuntimeHost = NULL;
    DWORD dwLengthRet;
    wprintf(L"Load and start the .NET runtime %s \n", pszVersion);
    hr = CLRCreateInstance(CLSID_CLRMetaHost, IID_PPV_ARGS(&pMetaHost));
    if (FAILED(hr))
    {
        wprintf(L"[!]CLRCreateInstance failed w/hr 0x%08lx\n", hr);
        goto Cleanup;
    }
    hr = pMetaHost->GetRuntime(pszVersion, IID_PPV_ARGS(&pRuntimeInfo));
    if (FAILED(hr))
    {
        wprintf(L"[!]ICLRMetaHost::GetRuntime failed w/hr 0x%08lx\n", hr);
        goto Cleanup;
    }
    BOOL fLoadable;
    hr = pRuntimeInfo->IsLoadable(&fLoadable);
    if (FAILED(hr))
    {
        wprintf(L"[!]ICLRRuntimeInfo::IsLoadable failed w/hr 0x%08lx\n", hr);
        goto Cleanup;
    }
    if (!fLoadable)
    {
        wprintf(L"[!].NET runtime %s cannot be loaded\n", pszVersion);
        goto Cleanup;
    }
    hr = pRuntimeInfo->GetInterface(CLSID_CLRRuntimeHost, IID_PPV_ARGS(&pClrRuntimeHost));
    if (FAILED(hr))
    {
        wprintf(L"[!]ICLRRuntimeInfo::GetInterface failed w/hr 0x%08lx\n", hr);
        goto Cleanup;
    }
    hr = pClrRuntimeHost->Start();
    if (FAILED(hr))
    {
        wprintf(L"[!]CLR failed to start w/hr 0x%08lx\n", hr);
        goto Cleanup;
    }
    wprintf(L"[+]Load the assembly %s\n", pszAssemblyName);
    hr = pClrRuntimeHost->ExecuteInDefaultAppDomain(pszAssemblyName, pszClassName, pszMethodName, pszArgName, &dwLengthRet);
    if (FAILED(hr))
    {
        wprintf(L"[!]Failed to call %s w/hr 0x%08lx\n", pszMethodName, hr);
        goto Cleanup;
    }
    wprintf(L"[+]Call %s.%s(\"%s\") => %d\n", pszClassName, pszMethodName, pszArgName, dwLengthRet);

Cleanup:
    if (pMetaHost)
    {
        pMetaHost->Release();
        pMetaHost = NULL;
    }
    if (pRuntimeInfo)
    {
        pRuntimeInfo->Release();
        pRuntimeInfo = NULL;
    }
    if (pClrRuntimeHost)
    {
        pClrRuntimeHost->Release();
        pClrRuntimeHost = NULL;
    }
    return hr;
}

int main()
{
    RuntimeHost_GetRuntime_ICLRRuntimeInfo(L"v4.0.30319", L"ClassLibrary1.dll", L"ClassLibrary1.Class1", L"TestMethod", L"argstring");
    return 0;
}

代码将会加载同级目录下.Net4.0开发的ClassLibrary1.dll,类名为Class1,方法为TestMethod,传入的参数为argstring

ClassLibrary1.dll的代码如下:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace ClassLibrary1
{
    public class Class1
    {
        public static int TestMethod(string str)
        {
            System.Diagnostics.Process p = new System.Diagnostics.Process();
            p.StartInfo.FileName = "c:\\windows\\system32\\calc.exe";
            p.Start();
            return 0;
        }
    }
}

以后会分析以下一些开源的Assembly loader工具,以上两个方法都是在硬盘中加载,而根据三好学生大佬的总结

execute-assembly通常有以下两种利用思路:

1.从内存中读取shellcode并加载.NET程序集

2.从硬盘读取并加载.NET程序集

第一种利用思路要优于第二种,完整的利用过程如下:

创建一个正常的进程

通过Dll反射向进程注入dll

dll实现从内存中读取shellcode并加载最终的.NET程序集

优点如下:

整个过程在内存执行,不写入文件系统

Payload以dll形式存在,不会产生可疑的进程

最终的Payload为C#程序,现有的Powershell利用脚本转换为C#代码很方便