免杀基础–windows shellcode编写入门
0x00 介绍
作为一名红队人员,CobalStrike和metasploit是我们常用的工具。Cobalt Strike 从3.11版本开始,加入了一个名为”execute-assembly”的命令,能够从内存中加载.NET程序集。这个功能不需要向硬盘写入文件,十分隐蔽。不仅能直接利用现成C#的程序,而且现有的Powershell利用脚本能够很容易的转换为C#代码。在免杀上也能起到一定的效果。
我们知道CS或者MSF的被控端都是C/C的代码编写生成的可执行程序,而execute-assembly执行的是C#程序。之前在分析一个恶意样本时,也见过这这种方式。C/C代码是怎么执行C#程序的呢?
0x01 利用思路
execute-assembly的实现通常有以下两种思路:
1.从内存中读取shellcode并加载.NET程序集
1.调用ICLRMetaHost::EnumerateInstalledRuntimes, ICLRMetaHost::GetRuntime或者ICLRMetaHostPolicy::GetRequestedRuntime方法以获取有效的ICLRRuntimeInfo指针 2.使用ICorRuntimeHost接口 3.使用Load_3(…)从内存中读取并加载.NET程序集 4.调用静态方法
2.从硬盘读取并加载.NET程序集
1.调用ICLRMetaHost::EnumerateInstalledRuntimes, ICLRMetaHost::GetRuntime或者ICLRMetaHostPolicy::GetRequestedRuntime方法以获取有效的ICLRRuntimeInfo指针 2.使用ICorRuntimeHost(使用Load_2(…))或者ICLRRuntimeHost接口 3.加载.NET程序集并调用静态方法
第一种利用思路要优于第二种,完整的利用过程如下:
- 创建一个正常的进程
- 通过Dll反射向进程注入dll
- dll实现从内存中读取shellcode并加载最终的.NET程序集
优点如下:
- 整个过程在内存执行,不写入文件系统(俗称不落地)
- Payload以dll形式存在,不会产生可疑的进程
- 最终的Payload为C#程序
0x02 代码实现分析
参考开源代码:https://github.com/etormadiv/HostingCLR
提示:
若不能成功生成目标可执行文件,在stdafx.h开头加上
#pragma warning(disable:4146)
再次生成。
- 调用CLRCreateInstance函数以获取ICLRMetaHost接口实例
ICLRMetaHost* pMetaHost = NULL;
HRESULT hr;
/* 获取 ICLRMetaHost 实例*/
hr = CLRCreateInstance(CLSID_CLRMetaHost, IID_ICLRMetaHost, (VOID**)&pMetaHost);
if(FAILED(hr))
{
printf("[!] CLRCreateInstance(...) failed\n");
getchar();
return -1;
}
printf("[+] CLRCreateInstance(...) succeeded\n");
- 调用ICLRMetaHost::GetRuntime方法以获取有效的ICLRRuntimeInfo指针.(在此固定加载v4.0.30319版本的.net,可通过目标程序集的版本加载对应版本的.net)
ICLRRuntimeInfo* pRuntimeInfo = NULL;
/* Get ICLRRuntimeInfo instance */
hr = pMetaHost->GetRuntime(L"v4.0.30319", IID_ICLRRuntimeInfo, (VOID**)&pRuntimeInfo);
if(FAILED(hr))
{
printf("[!] pMetaHost->GetRuntime(...) failed\n");
getchar();
return -1;
}
printf("[+] pMetaHost->GetRuntime(...) succeeded\n");
- 使用ICorRuntimeHost
IUnknownPtr pAppDomainThunk = NULL;
hr = pRuntimeHost->GetDefaultDomain(&pAppDomainThunk);
if(FAILED(hr))
{
printf("[!] pRuntimeHost->GetDefaultDomain(...) failed\n");
getchar();
return -1;
}
printf("[+] pRuntimeHost->GetDefaultDomain(...) succeeded\n");
_AppDomainPtr pDefaultAppDomain = NULL;
/* Equivalent of System.AppDomain.CurrentDomain in C# */
hr = pAppDomainThunk->QueryInterface(__uuidof(_AppDomain), (VOID**) &pDefaultAppDomain);
if(FAILED(hr))
{
printf("[!] pAppDomainThunk->QueryInterface(...) failed\n");
getchar();
return -1;
}
printf("[+] pAppDomainThunk->QueryInterface(...) succeeded\n");
- 程序集载入内存 AssemblyPtr
- 执行程序集
_MethodInfoPtr pMethodInfo = NULL;
/* Assembly.EntryPoint Property */
hr = pAssembly->get_EntryPoint(&pMethodInfo);
if(FAILED(hr))
{
printf("[!] pAssembly->get_EntryPoint(...) failed\n");
getchar();
return -1;
}
printf("[+] pAssembly->get_EntryPoint(...) succeeded\n");
VARIANT retVal;
ZeroMemory(&retVal, sizeof(VARIANT));
VARIANT obj;
ZeroMemory(&obj, sizeof(VARIANT));
obj.vt = VT_NULL;
SAFEARRAY *psaStaticMethodArgs = SafeArrayCreateVector(VT_VARIANT, 0, 0);
//
hr = pMethodInfo->Invoke_3(obj, psaStaticMethodArgs, &retVal);
if(FAILED(hr))
{
printf("[!] pMethodInfo->Invoke_3(...) failed, hr = %X\n", hr);
getchar();
return -1;
}
printf("[+] pMethodInfo->Invoke_3(...) succeeded\n");
测试程序
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows.Forms;
namespace TestHostCLR
{
static class Program
{
/// <summary>
/// 应用程序的主入口点。
/// </summary>
[STAThread]
static void Main()
{
System.Diagnostics.Process p = new System.Diagnostics.Process();
p.StartInfo.FileName = "c:\\windows\\system32\\calc.exe";
p.Start();
}
}
}