0x00 前言
这个系列是关于Windows计划任务中一些更为本质化的使用,初步估计大概四章。
相比于工具文档或技术文章,我更倾向于将这几篇文章作为传统安全研究的思维笔记,一方面阐述研究过程与思维逻辑,另一方面记录研究成果落地为实战工具的过程。
武器化也好安全开发也罢,将理论基础作为依据,以研究成果作补充,从实战效果作证明的三板斧不能变。
希望在使用之余,能为大家带来研究思路上的启发。
0x01 现象
对Windows对抗有一定研究的,大多都接触过计划任务的相关知识。
作为文档化的组件之一,好处是有完整的官方文档https://docs.microsoft.com/en-us/windows/win32/taskschd/task-scheduler-start-page作为参考,例如我们可以几乎不费力气找到很常用的登录自启动代码https://docs.microsoft.com/en-us/windows/win32/taskschd/logon-trigger-example–c—,稍作修改即可直接使用。
坏处是,文档太长了,面向对象的代码也太复杂了(相对于脚本尤其是安全工具而言)。以上文登录自启动的代码为例,十几个API调用,无故引入且无法去掉的taskschd.dll
导入,为什么普通用户执行不成功,S-1-5-32-544
是什么,TASK_LOGON_GROUP
的定义又在哪?
好在我们是安全研究者,安全研究更擅长从结论/状况反推原因,现在来发挥所长:
我们知道计划任务可以通过UI或者命令行方式进行创建,其参数和选项大部分是对应的。
我们知道计划任务可以通过ITaskService
接口或是TaskSchedulerClass
类以及一系列对象进行操作。
我们知道计划任务可以导出一个XML,通过UI或是命令行均可再将其导入。
我们知道每一个计划任务文件都存放于%SystemRoot%System32Tasks
目录下,内容和导出的XML完全相同。
所以,从安全研究的角度,这里可以提出一个问题:计划任务的本质是什么?是那些类,还是XML?
如果是类的话,那么XML在其中充当着什么角色,是如何解析的?
如果是XML的话,那么类充当的又是什么角色?
0x02 依据
虽然Windows提供了绝大部分符号,但在此时还没有调试Windows服务的必要。我们在横向移动的过程中依然会用到计划任务程序,那么首先抓个包:
看到了满屏的RPC调用,对其解密后可以看到以下信息:
我们看到了几个重点,首先调用号(Opnum)为1;其次RPC Stub Data即调用的参数中明显出现了新任务名称,以及随后的XML。
以windows task scheduler rpc
为关键字搜索,我们可以找到MS-TSCH
协议https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-tsch/21e8e86e-ee5a-469d-917f-28a41f3c25a4,依文档所述,这是建立在RPC协议之上、用于远程对计划任务进行增删改查的接口,同时,我们也看到了熟悉的ITaskSchedulerService
:
参考ITaskSchedulerService SchRpcRegisterTask (Opnum 1)
https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-tsch/849c131a-64e4-46ef-b015-9d4c599c5167一章,对比参数可基本进行确认:
最后,以impacket
作为佐证,众所周知atexec.py
采用计划任务方式进行利用,其中创建远程计划任务同样通过SchRpcRegisterTask
调用:
于是,我们得到了一个理论依据:微软通过MS-DCERPC
协议,在上层构建了MS-TSCH
协议,该协议通过XML作为参数,实现了对计划任务的管理。
0x03 本质
有了MS-TSCH
作为理论依据,让我们换个思路,尝试从设计者角度进行思考:
(现在,你是一名架构师了)
假设现在一无所有,你会如何设计一个计划任务程序?
首先,所有人都可能调用计划任务,意味着进程应当常驻后台;低权限用户并不能以高权限用户身份进行操作,所以进程需要高权限,并实现模拟机制;高权限后台进程要考虑到特权提升的问题,所以需要存在合理的鉴权机制;计划任务不涉及硬件管理,也并非系统运行所必需,所以无需进入内核。
其次,接受其它进程调用需要有一个合理的通信机制。Windows进程间通信方式众多,出于鉴权考虑,命名管道和alpc均可作为可选项;在易用性方面,alpc和命名管道均有RPC上层封装可用;在性能方面,alpc是毫无疑问的首选(详参微软官方博客alpcport相关)。
之后,出于管理需要,需要支持远程调用。考虑到稳定性,远程通信的方式大多建立在TCP上层;考虑到防火墙与安全性因素,支持加密的HTTPS/SMB/RPC/DCOM是几个可选项;鉴于远程管理往往有着最小配置与降级原则,RPC由于可独立配置、能够通过ncacn_np使用SMB协议通信且不受额外选项干扰,在此优于DCOM;鉴于API统一的原则,统一了本地通信与远程通信的RPC是唯一可选项。
最后,考虑到拓展的需要,需要可拓展的存储方式。考虑到MS-TSCH
至少有着十五年的历史,采用XML兼顾可读性与拓展性无可厚非。
于是,有了基于MS-DCERPC
与直接XML传递的MS-TSCH
协议。
在微软的实现中,Schedule
服务以SYSTEM
权限运行,同时拥有SeImpersoante、SeAssignPrimaryToken
等特权提供不同用户权限的切换。服务通过注册ncalrpc、ncacn_np(atsvc)
以及向epmapper
注册三种方式公开了本地与远程的RPC调用端点(EndPoint),为调用方提供MS-TSCH
协议规定的服务。
好的,我们有了一个通过XML进行通信、且会进行透明鉴权的计划任务服务。
现在,把思路再次转回调用者。
(现在,你是一名程序员。这个功能很重要,怎么实现没人管,明天上线)
不可否认,对照模板编写XML这一做法,对于懒人(我特指初级代码开发人员,无贬义)固然有着无以伦比的方便。但对接过API的都知道,世界上第一痛苦的API就是调用万能接口,第二绝对是通过XML进行数据传递。
MS-TSCH
出生在至少十五年前,很不幸,两毒俱全。来想象一下你是个防守方,现在应用一个临时缓解措施,需要建立并下发以下计划任务监控:当事件ID 1234触发时,执行powershell命令调用某个API。
想到要看协议文档就很头疼对吧,想到要写C来调用RPC就更头大了对吧。
所以微软通过COM
,在Taskschd.dll
内对MS-TSCH
进行面向对象封装,其CLSID
为148BD52A-A2AB-11CE-B11F-00AA00530503
,并提供了一系列帮助接口提供Trigger、Action、Folder的抽象。
为了支持脚本功能,为这个类注册了名为Schedule.Service
的ProgId
,并实现了IDispatch
接口,使得VBS/Powershell等脚本语言能够进行快速调用。
这些是纯粹的封装与帮助类,和实际的协议完全无关。
到这里,TaskScheduler服务(Service或RPC EP)的本质也就呼之欲出:鉴权,接收一个XML(无论是帮助类生成的还是自己构建的),注册到自己业务环境内。
从这个角度看来,计划任务的本质和传统WEB并没有任何区别,甚至可以直接用下面这张图进行类比:
RPC对应HTTP,OPNUM对应Action/Method,XML对应Body。语法、语义、时序完全对应,是的,完美。
实际上,除却纯粹二进制的领域,至少一半的Windows组件能够用这样的方式进行类比。
最后,我们把思维转回安全角度。
(放开我,我是信息安全工程师.jpg)
从攻击者视角看,由于绝大部分文档都仅仅讲述对COM API
的调用,进而可猜想绝大部分防御措施会针对Taskschd.dll
,通过RPC进行绕过可能是一个可行的突破方案。
而从防御者视角看,绕过Taskschd.dll
这一wrapper
可能会对自身防御体系造成绕过甚至击穿(这里“击穿”二字绝非危言耸听)。
0x04 COM
了解到部分本质之后,我们开始进行更为简洁,更贴近于安全思维的调用。
(不要忘记,我们已经把思维转换回了安全角度)
在参考c++版本示例代码的时候,我们可以看到微软同时提供了XML参考:https://docs.microsoft.com/en-us/windows/win32/taskschd/logon-trigger-example–xml-,并提示了可以使用ITaskFolder::RegisterTask
通过XML直接注册计划任务。
随后调用ITaskFolder::RegisterTask
来替代之前的繁琐方式(参考代码依然来自MSDN):
ITaskFolder* pRootFolder = NULL;
hr = pService->GetFolder(_bstr_t(L"\"), &pRootFolder);
if (FAILED(hr))
{
printf("Cannot get Root folder pointer: %x", hr);
pService->Release();
CoUninitialize();
return 1;
}
IRegisteredTask* pRegisteredTask = NULL;
pRootFolder->RegisterTask
(
_bstr_t(wszTaskName),
_bstr_t("xml"),
TASK_CREATE_OR_UPDATE,
_variant_t(),
_variant_t(),
TASK_LOGON_INTERACTIVE_TOKEN,
_variant_t(),
&pRegisteredTask
);
0x05 RPC
同样的,MS-TSCH 6.3 Appendix A.3: SchRpc.idl
https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-tsch/96c9b399-c373-4490-b7f5-78ec3849444e提供了完整的IDL,通过编译IDL即可直接进行简单的RPC调用:
RpcTryExcept
{
wchar_t* pActualPath = 0;
const wchar_t* xml = L"<!--snipped xml-->";
_TASK_XML_ERROR_INFO *errorInfo = 0;
SchRpcRegisterTask
(
schrpc_binding_handle,
L"\Test Task",
xml,
6,
0,
0,
0,
0,
&pActualPath,
&errorInfo
);
}
RpcExcept(1)
{
DWORD code = RpcExceptionCode();
printf("RPC Exception %dn", code);
}
RpcEndExcept;
至少在本文发布的时候,利用直接RPC调用可以绕过相当一部分防护软件对计划任务自启动的拦截。
0x06 总结
本章从协议层面,讲述了Windows计划任务程序从设计、协议、实现均基于XML格式这一基础事实,并以此为基础介绍了更为简单方便的调用。
基础之所以是基础,在于后续相关知识与应用一定会与其具备强关联,而绝非单纯的浅显易懂。
我一直认为,编程思想与设计模式才是最基础的安全技术。在这冗长而无趣的第一章中,我们通过面向对象中抽象
、封装
这两大基础概念,以及背后隐藏的Transport/Channel
这个被微软大肆使用的名词(相信如果搜索了上面几节其中的关键字,并且看了原文就一定有印象)来从侧面分析微软的设计思想,从而能够更好地理解组件的运作方式,最终找到其中的薄弱点,并加以利用。
后续几章无一例外,均将以此为基础,来讲几个有趣的应用案例。
原文始发于微信公众号(头像哥老草):Advanced Windows Taskscheduler Playbook – Part.1 basic