权限维持-计划任务

深入理解计划任务

计划任务的父进程

运行计划任务的相关服务是:Task Scheduler。该服务使用 svchost.exe 的 netsvcs 组进行托管。

image-20240715160516708

通过进程树看到 svchost.exe 进程的命令行为

svchost.exe -k netsvcs -p -s Schedule

在 Windows 10.1703以下可能看不到 -s Schedule 参数。

taskeng.exe

在旧系统中计划任务的进程派生顺序是这样子的: svchost.exe -> taskeng.exe -> [SpecialTaskProcess]

taskeng.exe 的进程参数语法如下:

taskeng.exe {GUID} [User SID]:[Domain]\[User Name]:[Options]

某些情况下,可能只有 {GUID} 一个参数,Options 参数可能会标识一些其他信息,例如权限信息,如果一个任务运行在高权限模式下,Options 的内容会是:Interactive:Highest[1]

所以在旧的系统中可以查看进程树排查恶意进程,例如找到 taskeng.exe 的进程树,它的父进程为svchost.exe -k netsvcs。 子进程就是运行中的任务对应的进程,通过排查子进程确定恶意进程。

svchost.exe

win7之后,计划任务托管程序从 taskeng.exe 慢慢迁移到 svchost.exe,这期间计划任务进程派生可能有两种顺序:

  • wininit.exe -> services.exe -> svchost.exe -> [SpecialTaskProcess]
  • wininit.exe -> services.exe -> svchost.exe -> taskeng.exe -> [SpecialTaskProcess]

从 Windows 10.1511 版本开始,不再有 taskeng.exe 了,新版本系统中找不到该程序,任务进程直接运行在托管 Task Scheduler 服务的 svchost.exe 进程下:

所以高版本系统中,我们可以通过

svchost.exe -k netsvcs

排查进程的子进程来确定恶意或可疑程序。

taskhostw.exe

在上面的图里,可以很明显注意到

svchost.exe -k netsvcs

下还有一个名为 taskhostw.exe 的进程。

在 Windows 7 上该进程名为:taskhost.exe。

在 Windows 8 上该进程名为:taskhostex.exe。

该进程的作用同 dllhost.exe 和 svchost.exe 相似,起到一个 DLL 托管的作用。

通过搜索 %SystemRoot%\System32\Tasks 文件夹,我们能发现一些任务对应的动作不是 Exec,而是 ComHandler。

我们找到一些能起到对比效果的任务 XML,隐藏了一些不必要的字段。

这是我们利用 schtasks.exe 命令创建的任务 XML:

<?xml version="1.0" encoding="UTF-16"?>
<Task version="1.2" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
  <Actions Context="Author">
    <Exec>
      <Command>"C:\Program Files (x86)\IObit\Advanced SystemCare\ASC.exe"</Command>
      <Arguments>/SkipUac</Arguments>
    </Exec>
  </Actions>
</Task>

这个是系统 .NET Framework 的计划任务 XML 内容,如果安装了 .NET 框架可以在

%SystemRoot%\System32\Tasks\Microsoft\Windows\.NET Framework 

目录下找到:

<?xml version="1.0" encoding="UTF-16"?>
<Task version="1.6" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
  <Actions Context="Author">
    <ComHandler>
      <ClassId>{84F0FAE1-C27B-4F6F-807B-28CF6F96287D}</ClassId>
      <Data><![CDATA[/RuntimeWide]]></Data>
    </ComHandler>
  </Actions>
</Task>

手动触发 .NET Framework 任务,该任务调用 ngentasklauncher.dll ,通过 ProcExp.exe 监控进程观察到的进程树:

image-20240715160812311

可以注意到 taskhostw.exe 的参数 /RuntimeWide 和 xml 中 \<Data\> 标签指定的一样。

在观察 taskhostw.exe 进程树的过程中,发现下面的命令行参数:

taskhosw.exe Install $(Arg0)

这个东西在 MSDN 中做了介绍

由此可知,$(Arg0) 这个参数是在通过 IRegisteredTask::Run[Ex] 接口运行任务时动态指定的。在一些 Windows 默认的任务中常见:

\Microsoft\Windows\Workplace Join\Automatic-Device-Join 任务:

<?xml version="1.0" encoding="UTF-16"?>
<Task xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
  <Actions Context="LocalSystem">
    <Exec>
      <Command>%SystemRoot%\System32\dsregcmd.exe</Command>
      <Arguments>$(Arg0) $(Arg1) $(Arg2)</Arguments>
    </Exec>
  </Actions>
