长亭百川云 - 文章详情

Advanced Windows Taskscheduler Playbook - Part.1 basic

头像哥老草

71

2024-07-13

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%\System32\Tasks目录下,内容和导出的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进行面向对象封装,其CLSID148BD52A-A2AB-11CE-B11F-00AA00530503,并提供了一系列帮助接口提供Trigger、Action、Folder的抽象。

为了支持脚本功能,为这个类注册了名为Schedule.ServiceProgId,并实现了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.idlhttps://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 %d\\n", code);  
  }  
RpcEndExcept;  

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

0x06 总结

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

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

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

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

相关推荐
关注或联系我们
添加百川云公众号,移动管理云安全产品
咨询热线:
4000-327-707
百川公众号
百川公众号
百川云客服
百川云客服

Copyright ©2024 北京长亭科技有限公司
icon
京ICP备 2024055124号-2