这个系列是关于Windows计划任务中一些更为本质化的使用,初步估计大概四章。
相比于工具文档或技术文章,我更倾向于将这几篇文章作为传统安全研究的思维笔记,一方面阐述研究过程与思维逻辑,另一方面记录研究成果落地为实战工具的过程。
武器化也好,安全开发也罢,将理论基础作为依据,以研究成果作补充,从实战效果作证明的三板斧不能变。
希望在使用之余,能为大家带来研究思路上的启发。
对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%\System32\Tasks目录下,内容和导出的XML完全相同。
所以,从安全研究的角度,这里可以提出一个问题:计划任务的本质是什么?是那些类,还是XML?
如果是类的话,那么XML在其中充当着什么角色,是如何解析的?
如果是XML的话,那么类充当的又是什么角色?
虽然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作为参数,实现了对计划任务的管理。有了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为0F87369F-A4E5-4CFC-BD3E-73E6154572DD,并提供了一系列帮助接口提供Trigger、Action、Folder的抽象。为了支持脚本功能,为这个类注册了名为Schedule.Service的ProgId,并实现了IDispatch接口,使得VBS/Powershell等脚本语言能够进行快速调用。到这里,TaskScheduler服务(Service或RPC EP)的本质也就呼之欲出:鉴权,接收一个XML(无论是帮助类生成的还是自己构建的),注册到自己业务环境内。从这个角度看来,计划任务的本质和传统WEB并没有任何区别,甚至可以直接用下面这张图进行类比:RPC对应HTTP,OPNUM对应Action/Method,XML对应Body。语法、语义、时序完全对应,是的,完美。实际上,除却纯粹二进制的领域,至少一半的Windows组件能够用这样的方式进行类比。从攻击者视角看,由于绝大部分文档都仅仅讲述对COM API的调用,进而可猜想,绝大部分防御措施会针对Taskschd.dll,通过RPC进行绕过可能是一个可行的突破方案。而从防御者视角看,绕过Taskschd.dll这一wrapper可能会对自身防御体系造成绕过甚至击穿(这里“击穿”二字绝非危言耸听)。了解到部分本质之后,我们开始进行更为简洁,更贴近于安全思维的调用。在参考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
);
同样的:
"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"";
_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 %d\n", code);
}
RpcEndExcept;
至少在本文发布的时候,利用直接RPC调用可以绕过相当一部分防护软件对计划任务自启动的拦截。
本章从协议层面,讲述了Windows计划任务程序从设计、协议、实现均基于XML格式这一基础事实,并以此为基础介绍了更为简单方便的调用。
基础之所以是基础,在于后续相关知识与应用一定会与其具备强关联,而绝非单纯的浅显易懂。
我一直认为,编程思想与设计模式才是最基础的安全技术。在这冗长而无趣的第一章中,我们通过面向对象中抽象、封装这两大基础概念,以及背后隐藏的Transport/Channel这个被微软大肆使用的名词(相信如果搜索了上面几节其中的关键字,并且看了原文就一定有印象)来从侧面分析微软的设计思想,从而能够更好地理解组件的运作方式,最终找到其中的薄弱点,并加以利用。
后续几章无一例外,均将以此为基础,来讲几个有趣的应用案例。