</Task>

\Microsoft\Windows\Maps\MapsToastTask 任务:

<?xml version="1.0" encoding="UTF-16"?>
<Task xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
  <Actions Context="Users">
    <ComHandler>
      <ClassId>{9885AEF2-BD9F-41E0-B15E-B3141395E803}</ClassId>
      <Data><![CDATA[$(Arg0);$(Arg1);$(Arg2);$(Arg3);$(Arg4);$(Arg5);$(Arg6);$(Arg7)]]></Data>
    </ComHandler>
  </Actions>
</Task>

查阅 MSDN 可知,该参数可以通过以下 API 进行传递:

计划任务相关注册表项

在一次应急排查中,只能从计划任务日志中看到恶意计划任务在周期性的执行,但却无法通过 taskschd.msc 或 schtasks 查询到恶意任务,并且通过排查 %SystemRoot%\System32\Tasks 目录后仍无法找到它。

在测试中发现,创建计划任务 test 后,无论是手动修改任务 xml 文件,还是删除任务 xml 文件,都无法影响该任务的运行。于是对注册表项进行监控,发现在创建任务后,下面的注册表项发生变化:

HKEY_LOCAL_MACHINE\Software\Microsoft\Windows NT\CurrentVersion\Schedule

下面是从 [winreg-kb](https://github.com/libyal/winreg-kb/blob/master/documentation/Task Scheduler Keys.asciidoc) 项目中得到该注册表对应的信息:

在 XP 时,计划任务注册表路径为

HKEY_LOCAL_MACHINE\Software\Microsoft\SchedulingAgent

Win7 以后发生变化,变成

HKEY_LOCAL_MACHINE\Software\Microsoft\Windows NT\CurrentVersion\Schedule

子项有:

名称 描述
Aliases 存储AtServiceAccount,默认NT AUTHORITY\System
CompatibilityAdapter
Configuration
CredWom
Handlers
Handshake
TaskCache 存储任务项信息

任务项信息除了在磁盘中的 %SystemRoot%\System32\Tasks 下之外,还在下面的注册表项中存在:

HKEY_LOCAL_MACHINE\Software\Microsoft\Windows NT\CurrentVersion\Schedule\TaskCache

Schedule\TaskCache :

名称 描述
Boot
Logon
Plain
Plain
Tree

TaskCache\Tree 子项以任务名称命名,每个任务下的 Value 结构如下:

名称 类型 描述
Id REG_SZ {GUID},任务对应的guid编号
Index REG_DWORD 一般任务为3,其他值未知
SD REG_BINARY 该任务项的安全描述信息,二进制值,结构未知

每个 Schedule\TaskCache\Tasks%GUID% 对应一个任务,有这些 Value :

名称 类型 描述
Actions REG_BINARY 二进制值,动作信息,中间包含 UNICODE 形式 COMMAND 信息
Date REG_SZ 任务创建日期?
Description REG_SZ 任务描述
DynamicInfo REG_BINARY Win7 以下 28 位,Win8 以上 32 位
Hash REG_BINARY SHA-256 or CRC32, 疑似对应 xml 文件 Hash
Path REG_SZ 在 TaskCache\Tree 中的任务路径
Schema REG_DWORD
Triggers REG_BINARY 二进制,触发器信息
URI REG_SZ 任务路径

如果是 at 命令创建的计划任务,对应的注册表位置在

Microsoft\Windows NT\CurrentVersion\Schedule\TaskCache\Tree\At1

计划任务的安全描述符(SD)

计划任务的 SD 配置在注册表中的位置:

HKEY_LOCAL_MACHINE\Software\Microsoft\Windows NT\CurrentVersion\Schedule\TaskCache\Tree\{TaskName}\SD

为人类不可读的二进制格式

在一篇关于隐藏windows服务的文章中,了解到通过修改对象的安全描述信息可以达到隐藏的目的。触类旁通一下,计划任务的隐藏应该也是可以通过改变安全描述信息(SD)实现。

Windows 自带的工具 schtasks.exe 并不支持 sd 的设置,需要通过 API 实现。通过查阅 MSDN 并未发现对于 TASK 的 SDDL 该怎么写,相关 API 列表: - IRegistrationInfo::put_SecurityDescriptor - ITaskFolder::CreateFolder - ITaskFolder::CreateFolder - ITaskFolder::RegisterTaskDefinition - ITaskFolder::SetSecurityDescriptor - IRegisteredTask::SetSecurityDescriptor

通过这些 API 可以实现设置 TASK 的 SD 信息。我尝试使用项目 TaskScheduler 进行任务的创建,它用起来会更方便一些:

using System;
using System.Security.Principal;
using System.Security.AccessControl;
using Microsoft.Win32.TaskScheduler;

namespace SchTaskOpt
{
    class Program
    {
        static void Main(string[] args)
        {
            TaskDefinition td = TaskService.Instance.NewTask();
            td.RegistrationInfo.Description = "do something";
            td.Principal.RunLevel = TaskRunLevel.Highest;
            td.Principal.LogonType = TaskLogonType.ServiceAccount;
            td.Principal.UserId = "SYSTEM";
            td.RegistrationInfo.SecurityDescriptorSddlForm = @"D:P(D;;DCLCWPDTSD;;;IU)(A;;CCLCSWLOCRRC;;;IU)(A;;CCLCSWLOCRRC;;;SU)(A;;CCLCSWRPWPDTLOCRRC;;;SY)";

            DailyTrigger dt = new DailyTrigger();
            dt.StartBoundary = DateTime.Now;
            dt.DaysInterval = 1;
            dt.Repetition.Interval = TimeSpan.FromMinutes(1);

            //td.Triggers.Add(dt);
            td.Actions.Add("notepad", null, null);
            Task t =  TaskService.Instance.RootFolder.RegisterTaskDefinition(path:"test2", definition:td, TaskCreation.CreateOrUpdate, null, null, TaskLogonType.ServiceAccount);
            Console.WriteLine("success!!");
            //TaskSecurity ts = new TaskSecurity(t);
            //ts.AddAccessRule(new TaskAccessRule(new SecurityIdentifier(WellKnownSidType.AuthenticatedUserSid, null), TaskRights.Read | TaskRights.Write | TaskRights.ReadAttributes, AccessControlType.Deny));

            //t.SetAccessControl(ts);

            Console.WriteLine("success!!");
        }
    }
}

但不幸的是报错了,无论是通过管理员运行还是 SYSTEM,都无法另其正常运作:

PS C:\source\SchTaskOpt\bin\> .\SchTaskOpt.exe

未经处理的异常:  System.Runtime.InteropServices.COMException: 异常来自 HRESULT:0xD0000061
    Microsoft.Win32.TaskScheduler.V2Interop.ITaskFolder.RegisterTaskDefinition(String Path, ITaskDefinition pDefinition, Int32 flags, Object UserId, Object password, TaskLogonType LogonType, Object sddl)
    Microsoft.Win32.TaskScheduler.TaskFolder.RegisterTaskDefinition(String path, TaskDefinition definition, TaskCreation createType, String userId, String password, TaskLogonType logonType, String sddl)

这个疑问先保留一下吧,需要清楚知道注册表中每个任务自动生成的 SD 格式才有头绪,这份类型结构清点也许会排上用场。

计划任务底层实现

官方文档:https://learn.microsoft.com/zh-cn/windows/win32/taskschd/task-scheduler-start-page

登录自启动代码:https://learn.microsoft.com/zh-cn/windows/win32/taskschd/logon-trigger-example–c—

已知:

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

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

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

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

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

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

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

MS-TSCH协议

windows task scheduler rpc为关键字搜索,我们可以找到[MS-TSCH](https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-tsch/21e8e86e-ee5a-469d-917f-28a41f3c25a4)协议,依文档所述,这是建立在RPC协议之上、用于远程对计划任务进行增删改查的接口,同时,我们也看到了熟悉的ITaskSchedulerService

微软通过MS-DCERPC协议,在上层构建了MS-TSCH协议,该协议通过XML作为参数,实现了对计划任务的管理。

MS-TSCH添加计划任务

https://mp.weixin.qq.com/s/vp-7l2kAihE12h58oTIIRQ

创建计划任务

https://mp.weixin.qq.com/s/vp-7l2kAihE12h58oTIIRQ

Windows的计划任务有多种添加方式,比如通过命令、PowerShell、C++调用COM组件等

C++调用COM组件添加自启动的方法:https://github.com/evilashz/PigScheduleTask

对于开启核晶的360已经失效。

rpc创建

使用的是ITaskSchedulerService这个RPC接口,接口实现了使用XML来添加计划任务的方法。

隐藏计划任务

https://paper.seebug.org/1464/

0%