现在的位置: 首页 Windows >正文

Advanced Windows Task Scheduler Playbook - Part.1 basic

0x00 前言


这个系列是关于Windows计划任务中一些更为本质化的使用,初步估计大概四章。

相比于工具文档或技术文章,我更倾向于将这几篇文章作为传统安全研究的思维笔记,一方面阐述研究过程与思维逻辑,另一方面记录研究成果落地为实战工具的过程。

武器化也好安全开发也罢,将理论基础作为依据,以研究成果作补充,从实战效果作证明的三板斧不能变。

希望在使用之余,能为大家带来研究思路上的启发。


0x01 现象


对Windows对抗有一定研究的,大多都接触过计划任务的相关知识。

作为文档化的组件之一,好处是有完整的官方文档作为参考,例如我们可以几乎不费力气找到很常用的登录自启动代码,稍作修改即可直接使用。

坏处是,文档太长了,面向对象的代码也太复杂了(相对于脚本尤其是安全工具而言)。以上文登录自启动的代码为例,十几个API调用,无故引入且无法去掉的`taskschd.dll`导入,为什么普通用户执行不成功,`S-1-5-32-544`是什么,`TASK_LOGON_GROUP`的定义又在哪?


好在我们是安全研究者,安全研究更擅长从结论/状况反推原因,现在来发挥所长:

我们知道计划任务可以通过UI或者命令行方式进行创建,其参数和选项大部分是对应的。

我们知道计划任务可以通过`ITaskService`接口或是`TaskSchedulerClass`类以及一系列对象进行操作。

我们知道计划任务可以导出一个XML,通过UI或是命令行均可再将其导入。

我们知道每一个计划任务文件都存放于`%SystemRoot%\System32\Tasks`目录下,内容和导出的XML完全相同。


所以,从安全研究的角度,这里可以提出一个问题:计划任务的本质是什么?是那些类,还是XML?

如果是类的话,那么XML在其中充当着什么角色,是如何解析的?

如果是XML的话,那么类充当的又是什么角色?


0x02 依据


虽然Windows提供了绝大部分符号,但在此时还没有调试Windows服务的必要。我们在横向移动的过程中依然会用到计划任务程序,那么首先抓个包:

看到了满屏的RPC调用,对其解密后可以看到以下信息:

我们看到了几个重点,首先调用号(Opnum)为1;其次RPC Stub Data即调用的参数中明显出现了新任务名称,以及随后的XML。


以`windows task scheduler rpc`为关键字搜索,我们可以找到`MS-TSCH`协议,依文档所述,这是建立在RPC协议之上、用于远程对计划任务进行增删改查的接口,同时,我们也看到了熟悉的`ITaskSchedulerService`:

参考`ITaskSchedulerService SchRpcRegisterTask (Opnum 1)`一章,对比参数可基本进行确认:

最后,以`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`为`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组件能够用这样的方式进行类比。


最后,我们把思维转回安全角度。

(放开我,我是信息安全工程师.jpg)


从攻击者视角看,由于绝大部分文档都仅仅讲述对`COM API`的调用,进而可猜想绝大部分防御措施会针对`Taskschd.dll`,通过RPC进行绕过可能是一个可行的突破方案。

而从防御者视角看,绕过`Taskschd.dll`这一`wrapper`可能会对自身防御体系造成绕过甚至击穿(这里“击穿”二字绝非危言耸听)。


0x04 COM


了解到部分本质之后,我们开始进行更为简洁,更贴近于安全思维的调用。

(不要忘记,我们已经把思维转换回了安全角度)


在参考c++版本示例代码的时候,我们可以看到微软同时提供了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`提供了完整的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 %d\n", code);
  }
RpcEndExcept;

至少在本文发布的时候,利用直接RPC调用可以绕过相当一部分防护软件对计划任务自启动的拦截。


0x06 总结


本章从协议层面,讲述了Windows计划任务程序从设计、协议、实现均基于XML格式这一基础事实,并以此为基础介绍了更为简单方便的调用。


基础之所以是基础,在于后续相关知识与应用一定会与其具备强关联,而绝非单纯的浅显易懂。

我一直认为,编程思想与设计模式才是最基础的安全技术。在这冗长而无趣的第一章中,我们通过面向对象中`抽象`、`封装`这两大基础概念,以及背后隐藏的`Transport/Channel`这个被微软大肆使用的名词(相信如果搜索了上面几节其中的关键字,并且看了原文就一定有印象)来从侧面分析微软的设计思想,从而能够更好地理解组件的运作方式,最终找到其中的薄弱点,并加以利用。

后续几章无一例外,均将以此为基础,来讲几个有趣的应用案例。